resource 0.1.0
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/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +26 -0
- data/Guardfile +22 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/api_resource.rb +51 -0
- data/lib/api_resource/associations.rb +472 -0
- data/lib/api_resource/attributes.rb +154 -0
- data/lib/api_resource/base.rb +517 -0
- data/lib/api_resource/callbacks.rb +49 -0
- data/lib/api_resource/connection.rb +162 -0
- data/lib/api_resource/core_extensions.rb +7 -0
- data/lib/api_resource/custom_methods.rb +119 -0
- data/lib/api_resource/exceptions.rb +74 -0
- data/lib/api_resource/formats.rb +14 -0
- data/lib/api_resource/formats/json_format.rb +25 -0
- data/lib/api_resource/formats/xml_format.rb +36 -0
- data/lib/api_resource/log_subscriber.rb +15 -0
- data/lib/api_resource/mocks.rb +249 -0
- data/lib/api_resource/model_errors.rb +86 -0
- data/lib/api_resource/observing.rb +29 -0
- data/resource.gemspec +125 -0
- data/spec/lib/associations_spec.rb +412 -0
- data/spec/lib/attributes_spec.rb +109 -0
- data/spec/lib/base_spec.rb +454 -0
- data/spec/lib/callbacks_spec.rb +68 -0
- data/spec/lib/model_errors_spec.rb +29 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/mocks/association_mocks.rb +18 -0
- data/spec/support/mocks/error_resource_mocks.rb +21 -0
- data/spec/support/mocks/test_resource_mocks.rb +23 -0
- data/spec/support/requests/association_requests.rb +14 -0
- data/spec/support/requests/error_resource_requests.rb +25 -0
- data/spec/support/requests/test_resource_requests.rb +31 -0
- data/spec/support/test_resource.rb +19 -0
- metadata +277 -0
@@ -0,0 +1,154 @@
|
|
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_inheritable_accessor :attribute_names, :public_attribute_names, :protected_attribute_names
|
14
|
+
|
15
|
+
attr_accessor :attributes
|
16
|
+
|
17
|
+
self.attribute_names = []
|
18
|
+
self.public_attribute_names = []
|
19
|
+
self.protected_attribute_names = []
|
20
|
+
|
21
|
+
define_method(:attributes) do
|
22
|
+
return @attributes if @attributes
|
23
|
+
# Otherwise make the attributes hash of all the attributes
|
24
|
+
@attributes = HashWithIndifferentAccess.new
|
25
|
+
self.class.attribute_names.each do |attr|
|
26
|
+
@attributes[attr] = self.send("#{attr}")
|
27
|
+
end
|
28
|
+
@attributes
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
|
35
|
+
def define_attributes(*args)
|
36
|
+
# This is provided by ActiveModel::AttributeMethods, it should define the basic methods
|
37
|
+
# but we need to override all the setters so we do dirty tracking
|
38
|
+
define_attribute_methods args
|
39
|
+
args.each do |arg|
|
40
|
+
self.attribute_names << arg.to_sym
|
41
|
+
self.public_attribute_names << arg.to_sym
|
42
|
+
|
43
|
+
# Override the setter for dirty tracking
|
44
|
+
self.class_eval <<-EOE, __FILE__, __LINE__ + 1
|
45
|
+
def #{arg}
|
46
|
+
self.attributes[:#{arg}]
|
47
|
+
end
|
48
|
+
|
49
|
+
def #{arg}=(val)
|
50
|
+
#{arg}_will_change! unless self.#{arg} == val
|
51
|
+
self.attributes[:#{arg}] = val
|
52
|
+
end
|
53
|
+
|
54
|
+
def #{arg}?
|
55
|
+
self.attributes[:#{arg}].present?
|
56
|
+
end
|
57
|
+
EOE
|
58
|
+
end
|
59
|
+
self.attribute_names.uniq!
|
60
|
+
self.public_attribute_names.uniq!
|
61
|
+
end
|
62
|
+
|
63
|
+
def define_protected_attributes(*args)
|
64
|
+
define_attribute_methods args
|
65
|
+
args.each do |arg|
|
66
|
+
self.attribute_names << arg.to_sym
|
67
|
+
self.protected_attribute_names << arg.to_sym
|
68
|
+
|
69
|
+
# These attributes cannot be set, throw an error if you try
|
70
|
+
self.class_eval <<-EOE, __FILE__, __LINE__ + 1
|
71
|
+
|
72
|
+
def #{arg}
|
73
|
+
self.attributes[:#{arg}]
|
74
|
+
end
|
75
|
+
|
76
|
+
def #{arg}=(val)
|
77
|
+
raise "#{arg} is a protected attribute and cannot be set"
|
78
|
+
end
|
79
|
+
|
80
|
+
def #{arg}?
|
81
|
+
self.attributes[:#{arg}].present?
|
82
|
+
end
|
83
|
+
EOE
|
84
|
+
end
|
85
|
+
self.attribute_names.uniq!
|
86
|
+
self.protected_attribute_names.uniq!
|
87
|
+
end
|
88
|
+
|
89
|
+
def attribute?(name)
|
90
|
+
self.attribute_names.include?(name.to_sym)
|
91
|
+
end
|
92
|
+
|
93
|
+
def protected_attribute?(name)
|
94
|
+
self.protected_attribute_names.include?(name.to_sym)
|
95
|
+
end
|
96
|
+
|
97
|
+
def clear_attributes
|
98
|
+
self.attribute_names.clear
|
99
|
+
self.public_attribute_names.clear
|
100
|
+
self.protected_attribute_names.clear
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
module InstanceMethods
|
105
|
+
|
106
|
+
def save_with_dirty_tracking(*args)
|
107
|
+
if save_without_dirty_tracking(*args)
|
108
|
+
@previously_changed = self.changes
|
109
|
+
@changed_attributes.clear
|
110
|
+
return true
|
111
|
+
else
|
112
|
+
return false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_attributes_as_current(*attrs)
|
117
|
+
@changed_attributes.clear and return if attrs.blank?
|
118
|
+
attrs.each do |attr|
|
119
|
+
@changed_attributes.delete(attr.to_s)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def reset_attribute_changes(*attrs)
|
124
|
+
attrs = self.class.public_attribute_names if attrs.blank?
|
125
|
+
attrs.each do |attr|
|
126
|
+
self.send("reset_#{attr}!")
|
127
|
+
end
|
128
|
+
|
129
|
+
set_attributes_as_current(*attrs)
|
130
|
+
end
|
131
|
+
|
132
|
+
def attribute?(name)
|
133
|
+
self.class.attribute?(name)
|
134
|
+
end
|
135
|
+
|
136
|
+
def protected_attribute?(name)
|
137
|
+
self.class.protected_attribute?(name)
|
138
|
+
end
|
139
|
+
|
140
|
+
def respond_to?(sym)
|
141
|
+
if sym =~ /\?$/
|
142
|
+
return true if self.attribute?($`)
|
143
|
+
elsif sym =~ /=$/
|
144
|
+
return true if self.class.public_attribute_names.include?($`)
|
145
|
+
else
|
146
|
+
return true if self.attribute?(sym.to_sym)
|
147
|
+
end
|
148
|
+
super
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
@@ -0,0 +1,517 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
require 'active_support/string_inquirer'
|
4
|
+
|
5
|
+
module ApiResource
|
6
|
+
|
7
|
+
class Base
|
8
|
+
|
9
|
+
class_inheritable_accessor :site, :proxy, :user, :password, :auth_type, :format, :timeout, :ssl_options
|
10
|
+
|
11
|
+
class_inheritable_accessor :include_root_in_json; self.include_root_in_json = true
|
12
|
+
class_inheritable_accessor :include_blank_attributes_on_create; self.include_blank_attributes_on_create = false
|
13
|
+
class_inheritable_accessor :include_all_attributes_on_update; self.include_blank_attributes_on_create = false
|
14
|
+
|
15
|
+
attr_accessor_with_default :primary_key, 'id'
|
16
|
+
|
17
|
+
attr_accessor :prefix_options
|
18
|
+
|
19
|
+
class << self
|
20
|
+
|
21
|
+
attr_accessor_with_default(:element_name) { model_name.element }
|
22
|
+
attr_accessor_with_default(:collection_name) { ActiveSupport::Inflector.pluralize(element_name) }
|
23
|
+
|
24
|
+
|
25
|
+
def inherited(klass)
|
26
|
+
# Call the methods of the superclass to make sure inheritable accessors and the like have been inherited
|
27
|
+
super
|
28
|
+
# Now we need to define the inherited method on the klass that's doing the inheriting
|
29
|
+
# it calls super which will allow the chaining effect we need
|
30
|
+
klass.instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
31
|
+
def inherited(klass)
|
32
|
+
super
|
33
|
+
end
|
34
|
+
EOE
|
35
|
+
# Now we can make a call to setup the inheriting klass with its attributes
|
36
|
+
klass.set_class_attributes_upon_load unless klass.instance_variable_defined?(:@class_data)
|
37
|
+
klass.instance_variable_set(:@class_data, true)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# This makes a request to new_element_path
|
42
|
+
def set_class_attributes_upon_load
|
43
|
+
begin
|
44
|
+
class_data = self.connection.get(self.new_element_path)
|
45
|
+
# Attributes go first
|
46
|
+
if class_data["attributes"]
|
47
|
+
define_attributes *(class_data["attributes"]["public"] || [])
|
48
|
+
define_protected_attributes *(class_data["attributes"]["protected"] || [])
|
49
|
+
end
|
50
|
+
# Then scopes
|
51
|
+
if class_data["scopes"]
|
52
|
+
class_data["scopes"].each do |hash|
|
53
|
+
self.scope hash.first[0], hash.first[1]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
# Then associations
|
57
|
+
if class_data["associations"]
|
58
|
+
class_data["associations"].each do |(key,hash)|
|
59
|
+
hash.each do |assoc_name, assoc_options|
|
60
|
+
self.send(key, assoc_name, assoc_options)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
# Swallow up any loading errors because the site may be incorrect
|
65
|
+
rescue Exception => e
|
66
|
+
return nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def reload_class_attributes
|
71
|
+
# clear the public_attribute_names, protected_attribute_names
|
72
|
+
self.clear_attributes
|
73
|
+
self.clear_associations
|
74
|
+
self.set_class_attributes_upon_load
|
75
|
+
end
|
76
|
+
|
77
|
+
def site=(site)
|
78
|
+
@connection = nil
|
79
|
+
# reset class attributes and try to reload them if the site changed
|
80
|
+
unless site.to_s == self.site.to_s
|
81
|
+
self.reload_class_attributes
|
82
|
+
end
|
83
|
+
if site.nil?
|
84
|
+
write_inheritable_attribute(:site, nil)
|
85
|
+
else
|
86
|
+
write_inheritable_attribute(:site, create_site_uri_from(site))
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def format=(mime_type_or_format)
|
92
|
+
format = mime_type_or_format.is_a?(Symbol) ? ApiResource::Formats[mime_type_or_format] : mime_type_or_format
|
93
|
+
write_inheritable_attribute(:format, format)
|
94
|
+
self.connection.format = format if self.site
|
95
|
+
end
|
96
|
+
|
97
|
+
# Default format is json
|
98
|
+
def format
|
99
|
+
read_inheritable_attribute(:format) || ApiResource::Formats::JsonFormat
|
100
|
+
end
|
101
|
+
|
102
|
+
def timeout=(timeout)
|
103
|
+
@connection = nil
|
104
|
+
write_inheritable_attribute(:timeout, timeout)
|
105
|
+
end
|
106
|
+
|
107
|
+
def connection(refresh = false)
|
108
|
+
@connection = Connection.new(self.site, self.format) if refresh || @connection.nil?
|
109
|
+
@connection.timeout = self.timeout
|
110
|
+
@connection
|
111
|
+
end
|
112
|
+
|
113
|
+
def headers
|
114
|
+
@headers || {}
|
115
|
+
end
|
116
|
+
|
117
|
+
def prefix(options = {})
|
118
|
+
default = (self.site ? self.site.path : '/')
|
119
|
+
default << '/' unless default[-1..-1] == '/'
|
120
|
+
self.prefix = default
|
121
|
+
prefix(options)
|
122
|
+
end
|
123
|
+
|
124
|
+
def prefix_source
|
125
|
+
prefix
|
126
|
+
prefix_source
|
127
|
+
end
|
128
|
+
|
129
|
+
def prefix=(value = '/')
|
130
|
+
prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.escape options[#{key}].to_s}"}
|
131
|
+
@prefix_parameters = nil
|
132
|
+
silence_warnings do
|
133
|
+
instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
134
|
+
def prefix_source() "#{value}" end
|
135
|
+
def prefix(options={}) "#{prefix_call}" end
|
136
|
+
EOE
|
137
|
+
end
|
138
|
+
rescue Exception => e
|
139
|
+
logger.error "Couldn't set prefix: #{e}\n #{code}" if logger
|
140
|
+
raise
|
141
|
+
end
|
142
|
+
|
143
|
+
# alias_method :set_prefix, :prefix=
|
144
|
+
# alias_method :set_element_name, :element_name=
|
145
|
+
# alias_method :set_collection_name, :collection_name=
|
146
|
+
|
147
|
+
def element_path(id, prefix_options = {}, query_options = nil)
|
148
|
+
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
149
|
+
"#{prefix(prefix_options)}#{collection_name}/#{URI.escape id.to_s}.#{format.extension}#{query_string(query_options)}"
|
150
|
+
end
|
151
|
+
|
152
|
+
def new_element_path(prefix_options = {})
|
153
|
+
"#{prefix(prefix_options)}#{collection_name}/new.#{format.extension}"
|
154
|
+
end
|
155
|
+
|
156
|
+
def collection_path(prefix_options = {}, query_options = nil)
|
157
|
+
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
158
|
+
"#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
|
159
|
+
end
|
160
|
+
|
161
|
+
def build(attributes = {})
|
162
|
+
self.new(attributes)
|
163
|
+
end
|
164
|
+
|
165
|
+
def create(attributes = {})
|
166
|
+
self.new(attributes).tap{ |resource| resource.save }
|
167
|
+
end
|
168
|
+
|
169
|
+
def find(*arguments)
|
170
|
+
scope = arguments.slice!(0)
|
171
|
+
options = arguments.slice!(0) || {}
|
172
|
+
|
173
|
+
case scope
|
174
|
+
when :all then find_every(options)
|
175
|
+
when :first then find_every(options).first
|
176
|
+
when :last then find_every(options).last
|
177
|
+
when :one then find_one(options)
|
178
|
+
else find_single(scope, options)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass
|
184
|
+
# in all the same arguments to this method as you can to
|
185
|
+
# <tt>find(:first)</tt>.
|
186
|
+
def first(*args)
|
187
|
+
find(:first, *args)
|
188
|
+
end
|
189
|
+
|
190
|
+
# A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass
|
191
|
+
# in all the same arguments to this method as you can to
|
192
|
+
# <tt>find(:last)</tt>.
|
193
|
+
def last(*args)
|
194
|
+
find(:last, *args)
|
195
|
+
end
|
196
|
+
|
197
|
+
# This is an alias for find(:all). You can pass in all the same
|
198
|
+
# arguments to this method as you can to <tt>find(:all)</tt>
|
199
|
+
def all(*args)
|
200
|
+
find(:all, *args)
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
# Deletes the resources with the ID in the +id+ parameter.
|
205
|
+
#
|
206
|
+
# ==== Options
|
207
|
+
# All options specify \prefix and query parameters.
|
208
|
+
#
|
209
|
+
# ==== Examples
|
210
|
+
# Event.delete(2) # sends DELETE /events/2
|
211
|
+
#
|
212
|
+
# Event.create(:name => 'Free Concert', :location => 'Community Center')
|
213
|
+
# my_event = Event.find(:first) # let's assume this is event with ID 7
|
214
|
+
# Event.delete(my_event.id) # sends DELETE /events/7
|
215
|
+
#
|
216
|
+
# # Let's assume a request to events/5/cancel.xml
|
217
|
+
# Event.delete(params[:id]) # sends DELETE /events/5
|
218
|
+
def delete(id, options = {})
|
219
|
+
connection.delete(element_path(id, options))
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
# Find every resource
|
224
|
+
def find_every(options)
|
225
|
+
begin
|
226
|
+
case from = options[:from]
|
227
|
+
when Symbol
|
228
|
+
instantiate_collection(get(from, options[:params]))
|
229
|
+
when String
|
230
|
+
path = "#{from}#{query_string(options[:params])}"
|
231
|
+
instantiate_collection(connection.get(path, headers) || [])
|
232
|
+
else
|
233
|
+
prefix_options, query_options = split_options(options[:params])
|
234
|
+
path = collection_path(prefix_options, query_options)
|
235
|
+
instantiate_collection( (connection.get(path, headers) || []), prefix_options )
|
236
|
+
end
|
237
|
+
rescue ApiResource::ResourceNotFound
|
238
|
+
# Swallowing ResourceNotFound exceptions and return nil - as per
|
239
|
+
# ActiveRecord.
|
240
|
+
nil
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Find a single resource from a one-off URL
|
245
|
+
def find_one(options)
|
246
|
+
case from = options[:from]
|
247
|
+
when Symbol
|
248
|
+
instantiate_record(get(from, options[:params]))
|
249
|
+
when String
|
250
|
+
path = "#{from}#{query_string(options[:params])}"
|
251
|
+
instantiate_record(connection.get(path, headers))
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Find a single resource from the default URL
|
256
|
+
def find_single(scope, options)
|
257
|
+
prefix_options, query_options = split_options(options[:params])
|
258
|
+
path = element_path(scope, prefix_options, query_options)
|
259
|
+
instantiate_record(connection.get(path, headers), prefix_options)
|
260
|
+
end
|
261
|
+
|
262
|
+
def instantiate_collection(collection, prefix_options = {})
|
263
|
+
collection.collect! { |record| instantiate_record(record, prefix_options) }
|
264
|
+
end
|
265
|
+
|
266
|
+
def instantiate_record(record, prefix_options = {})
|
267
|
+
new(record).tap do |resource|
|
268
|
+
resource.prefix_options = prefix_options
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
|
273
|
+
# Accepts a URI and creates the site URI from that.
|
274
|
+
def create_site_uri_from(site)
|
275
|
+
site.is_a?(URI) ? site.dup : uri_parser.parse(site)
|
276
|
+
end
|
277
|
+
|
278
|
+
# Accepts a URI and creates the proxy URI from that.
|
279
|
+
def create_proxy_uri_from(proxy)
|
280
|
+
proxy.is_a?(URI) ? proxy.dup : uri_parser.parse(proxy)
|
281
|
+
end
|
282
|
+
|
283
|
+
# contains a set of the current prefix parameters.
|
284
|
+
def prefix_parameters
|
285
|
+
@prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
|
286
|
+
end
|
287
|
+
|
288
|
+
# Builds the query string for the request.
|
289
|
+
def query_string(options)
|
290
|
+
"?#{options.to_query}" unless options.nil? || options.empty?
|
291
|
+
end
|
292
|
+
|
293
|
+
# split an option hash into two hashes, one containing the prefix options,
|
294
|
+
# and the other containing the leftovers.
|
295
|
+
def split_options(options = {})
|
296
|
+
prefix_options, query_options = {}, {}
|
297
|
+
(options || {}).each do |key, value|
|
298
|
+
next if key.blank?
|
299
|
+
(prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
|
300
|
+
end
|
301
|
+
|
302
|
+
[ prefix_options, query_options ]
|
303
|
+
end
|
304
|
+
|
305
|
+
def uri_parser
|
306
|
+
@uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
307
|
+
end
|
308
|
+
|
309
|
+
end
|
310
|
+
|
311
|
+
def initialize(attributes = {})
|
312
|
+
@prefix_options = {}
|
313
|
+
load(attributes)
|
314
|
+
end
|
315
|
+
|
316
|
+
def new?
|
317
|
+
id.nil?
|
318
|
+
end
|
319
|
+
alias :new_record? :new?
|
320
|
+
|
321
|
+
def persisted?
|
322
|
+
!new?
|
323
|
+
end
|
324
|
+
|
325
|
+
def id
|
326
|
+
self.attributes[self.class.primary_key]
|
327
|
+
end
|
328
|
+
|
329
|
+
# Bypass dirty tracking for this field
|
330
|
+
def id=(id)
|
331
|
+
attributes[self.class.primary_key] = id
|
332
|
+
end
|
333
|
+
|
334
|
+
def ==(other)
|
335
|
+
other.equal?(self) || (other.instance_of?(self.class) && other.id == self.id && other.prefix_options == self.prefix_options)
|
336
|
+
end
|
337
|
+
|
338
|
+
def eql?(other)
|
339
|
+
self == other
|
340
|
+
end
|
341
|
+
|
342
|
+
def hash
|
343
|
+
id.hash
|
344
|
+
end
|
345
|
+
|
346
|
+
def dup
|
347
|
+
self.class.new.tap do |resource|
|
348
|
+
resource.attributes = self.attributes
|
349
|
+
resource.prefix_options = @prefix_options
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def save(*args)
|
354
|
+
new? ? create(*args) : update(*args)
|
355
|
+
end
|
356
|
+
|
357
|
+
def save!(*args)
|
358
|
+
save(*args) || raise(ApiResource::ResourceInvalid.new(self))
|
359
|
+
end
|
360
|
+
|
361
|
+
def destroy
|
362
|
+
connection.delete(element_path(self.id), self.class.headers)
|
363
|
+
end
|
364
|
+
|
365
|
+
def encode(options = {})
|
366
|
+
self.send("to_#{self.class.format.extension}", options)
|
367
|
+
end
|
368
|
+
|
369
|
+
def reload
|
370
|
+
self.load(self.class.find(to_param, :params => @prefix_options).attributes)
|
371
|
+
end
|
372
|
+
|
373
|
+
def load(attributes)
|
374
|
+
return if attributes.nil?
|
375
|
+
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
|
376
|
+
@prefix_options, attributes = split_options(attributes)
|
377
|
+
|
378
|
+
attributes.symbolize_keys.each do |key, value|
|
379
|
+
# If this attribute doesn't exist define it as a protected attribute
|
380
|
+
self.class.define_protected_attributes(key) unless self.respond_to?(key)
|
381
|
+
self.attributes[key] =
|
382
|
+
case value
|
383
|
+
when Array
|
384
|
+
if self.has_many?(key)
|
385
|
+
MultiObjectProxy.new(self.has_many_class_name(key), value)
|
386
|
+
elsif self.association?(key)
|
387
|
+
raise ArgumentError, "Expected a hash value or nil, got: #{value.inspect}"
|
388
|
+
else
|
389
|
+
value
|
390
|
+
end
|
391
|
+
when Hash
|
392
|
+
if self.has_many?(key)
|
393
|
+
MultiObjectProxy.new(self.has_many_class_name(key), value)
|
394
|
+
elsif self.association?(key)
|
395
|
+
SingleObjectProxy.new(self.association_class_name(key), value)
|
396
|
+
else
|
397
|
+
value
|
398
|
+
end
|
399
|
+
when NilClass
|
400
|
+
# If it's nil and an association then create a blank object
|
401
|
+
if self.has_many?(key)
|
402
|
+
return MultiObjectProxy.new(self.has_many_class_name(key), [])
|
403
|
+
elsif self.association?(key)
|
404
|
+
SingleObjectProxy.new(self.association_class_name(key), value)
|
405
|
+
end
|
406
|
+
else
|
407
|
+
raise ArgumentError, "expected an array or a hash for the association #{key}, got: #{value.inspect}" if self.association?(key)
|
408
|
+
value.dup rescue value
|
409
|
+
end
|
410
|
+
end
|
411
|
+
return self
|
412
|
+
end
|
413
|
+
|
414
|
+
# Override to_s and inspect so they only show attributes
|
415
|
+
# and not associations, this prevents force loading of associations
|
416
|
+
# when we call to_s or inspect on a descendent of base but allows it if we
|
417
|
+
# try to evaluate an association directly
|
418
|
+
def to_s
|
419
|
+
return "#<#{self.class}:0x%08x @attributes=#{self.attributes.inject({}){|accum,(k,v)| self.association?(k) ? accum : accum.merge(k => v)}}" % (self.object_id * 2)
|
420
|
+
end
|
421
|
+
|
422
|
+
alias_method :inspect, :to_s
|
423
|
+
|
424
|
+
# Methods for serialization as json or xml, relying on the serializable_hash method
|
425
|
+
def to_xml(options = {})
|
426
|
+
self.serializable_hash(options).to_xml(:root => self.class.element_name)
|
427
|
+
end
|
428
|
+
|
429
|
+
def to_json(options = {})
|
430
|
+
self.class.include_root_in_json ? {self.class.element_name => self.serializable_hash(options)}.to_json : self.serializable_hash(options).to_json
|
431
|
+
end
|
432
|
+
|
433
|
+
def serializable_hash(options = {})
|
434
|
+
options[:include_associations] = options[:include_associations] ? options[:include_associations].symbolize_array : []
|
435
|
+
options[:include_extras] = options[:include_extras] ? options[:include_extras].symbolize_array : []
|
436
|
+
options[:except] ||= []
|
437
|
+
ret = self.attributes.inject({}) do |accum, (key,val)|
|
438
|
+
# If this is an association and it's in include_associations then include it
|
439
|
+
if self.association?(key) && options[:include_associations].include?(key.to_sym)
|
440
|
+
accum.merge(key => val.serializable_hash({}))
|
441
|
+
elsif options[:include_extras].include?(key.to_sym)
|
442
|
+
accum.merge(key => val)
|
443
|
+
elsif options[:except].include?(key.to_sym)
|
444
|
+
accum
|
445
|
+
else
|
446
|
+
self.association?(key) || !self.attribute?(key) || self.protected_attribute?(key) ? accum : accum.merge(key => val)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
protected
|
452
|
+
def connection(refresh = false)
|
453
|
+
self.class.connection(refresh)
|
454
|
+
end
|
455
|
+
|
456
|
+
def load_attributes_from_response(response)
|
457
|
+
load(response)
|
458
|
+
end
|
459
|
+
|
460
|
+
def element_path(id, prefix_options = {}, query_options = nil)
|
461
|
+
self.class.element_path(id, prefix_options, query_options)
|
462
|
+
end
|
463
|
+
|
464
|
+
def new_element_path(prefix_options = {})
|
465
|
+
self.class.new_element_path(prefix_options)
|
466
|
+
end
|
467
|
+
|
468
|
+
def collection_path(prefix_options = {},query_options = nil)
|
469
|
+
self.class.collection_path(prefix_options, query_options)
|
470
|
+
end
|
471
|
+
|
472
|
+
def create(*args)
|
473
|
+
opts = args.extract_options!
|
474
|
+
# When we create we should not include any blank attributes unless they are associations
|
475
|
+
except = self.class.include_blank_attributes_on_create ? {} : self.attributes.select{|k,v| v.blank?}
|
476
|
+
opts[:except] = opts[:except] ? opts[:except].concat(except.keys).uniq.symbolize_array : except.keys.symbolize_array
|
477
|
+
opts[:include_associations] = opts[:include_associations] ? opts[:include_associations].concat(args) : []
|
478
|
+
opts[:include_extras] ||= []
|
479
|
+
body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
|
480
|
+
connection.post(collection_path, body, self.class.headers).tap do |response|
|
481
|
+
load_attributes_from_response(response)
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def update(*args)
|
486
|
+
opts = args.extract_options!
|
487
|
+
# When we create we should not include any blank attributes
|
488
|
+
except = self.class.attribute_names - self.changed.symbolize_array
|
489
|
+
changed_associations = self.changed.symbolize_array.select{|item| self.association?(item)}
|
490
|
+
opts[:except] = opts[:except] ? opts[:except].concat(except).uniq.symbolize_array : except.symbolize_array
|
491
|
+
opts[:include_associations] = opts[:include_associations] ? opts[:include_associations].concat(args).concat(changed_associations).uniq : changed_associations.concat(args)
|
492
|
+
opts[:include_extras] ||= []
|
493
|
+
opts[:except] = [:id] if self.class.include_all_attributes_on_update
|
494
|
+
body = RestClient::Payload.has_file?(self.attributes) ? self.serializable_hash(opts) : encode(opts)
|
495
|
+
# We can just ignore the response
|
496
|
+
connection.put(element_path(self.id, prefix_options), body, self.class.headers).tap do |response|
|
497
|
+
load_attributes_from_response(response)
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
private
|
502
|
+
|
503
|
+
def split_options(options = {})
|
504
|
+
self.class.__send__(:split_options, options)
|
505
|
+
end
|
506
|
+
|
507
|
+
end
|
508
|
+
|
509
|
+
class Base
|
510
|
+
extend ActiveModel::Naming
|
511
|
+
# Order is important here
|
512
|
+
# It should be Validations, Dirty Tracking, Callbacks so the include order is the opposite
|
513
|
+
include Associations, Callbacks, Attributes, ModelErrors
|
514
|
+
|
515
|
+
end
|
516
|
+
|
517
|
+
end
|