api_resource 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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