api_resource 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. data/Gemfile +37 -0
  2. data/Gemfile.lock +190 -0
  3. data/Guardfile +27 -0
  4. data/Rakefile +49 -0
  5. data/VERSION +1 -0
  6. data/api_resource.gemspec +111 -0
  7. data/coverage/assets/0.5.3/app.js +88 -0
  8. data/coverage/assets/0.5.3/fancybox/blank.gif +0 -0
  9. data/coverage/assets/0.5.3/fancybox/fancy_close.png +0 -0
  10. data/coverage/assets/0.5.3/fancybox/fancy_loading.png +0 -0
  11. data/coverage/assets/0.5.3/fancybox/fancy_nav_left.png +0 -0
  12. data/coverage/assets/0.5.3/fancybox/fancy_nav_right.png +0 -0
  13. data/coverage/assets/0.5.3/fancybox/fancy_shadow_e.png +0 -0
  14. data/coverage/assets/0.5.3/fancybox/fancy_shadow_n.png +0 -0
  15. data/coverage/assets/0.5.3/fancybox/fancy_shadow_ne.png +0 -0
  16. data/coverage/assets/0.5.3/fancybox/fancy_shadow_nw.png +0 -0
  17. data/coverage/assets/0.5.3/fancybox/fancy_shadow_s.png +0 -0
  18. data/coverage/assets/0.5.3/fancybox/fancy_shadow_se.png +0 -0
  19. data/coverage/assets/0.5.3/fancybox/fancy_shadow_sw.png +0 -0
  20. data/coverage/assets/0.5.3/fancybox/fancy_shadow_w.png +0 -0
  21. data/coverage/assets/0.5.3/fancybox/fancy_title_left.png +0 -0
  22. data/coverage/assets/0.5.3/fancybox/fancy_title_main.png +0 -0
  23. data/coverage/assets/0.5.3/fancybox/fancy_title_over.png +0 -0
  24. data/coverage/assets/0.5.3/fancybox/fancy_title_right.png +0 -0
  25. data/coverage/assets/0.5.3/fancybox/fancybox-x.png +0 -0
  26. data/coverage/assets/0.5.3/fancybox/fancybox-y.png +0 -0
  27. data/coverage/assets/0.5.3/fancybox/fancybox.png +0 -0
  28. data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.css +363 -0
  29. data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.pack.js +44 -0
  30. data/coverage/assets/0.5.3/favicon_green.png +0 -0
  31. data/coverage/assets/0.5.3/favicon_red.png +0 -0
  32. data/coverage/assets/0.5.3/favicon_yellow.png +0 -0
  33. data/coverage/assets/0.5.3/highlight.css +129 -0
  34. data/coverage/assets/0.5.3/highlight.pack.js +1 -0
  35. data/coverage/assets/0.5.3/jquery-1.6.2.min.js +18 -0
  36. data/coverage/assets/0.5.3/jquery.dataTables.min.js +152 -0
  37. data/coverage/assets/0.5.3/jquery.timeago.js +141 -0
  38. data/coverage/assets/0.5.3/jquery.url.js +174 -0
  39. data/coverage/assets/0.5.3/loading.gif +0 -0
  40. data/coverage/assets/0.5.3/magnify.png +0 -0
  41. data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  42. data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  43. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  44. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  45. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  46. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  47. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  48. data/coverage/assets/0.5.3/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  49. data/coverage/assets/0.5.3/smoothness/images/ui-icons_222222_256x240.png +0 -0
  50. data/coverage/assets/0.5.3/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  51. data/coverage/assets/0.5.3/smoothness/images/ui-icons_454545_256x240.png +0 -0
  52. data/coverage/assets/0.5.3/smoothness/images/ui-icons_888888_256x240.png +0 -0
  53. data/coverage/assets/0.5.3/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  54. data/coverage/assets/0.5.3/smoothness/jquery-ui-1.8.4.custom.css +295 -0
  55. data/coverage/assets/0.5.3/stylesheet.css +383 -0
  56. data/coverage/index.html +3573 -0
  57. data/lib/api_resource.rb +130 -0
  58. data/lib/api_resource/association_activation.rb +19 -0
  59. data/lib/api_resource/associations.rb +218 -0
  60. data/lib/api_resource/associations/association_proxy.rb +116 -0
  61. data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +16 -0
  62. data/lib/api_resource/associations/dynamic_resource_scope.rb +23 -0
  63. data/lib/api_resource/associations/generic_scope.rb +68 -0
  64. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +16 -0
  65. data/lib/api_resource/associations/has_many_through_remote_object_proxy.rb +13 -0
  66. data/lib/api_resource/associations/has_one_remote_object_proxy.rb +24 -0
  67. data/lib/api_resource/associations/multi_argument_resource_scope.rb +15 -0
  68. data/lib/api_resource/associations/multi_object_proxy.rb +84 -0
  69. data/lib/api_resource/associations/related_object_hash.rb +12 -0
  70. data/lib/api_resource/associations/relation_scope.rb +25 -0
  71. data/lib/api_resource/associations/resource_scope.rb +32 -0
  72. data/lib/api_resource/associations/scope.rb +132 -0
  73. data/lib/api_resource/associations/single_object_proxy.rb +82 -0
  74. data/lib/api_resource/attributes.rb +243 -0
  75. data/lib/api_resource/base.rb +717 -0
  76. data/lib/api_resource/callbacks.rb +45 -0
  77. data/lib/api_resource/connection.rb +195 -0
  78. data/lib/api_resource/core_extensions.rb +7 -0
  79. data/lib/api_resource/custom_methods.rb +117 -0
  80. data/lib/api_resource/decorators.rb +6 -0
  81. data/lib/api_resource/decorators/caching_decorator.rb +20 -0
  82. data/lib/api_resource/exceptions.rb +99 -0
  83. data/lib/api_resource/formats.rb +22 -0
  84. data/lib/api_resource/formats/json_format.rb +25 -0
  85. data/lib/api_resource/formats/xml_format.rb +36 -0
  86. data/lib/api_resource/local.rb +12 -0
  87. data/lib/api_resource/log_subscriber.rb +15 -0
  88. data/lib/api_resource/mocks.rb +285 -0
  89. data/lib/api_resource/model_errors.rb +82 -0
  90. data/lib/api_resource/observing.rb +27 -0
  91. data/lib/api_resource/railtie.rb +24 -0
  92. data/lib/api_resource/scopes.rb +48 -0
  93. data/nohup.out +63 -0
  94. data/spec/lib/api_resource_spec.rb +43 -0
  95. data/spec/lib/associations_spec.rb +751 -0
  96. data/spec/lib/attributes_spec.rb +191 -0
  97. data/spec/lib/base_spec.rb +655 -0
  98. data/spec/lib/callbacks_spec.rb +68 -0
  99. data/spec/lib/connection_spec.rb +137 -0
  100. data/spec/lib/local_spec.rb +20 -0
  101. data/spec/lib/mocks_spec.rb +74 -0
  102. data/spec/lib/model_errors_spec.rb +29 -0
  103. data/spec/lib/prefixes_spec.rb +107 -0
  104. data/spec/spec_helper.rb +82 -0
  105. data/spec/support/mocks/association_mocks.rb +63 -0
  106. data/spec/support/mocks/error_resource_mocks.rb +21 -0
  107. data/spec/support/mocks/prefix_model_mocks.rb +5 -0
  108. data/spec/support/mocks/test_resource_mocks.rb +44 -0
  109. data/spec/support/requests/association_requests.rb +31 -0
  110. data/spec/support/requests/error_resource_requests.rb +25 -0
  111. data/spec/support/requests/prefix_model_requests.rb +7 -0
  112. data/spec/support/requests/test_resource_requests.rb +38 -0
  113. data/spec/support/test_resource.rb +72 -0
  114. data/spec/tmp/DIR +0 -0
  115. data/spec/tmp/api_resource_test_db.sqlite +0 -0
  116. metadata +119 -3
@@ -0,0 +1,82 @@
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
@@ -0,0 +1,243 @@
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
@@ -0,0 +1,717 @@
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