Class: Conexa::Request
Constant Summary collapse
- DEFAULT_HEADERS =
{ 'Content-Type' => 'application/json; charset=utf8', 'Accept' => 'application/json', 'User-Agent' => "conexa-ruby/#{Conexa::VERSION}" }
- READ_METHODS =
Verbs allowed while Conexa.read_only? — GET, plus authentication, without which read-only mode could not obtain a token in the first place.
%w(GET).freeze
- AUTH_PATHS =
The authentication exemption is tied to these paths, not to the caller's
auth:flag.Request.authis public, so trusting the flag alone let any write opt out of the guard withRequest.auth("/charge/settle/1", …). %w(/auth).freeze
Instance Attribute Summary collapse
-
#headers ⇒ Object
Returns the value of attribute headers.
-
#method ⇒ Object
Returns the value of attribute method.
-
#parameters ⇒ Object
Returns the value of attribute parameters.
-
#path ⇒ Object
Returns the value of attribute path.
-
#query ⇒ Object
Returns the value of attribute query.
Class Method Summary collapse
- .auth(url, options = {}) ⇒ Object
- .delete(url, options = {}) ⇒ Object
- .get(url, options = {}) ⇒ Object
- .patch(url, options = {}) ⇒ Object
- .post(url, options = {}) ⇒ Object
- .put(url, options = {}) ⇒ Object
Instance Method Summary collapse
- #call(resource_name, query_context: nil) ⇒ Object
-
#describe_api_error(parsed_error) ⇒ Object
The API's
messageplus itserrors, rendered as prose. - #enforce_read_only! ⇒ Object
- #full_api_url ⇒ Object
-
#initialize(path, method, options = {}) ⇒ Request
constructor
A new instance of Request.
- #request_params ⇒ Object
- #run ⇒ Object
Constructor Details
#initialize(path, method, options = {}) ⇒ Request
Returns a new instance of Request.
17 18 19 20 21 22 23 24 |
# File 'lib/conexa/request.rb', line 17 def initialize(path, method, ={}) @path = path @method = method @parameters = [:params] || nil @query = [:query] || Hash.new @headers = [:headers] || Hash.new @auth = [:auth] || false end |
Instance Attribute Details
#headers ⇒ Object
Returns the value of attribute headers.
15 16 17 |
# File 'lib/conexa/request.rb', line 15 def headers @headers end |
#method ⇒ Object
Returns the value of attribute method.
15 16 17 |
# File 'lib/conexa/request.rb', line 15 def method @method end |
#parameters ⇒ Object
Returns the value of attribute parameters.
15 16 17 |
# File 'lib/conexa/request.rb', line 15 def parameters @parameters end |
#path ⇒ Object
Returns the value of attribute path.
15 16 17 |
# File 'lib/conexa/request.rb', line 15 def path @path end |
#query ⇒ Object
Returns the value of attribute query.
15 16 17 |
# File 'lib/conexa/request.rb', line 15 def query @query end |
Class Method Details
.auth(url, options = {}) ⇒ Object
150 151 152 153 |
# File 'lib/conexa/request.rb', line 150 def self.auth(url, ={}) [:auth] = true self.new url, 'POST', end |
.delete(url, options = {}) ⇒ Object
167 168 169 |
# File 'lib/conexa/request.rb', line 167 def self.delete(url, ={}) self.new url, 'DELETE', end |
.get(url, options = {}) ⇒ Object
146 147 148 |
# File 'lib/conexa/request.rb', line 146 def self.get(url, ={}) self.new url, 'GET', end |
.patch(url, options = {}) ⇒ Object
163 164 165 |
# File 'lib/conexa/request.rb', line 163 def self.patch(url, ={}) self.new url, 'PATCH', end |
.post(url, options = {}) ⇒ Object
155 156 157 |
# File 'lib/conexa/request.rb', line 155 def self.post(url, ={}) self.new url, 'POST', end |
.put(url, options = {}) ⇒ Object
159 160 161 |
# File 'lib/conexa/request.rb', line 159 def self.put(url, ={}) self.new url, 'PUT', end |
Instance Method Details
#call(resource_name, query_context: nil) ⇒ Object
132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/conexa/request.rb', line 132 def call(resource_name, query_context: nil) dt = run if dt[:pagination] result = ConexaObject.convert({ data: ConexaObject.convert(dt[:data], resource_name), pagination: ConexaObject.convert(dt[:pagination], "pagination")}, "result") result.instance_variable_set(:@query_context, query_context) if query_context return result end ConexaObject.convert(dt[:data], resource_name) end |
#describe_api_error(parsed_error) ⇒ Object
The API's message plus its errors, rendered as prose.
This used to be message + "=> Erros: " + errors.to_s, which appended a
dangling "=> Erros: " to the 75 documented responses that carry no errors
array, and dumped Ruby's #inspect of an array of hashes for the ones that
do. ResponseError#api_error_messages already normalises both shapes.
107 108 109 110 111 112 113 |
# File 'lib/conexa/request.rb', line 107 def describe_api_error(parsed_error) = parsed_error['message'].to_s details = Conexa::ResponseError.new({}, nil, nil, parsed_error). return if details.empty? "#{} — #{details.join("; ")}" end |
#enforce_read_only! ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/conexa/request.rb', line 118 def enforce_read_only! return unless Conexa.read_only? return if READ_METHODS.include?(method.to_s.upcase) return if @auth && AUTH_PATHS.include?(path) # Deliberately `path`, not `full_api_url`: the latter validates the URL and # can raise RequestError, which would win over this one purely because the # message is interpolated first. Read-only is a policy — it applies whatever # the path looks like. raise Conexa::ReadOnlyError, "Conexa is in read-only mode: refusing #{method.to_s.upcase} #{path}. " \ "Unset config.read_only (or CONEXA_READ_ONLY) to allow writes." end |
#full_api_url ⇒ Object
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/conexa/request.rb', line 186 def full_api_url url = Conexa.api_endpoint + path if @query.present? url += '?' + URI.encode_www_form(query) end # An unusable path (a stray space in an id, say) would otherwise surface as # URI::InvalidURIError from inside RestClient — outside Conexa::ConexaError, # so no caller could rescue it meaningfully. begin URI.parse(url) rescue URI::InvalidURIError raise Conexa::RequestError, "Invalid request path: #{path.inspect}" end url end |
#request_params ⇒ Object
171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/conexa/request.rb', line 171 def request_params aux = { method: method, url: full_api_url, } @parameters = Util.camelize_hash(@parameters) aux.merge!({ payload: MultiJson.encode(@parameters)}) unless %w(GET DELETE).include? method extra_headers = DEFAULT_HEADERS.dup extra_headers[:authorization] = "Bearer #{Conexa.configuration.api_token}" unless @auth extra_headers[:params] = @parameters if method == "GET" aux.merge!({ headers: extra_headers }) aux end |
#run ⇒ Object
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/conexa/request.rb', line 35 def run enforce_read_only! response = RestClient::Request.execute request_params # A successful write may answer with no body at all: PATCH /charge/settle/:id # documents 204 + empty body as its success response, and # PATCH /contract/end/:id answers 200 with one. With the Oj adapter, # MultiJson.decode("") returns nil *without* raising ParseError, so the # nil has to be caught here rather than in a rescue. body = response.body.to_s return {} if body.strip.empty? decoded = MultiJson.decode(body) return {} if decoded.nil? # A top-level array (some list endpoints) has no #dig(String). return {data: decoded, pagination: nil} unless decoded.is_a?(Hash) {data: decoded["data"] || decoded, pagination: decoded["pagination"]} # Connection-level failures first. These subclass RestClient::Exception, so # listing them after it made them unreachable — Ruby matches rescue clauses # top-down. The broad clause then tried to decode their (nil) http_body and # raised NoMethodError instead of the documented ConnectionError. # # All of these carry no response, so there is nothing to classify: they are # failures to reach the API, not answers from it. Note that a real HTTP 408 # is RestClient::RequestTimeout, a *superclass* of Exceptions::Timeout, so # it correctly stays in the response taxonomy below. rescue SocketError, RestClient::ServerBrokeConnection, RestClient::SSLCertificateNotVerified, RestClient::Exceptions::Timeout => error raise Conexa::ConnectionError.new error rescue RestClient::Exception => error begin # nil for an error carrying no body; MultiJson.decode(nil) returns nil # rather than raising, so the guard has to be here. An error body that # decodes to an array or a scalar has no #[](String) either, and used # to raise TypeError from inside this handler. parsed_error = MultiJson.decode(error.http_body.to_s) parsed_error = {} unless parsed_error.is_a?(Hash) if error.is_a? RestClient::ResourceNotFound if parsed_error['message'] raise Conexa::NotFound.new(parsed_error, request_params, error) else raise Conexa::NotFound.new(nil, request_params, error) end else if parsed_error['message'] raise Conexa::ResponseError.new(request_params, error, describe_api_error(parsed_error), parsed_error) else raise Conexa::ValidationError.new parsed_error end end rescue MultiJson::ParseError raise Conexa::ResponseError.new(request_params, error) end rescue MultiJson::ParseError # Only genuinely malformed JSON reaches here — empty and null bodies are # handled above, for every status. raise Conexa::ResponseError.new(request_params, response) end |