api_resource 0.4.0 → 0.4.1

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.
Files changed (68) hide show
  1. metadata +179 -123
  2. data/.document +0 -5
  3. data/.rspec +0 -5
  4. data/.travis.yml +0 -4
  5. data/Gemfile +0 -37
  6. data/Gemfile.lock +0 -190
  7. data/Guardfile +0 -27
  8. data/Rakefile +0 -49
  9. data/VERSION +0 -1
  10. data/api_resource.gemspec +0 -180
  11. data/lib/api_resource.rb +0 -130
  12. data/lib/api_resource/association_activation.rb +0 -19
  13. data/lib/api_resource/associations.rb +0 -218
  14. data/lib/api_resource/associations/association_proxy.rb +0 -116
  15. data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +0 -16
  16. data/lib/api_resource/associations/dynamic_resource_scope.rb +0 -23
  17. data/lib/api_resource/associations/generic_scope.rb +0 -68
  18. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +0 -16
  19. data/lib/api_resource/associations/has_many_through_remote_object_proxy.rb +0 -13
  20. data/lib/api_resource/associations/has_one_remote_object_proxy.rb +0 -24
  21. data/lib/api_resource/associations/multi_argument_resource_scope.rb +0 -15
  22. data/lib/api_resource/associations/multi_object_proxy.rb +0 -84
  23. data/lib/api_resource/associations/related_object_hash.rb +0 -12
  24. data/lib/api_resource/associations/relation_scope.rb +0 -25
  25. data/lib/api_resource/associations/resource_scope.rb +0 -32
  26. data/lib/api_resource/associations/scope.rb +0 -132
  27. data/lib/api_resource/associations/single_object_proxy.rb +0 -82
  28. data/lib/api_resource/attributes.rb +0 -243
  29. data/lib/api_resource/base.rb +0 -717
  30. data/lib/api_resource/callbacks.rb +0 -45
  31. data/lib/api_resource/connection.rb +0 -195
  32. data/lib/api_resource/core_extensions.rb +0 -7
  33. data/lib/api_resource/custom_methods.rb +0 -117
  34. data/lib/api_resource/decorators.rb +0 -6
  35. data/lib/api_resource/decorators/caching_decorator.rb +0 -20
  36. data/lib/api_resource/exceptions.rb +0 -99
  37. data/lib/api_resource/formats.rb +0 -22
  38. data/lib/api_resource/formats/json_format.rb +0 -25
  39. data/lib/api_resource/formats/xml_format.rb +0 -36
  40. data/lib/api_resource/local.rb +0 -12
  41. data/lib/api_resource/log_subscriber.rb +0 -15
  42. data/lib/api_resource/mocks.rb +0 -277
  43. data/lib/api_resource/model_errors.rb +0 -82
  44. data/lib/api_resource/observing.rb +0 -27
  45. data/lib/api_resource/railtie.rb +0 -24
  46. data/lib/api_resource/scopes.rb +0 -48
  47. data/nohup.out +0 -63
  48. data/spec/lib/api_resource_spec.rb +0 -43
  49. data/spec/lib/associations_spec.rb +0 -751
  50. data/spec/lib/attributes_spec.rb +0 -191
  51. data/spec/lib/base_spec.rb +0 -655
  52. data/spec/lib/callbacks_spec.rb +0 -68
  53. data/spec/lib/connection_spec.rb +0 -137
  54. data/spec/lib/local_spec.rb +0 -20
  55. data/spec/lib/mocks_spec.rb +0 -45
  56. data/spec/lib/model_errors_spec.rb +0 -29
  57. data/spec/lib/prefixes_spec.rb +0 -107
  58. data/spec/spec_helper.rb +0 -82
  59. data/spec/support/mocks/association_mocks.rb +0 -63
  60. data/spec/support/mocks/error_resource_mocks.rb +0 -21
  61. data/spec/support/mocks/prefix_model_mocks.rb +0 -5
  62. data/spec/support/mocks/test_resource_mocks.rb +0 -44
  63. data/spec/support/requests/association_requests.rb +0 -31
  64. data/spec/support/requests/error_resource_requests.rb +0 -25
  65. data/spec/support/requests/prefix_model_requests.rb +0 -7
  66. data/spec/support/requests/test_resource_requests.rb +0 -38
  67. data/spec/support/test_resource.rb +0 -72
  68. data/spec/tmp/DIR +0 -0
@@ -1,82 +0,0 @@
1
- require 'api_resource/associations/association_proxy'
2
-
3
- module ApiResource
4
-
5
- module Associations
6
-
7
- class SingleObjectProxy < AssociationProxy
8
-
9
- def serializable_hash(options = {})
10
- return if self.internal_object.nil?
11
- self.internal_object.serializable_hash(options)
12
- end
13
-
14
- def internal_object=(contents)
15
- if contents.is_a?(self.klass) ||
16
- contents.respond_to?(:internal_object) && contents.internal_object.is_a?(self.klass) ||
17
- contents.nil?
18
- return @internal_object = contents
19
- else
20
- return load(contents)
21
- end
22
- end
23
-
24
- def ==(other)
25
- return false if self.class != other.class
26
- return false if other.internal_object.attributes != self.internal_object.attributes
27
- return true
28
- end
29
-
30
- protected
31
- def load_scope_with_options(scope, options)
32
- scope = self.loaded_hash_key(scope.to_s, options)
33
- # If the service uri is blank you can't load
34
- return nil if self.remote_path.blank?
35
- self.loaded[scope] ||= begin
36
- self.times_loaded += 1
37
- self.klass.new(self.load_from_remote(options))
38
- end
39
- end
40
-
41
- def load(contents)
42
- # If we get something nil this should just behave like nil
43
- return if contents.nil?
44
- # if we get an array with a length of one, make it a hash
45
- if contents.is_a?(self.class)
46
- contents = contents.internal_object.serializable_hash
47
- elsif contents.is_a?(Array) && contents.length == 1
48
- contents = contents.first
49
- end
50
- raise "Expected an attributes hash got #{contents}" unless contents.is_a?(Hash)
51
- contents = contents.with_indifferent_access
52
- # If we don't have a 'service_uri' just assume that these are all attributes and make an object
53
- return @internal_object = self.klass.new(contents) unless contents[self.class.remote_path_element]
54
- # allow for symbols vs strings with these elements
55
- self.remote_path = contents.delete(self.class.remote_path_element)
56
- # There's only one hash here so it's hard to distinguish attributes from scopes, the key scopes_only says everything
57
- # in this hash is a scope
58
- no_attrs = (contents.delete(:scopes_only) || false)
59
- attrs = {}
60
- contents.each do |key, val|
61
- # if this key is an attribute add it to attrs, warn if we've set scopes_only
62
- if self.klass.attribute_names.include?(key.to_sym) && !no_attrs
63
- attrs[key] = val
64
- else
65
- warn("#{key} is an attribute of #{self.klass}, beware of name collisions") if no_attrs && self.klass.attribute_names.include?(key)
66
- raise "Expected the scope #{key} to have a hash for a value, got #{val}" unless val.is_a?(Hash)
67
- self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
68
- def #{key}(opts = {})
69
- @#{key} ||= ApiResource::Associations::RelationScope.new(self, :#{key}, opts)
70
- end
71
- EOE
72
- self.scopes[key.to_s] = val
73
- end
74
- end
75
- @internal_object = attrs.present? ? self.klass.new(attrs) : nil
76
- end
77
-
78
- end
79
-
80
- end
81
-
82
- end
@@ -1,243 +0,0 @@
1
- module ApiResource
2
-
3
- module Attributes
4
-
5
- extend ActiveSupport::Concern
6
- include ActiveModel::AttributeMethods
7
- include ActiveModel::Dirty
8
-
9
- included do
10
-
11
- alias_method_chain :save, :dirty_tracking
12
-
13
- class_attribute :attribute_names, :public_attribute_names, :protected_attribute_names, :attribute_types
14
-
15
- cattr_accessor :valid_typecasts; self.valid_typecasts = [:date, :time, :float, :integer, :int, :fixnum, :string, :array]
16
-
17
- attr_reader :attributes
18
-
19
- self.attribute_names = []
20
- self.public_attribute_names = []
21
- self.protected_attribute_names = []
22
- self.attribute_types = {}.with_indifferent_access
23
-
24
- define_method(:attributes) do
25
- return @attributes if @attributes
26
- # Otherwise make the attributes hash of all the attributes
27
- @attributes = HashWithIndifferentAccess.new
28
- self.class.attribute_names.each do |attr|
29
- @attributes[attr] = self.send("#{attr}")
30
- end
31
- @attributes
32
- end
33
-
34
-
35
- # This method is important for reloading an object. If the
36
- # object has already been loaded, its associations will trip
37
- # up the load method unless we pass in the internal objects.
38
-
39
- define_method(:attributes_without_proxies) do
40
- attributes = @attributes
41
-
42
- if attributes.nil?
43
- attributes = self.class.attribute_names.each do |attr|
44
- attributes[attr] = self.send("#{attr}")
45
- end
46
- end
47
-
48
- attributes.each do |k,v|
49
- if v.respond_to?(:internal_object)
50
- if v.internal_object.present?
51
- internal = v.internal_object
52
- if internal.is_a?(Array)
53
- attributes[k] = internal.collect{|item| item.attributes}
54
- else
55
- attributes[k] = internal.attributes
56
- end
57
- else
58
- attributes[k] = nil
59
- end
60
- end
61
- end
62
-
63
- attributes
64
- end
65
-
66
- end
67
-
68
- module ClassMethods
69
-
70
- def define_attributes(*args)
71
- args.each do |arg|
72
- if arg.is_a?(Array)
73
- self.define_attribute_type(arg.first, arg.second)
74
- arg = arg.first
75
- end
76
- self.attribute_names += [arg.to_sym]
77
- self.public_attribute_names += [arg.to_sym]
78
-
79
- # Override the setter for dirty tracking
80
- self.class_eval <<-EOE, __FILE__, __LINE__ + 1
81
- def #{arg}
82
- attribute_with_default(:#{arg})
83
- end
84
-
85
- def #{arg}=(val)
86
- real_val = typecast_attribute(:#{arg}, val)
87
- #{arg}_will_change! unless self.#{arg} == real_val
88
- attributes[:#{arg}] = real_val
89
- end
90
-
91
- def #{arg}?
92
- attributes[:#{arg}].present?
93
- end
94
- EOE
95
- end
96
- self.attribute_names.uniq!
97
- self.public_attribute_names.uniq!
98
- end
99
-
100
- def define_protected_attributes(*args)
101
- args.each do |arg|
102
-
103
- if arg.is_a?(Array)
104
- self.define_attribute_type(arg.first, arg.second)
105
- arg = arg.first
106
- end
107
-
108
- self.attribute_names += [arg.to_sym]
109
- self.protected_attribute_names += [arg.to_sym]
110
-
111
- # These attributes cannot be set, throw an error if you try
112
- self.class_eval <<-EOE, __FILE__, __LINE__ + 1
113
-
114
- def #{arg}
115
- self.attribute_with_default(:#{arg})
116
- end
117
-
118
- def #{arg}=(val)
119
- raise "#{arg} is a protected attribute and cannot be set"
120
- end
121
-
122
- def #{arg}?
123
- self.attributes[:#{arg}].present?
124
- end
125
- EOE
126
- end
127
- self.attribute_names.uniq!
128
- self.protected_attribute_names.uniq!
129
- end
130
-
131
- def define_attribute_type(field, type)
132
- raise "#{type} is not a valid type" unless self.valid_typecasts.include?(type.to_sym)
133
- self.attribute_types[field] = type.to_sym
134
- end
135
-
136
-
137
- def attribute?(name)
138
- self.attribute_names.include?(name.to_sym)
139
- end
140
-
141
- def protected_attribute?(name)
142
- self.protected_attribute_names.include?(name.to_sym)
143
- end
144
-
145
- def clear_attributes
146
- self.attribute_names.clear
147
- self.public_attribute_names.clear
148
- self.protected_attribute_names.clear
149
- end
150
- end
151
-
152
- # set new attributes
153
- def attributes=(new_attrs)
154
- new_attrs.each_pair do |k,v|
155
- self.send("#{k}=",v) unless k.to_sym == :id
156
- end
157
- new_attrs
158
- end
159
-
160
- def save_with_dirty_tracking(*args)
161
- if save_without_dirty_tracking(*args)
162
- @previously_changed = self.changes
163
- @changed_attributes.clear
164
- return true
165
- else
166
- return false
167
- end
168
- end
169
-
170
- def set_attributes_as_current(*attrs)
171
- @changed_attributes.clear and return if attrs.blank?
172
- attrs.each do |attr|
173
- @changed_attributes.delete(attr.to_s)
174
- end
175
- end
176
-
177
- def reset_attribute_changes(*attrs)
178
- attrs = self.class.public_attribute_names if attrs.blank?
179
- attrs.each do |attr|
180
- self.send("reset_#{attr}!")
181
- end
182
-
183
- set_attributes_as_current(*attrs)
184
- end
185
-
186
- def attribute?(name)
187
- self.class.attribute?(name)
188
- end
189
-
190
- def protected_attribute?(name)
191
- self.class.protected_attribute?(name)
192
- end
193
-
194
- def respond_to?(sym, include_private_methods = false)
195
- if sym =~ /\?$/
196
- return true if self.attribute?($`)
197
- elsif sym =~ /=$/
198
- return true if self.class.public_attribute_names.include?($`)
199
- else
200
- return true if self.attribute?(sym.to_sym)
201
- end
202
- super
203
- end
204
-
205
- protected
206
-
207
- def attribute_with_default(field)
208
- self.attributes[field].nil? ? self.default_value_for_field(field) : self.attributes[field]
209
- end
210
-
211
- def default_value_for_field(field)
212
- case self.class.attribute_types[field.to_sym]
213
- when :array
214
- return []
215
- else
216
- return nil
217
- end
218
- end
219
-
220
- def typecast_attribute(field, val)
221
- return val unless self.class.attribute_types.include?(field)
222
- case self.class.attribute_types[field.to_sym]
223
- when :date
224
- return val.class == Date ? val.dup : Date.parse(val)
225
- when :time
226
- return val.class == Time ? val.dup : Time.parse(val)
227
- when :integer, :int, :fixnum
228
- return val.class == Fixnum ? val.dup : val.to_i rescue val
229
- when :float
230
- return val.class == Float ? val.dup : val.to_f rescue val
231
- when :string
232
- return val.class == String ? val.dup : val.to_s rescue val
233
- when :array
234
- return val.class == Array ? val.dup : Array.wrap(val)
235
- else
236
- # catches the nil case and just leaves it alone
237
- return val.dup rescue val
238
- end
239
- end
240
-
241
- end
242
-
243
- end
@@ -1,717 +0,0 @@
1
- require 'pp'
2
- require 'active_support'
3
- require 'active_support/core_ext'
4
- require 'active_support/string_inquirer'
5
-
6
- module ApiResource
7
-
8
- class Base
9
-
10
- class_attribute :site, :proxy, :user, :password, :auth_type, :format,
11
- :timeout, :open_timeout, :ssl_options, :token, :ttl
12
-
13
-
14
- class_attribute :include_root_in_json
15
- self.include_root_in_json = true
16
-
17
- class_attribute :include_nil_attributes_on_create
18
- self.include_nil_attributes_on_create = false
19
-
20
- class_attribute :include_all_attributes_on_update
21
- self.include_nil_attributes_on_create = false
22
-
23
- class_attribute :format
24
- self.format = ApiResource::Formats::JsonFormat
25
-
26
- class_attribute :primary_key
27
- self.primary_key = "id"
28
-
29
- class << self
30
-
31
- # writers - accessors with defaults were not working
32
- attr_writer :element_name, :collection_name
33
-
34
- def inherited(klass)
35
- # Call the methods of the superclass to make sure inheritable accessors and the like have been inherited
36
- super
37
- # Now we need to define the inherited method on the klass that's doing the inheriting
38
- # it calls super which will allow the chaining effect we need
39
- klass.instance_eval <<-EOE, __FILE__, __LINE__ + 1
40
- def inherited(klass)
41
- klass.send(:define_singleton_method, :collection_name, lambda {self.superclass.collection_name})
42
- super(klass)
43
- end
44
- EOE
45
- true
46
- end
47
- # This makes a request to new_element_path
48
- def set_class_attributes_upon_load
49
- return true if self == ApiResource::Base
50
- begin
51
- class_data = self.connection.get(
52
- self.new_element_path, self.headers
53
- )
54
- # Attributes go first
55
- if class_data["attributes"]
56
-
57
- define_attributes(
58
- *(class_data["attributes"]["public"] || [])
59
- )
60
- define_protected_attributes(
61
- *(class_data["attributes"]["protected"] || [])
62
- )
63
-
64
- end
65
- # Then scopes
66
- if class_data["scopes"]
67
- class_data["scopes"].each_pair do |scope_name, opts|
68
- self.scope(scope_name, opts)
69
- end
70
- end
71
- # Then associations
72
- if class_data["associations"]
73
- class_data["associations"].each_pair do |key, hash|
74
- hash.each_pair do |assoc_name, assoc_options|
75
- self.send(key, assoc_name, assoc_options)
76
- end
77
- end
78
- end
79
-
80
- # This is provided by ActiveModel::AttributeMethods, it should
81
- # define the basic methods but we need to override all the setters
82
- # so we do dirty tracking
83
- attrs = []
84
- if class_data["attributes"] && class_data["attributes"]["public"]
85
- attrs += class_data["attributes"]["public"].collect{|v|
86
- v.is_a?(Array) ? v.first : v
87
- }.flatten
88
- end
89
- if class_data["associations"]
90
- attrs += class_data["associations"].values.collect(&:keys).flatten
91
- end
92
- define_attribute_methods(attrs)
93
-
94
- # Swallow up any loading errors because the site may be incorrect
95
- rescue Exception => e
96
- if ApiResource.raise_missing_definition_error
97
- raise e
98
- end
99
- ApiResource.logger.warn(
100
- "#{self} accessing #{self.new_element_path}"
101
- )
102
- ApiResource.logger.warn(
103
- "#{self}: #{e.message[0..60].gsub(/[\n\r]/, '')} ...\n"
104
- )
105
- ApiResource.logger.debug(e.backtrace.pretty_inspect)
106
- return e.respond_to?(:request) ? e.request : nil
107
- end
108
- end
109
-
110
- def reset_connection
111
- remove_instance_variable(:@connection) if @connection.present?
112
- end
113
-
114
- # load our resource definition to make sure we know what this class
115
- # responds to
116
- def respond_to?(*args)
117
- unless self.instance_variable_defined?(:@class_data)
118
- self.instance_variable_set(:@class_data, true)
119
- self.set_class_attributes_upon_load
120
- end
121
- super
122
- end
123
-
124
- def reload_class_attributes
125
- # clear the public_attribute_names, protected_attribute_names
126
- remove_instance_variable(:@class_data) if instance_variable_defined?(:@class_data)
127
- self.clear_attributes
128
- self.clear_related_objects
129
- self.set_class_attributes_upon_load
130
- end
131
-
132
- def token_with_new_token_set=(new_token)
133
- self.token_without_new_token_set = new_token
134
- self.connection(true)
135
- self.descendants.each do |child|
136
- child.send(:token=, new_token)
137
- end
138
- end
139
-
140
- alias_method_chain :token=, :new_token_set
141
-
142
- def site_with_connection_reset=(site)
143
- # store so we can reload attributes if the site changed
144
- old_site = self.site.to_s.clone
145
- @connection = nil
146
-
147
- if site.nil?
148
- self.site_without_connection_reset = nil
149
- # no site, so we'll skip the reload
150
- return site
151
- else
152
- self.site_without_connection_reset = create_site_uri_from(site)
153
- end
154
-
155
- # reset class attributes and try to reload them if the site changed
156
- unless self.site.to_s == old_site
157
- self.reload_class_attributes
158
- end
159
-
160
- return site
161
- end
162
-
163
- alias_method_chain :site=, :connection_reset
164
-
165
-
166
- def format_with_mimetype_or_format_set=(mime_type_or_format)
167
- format = mime_type_or_format.is_a?(Symbol) ? ApiResource::Formats[mime_type_or_format] : mime_type_or_format
168
- self.format_without_mimetype_or_format_set = format
169
- self.connection.format = format if self.site
170
- end
171
-
172
- alias_method_chain :format=, :mimetype_or_format_set
173
-
174
- def timeout_with_connection_reset=(timeout)
175
- @connection = nil
176
- self.timeout_without_connection_reset = timeout
177
- end
178
-
179
- alias_method_chain :timeout=, :connection_reset
180
-
181
- def open_timeout_with_connection_reset=(timeout)
182
- @connection = nil
183
- self.open_timeout_without_connection_reset = timeout
184
- end
185
-
186
- alias_method_chain :open_timeout=, :connection_reset
187
-
188
- def connection(refresh = false)
189
- @connection = Connection.new(self.site, self.format, self.headers) if refresh || @connection.nil?
190
- @connection.timeout = self.timeout
191
- @connection
192
- end
193
-
194
- def headers
195
- {}.tap do |ret|
196
- ret['Lifebooker-Token'] = self.token if self.token.present?
197
- end
198
- end
199
-
200
- def prefix(options = {})
201
- default = (self.site ? self.site.path : '/')
202
- default << '/' unless default[-1..-1] == '/'
203
- self.prefix = default
204
- prefix(options)
205
- end
206
-
207
- def prefix_source
208
- prefix
209
- prefix_source
210
- end
211
-
212
- def prefix=(value = '/')
213
- prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.escape options[#{key}].to_s}"}
214
- @prefix_parameters = nil
215
- silence_warnings do
216
- instance_eval <<-EOE, __FILE__, __LINE__ + 1
217
- def prefix_source() "#{value}" end
218
- def prefix(options={}) "#{prefix_call}" end
219
- EOE
220
- end
221
- rescue Exception => e
222
- logger.error "Couldn't set prefix: #{e}\n #{code}" if logger
223
- raise
224
- end
225
-
226
- # element_name with default
227
- def element_name
228
- @element_name ||= self.model_name.element
229
- end
230
- # collection_name with default
231
- def collection_name
232
- @collection_name ||= ActiveSupport::Inflector.pluralize(self.element_name)
233
- end
234
-
235
- # alias_method :set_prefix, :prefix=
236
- # alias_method :set_element_name, :element_name=
237
- # alias_method :set_collection_name, :collection_name=
238
-
239
- def element_path(id, prefix_options = {}, query_options = nil)
240
- prefix_options, query_options = split_options(prefix_options) if query_options.nil?
241
-
242
- # If we have a prefix, we need a foreign key id
243
- # This regex detects '//', which means no foreign key id is present.
244
- if prefix(prefix_options) =~ /\/\/$/
245
- "/#{collection_name}/#{URI.escape id.to_s}.#{format.extension}#{query_string(query_options)}"
246
- else
247
- # Fall back on this rather than search without the id
248
- "#{prefix(prefix_options)}#{collection_name}/#{URI.escape id.to_s}.#{format.extension}#{query_string(query_options)}"
249
- end
250
- end
251
-
252
- # TODO: Add back in support for non-dynamic prefix paths (e.g. /subdir/resources/new.json)
253
- def new_element_path
254
- "/#{collection_name}/new.#{format.extension}"
255
- end
256
-
257
- def collection_path(prefix_options = {}, query_options = nil)
258
- prefix_options, query_options = split_options(prefix_options) if query_options.nil?
259
-
260
- # If we have a prefix, we need a foreign key id
261
- # This regex detects '//', which means no foreign key id is present.
262
- if prefix(prefix_options) =~ /\/\/$/
263
- "/#{collection_name}.#{format.extension}#{query_string(query_options)}"
264
- else
265
- # Fall back on this rather than search without the id
266
- "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
267
- end
268
- end
269
-
270
- def build(attributes = {})
271
- self.new(attributes)
272
- end
273
-
274
- def create(attributes = {})
275
- self.new(attributes).tap{ |resource| resource.save }
276
- end
277
-
278
- # This decides which finder method to call.
279
- # It accepts arguments of the form "scope", "options={}"
280
- # where options can be standard rails options or :expires_in.
281
- # If :expires_in is set, it caches it for expires_in seconds.
282
- def find(*arguments)
283
- scope = arguments.slice!(0)
284
- options = arguments.slice!(0) || {}
285
-
286
- expiry = options.delete(:expires_in) || ApiResource::Base.ttl || 0
287
- ApiResource.with_ttl(expiry.to_f) do
288
- case scope
289
- when :all then find_every(options)
290
- when :first then find_every(options).first
291
- when :last then find_every(options).last
292
- when :one then find_one(options)
293
- else find_single(scope, options)
294
- end
295
- end
296
- end
297
-
298
-
299
- # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass
300
- # in all the same arguments to this method as you can to
301
- # <tt>find(:first)</tt>.
302
- def first(*args)
303
- find(:first, *args)
304
- end
305
-
306
- # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass
307
- # in all the same arguments to this method as you can to
308
- # <tt>find(:last)</tt>.
309
- def last(*args)
310
- find(:last, *args)
311
- end
312
-
313
- # This is an alias for find(:all). You can pass in all the same
314
- # arguments to this method as you can to <tt>find(:all)</tt>
315
- def all(*args)
316
- find(:all, *args)
317
- end
318
-
319
-
320
- # Deletes the resources with the ID in the +id+ parameter.
321
- #
322
- # ==== Options
323
- # All options specify \prefix and query parameters.
324
- #
325
- # ==== Examples
326
- # Event.delete(2) # sends DELETE /events/2
327
- #
328
- # Event.create(:name => 'Free Concert', :location => 'Community Center')
329
- # my_event = Event.find(:first) # let's assume this is event with ID 7
330
- # Event.delete(my_event.id) # sends DELETE /events/7
331
- #
332
- # # Let's assume a request to events/5/cancel.xml
333
- # Event.delete(params[:id]) # sends DELETE /events/5
334
- def delete(id, options = {})
335
- connection.delete(element_path(id, options))
336
- end
337
-
338
- protected
339
- def method_missing(meth, *args, &block)
340
- # make one attempt to load remote attrs
341
- unless self.instance_variable_defined?(:@class_data)
342
- self.set_class_attributes_upon_load
343
- self.instance_variable_set(:@class_data, true)
344
- return self.send(meth, *args, &block)
345
- end
346
- super
347
- end
348
-
349
- private
350
- # Find every resource
351
- def find_every(options)
352
- begin
353
- case from = options[:from]
354
- when Symbol
355
- instantiate_collection(get(from, options[:params]))
356
- when String
357
- path = "#{from}#{query_string(options[:params])}"
358
- instantiate_collection(connection.get(path, headers) || [])
359
- else
360
- prefix_options, query_options = split_options(options[:params])
361
- path = collection_path(prefix_options, query_options)
362
- instantiate_collection( (connection.get(path, headers) || []))
363
- end
364
- rescue ApiResource::ResourceNotFound
365
- # Swallowing ResourceNotFound exceptions and return nil - as per
366
- # ActiveRecord.
367
- nil
368
- end
369
- end
370
-
371
- # Find a single resource from a one-off URL
372
- def find_one(options)
373
- case from = options[:from]
374
- when Symbol
375
- instantiate_record(get(from, options[:params]))
376
- when String
377
- path = "#{from}#{query_string(options[:params])}"
378
- instantiate_record(connection.get(path, headers))
379
- end
380
- end
381
-
382
- # Find a single resource from the default URL
383
- def find_single(scope, options)
384
- prefix_options, query_options = split_options(options[:params])
385
- path = element_path(scope, prefix_options, query_options)
386
- instantiate_record(connection.get(path, headers))
387
- end
388
-
389
- def instantiate_collection(collection)
390
- collection.collect! { |record| instantiate_record(record) }
391
- end
392
-
393
- def instantiate_record(record)
394
- new(record)
395
- end
396
-
397
-
398
- # Accepts a URI and creates the site URI from that.
399
- def create_site_uri_from(site)
400
- site.is_a?(URI) ? site.dup : uri_parser.parse(site)
401
- end
402
-
403
- # Accepts a URI and creates the proxy URI from that.
404
- def create_proxy_uri_from(proxy)
405
- proxy.is_a?(URI) ? proxy.dup : uri_parser.parse(proxy)
406
- end
407
-
408
- # contains a set of the current prefix parameters.
409
- def prefix_parameters
410
- @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
411
- end
412
-
413
- # Builds the query string for the request.
414
- def query_string(options)
415
- "?#{options.to_query}" unless options.nil? || options.empty?
416
- end
417
-
418
- # split an option hash into two hashes, one containing the prefix options,
419
- # and the other containing the leftovers.
420
- def split_options(options = {})
421
- prefix_options, query_options = {}, {}
422
- (options || {}).each do |key, value|
423
- next if key.blank?
424
- (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
425
- end
426
-
427
- [ prefix_options, query_options ]
428
- end
429
-
430
- def uri_parser
431
- @uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
432
- end
433
-
434
- end
435
-
436
- def initialize(attributes = {})
437
- # if we initialize this class, load the attributes
438
- unless self.class.instance_variable_defined?(:@class_data)
439
- self.class.set_class_attributes_upon_load
440
- self.class.instance_variable_set(:@class_data, true)
441
- end
442
- # Now we can make a call to setup the inheriting klass with its attributes
443
- load(attributes)
444
- end
445
-
446
- def new?
447
- id.blank?
448
- end
449
- alias :new_record? :new?
450
-
451
- def persisted?
452
- !new?
453
- end
454
-
455
- def id
456
- self.attributes[self.class.primary_key]
457
- end
458
-
459
- # Bypass dirty tracking for this field
460
- def id=(id)
461
- attributes[self.class.primary_key] = id
462
- end
463
-
464
- def ==(other)
465
- other.equal?(self) || (other.instance_of?(self.class) && other.id == self.id)
466
- end
467
-
468
- def eql?(other)
469
- self == other
470
- end
471
-
472
- def hash
473
- id.hash
474
- end
475
-
476
- def dup
477
- self.class.new.tap do |resource|
478
- resource.attributes = self.attributes
479
- end
480
- end
481
-
482
- def update_attributes(attrs)
483
- self.attributes = attrs
484
- self.save
485
- end
486
-
487
- def save(*args)
488
- new? ? create(*args) : update(*args)
489
- end
490
-
491
- def save!(*args)
492
- save(*args) || raise(ApiResource::ResourceInvalid.new(self))
493
- end
494
-
495
- def destroy
496
- connection.delete(element_path(self.id), self.class.headers)
497
- end
498
-
499
- def encode(options = {})
500
- self.send("to_#{self.class.format.extension}", options)
501
- end
502
-
503
- def reload
504
- remove_instance_variable(:@assoc_attributes) if instance_variable_defined?(:@assoc_attributes)
505
- self.load(self.class.find(to_param, :params => @prefix_options).attributes_without_proxies)
506
- end
507
-
508
- def to_param
509
- # Stolen from active_record.
510
- # We can't use alias_method here, because method 'id' optimizes itself on the fly.
511
- id && id.to_s # Be sure to stringify the id for routes
512
- end
513
-
514
- def prefix_options
515
- return {} unless self.class.prefix_source =~ /\:/
516
- ret = {}
517
- self.prefix_attribute_names.each do |name|
518
- ret[name] = self.send(name)
519
- end
520
- ret
521
- end
522
-
523
- def prefix_attribute_names
524
- return [] unless self.class.prefix_source =~ /\:/
525
- self.class.prefix_source.scan(/\:(\w+)/).collect{|match| match.first.to_sym}
526
- end
527
-
528
- def load(attributes)
529
- return if attributes.nil?
530
- raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
531
-
532
- attributes.symbolize_keys.each do |key, value|
533
- # If this attribute doesn't exist define it as a protected attribute
534
- self.class.define_protected_attributes(key) unless self.respond_to?(key)
535
- #self.send("#{key}_will_change!") if self.respond_to?("#{key}_will_change!")
536
- self.attributes[key] =
537
- case value
538
- when Array
539
- if self.has_many?(key)
540
- MultiObjectProxy.new(self.has_many_class_name(key), value)
541
- elsif self.association?(key)
542
- raise ArgumentError, "Expected a hash value or nil, got: #{value.inspect}"
543
- else
544
- typecast_attribute(key, value)
545
- end
546
- when Hash
547
- if self.has_many?(key)
548
- MultiObjectProxy.new(self.has_many_class_name(key), value)
549
- elsif self.association?(key)
550
- #binding.pry
551
- SingleObjectProxy.new(self.association_class_name(key), value)
552
- else
553
- typecast_attribute(key, value)
554
- end
555
- when NilClass
556
- # If it's nil and an association then create a blank object
557
- if self.has_many?(key)
558
- return MultiObjectProxy.new(self.has_many_class_name(key), [])
559
- elsif self.association?(key)
560
- SingleObjectProxy.new(self.association_class_name(key), value)
561
- end
562
- else
563
- raise ArgumentError, "expected an array or a hash for the association #{key}, got: #{value.inspect}" if self.association?(key)
564
- typecast_attribute(key, value)
565
- end
566
- end
567
- return self
568
- end
569
-
570
- # Override to_s and inspect so they only show attributes
571
- # and not associations, this prevents force loading of associations
572
- # when we call to_s or inspect on a descendent of base but allows it if we
573
- # try to evaluate an association directly
574
- def to_s
575
- return "#<#{self.class}:#{(self.object_id * 2).to_s(16)} @attributes=#{self.attributes.inject({}){|accum,(k,v)| self.association?(k) ? accum : accum.merge(k => v)}}"
576
- end
577
-
578
- alias_method :inspect, :to_s
579
-
580
- # Methods for serialization as json or xml, relying on the serializable_hash method
581
- def to_xml(options = {})
582
- self.serializable_hash(options).to_xml(:root => self.class.element_name)
583
- end
584
-
585
- def to_json(options = {})
586
- self.class.include_root_in_json ? {self.class.element_name => self.serializable_hash(options)}.to_json : self.serializable_hash(options).to_json
587
- end
588
-
589
- def serializable_hash(options = {})
590
- action = options[:action]
591
- include_nil_attributes = options[:include_nil_attributes]
592
- options[:include_associations] = options[:include_associations] ? options[:include_associations].symbolize_array : self.changes.keys.symbolize_array.select{|k| self.association?(k)}
593
- options[:include_extras] = options[:include_extras] ? options[:include_extras].symbolize_array : []
594
- options[:except] ||= []
595
- ret = self.attributes.inject({}) do |accum, (key,val)|
596
- # If this is an association and it's in include_associations then include it
597
- if options[:include_extras].include?(key.to_sym)
598
- accum.merge(key => val)
599
- elsif options[:except].include?(key.to_sym)
600
- accum
601
- # this attribute is already accounted for in the URL
602
- elsif self.prefix_attribute_names.include?(key.to_sym)
603
- accum
604
- elsif(!include_nil_attributes && val.nil? && self.changes[key].blank?)
605
- accum
606
- else
607
- !self.attribute?(key) || self.protected_attribute?(key) ? accum : accum.merge(key => val)
608
- end
609
- end
610
- options[:include_associations].each do |assoc|
611
- ret[assoc] = self.send(assoc).serializable_hash({:include_id => true, :include_nil_attributes => include_nil_attributes, :action => action}) if self.association?(assoc)
612
- end
613
- # include id - this is for nested updates
614
- ret[:id] = self.id if options[:include_id] && !self.new?
615
- ret
616
- end
617
-
618
- protected
619
- def connection(refresh = false)
620
- self.class.connection(refresh)
621
- end
622
-
623
- def load_attributes_from_response(response)
624
- load(response)
625
- end
626
-
627
- def element_path(id, prefix_override_options = {}, query_options = nil)
628
- self.class.element_path(
629
- id,
630
- self.prefix_options.merge(prefix_override_options),
631
- query_options
632
- )
633
- end
634
-
635
- # list of all attributes that are not nil
636
- def nil_attributes
637
- self.attributes.select{|k,v|
638
- # if our value is actually nil or if we are an association
639
- # or array and we are blank
640
- v.nil? || ((self.association?(k) || v.is_a?(Array)) && v.blank?)
641
- }
642
- end
643
-
644
- def new_element_path(prefix_options = {})
645
- self.class.new_element_path(prefix_options)
646
- end
647
-
648
- def collection_path(override_prefix_options = {},query_options = nil)
649
- self.class.collection_path(
650
- self.prefix_options.merge(override_prefix_options),
651
- query_options
652
- )
653
- end
654
-
655
- def create(*args)
656
- body = setup_create_call(*args)
657
- connection.post(collection_path, body, self.class.headers).tap do |response|
658
- load_attributes_from_response(response)
659
- end
660
- end
661
-
662
- def setup_create_call(*args)
663
- opts = args.extract_options!
664
- # When we create we should not include any blank attributes unless they are associations
665
- except = self.class.include_nil_attributes_on_create ?
666
- {} : self.nil_attributes
667
- opts[:except] = opts[:except] ? opts[:except].concat(except.keys).uniq.symbolize_array : except.keys.symbolize_array
668
- opts[:include_nil_attributes] = self.class.include_nil_attributes_on_create
669
- opts[:include_associations] = opts[:include_associations] ? opts[:include_associations].concat(args) : []
670
- opts[:include_extras] ||= []
671
- opts[:action] = "create"
672
- body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
673
- end
674
-
675
-
676
- def update(*args)
677
- body = setup_update_call(*args)
678
- # We can just ignore the response
679
- connection.put(element_path(self.id), body, self.class.headers).tap do |response|
680
- load_attributes_from_response(response)
681
- end
682
- end
683
-
684
- def setup_update_call(*args)
685
- opts = args.extract_options!
686
- # When we create we should not include any blank attributes
687
- except = self.class.attribute_names - self.changed.symbolize_array
688
- changed_associations = self.changed.symbolize_array.select{|item| self.association?(item)}
689
- opts[:except] = opts[:except] ? opts[:except].concat(except).uniq.symbolize_array : except.symbolize_array
690
- opts[:include_nil_attributes] = self.include_all_attributes_on_update
691
- opts[:include_associations] = opts[:include_associations] ? opts[:include_associations].concat(args).concat(changed_associations).uniq : changed_associations.concat(args)
692
- opts[:include_extras] ||= []
693
- opts[:action] = "update"
694
- opts[:except] = [:id] if self.class.include_all_attributes_on_update
695
- body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
696
- end
697
-
698
- private
699
-
700
- def split_options(options = {})
701
- self.class.__send__(:split_options, options)
702
- end
703
-
704
- end
705
-
706
- class Base
707
- extend ActiveModel::Naming
708
- # Order is important here
709
- # It should be Validations, Dirty Tracking, Callbacks so the include order is the opposite
710
- include AssociationActivation
711
- self.activate_associations
712
-
713
- include Scopes, Callbacks, Attributes, ModelErrors
714
-
715
- end
716
-
717
- end