well_rested-core 0.0.3.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|