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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in well_rested-core.gemspec
4
+ gemspec
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,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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,5 @@
1
+ module WellRested
2
+ module Core
3
+ VERSION = "0.0.3.pre"
4
+ end
5
+ 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: []