well_rested-core 0.0.3.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/lib/well_rested/api.rb +365 -0
- data/lib/well_rested/base.rb +291 -0
- data/lib/well_rested/core_formatter.rb +16 -0
- data/lib/well_rested-core/version.rb +5 -0
- data/lib/well_rested-core.rb +34 -0
- data/well_rested-core.gemspec +20 -0
- metadata +73 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Christian Rohrer
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# WellRested::Core
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'well_rested-core'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install well_rested-core
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,365 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'json'
|
3
|
+
require 'active_support/core_ext/object/to_query'
|
4
|
+
require 'key_transformer'
|
5
|
+
require 'cgi'
|
6
|
+
|
7
|
+
require 'well_rested/utils'
|
8
|
+
|
9
|
+
module WellRested
|
10
|
+
# All REST requests are made through an API object.
|
11
|
+
# API objects store cross-resource settings such as user and password (for HTTP basic auth).
|
12
|
+
class API
|
13
|
+
include WellRested # for logger
|
14
|
+
include WellRested::Utils
|
15
|
+
|
16
|
+
attr_accessor :user
|
17
|
+
attr_accessor :password
|
18
|
+
attr_accessor :default_path_parameters
|
19
|
+
attr_accessor :client
|
20
|
+
attr_reader :last_response
|
21
|
+
attr_accessor :unique_id
|
22
|
+
attr_accessor :version
|
23
|
+
|
24
|
+
def initialize(path_params = {}, session_params = {}, version = "")
|
25
|
+
self.default_path_parameters = path_params.with_indifferent_access
|
26
|
+
self.client = RestClient
|
27
|
+
self.unique_id = session_params.try(:uid) || 'unauthorized'
|
28
|
+
self.version = version
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Issue a request of method 'method' (:get, :put, :post, :delete) for the resource identified by 'klass'.
|
33
|
+
# If it is a PUT or a POST, the payload_hash should be specified.
|
34
|
+
def request(klass, method, path, payload_hash = {}, headers = {})
|
35
|
+
auth = (self.user or self.password) ? "#{CGI.escape(user)}:#{CGI.escape(password)}@" : ''
|
36
|
+
|
37
|
+
# If path starts with a slash, assume it is relative to the default server.
|
38
|
+
if path[0..0] == '/'
|
39
|
+
url = "#{klass.protocol}://#{auth}#{klass.server}#{path}"
|
40
|
+
else
|
41
|
+
# Otherwise, treat it as a fully qualified URL and do not modify it.
|
42
|
+
url = path
|
43
|
+
end
|
44
|
+
|
45
|
+
hash = klass.attribute_formatter.encode(payload_hash)
|
46
|
+
payload = klass.body_formatter.encode(hash)
|
47
|
+
|
48
|
+
#logger.info "#{method.to_s.upcase} #{url} (#{payload.inspect})"
|
49
|
+
|
50
|
+
if [:put, :post].include?(method) # RestClient.put and .post take an extra payload argument.
|
51
|
+
client.send(method, url, payload, request_headers.merge(headers)) do |response, request, result, &block|
|
52
|
+
@last_response = response
|
53
|
+
response.return!(request, result, &block)
|
54
|
+
end
|
55
|
+
else
|
56
|
+
client.send(method, url, request_headers.merge(headers)) do |response, request, result, &block|
|
57
|
+
@last_response = response
|
58
|
+
response.return!(request, result, &block)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# GET a single resource.
|
65
|
+
# 'klass' is a class that descends from WellRested::Base
|
66
|
+
# 'path_params_or_url' is either a url string or a hash of params to substitute into the url pattern specified in klass.path
|
67
|
+
# e.g. if klass.path is '/accounts/:account_id/users', then the path_params hash should include 'account_id'
|
68
|
+
# 'query_params' is an optional hash of query parameters
|
69
|
+
#
|
70
|
+
# If path_params includes 'id', it will be added to the end of the path (e.g. /accounts/1/users/1)
|
71
|
+
# 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
|
72
|
+
# If it is a string, query_params is ignored.
|
73
|
+
#
|
74
|
+
# Returns an object of class klass representing that resource.
|
75
|
+
# If the resource is not found, raises a RestClient::ResourceNotFound exception.
|
76
|
+
def find(klass, path_params_or_url = {}, query_params = {})
|
77
|
+
if klass.respond_to?(:path_parameters)
|
78
|
+
path_params_or_url = klass.path_parameters
|
79
|
+
klass = klass.class
|
80
|
+
end
|
81
|
+
|
82
|
+
url = url_for(klass, path_params_or_url, query_params)
|
83
|
+
#logger.info "GET #{url}"
|
84
|
+
|
85
|
+
response = client.get(url, request_headers) do |response, request, result, &block|
|
86
|
+
@last_response = response
|
87
|
+
response.return!(request, result, &block) # default RestClient response handling (raise exceptions on errors, etc.)
|
88
|
+
end
|
89
|
+
|
90
|
+
raise "Invalid body formatter for #{klass.name}!" if klass.body_formatter.nil? or !klass.body_formatter.respond_to?(:decode)
|
91
|
+
|
92
|
+
hash = klass.body_formatter.decode(response)
|
93
|
+
decoded_hash = klass.attribute_formatter.nil? ? hash : klass.attribute_formatter.decode(hash)
|
94
|
+
klass.new_from_api(decoded_hash)
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# GET a collection of resources.
|
99
|
+
# This works the same as find, except it expects and returns an array of resources instead of a single resource.
|
100
|
+
def find_many(klass, path_params_or_url = {}, query_params = {})
|
101
|
+
url = url_for(klass, path_params_or_url, query_params)
|
102
|
+
|
103
|
+
logger.info "GET #{url}"
|
104
|
+
# response = client.get(url, request_headers) do |response, request, result, &block|
|
105
|
+
response = client.get(url, request_headers) do |response, request, result, &block|
|
106
|
+
@last_response = response
|
107
|
+
response.return!(request, result, &block)
|
108
|
+
end
|
109
|
+
|
110
|
+
raise "Invalid body formatter for #{klass.name}!" if klass.body_formatter.nil? or !klass.body_formatter.respond_to?(:decode)
|
111
|
+
array = klass.body_formatter.decode(response)
|
112
|
+
|
113
|
+
processed_array = klass.attribute_formatter.nil? ? array : klass.attribute_formatter.decode(array)
|
114
|
+
|
115
|
+
raise "Response did not parse to an array" unless array.is_a?(Array)
|
116
|
+
|
117
|
+
processed_array.map { |e| klass.new_from_api(e) }
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Create the resource of klass from the given attributes.
|
122
|
+
# The class will be instantiated, and its new_from_api and attributes_for_api methods
|
123
|
+
# will be used to determine which attributes actually get sent.
|
124
|
+
# If url is specified, it overrides the default url.
|
125
|
+
def create(klass, attributes = {}, url = nil)
|
126
|
+
obj = klass.new(default_path_parameters.merge(attributes))
|
127
|
+
|
128
|
+
create_or_update_resource(obj, url)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Save a resource.
|
132
|
+
# Return false if doesn't pass validation.
|
133
|
+
# If the update succeeds, return the resource.
|
134
|
+
# Otherwise, return a hash containing whatever the server returned (usually includes an array of errors).
|
135
|
+
def save(resource, url = nil)
|
136
|
+
# convert any hashes that should be objects into objects before saving,
|
137
|
+
# so that we can use their attributes_for_api methods in case they need to override what gets sent
|
138
|
+
resource.convert_attributes_to_objects
|
139
|
+
create_or_update_resource(resource, url)
|
140
|
+
end
|
141
|
+
|
142
|
+
# DELETE a resource.
|
143
|
+
# There are two main ways to call delete.
|
144
|
+
# 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.
|
145
|
+
# (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)
|
146
|
+
# 2) The first argument can be an object to delete. It should include all of the path params in its attributes.
|
147
|
+
def delete(klass_or_object, path_params_or_url = {})
|
148
|
+
if klass_or_object.respond_to?(:attributes_for_api) # klass_or_object is an object
|
149
|
+
klass = klass_or_object.class
|
150
|
+
if path_params_or_url.kind_of?(String)
|
151
|
+
url = url_for(klass, path_params_or_url)
|
152
|
+
else
|
153
|
+
params = default_path_parameters.merge(klass_or_object.attributes_for_api)
|
154
|
+
url = url_for(klass, params)
|
155
|
+
end
|
156
|
+
else # klass_or_object is a class
|
157
|
+
klass = klass_or_object
|
158
|
+
#logger.debug "Calling delete with class #{klass.name} and params: #{path_params.inspect}"
|
159
|
+
if path_params_or_url.kind_of?(String)
|
160
|
+
url = url_for(klass, path_params_or_url)
|
161
|
+
else
|
162
|
+
params = default_path_parameters.merge(path_params_or_url)
|
163
|
+
url = url_for(klass, params)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
#logger.info "DELETE #{url}"
|
168
|
+
response = client.delete(url, request_headers) do |response, request, result, &block|
|
169
|
+
@last_response = response
|
170
|
+
response.return!(request, result, &block)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Issue a PUT request to the given url.
|
176
|
+
# The post body is specified by 'payload', which can either be a string, an object, a hash, or an array of hashes.
|
177
|
+
# If it is not a string, it will be recurisvely converted into JSON using any objects' attributes_for_api methods.
|
178
|
+
# TODO: Update this to do something that makes more sense with the formatters.
|
179
|
+
# e.g. def put(url, payload, formatter)
|
180
|
+
def put(url, payload, options = {})
|
181
|
+
default_options = { :json => true }
|
182
|
+
opts = default_options.merge(options)
|
183
|
+
|
184
|
+
payload = payload.kind_of?(String) ? payload : KeyTransformer.camelize_keys(objects_to_attributes(payload)).to_json
|
185
|
+
response = run_update(:put, url, payload)
|
186
|
+
|
187
|
+
if opts[:json] and !response.blank?
|
188
|
+
objs = JSON.parse(response)
|
189
|
+
return KeyTransformer.underscore_keys(objs)
|
190
|
+
end
|
191
|
+
|
192
|
+
return response
|
193
|
+
end
|
194
|
+
|
195
|
+
##
|
196
|
+
# Issue a POST request to the given url.
|
197
|
+
# The post body is specified by 'payload', which can either be a string, an object, a hash, or an array of hashes.
|
198
|
+
# If it is not a string, it will be recurisvely converted into JSON using any objects' attributes_for_api methods.
|
199
|
+
# TODO: Same issue as with put, get, etc.
|
200
|
+
def post(url, payload, json = true)
|
201
|
+
response = client.post(url, payload, request_headers)
|
202
|
+
return response unless json
|
203
|
+
parsed = JSON.parse(response)
|
204
|
+
KeyTransformer.underscore_keys(parsed)
|
205
|
+
end
|
206
|
+
|
207
|
+
##
|
208
|
+
# Issue a GET request to the given url.
|
209
|
+
# If json is passed as true, it will be interpreted as JSON and converted into a hash / array of hashes.
|
210
|
+
# Otherwise, the body is returned as a string.
|
211
|
+
# TODO: Same issue as with put. def get(url, body_formatter, attribute_formatter) ?
|
212
|
+
def get(url, json = true)
|
213
|
+
response = client.get(url, request_headers)
|
214
|
+
return response unless json
|
215
|
+
parsed = JSON.parse(response)
|
216
|
+
KeyTransformer.underscore_keys(parsed)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Generate a full URL for the class klass with the given path_params and query_params
|
220
|
+
# In the case of an update, path params will usually be resource.attributes_for_api.
|
221
|
+
# In the case of a find(many), query_params might be count, start, etc.
|
222
|
+
def url_for(klass, path_params_or_url = {}, query_params = {})
|
223
|
+
# CONSIDERATION: Defaults should be settable at the global level on the @api object.
|
224
|
+
# They should be overrideable at the class-level (e.g. User) and again at the time of the method call.
|
225
|
+
# url_for is currently not overrideable at the class level.
|
226
|
+
|
227
|
+
auth = (self.user or self.password) ? "#{CGI.escape(user)}:#{CGI.escape(password)}@" : ''
|
228
|
+
|
229
|
+
if path_params_or_url.kind_of?(String)
|
230
|
+
# if it starts with a slash, we assume its part of a
|
231
|
+
if path_params_or_url[0..0] == '/'
|
232
|
+
url = "#{klass.protocol}://#{auth}#{klass.server}#{path_params_or_url}#{klass.extension}"
|
233
|
+
else
|
234
|
+
# if not, we treat it as fully qualified and do not modify it
|
235
|
+
url = path_params_or_url
|
236
|
+
end
|
237
|
+
else
|
238
|
+
path = self.class.fill_path(klass.path, default_path_parameters.merge(path_params_or_url).with_indifferent_access)
|
239
|
+
url = "#{klass.protocol}://#{auth}#{klass.server}#{path}"
|
240
|
+
end
|
241
|
+
url += '?' + klass.attribute_formatter.encode(query_params).to_query unless query_params.empty?
|
242
|
+
url
|
243
|
+
end
|
244
|
+
|
245
|
+
# Convenience method. Also allows request_headers to be can be set on a per-instance basis.
|
246
|
+
def request_headers
|
247
|
+
self.class.request_headers(self.unique_id, self.version)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Return the default headers sent with all HTTP requests.
|
251
|
+
def self.request_headers(unique_id, version)
|
252
|
+
# Accept necessary for fetching results by result ID, but not in most places.
|
253
|
+
{
|
254
|
+
:content_type => 'application/json',
|
255
|
+
:accept => 'application/json',
|
256
|
+
"Authentication" => unique_id,
|
257
|
+
:version => version
|
258
|
+
}
|
259
|
+
end
|
260
|
+
|
261
|
+
# # 20120627 CR: Add authentication to header
|
262
|
+
# def request_headers_with_authentication(authentication_id)
|
263
|
+
# self.class.request_headers_with_authentication(authentication_id)
|
264
|
+
# end
|
265
|
+
|
266
|
+
# # Return the default headers sent with all HTTP requests.
|
267
|
+
# def self.request_headers_with_authentication(authentication_id)
|
268
|
+
# # Accept necessary for fetching results by result ID, but not in most places.
|
269
|
+
# { :content_type => 'application/json', :accept => 'application/json', :Authentication => authentication_id}
|
270
|
+
# end
|
271
|
+
|
272
|
+
|
273
|
+
# TODO: Move this into a utility module? It can then be called from Base#fill_path or directly if needed.
|
274
|
+
def self.fill_path(path_template, params)
|
275
|
+
raise "Cannot fill nil path" if path_template.nil?
|
276
|
+
|
277
|
+
params = params.with_indifferent_access
|
278
|
+
|
279
|
+
# substitute marked params
|
280
|
+
path = path_template.gsub(/\:\w+/) do |match|
|
281
|
+
sym = match[1..-1].to_sym
|
282
|
+
val = params.include?(sym) ? params[sym] : match
|
283
|
+
raise ArgumentError.new "Blank parameter #{sym} in path #{path}!" if val.blank?
|
284
|
+
val
|
285
|
+
end
|
286
|
+
|
287
|
+
# Raise an error if we have un-filled parameters
|
288
|
+
if path.match(/(\:\w+)/)
|
289
|
+
raise ArgumentError.new "Unfilled parameter in path: #{$1} (path: #{path} params: #{params.inspect})"
|
290
|
+
end
|
291
|
+
|
292
|
+
# ID goes on the end of the resource path but isn't spec'd there
|
293
|
+
path += "/#{params[:id]}" unless params[:id].blank?
|
294
|
+
|
295
|
+
path
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
protected # internal methods follow
|
300
|
+
|
301
|
+
# Create or update a resource.
|
302
|
+
# If an ID is set, PUT will be used, else POST.
|
303
|
+
# If a 200 is returned, the returned attributes will be loaded into the resource, and the resource returned.
|
304
|
+
# Otherwise, the resource will not be modified, and a hash generated from the JSON response will be returned.
|
305
|
+
def create_or_update_resource(resource, url = nil)
|
306
|
+
return false unless resource.valid?
|
307
|
+
|
308
|
+
#logger.info "Creating a #{resource.class}"
|
309
|
+
|
310
|
+
path_params = default_path_parameters.merge(resource.path_parameters)
|
311
|
+
payload_hash = resource.class.attribute_formatter.encode(resource.attributes_for_api)
|
312
|
+
payload = resource.class.body_formatter.encode(payload_hash)
|
313
|
+
|
314
|
+
#logger.debug " payload: #{payload.inspect}"
|
315
|
+
|
316
|
+
if url.nil?
|
317
|
+
url = url_for(resource.class, path_params) # allow default URL to be overriden by url argument
|
318
|
+
else
|
319
|
+
url = url_for(resource.class, url)
|
320
|
+
end
|
321
|
+
|
322
|
+
# If ID is set in path parameters, do a PUT. Otherwise, do a POST.
|
323
|
+
method = resource.path_parameters[:key].blank? ? :post : :put
|
324
|
+
|
325
|
+
response = run_update(method, url, payload)
|
326
|
+
|
327
|
+
hash = resource.class.body_formatter.decode(response.body)
|
328
|
+
decoded_hash = resource.class.attribute_formatter.decode(hash)
|
329
|
+
logger.info "* Errors: #{decoded_hash['errors'].inspect}" if decoded_hash.include?('errors')
|
330
|
+
|
331
|
+
if response.code.between?(200,299)
|
332
|
+
# If save succeeds, replace resource's attributes with the ones returned.
|
333
|
+
return decoded_hash.map { |hash| resource.class.new_from_api(hash) } if decoded_hash.kind_of?(Array)
|
334
|
+
resource.load_from_api(decoded_hash)
|
335
|
+
return resource
|
336
|
+
elsif decoded_hash.include?('errors')
|
337
|
+
resource.handle_errors(decoded_hash['errors'])
|
338
|
+
return false
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def run_update(method, url, payload)
|
343
|
+
logger.debug "#{method.to_s.upcase} #{url} "
|
344
|
+
logger.debug " payload: #{payload.inspect}"
|
345
|
+
|
346
|
+
http_status = nil
|
347
|
+
client.send(method, url, payload, request_headers) do |response, request, result, &block|
|
348
|
+
@last_response = response
|
349
|
+
|
350
|
+
http_status = response.code
|
351
|
+
case response.code
|
352
|
+
when 400
|
353
|
+
#logger.debug "Got 400: #{response.inspect}"
|
354
|
+
response.return!(request, result, &block)
|
355
|
+
when 422
|
356
|
+
#logger.debug "Got 422: errors should be set"
|
357
|
+
response
|
358
|
+
else
|
359
|
+
# default handling (raise exceptions on errors, etc.)
|
360
|
+
response.return!(request, result, &block)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'active_model'
|
4
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
5
|
+
require 'active_support/core_ext/class/attribute'
|
6
|
+
require 'active_support/core_ext/string/inflections'
|
7
|
+
|
8
|
+
require 'well_rested/json_formatter'
|
9
|
+
require 'well_rested/camel_case_formatter'
|
10
|
+
require 'well_rested/utils'
|
11
|
+
|
12
|
+
module WellRested
|
13
|
+
class Base
|
14
|
+
include Utils
|
15
|
+
include WellRested::Utils
|
16
|
+
|
17
|
+
include ActiveModel::Validations
|
18
|
+
include ActiveModel::Serializers::JSON
|
19
|
+
|
20
|
+
class_attribute :protocol
|
21
|
+
class_attribute :server
|
22
|
+
class_attribute :path
|
23
|
+
class_attribute :schema
|
24
|
+
|
25
|
+
class_attribute :body_formatter
|
26
|
+
class_attribute :attribute_formatter
|
27
|
+
class_attribute :extension # e.g. .json, .xml
|
28
|
+
|
29
|
+
# class-level defaults
|
30
|
+
self.protocol = 'http'
|
31
|
+
# a body formatter must respond to the methods encode(hash_or_array) => string and decode(string) => hash_or_array
|
32
|
+
self.body_formatter = JSONFormatter.new
|
33
|
+
self.extension = ''
|
34
|
+
# an attribute formatter must respond to encode(attribute_name_string) => string and decode(attribute_name_string) => string
|
35
|
+
self.attribute_formatter = CamelCaseFormatter.new
|
36
|
+
|
37
|
+
attr_reader :attributes
|
38
|
+
attr_accessor :new_record
|
39
|
+
|
40
|
+
##
|
41
|
+
# Define the schema for this resource.
|
42
|
+
#
|
43
|
+
# Either takes an array, or a list of arguments which we treat as an array.
|
44
|
+
# Each element of the array should be either a symbol or a hash.
|
45
|
+
# If it's a symbol, we create an attribute using the symol as the name and with a null default value.
|
46
|
+
# If it's a hash, we use the keys as attribute names.
|
47
|
+
# - Any values that are hashes, we use to specify further options (currently, the only option is :default).
|
48
|
+
# - Any value that is not a hash is treated as a default.
|
49
|
+
# e.g.
|
50
|
+
# define_schema :x, :y, :z # x, y, and z all default to nil
|
51
|
+
# define_schema :id, :name => 'John' # id defaults to nil, name defaults to 'John'
|
52
|
+
# define_schema :id, :name => { :default => 'John' } # same as above
|
53
|
+
def self.define_schema(*args)
|
54
|
+
return schema if args.empty?
|
55
|
+
|
56
|
+
attrs = args.first.is_a?(Array) ? args.first : args
|
57
|
+
self.schema = {}.with_indifferent_access
|
58
|
+
attrs.each do |attr|
|
59
|
+
if attr.is_a?(Symbol)
|
60
|
+
self.schema[attr] = { :default => nil }
|
61
|
+
elsif attr.is_a?(Hash)
|
62
|
+
attr.each do |k,v|
|
63
|
+
if v.is_a?(Hash)
|
64
|
+
self.schema[k] = v
|
65
|
+
else
|
66
|
+
self.schema[k] = { :default => v }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
=begin
|
73
|
+
# Possible alternative to using method_missing:
|
74
|
+
# define getter/setter methods for attributes.
|
75
|
+
@attributes.keys.each do |attr_name|
|
76
|
+
define_method(attr_name) { @attributes[attr_name] }
|
77
|
+
define_method("#{attr_name}=") { |val| @attributes[attr_name] = val }
|
78
|
+
end
|
79
|
+
=end
|
80
|
+
|
81
|
+
self.schema
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize(attrs = {})
|
85
|
+
raise "Attrs must be hash" unless attrs.is_a? Hash
|
86
|
+
|
87
|
+
self.load(attrs, false)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Define an actual method for ID. This is important in Ruby 1.8 where the object_id method is also aliased to id.
|
91
|
+
def id
|
92
|
+
attributes[:id]
|
93
|
+
end
|
94
|
+
|
95
|
+
# borrowed from http://stackoverflow.com/questions/2393697/look-up-all-descendants-of-a-class-in-ruby
|
96
|
+
# Show all subclasses
|
97
|
+
def self.descendants
|
98
|
+
ObjectSpace.each_object(::Class).select { |klass| klass < self }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Create a map of all descendants of Base to lookup classes from names when converting hashes to objects.
|
102
|
+
def self.descendant_map
|
103
|
+
return @descendant_map if @descendant_map
|
104
|
+
@descendant_map = {}.with_indifferent_access
|
105
|
+
self.descendants.each do |des|
|
106
|
+
unless des.name.blank?
|
107
|
+
sep_index = des.name.rindex('::')
|
108
|
+
short_name = sep_index ? des.name[sep_index+2..-1] : des.name
|
109
|
+
@descendant_map[short_name] = des
|
110
|
+
end
|
111
|
+
end
|
112
|
+
@descendant_map
|
113
|
+
end
|
114
|
+
|
115
|
+
# Convenience method for creating an object and calling load_from_api
|
116
|
+
# The API should call this method when creating representations of objects that are already persisted.
|
117
|
+
#
|
118
|
+
# By default, attributes loaded from the API have new_record set to true. This has implications for Rails form handling.
|
119
|
+
# (Rails uses POST for records that it thinks are new, but PUT for records that it thinks are already persisted.)
|
120
|
+
def self.new_from_api(attrs)
|
121
|
+
obj = self.new
|
122
|
+
obj.load_from_api(attrs)
|
123
|
+
return obj
|
124
|
+
end
|
125
|
+
|
126
|
+
# Load this resource from attributes. If these attributes were received from the API, true should be passed for from_api.
|
127
|
+
# This will ensure any object-specific loading behavior is respected.
|
128
|
+
# example:
|
129
|
+
# res = Resource.new
|
130
|
+
# res.load(:name => 'New')
|
131
|
+
def load(attrs_to_load, from_api = false)
|
132
|
+
raise "Attrs is not a hash: #{attrs_to_load.inspect}" unless attrs_to_load.kind_of? Hash
|
133
|
+
|
134
|
+
#puts "*** Warning: loading a resource without a schema (#{self.class})!" if schema.nil?
|
135
|
+
#raise "Tried to load attributes for a resource with no schema (#{self.class})!" if schema.nil?
|
136
|
+
|
137
|
+
# We mark a record as new if it doesn't come from the API and it doesn't have an ID.
|
138
|
+
self.new_record = !from_api
|
139
|
+
self.new_record = false if attrs_to_load.include?(:id) or attrs_to_load.include?('id')
|
140
|
+
|
141
|
+
new_attrs = {}.with_indifferent_access
|
142
|
+
|
143
|
+
# Take default values from schema, but allow arbitrary args to be loaded.
|
144
|
+
# We will address the security issue by filtering in attributes_for_api.
|
145
|
+
schema.each { |key, opts| new_attrs[key] = opts[:default] } unless schema.blank?
|
146
|
+
new_attrs.merge!(attrs_to_load)
|
147
|
+
|
148
|
+
@attributes = self.class.hash_to_objects(new_attrs, from_api).with_indifferent_access
|
149
|
+
|
150
|
+
return self
|
151
|
+
end
|
152
|
+
|
153
|
+
# Load attributes from the API.
|
154
|
+
# This method exists to be overridden so that attributes created manually can be handled differently from those loaded from the API.
|
155
|
+
def load_from_api(attrs)
|
156
|
+
load(attrs, true)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Convert attribute hashes that represent objects into objects
|
160
|
+
def convert_attributes_to_objects
|
161
|
+
self.class.hash_to_objects(attributes, self.class)
|
162
|
+
end
|
163
|
+
|
164
|
+
# This method is called by API when a hash including 'errors' is returned along with an HTTP error code.
|
165
|
+
def handle_errors(received_errors)
|
166
|
+
received_errors.each do |err|
|
167
|
+
self.errors.add :base, err
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# When we are loading a resource from an API call, we will use this method to instantiate classes based on attribute names.
|
172
|
+
def self.find_resource_class(class_name)
|
173
|
+
klass = Utils.get_class(class_name)
|
174
|
+
#puts "**** descendant map: #{Base.descendant_map.inspect}"
|
175
|
+
return klass if klass.respond_to?(:new_from_api)
|
176
|
+
Base.descendant_map[class_name]
|
177
|
+
end
|
178
|
+
|
179
|
+
# Convert a hash received from the API into an object or array of objects.
|
180
|
+
# e.g. Base.hash_to_objects({'base' => {'name' => 'Test'} }) => {"base"=>#<WellRested::Base:0x10244de70 @attributes={"name"=>"Test"}}
|
181
|
+
def self.hash_to_objects(hash, from_api = false)
|
182
|
+
hash.each do |k,v|
|
183
|
+
if v.kind_of?(Hash)
|
184
|
+
class_name = k.camelize
|
185
|
+
klass = self.find_resource_class(class_name)
|
186
|
+
if klass
|
187
|
+
hash[k] = from_api ? klass.new_from_api(v) : klass.new(v)
|
188
|
+
end
|
189
|
+
elsif v.kind_of?(Array)
|
190
|
+
class_name = k.to_s.singularize.camelize
|
191
|
+
#puts "**** class_name=#{class_name}"
|
192
|
+
klass = find_resource_class(class_name)
|
193
|
+
if klass
|
194
|
+
#puts "**** class exists, instantiation"
|
195
|
+
hash[k] = v.map do |o|
|
196
|
+
if o.kind_of?(Hash)
|
197
|
+
from_api ? klass.new_from_api(o) : klass.new(o)
|
198
|
+
else
|
199
|
+
o
|
200
|
+
end
|
201
|
+
end
|
202
|
+
else
|
203
|
+
#puts "**** class does not exist"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
hash
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.fill_path(params)
|
211
|
+
API.fill_path(self.path, params)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Return the attributes that we want to send to the server when this resource is saved.
|
215
|
+
# If a schema is defined, only return elements defined in the schema.
|
216
|
+
# Override this for special attribute-handling.
|
217
|
+
def attributes_for_api
|
218
|
+
# by default, filter out nil elements
|
219
|
+
hash = objects_to_attributes(@attributes.reject { |k,v| v.nil? }.with_indifferent_access)
|
220
|
+
# and anything not included in the schema
|
221
|
+
hash.reject! { |k,v| !schema.include?(k) } unless schema.nil?
|
222
|
+
hash
|
223
|
+
end
|
224
|
+
|
225
|
+
# API should use these to generate the path.
|
226
|
+
# Override this to control how path variables get inserted.
|
227
|
+
def path_parameters
|
228
|
+
objects_to_attributes(@attributes.reject { |k,v| v.nil? }.with_indifferent_access)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Run active_model validations on @attributes hash.
|
232
|
+
def read_attribute_for_validation(key)
|
233
|
+
@attributes[key]
|
234
|
+
end
|
235
|
+
|
236
|
+
# Return a string form of this object for rails to use in routes.
|
237
|
+
def to_param
|
238
|
+
self.id.nil? ? nil : self.id.to_s
|
239
|
+
end
|
240
|
+
|
241
|
+
# Return a key for rails to use for... not sure exaclty what.
|
242
|
+
# Should be an array, or nil.
|
243
|
+
def to_key
|
244
|
+
self.id.nil? ? nil : [self.id]
|
245
|
+
end
|
246
|
+
|
247
|
+
# The following 3 methods were copied from active_record/persistence.rb
|
248
|
+
# Returns true if this object hasn't been saved yet -- that is, a record
|
249
|
+
# for the object doesn't exist in the data store yet; otherwise, returns false.
|
250
|
+
def new_record?
|
251
|
+
self.new_record
|
252
|
+
end
|
253
|
+
|
254
|
+
# Alias of new_record? Apparently used by Rails sometimes.
|
255
|
+
def new?
|
256
|
+
self.new_record
|
257
|
+
end
|
258
|
+
|
259
|
+
# Returns true if this object has been destroyed, otherwise returns false.
|
260
|
+
#def destroyed?
|
261
|
+
# @destroyed
|
262
|
+
#end
|
263
|
+
|
264
|
+
# Returns if the record is persisted, i.e. it's not a new record and it was
|
265
|
+
# not destroyed.
|
266
|
+
def persisted?
|
267
|
+
# !(new_record? || destroyed?)
|
268
|
+
!new_record?
|
269
|
+
end
|
270
|
+
|
271
|
+
# Equality is defined as having the same attributes.
|
272
|
+
def ==(other)
|
273
|
+
other.respond_to?(:attributes) ? (self.attributes == other.attributes) : false
|
274
|
+
end
|
275
|
+
|
276
|
+
# Respond to getter and setter methods for attributes.
|
277
|
+
def method_missing(method_sym, *args, &block)
|
278
|
+
method = method_sym.to_s
|
279
|
+
# Is this an attribute getter?
|
280
|
+
if args.empty? and attributes.include?(method)
|
281
|
+
attributes[method]
|
282
|
+
# Is it an attribute setter?
|
283
|
+
elsif args.length == 1 and method[method.length-1..method.length-1] == '=' and attributes.include?(attr_name = method[0..method.length-2])
|
284
|
+
attributes[attr_name] = args.first
|
285
|
+
else
|
286
|
+
super
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module WellRested
|
2
|
+
class CoreFormatter
|
3
|
+
|
4
|
+
def encode(obj)
|
5
|
+
obj.to_json
|
6
|
+
end
|
7
|
+
|
8
|
+
def decode(serialized_representation)
|
9
|
+
result = JSON.parse(serialized_representation)
|
10
|
+
# 20120628 CR - Core returns the collection results in an array "items" => find_many()
|
11
|
+
# If that doesn't exist, it is a single element, which can be returned as is => find()
|
12
|
+
return result["items"] || result
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "well_rested-core/version"
|
2
|
+
|
3
|
+
# require "well_rested"
|
4
|
+
|
5
|
+
# require 'well_rested/core_formatter'
|
6
|
+
|
7
|
+
|
8
|
+
# # require external dependencies
|
9
|
+
# require 'active_support/core_ext/hash/indifferent_access'
|
10
|
+
# require 'active_support/core_ext/hash/reverse_merge'
|
11
|
+
|
12
|
+
# # require internal general-use libs
|
13
|
+
# require 'key_transformer'
|
14
|
+
# require 'generic_utils'
|
15
|
+
|
16
|
+
# # require internal libs
|
17
|
+
require 'well_rested/api'
|
18
|
+
require 'well_rested/base'
|
19
|
+
require 'well_rested/utils'
|
20
|
+
# require 'well_rested/json_formatter'
|
21
|
+
require 'well_rested/core_formatter'
|
22
|
+
# require 'well_rested/camel_case_formatter'
|
23
|
+
|
24
|
+
# Make sure 'bases' singularizes to 'base' instead of 'basis'.
|
25
|
+
# Otherwise, we get an error that no class Basis is found in Base.
|
26
|
+
ActiveSupport::Inflector.inflections do |inflect|
|
27
|
+
inflect.irregular 'base', 'bases'
|
28
|
+
end
|
29
|
+
|
30
|
+
module WellRested
|
31
|
+
module Core
|
32
|
+
autoload :API, 'well_rested/api'
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/well_rested-core/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
|
6
|
+
gem.add_runtime_dependency 'well_rested', '~> 0.6'
|
7
|
+
|
8
|
+
gem.authors = ["Christian Rohrer"]
|
9
|
+
gem.email = ["hydrat@gmail.com"]
|
10
|
+
gem.description = %q{A fork of the well_rested gem with adaption for the SWITCHtoolbox Core API}
|
11
|
+
gem.summary = %q{A fork of the well_rested gem with adaption for the SWITCHtoolbox Core API}
|
12
|
+
gem.homepage = ""
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($\)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.name = "well_rested-core"
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.version = WellRested::Core::VERSION
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: well_rested-core
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3.pre
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Christian Rohrer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: well_rested
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0.6'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.6'
|
30
|
+
description: A fork of the well_rested gem with adaption for the SWITCHtoolbox Core
|
31
|
+
API
|
32
|
+
email:
|
33
|
+
- hydrat@gmail.com
|
34
|
+
executables: []
|
35
|
+
extensions: []
|
36
|
+
extra_rdoc_files: []
|
37
|
+
files:
|
38
|
+
- .gitignore
|
39
|
+
- Gemfile
|
40
|
+
- LICENSE
|
41
|
+
- README.md
|
42
|
+
- Rakefile
|
43
|
+
- lib/well_rested-core.rb
|
44
|
+
- lib/well_rested-core/version.rb
|
45
|
+
- lib/well_rested/api.rb
|
46
|
+
- lib/well_rested/base.rb
|
47
|
+
- lib/well_rested/core_formatter.rb
|
48
|
+
- well_rested-core.gemspec
|
49
|
+
homepage: ''
|
50
|
+
licenses: []
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>'
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 1.3.1
|
67
|
+
requirements: []
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.8.24
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: A fork of the well_rested gem with adaption for the SWITCHtoolbox Core API
|
73
|
+
test_files: []
|