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 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: []