Class: WellRested::API

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils

#objects_to_attributes

Methods included from GenericUtils

#class_exists?, #get_class

Methods included from WellRested

#logger

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

#clientObject

Returns the value of attribute client.



19
20
21
# File 'lib/well_rested/api.rb', line 19

def client
  @client
end

#default_path_parametersObject

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_responseObject (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

#passwordObject

Returns the value of attribute password.



17
18
19
# File 'lib/well_rested/api.rb', line 17

def password
  @password
end

#userObject

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_headersObject

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, options = {})
  default_options = { :json => true }
  opts = default_options.merge(options)

  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_headersObject

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