Class: WellRested::API
- Inherits:
-
Object
- Object
- WellRested::API
- Includes:
- WellRested, Utils
- Defined in:
- lib/well_rested/api.rb
Overview
All REST requests are made through an API object. API objects store cross-resource settings such as user and password (for HTTP basic auth).
Instance Attribute Summary collapse
-
#client ⇒ Object
Returns the value of attribute client.
-
#default_path_parameters ⇒ Object
Returns the value of attribute default_path_parameters.
-
#last_response ⇒ Object
readonly
Returns the value of attribute last_response.
-
#password ⇒ Object
Returns the value of attribute password.
-
#user ⇒ Object
Returns the value of attribute user.
Class Method Summary collapse
-
.fill_path(path_template, params) ⇒ Object
TODO: Move this into a utility module? It can then be called from Base#fill_path or directly if needed.
-
.request_headers ⇒ Object
Return the default headers sent with all HTTP requests.
Instance Method Summary collapse
-
#create(klass, attributes = {}, url = nil) ⇒ Object
Create the resource of klass from the given attributes.
-
#delete(klass_or_object, path_params_or_url = {}) ⇒ Object
DELETE a resource.
-
#find(klass, path_params_or_url = {}, query_params = {}) ⇒ Object
GET a single resource.
-
#find_many(klass, path_params_or_url = {}, query_params = {}) ⇒ Object
GET a collection of resources.
-
#get(url, json = true) ⇒ Object
Issue a GET request to the given url.
-
#initialize(path_params = {}) ⇒ API
constructor
A new instance of API.
-
#post(url, payload, json = true) ⇒ Object
Issue a POST request to the given url.
-
#put(url, payload, options = {}) ⇒ Object
Issue a PUT request to the given url.
-
#request(klass, method, path, payload_hash = {}, headers = {}) ⇒ Object
Issue a request of method ‘method’ (:get, :put, :post, :delete) for the resource identified by ‘klass’.
-
#request_headers ⇒ Object
Convenience method.
-
#save(resource, url = nil) ⇒ Object
Save a resource.
-
#url_for(klass, path_params_or_url = {}, query_params = {}) ⇒ Object
Generate a full URL for the class klass with the given path_params and query_params In the case of an update, path params will usually be resource.attributes_for_api.
Methods included from Utils
Methods included from GenericUtils
Methods included from WellRested
Constructor Details
#initialize(path_params = {}) ⇒ API
Returns a new instance of API.
22 23 24 25 |
# File 'lib/well_rested/api.rb', line 22 def initialize(path_params = {}) self.default_path_parameters = path_params.with_indifferent_access self.client = RestClient end |
Instance Attribute Details
#client ⇒ Object
Returns the value of attribute client.
19 20 21 |
# File 'lib/well_rested/api.rb', line 19 def client @client end |
#default_path_parameters ⇒ Object
Returns the value of attribute default_path_parameters.
18 19 20 |
# File 'lib/well_rested/api.rb', line 18 def default_path_parameters @default_path_parameters end |
#last_response ⇒ Object (readonly)
Returns the value of attribute last_response.
20 21 22 |
# File 'lib/well_rested/api.rb', line 20 def last_response @last_response end |
#password ⇒ Object
Returns the value of attribute password.
17 18 19 |
# File 'lib/well_rested/api.rb', line 17 def password @password end |
#user ⇒ Object
Returns the value of attribute user.
16 17 18 |
# File 'lib/well_rested/api.rb', line 16 def user @user end |
Class Method Details
.fill_path(path_template, params) ⇒ Object
TODO: Move this into a utility module? It can then be called from Base#fill_path or directly if needed.
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/well_rested/api.rb', line 252 def self.fill_path(path_template, params) raise "Cannot fill nil path" if path_template.nil? params = params.with_indifferent_access # substitute marked params path = path_template.gsub(/\:\w+/) do |match| sym = match[1..-1].to_sym val = params.include?(sym) ? params[sym] : match raise ArgumentError.new "Blank parameter #{sym} in path #{path}!" if val.blank? val end # Raise an error if we have un-filled parameters if path.match(/(\:\w+)/) raise ArgumentError.new "Unfilled parameter in path: #{$1} (path: #{path} params: #{params.inspect})" end # ID goes on the end of the resource path but isn't spec'd there path += "/#{params[:id]}" unless params[:id].blank? path end |
.request_headers ⇒ Object
Return the default headers sent with all HTTP requests.
246 247 248 249 |
# File 'lib/well_rested/api.rb', line 246 def self.request_headers # Accept necessary for fetching results by result ID, but not in most places. { :content_type => 'application/json', :accept => 'application/json' } end |
Instance Method Details
#create(klass, attributes = {}, url = nil) ⇒ Object
Create the resource of klass from the given attributes. The class will be instantiated, and its new_from_api and attributes_for_api methods will be used to determine which attributes actually get sent. If url is specified, it overrides the default url.
120 121 122 123 124 |
# File 'lib/well_rested/api.rb', line 120 def create(klass, attributes = {}, url = nil) obj = klass.new(default_path_parameters.merge(attributes)) create_or_update_resource(obj, url) end |
#delete(klass_or_object, path_params_or_url = {}) ⇒ Object
DELETE a resource. There are two main ways to call delete. 1) The first argument is a class, and the second argument is an array of path_params that resolve to a path to the resource to delete.
(e.g. for klass Post with path '/users/:user_id/posts', :user_id and :id would be required in path_params_or_url to delete /users/x/posts/y)
2) The first argument can be an object to delete. It should include all of the path params in its attributes.
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/well_rested/api.rb', line 142 def delete(klass_or_object, path_params_or_url = {}) if klass_or_object.respond_to?(:attributes_for_api) # klass_or_object is an object klass = klass_or_object.class if path_params_or_url.kind_of?(String) url = url_for(klass, path_params_or_url) else params = default_path_parameters.merge(klass_or_object.attributes_for_api) url = url_for(klass, params) end else # klass_or_object is a class klass = klass_or_object #logger.debug "Calling delete with class #{klass.name} and params: #{path_params.inspect}" if path_params_or_url.kind_of?(String) url = url_for(klass, path_params_or_url) else params = default_path_parameters.merge(path_params_or_url) url = url_for(klass, params) end end #logger.info "DELETE #{url}" response = client.delete(url, request_headers) do |response, request, result, &block| @last_response = response response.return!(request, result, &block) end end |
#find(klass, path_params_or_url = {}, query_params = {}) ⇒ Object
GET a single resource. ‘klass’ is a class that descends from WellRested::Base ‘path_params_or_url’ is either a url string or a hash of params to substitute into the url pattern specified in klass.path
e.g. if klass.path is '/accounts/:account_id/users', then the path_params hash should include 'account_id'
‘query_params’ is an optional hash of query parameters
If path_params includes ‘id’, it will be added to the end of the path (e.g. /accounts/1/users/1) If path_params_or_url is a hash, query_params will be added on the end (e.g. { :option => ‘x’ }) produces a url with ?option=x If it is a string, query_params is ignored.
Returns an object of class klass representing that resource. If the resource is not found, raises a RestClient::ResourceNotFound exception.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/well_rested/api.rb', line 72 def find(klass, path_params_or_url = {}, query_params = {}) if klass.respond_to?(:path_parameters) path_params_or_url = klass.path_parameters klass = klass.class end url = url_for(klass, path_params_or_url, query_params) #logger.info "GET #{url}" response = client.get(url, request_headers) do |response, request, result, &block| @last_response = response response.return!(request, result, &block) # default RestClient response handling (raise exceptions on errors, etc.) end raise "Invalid body formatter for #{klass.name}!" if klass.body_formatter.nil? or !klass.body_formatter.respond_to?(:decode) hash = klass.body_formatter.decode(response) decoded_hash = klass.attribute_formatter.nil? ? hash : klass.attribute_formatter.decode(hash) klass.new_from_api(decoded_hash) end |
#find_many(klass, path_params_or_url = {}, query_params = {}) ⇒ Object
GET a collection of resources. This works the same as find, except it expects and returns an array of resources instead of a single resource.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/well_rested/api.rb', line 96 def find_many(klass, path_params_or_url = {}, query_params = {}) url = url_for(klass, path_params_or_url, query_params) logger.info "GET #{url}" response = client.get(url, request_headers) do |response, request, result, &block| @last_response = response response.return!(request, result, &block) end raise "Invalid body formatter for #{klass.name}!" if klass.body_formatter.nil? or !klass.body_formatter.respond_to?(:decode) array = klass.body_formatter.decode(response) processed_array = klass.attribute_formatter.nil? ? array : klass.attribute_formatter.decode(array) raise "Response did not parse to an array" unless array.is_a?(Array) processed_array.map { |e| klass.new_from_api(e) } end |
#get(url, json = true) ⇒ Object
Issue a GET request to the given url. If json is passed as true, it will be interpreted as JSON and converted into a hash / array of hashes. Otherwise, the body is returned as a string. TODO: Same issue as with put. def get(url, body_formatter, attribute_formatter) ?
207 208 209 210 211 212 |
# File 'lib/well_rested/api.rb', line 207 def get(url, json = true) response = client.get(url, request_headers) return response unless json parsed = JSON.parse(response) KeyTransformer.underscore_keys(parsed) end |
#post(url, payload, json = true) ⇒ Object
Issue a POST request to the given url. The post body is specified by ‘payload’, which can either be a string, an object, a hash, or an array of hashes. If it is not a string, it will be recurisvely converted into JSON using any objects’ attributes_for_api methods. TODO: Same issue as with put, get, etc.
195 196 197 198 199 200 |
# File 'lib/well_rested/api.rb', line 195 def post(url, payload, json = true) response = client.post(url, payload, request_headers) return response unless json parsed = JSON.parse(response) KeyTransformer.underscore_keys(parsed) end |
#put(url, payload, options = {}) ⇒ Object
Issue a PUT request to the given url. The post body is specified by ‘payload’, which can either be a string, an object, a hash, or an array of hashes. If it is not a string, it will be recurisvely converted into JSON using any objects’ attributes_for_api methods. TODO: Update this to do something that makes more sense with the formatters. e.g. def put(url, payload, formatter)
175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/well_rested/api.rb', line 175 def put(url, payload, = {}) = { :json => true } opts = .merge() payload = payload.kind_of?(String) ? payload : KeyTransformer.camelize_keys(objects_to_attributes(payload)).to_json response = run_update(:put, url, payload) if opts[:json] and !response.blank? objs = JSON.parse(response) return KeyTransformer.underscore_keys(objs) end return response end |
#request(klass, method, path, payload_hash = {}, headers = {}) ⇒ Object
Issue a request of method ‘method’ (:get, :put, :post, :delete) for the resource identified by ‘klass’. If it is a PUT or a POST, the payload_hash should be specified.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/well_rested/api.rb', line 30 def request(klass, method, path, payload_hash = {}, headers = {}) auth = (self.user or self.password) ? "#{CGI.escape(user)}:#{CGI.escape(password)}@" : '' # If path starts with a slash, assume it is relative to the default server. if path[0..0] == '/' url = "#{klass.protocol}://#{auth}#{klass.server}#{path}" else # Otherwise, treat it as a fully qualified URL and do not modify it. url = path end hash = klass.attribute_formatter.encode(payload_hash) payload = klass.body_formatter.encode(hash) #logger.info "#{method.to_s.upcase} #{url} (#{payload.inspect})" if [:put, :post].include?(method) # RestClient.put and .post take an extra payload argument. client.send(method, url, payload, request_headers.merge(headers)) do |response, request, result, &block| @last_response = response response.return!(request, result, &block) end else client.send(method, url, request_headers.merge(headers)) do |response, request, result, &block| @last_response = response response.return!(request, result, &block) end end end |
#request_headers ⇒ Object
Convenience method. Also allows request_headers to be can be set on a per-instance basis.
241 242 243 |
# File 'lib/well_rested/api.rb', line 241 def request_headers self.class.request_headers end |
#save(resource, url = nil) ⇒ Object
Save a resource. Return false if doesn’t pass validation. If the update succeeds, return the resource. Otherwise, return a hash containing whatever the server returned (usually includes an array of errors).
130 131 132 133 134 135 |
# File 'lib/well_rested/api.rb', line 130 def save(resource, url = nil) # convert any hashes that should be objects into objects before saving, # so that we can use their attributes_for_api methods in case they need to override what gets sent resource.convert_attributes_to_objects create_or_update_resource(resource, url) end |
#url_for(klass, path_params_or_url = {}, query_params = {}) ⇒ Object
Generate a full URL for the class klass with the given path_params and query_params In the case of an update, path params will usually be resource.attributes_for_api. In the case of a find(many), query_params might be count, start, etc.
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/well_rested/api.rb', line 217 def url_for(klass, path_params_or_url = {}, query_params = {}) # CONSIDERATION: Defaults should be settable at the global level on the @api object. # They should be overrideable at the class-level (e.g. User) and again at the time of the method call. # url_for is currently not overrideable at the class level. auth = (self.user or self.password) ? "#{CGI.escape(user)}:#{CGI.escape(password)}@" : '' if path_params_or_url.kind_of?(String) # if it starts with a slash, we assume its part of a if path_params_or_url[0..0] == '/' url = "#{klass.protocol}://#{auth}#{klass.server}#{path_params_or_url}#{klass.extension}" else # if not, we treat it as fully qualified and do not modify it url = path_params_or_url end else path = self.class.fill_path(klass.path, default_path_parameters.merge(path_params_or_url).with_indifferent_access) url = "#{klass.protocol}://#{auth}#{klass.server}#{path}" end url += '?' + klass.attribute_formatter.encode(query_params).to_query unless query_params.empty? url end |