herr 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +15 -0
  5. data/.yardopts +2 -0
  6. data/CONTRIBUTING.md +26 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE +7 -0
  9. data/README.md +990 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +81 -0
  12. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  13. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  16. data/her.gemspec +30 -0
  17. data/lib/her.rb +16 -0
  18. data/lib/her/api.rb +115 -0
  19. data/lib/her/collection.rb +12 -0
  20. data/lib/her/errors.rb +27 -0
  21. data/lib/her/middleware.rb +10 -0
  22. data/lib/her/middleware/accept_json.rb +17 -0
  23. data/lib/her/middleware/first_level_parse_json.rb +36 -0
  24. data/lib/her/middleware/parse_json.rb +21 -0
  25. data/lib/her/middleware/second_level_parse_json.rb +36 -0
  26. data/lib/her/model.rb +72 -0
  27. data/lib/her/model/associations.rb +141 -0
  28. data/lib/her/model/associations/association.rb +103 -0
  29. data/lib/her/model/associations/association_proxy.rb +46 -0
  30. data/lib/her/model/associations/belongs_to_association.rb +96 -0
  31. data/lib/her/model/associations/has_many_association.rb +100 -0
  32. data/lib/her/model/associations/has_one_association.rb +79 -0
  33. data/lib/her/model/attributes.rb +266 -0
  34. data/lib/her/model/base.rb +33 -0
  35. data/lib/her/model/deprecated_methods.rb +61 -0
  36. data/lib/her/model/http.rb +114 -0
  37. data/lib/her/model/introspection.rb +65 -0
  38. data/lib/her/model/nested_attributes.rb +45 -0
  39. data/lib/her/model/orm.rb +205 -0
  40. data/lib/her/model/parse.rb +227 -0
  41. data/lib/her/model/paths.rb +121 -0
  42. data/lib/her/model/relation.rb +164 -0
  43. data/lib/her/version.rb +3 -0
  44. data/spec/api_spec.rb +131 -0
  45. data/spec/collection_spec.rb +26 -0
  46. data/spec/middleware/accept_json_spec.rb +10 -0
  47. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  48. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  49. data/spec/model/associations_spec.rb +416 -0
  50. data/spec/model/attributes_spec.rb +268 -0
  51. data/spec/model/callbacks_spec.rb +145 -0
  52. data/spec/model/dirty_spec.rb +86 -0
  53. data/spec/model/http_spec.rb +194 -0
  54. data/spec/model/introspection_spec.rb +76 -0
  55. data/spec/model/nested_attributes_spec.rb +134 -0
  56. data/spec/model/orm_spec.rb +479 -0
  57. data/spec/model/parse_spec.rb +373 -0
  58. data/spec/model/paths_spec.rb +341 -0
  59. data/spec/model/relation_spec.rb +226 -0
  60. data/spec/model/validations_spec.rb +42 -0
  61. data/spec/model_spec.rb +31 -0
  62. data/spec/spec_helper.rb +26 -0
  63. data/spec/support/extensions/array.rb +5 -0
  64. data/spec/support/extensions/hash.rb +5 -0
  65. data/spec/support/macros/her_macros.rb +17 -0
  66. data/spec/support/macros/model_macros.rb +29 -0
  67. data/spec/support/macros/request_macros.rb +27 -0
  68. metadata +280 -0
@@ -0,0 +1,100 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class HasManyAssociation < Association
5
+
6
+ # @private
7
+ def self.attach(klass, name, opts)
8
+ opts = {
9
+ :class_name => name.to_s.classify,
10
+ :name => name,
11
+ :data_key => name,
12
+ :default => Her::Collection.new,
13
+ :path => "/#{name}",
14
+ :inverse_of => nil
15
+ }.merge(opts)
16
+ klass.associations[:has_many] << opts
17
+
18
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
+ def #{name}
20
+ cached_name = :"@_her_association_#{name}"
21
+
22
+ cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
23
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.proxy(self, #{opts.inspect}))
24
+ end
25
+ RUBY
26
+ end
27
+
28
+ # @private
29
+ def self.parse(association, klass, data)
30
+ data_key = association[:data_key]
31
+ return {} unless data[data_key]
32
+
33
+ klass = klass.her_nearby_class(association[:class_name])
34
+ { association[:name] => Her::Model::Attributes.initialize_collection(klass, :data => data[data_key]) }
35
+ end
36
+
37
+ # Initialize a new object with a foreign key to the parent
38
+ #
39
+ # @example
40
+ # class User
41
+ # include Her::Model
42
+ # has_many :comments
43
+ # end
44
+ #
45
+ # class Comment
46
+ # include Her::Model
47
+ # end
48
+ #
49
+ # user = User.find(1)
50
+ # new_comment = user.comments.build(:body => "Hello!")
51
+ # new_comment # => #<Comment user_id=1 body="Hello!">
52
+ # TODO: This only merges the id of the parents, handle the case
53
+ # where this is more deeply nested
54
+ def build(attributes = {})
55
+ @klass.build(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
56
+ end
57
+
58
+ # Create a new object, save it and add it to the associated collection
59
+ #
60
+ # @example
61
+ # class User
62
+ # include Her::Model
63
+ # has_many :comments
64
+ # end
65
+ #
66
+ # class Comment
67
+ # include Her::Model
68
+ # end
69
+ #
70
+ # user = User.find(1)
71
+ # user.comments.create(:body => "Hello!")
72
+ # user.comments # => [#<Comment id=2 user_id=1 body="Hello!">]
73
+ def create(attributes = {})
74
+ resource = build(attributes)
75
+
76
+ if resource.save
77
+ @parent.attributes[@name] ||= Her::Collection.new
78
+ @parent.attributes[@name] << resource
79
+ end
80
+
81
+ resource
82
+ end
83
+
84
+ # @private
85
+ def fetch
86
+ super.tap do |o|
87
+ inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
88
+ o.each { |entry| entry.send("#{inverse_of}=", @parent) }
89
+ end
90
+ end
91
+
92
+ # @private
93
+ def assign_nested_attributes(attributes)
94
+ data = attributes.is_a?(Hash) ? attributes.values : attributes
95
+ @parent.attributes[@name] = Her::Model::Attributes.initialize_collection(@klass, :data => data)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,79 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class HasOneAssociation < Association
5
+
6
+ # @private
7
+ def self.attach(klass, name, opts)
8
+ opts = {
9
+ :class_name => name.to_s.classify,
10
+ :name => name,
11
+ :data_key => name,
12
+ :default => nil,
13
+ :path => "/#{name}"
14
+ }.merge(opts)
15
+ klass.associations[:has_one] << opts
16
+
17
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
18
+ def #{name}
19
+ cached_name = :"@_her_association_#{name}"
20
+
21
+ cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
22
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasOneAssociation.proxy(self, #{opts.inspect}))
23
+ end
24
+ RUBY
25
+ end
26
+
27
+ # @private
28
+ def self.parse(*args)
29
+ parse_single(*args)
30
+ end
31
+
32
+ # Initialize a new object with a foreign key to the parent
33
+ #
34
+ # @example
35
+ # class User
36
+ # include Her::Model
37
+ # has_one :role
38
+ # end
39
+ #
40
+ # class Role
41
+ # include Her::Model
42
+ # end
43
+ #
44
+ # user = User.find(1)
45
+ # new_role = user.role.build(:title => "moderator")
46
+ # new_role # => #<Role user_id=1 title="moderator">
47
+ def build(attributes = {})
48
+ @klass.build(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
49
+ end
50
+
51
+ # Create a new object, save it and associate it to the parent
52
+ #
53
+ # @example
54
+ # class User
55
+ # include Her::Model
56
+ # has_one :role
57
+ # end
58
+ #
59
+ # class Role
60
+ # include Her::Model
61
+ # end
62
+ #
63
+ # user = User.find(1)
64
+ # user.role.create(:title => "moderator")
65
+ # user.role # => #<Role id=2 user_id=1 title="moderator">
66
+ def create(attributes = {})
67
+ resource = build(attributes)
68
+ @parent.attributes[@name] = resource if resource.save
69
+ resource
70
+ end
71
+
72
+ # @private
73
+ def assign_nested_attributes(attributes)
74
+ assign_single_nested_attributes(attributes)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,266 @@
1
+ module Her
2
+ module Model
3
+ # This module handles all methods related to model attributes
4
+ module Attributes
5
+ extend ActiveSupport::Concern
6
+
7
+ # Initialize a new object with data
8
+ #
9
+ # @param [Hash] attributes The attributes to initialize the object with
10
+ # @option attributes [Hash,Array] :_metadata
11
+ # @option attributes [Hash,Array] :_errors
12
+ # @option attributes [Boolean] :_destroyed
13
+ #
14
+ # @example
15
+ # class User
16
+ # include Her::Model
17
+ # end
18
+ #
19
+ # User.new(name: "Tobias") # => #<User name="Tobias">
20
+ def initialize(attributes={})
21
+ attributes ||= {}
22
+ @metadata = attributes.delete(:_metadata) || {}
23
+ @response_errors = attributes.delete(:_errors) || {}
24
+ @destroyed = attributes.delete(:_destroyed) || false
25
+
26
+ attributes = self.class.default_scope.apply_to(attributes)
27
+ assign_attributes(attributes)
28
+ run_callbacks :initialize
29
+ end
30
+
31
+ # Initialize a collection of resources
32
+ #
33
+ # @private
34
+ def self.initialize_collection(klass, parsed_data={})
35
+ collection_data = klass.extract_array(parsed_data).map do |item_data|
36
+ if item_data.kind_of?(klass)
37
+ resource = item_data
38
+ else
39
+ resource = klass.new(klass.parse(item_data))
40
+ resource.run_callbacks :find
41
+ end
42
+ resource
43
+ end
44
+ Her::Collection.new(collection_data, parsed_data[:metadata], parsed_data[:errors])
45
+ end
46
+
47
+ # Use setter methods of model for each key / value pair in params
48
+ # Return key / value pairs for which no setter method was defined on the model
49
+ #
50
+ # @private
51
+ def self.use_setter_methods(model, params)
52
+ params ||= {}
53
+
54
+ reserved_keys = [:id, model.class.primary_key] + model.class.association_keys
55
+ model.class.attributes *params.keys.reject { |k| reserved_keys.include?(k) || reserved_keys.map(&:to_s).include?(k) }
56
+
57
+ setter_method_names = model.class.setter_method_names
58
+ params.inject({}) do |memo, (key, value)|
59
+ setter_method = key.to_s + '='
60
+ if setter_method_names.include?(setter_method)
61
+ model.send(setter_method, value)
62
+ else
63
+ key = key.to_sym if key.is_a?(String)
64
+ memo[key] = value
65
+ end
66
+ memo
67
+ end
68
+ end
69
+
70
+ # Handles missing methods
71
+ #
72
+ # @private
73
+ def method_missing(method, *args, &blk)
74
+ if method.to_s =~ /[?=]$/ || @attributes.include?(method)
75
+ # Extract the attribute
76
+ attribute = method.to_s.sub(/[?=]$/, '')
77
+
78
+ # Create a new `attribute` methods set
79
+ self.class.attributes(*attribute)
80
+
81
+ # Resend the method!
82
+ send(method, *args, &blk)
83
+ else
84
+ super
85
+ end
86
+ end
87
+
88
+ # @private
89
+ def respond_to_missing?(method, include_private = false)
90
+ method.to_s.end_with?('=') || method.to_s.end_with?('?') || @attributes.include?(method) || super
91
+ end
92
+
93
+ # Assign new attributes to a resource
94
+ #
95
+ # @example
96
+ # class User
97
+ # include Her::Model
98
+ # end
99
+ #
100
+ # user = User.find(1) # => #<User id=1 name="Tobias">
101
+ # user.assign_attributes(name: "Lindsay")
102
+ # user.changes # => { :name => ["Tobias", "Lindsay"] }
103
+ def assign_attributes(new_attributes)
104
+ @attributes ||= attributes
105
+ # Use setter methods first
106
+ unset_attributes = Her::Model::Attributes.use_setter_methods(self, new_attributes)
107
+
108
+ # Then translate attributes of associations into association instances
109
+ parsed_attributes = self.class.parse_associations(unset_attributes)
110
+
111
+ # Then merge the parsed_data into @attributes.
112
+ @attributes.merge!(parsed_attributes)
113
+ end
114
+ alias attributes= assign_attributes
115
+
116
+ def attributes
117
+ @attributes ||= HashWithIndifferentAccess.new
118
+ end
119
+
120
+ # Handles returning true for the accessible attributes
121
+ #
122
+ # @private
123
+ def has_attribute?(attribute_name)
124
+ @attributes.include?(attribute_name)
125
+ end
126
+
127
+ # Handles returning data for a specific attribute
128
+ #
129
+ # @private
130
+ def get_attribute(attribute_name)
131
+ @attributes[attribute_name]
132
+ end
133
+ alias attribute get_attribute
134
+
135
+ # Return the value of the model `primary_key` attribute
136
+ def id
137
+ @attributes[self.class.primary_key]
138
+ end
139
+
140
+ # Return `true` if the other object is also a Her::Model and has matching data
141
+ #
142
+ # @private
143
+ def ==(other)
144
+ other.is_a?(Her::Model) && @attributes == other.attributes
145
+ end
146
+
147
+ # Delegate to the == method
148
+ #
149
+ # @private
150
+ def eql?(other)
151
+ self == other
152
+ end
153
+
154
+ # Delegate to @attributes, allowing models to act correctly in code like:
155
+ # [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
156
+ # @private
157
+ def hash
158
+ @attributes.hash
159
+ end
160
+
161
+ module ClassMethods
162
+ # Initialize a collection of resources with raw data from an HTTP request
163
+ #
164
+ # @param [Array] parsed_data
165
+ # @private
166
+ def new_collection(parsed_data)
167
+ Her::Model::Attributes.initialize_collection(self, parsed_data)
168
+ end
169
+
170
+ # Initialize a new object with the "raw" parsed_data from the parsing middleware
171
+ #
172
+ # @private
173
+ def new_from_parsed_data(parsed_data)
174
+ parsed_data = parsed_data.with_indifferent_access
175
+ parsed = parse(parsed_data[:data])
176
+ new(parsed.merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
177
+ end
178
+
179
+ # Define the attributes that will be used to track dirty attributes and validations
180
+ #
181
+ # @param [Array] attributes
182
+ # @example
183
+ # class User
184
+ # include Her::Model
185
+ # attributes :name, :email
186
+ # end
187
+ def attributes(*attributes)
188
+ define_attribute_methods attributes
189
+
190
+ attributes.each do |attribute|
191
+ attribute = attribute.to_sym
192
+
193
+ unless instance_methods.include?(:"#{attribute}=")
194
+ define_method("#{attribute}=") do |value|
195
+ @attributes[:"#{attribute}"] = nil unless @attributes.include?(:"#{attribute}")
196
+ self.send(:"#{attribute}_will_change!") if @attributes[:'#{attribute}'] != value
197
+ @attributes[:"#{attribute}"] = value
198
+ end
199
+ end
200
+
201
+ unless instance_methods.include?(:"#{attribute}?")
202
+ define_method("#{attribute}?") do
203
+ @attributes.include?(:"#{attribute}") && @attributes[:"#{attribute}"].present?
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ # Define the accessor in which the API response errors (obtained from the parsing middleware) will be stored
210
+ #
211
+ # @param [Symbol] store_response_errors
212
+ #
213
+ # @example
214
+ # class User
215
+ # include Her::Model
216
+ # store_response_errors :server_errors
217
+ # end
218
+ def store_response_errors(value = nil)
219
+ store_her_data(:response_errors, value)
220
+ end
221
+
222
+ # Define the accessor in which the API response metadata (obtained from the parsing middleware) will be stored
223
+ #
224
+ # @param [Symbol] store_metadata
225
+ #
226
+ # @example
227
+ # class User
228
+ # include Her::Model
229
+ # store_metadata :server_data
230
+ # end
231
+ def store_metadata(value = nil)
232
+ store_her_data(:metadata, value)
233
+ end
234
+
235
+ # @private
236
+ def setter_method_names
237
+ @_her_setter_method_names ||= instance_methods.inject(Set.new) do |memo, method_name|
238
+ memo << method_name.to_s if method_name.to_s.end_with?('=')
239
+ memo
240
+ end
241
+ end
242
+
243
+ private
244
+ # @private
245
+ def store_her_data(name, value)
246
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
247
+ if @_her_store_#{name} && value.present?
248
+ remove_method @_her_store_#{name}.to_sym
249
+ remove_method @_her_store_#{name}.to_s + '='
250
+ end
251
+
252
+ @_her_store_#{name} ||= begin
253
+ superclass.store_#{name} if superclass.respond_to?(:store_#{name})
254
+ end
255
+
256
+ return @_her_store_#{name} unless value
257
+ @_her_store_#{name} = value
258
+
259
+ define_method(value) { @#{name} }
260
+ define_method(value.to_s+'=') { |value| @#{name} = value }
261
+ RUBY
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,33 @@
1
+ module Her
2
+ module Model
3
+ # This module includes basic functionnality to Her::Model
4
+ module Base
5
+ extend ActiveSupport::Concern
6
+
7
+ # Returns true if attribute_name is
8
+ # * in resource attributes
9
+ # * an association
10
+ #
11
+ # @private
12
+ def has_key?(attribute_name)
13
+ has_attribute?(attribute_name) ||
14
+ has_association?(attribute_name)
15
+ end
16
+
17
+ # Returns
18
+ # * the value of the attribute_name attribute if it's in orm data
19
+ # * the resource/collection corrsponding to attribute_name if it's an association
20
+ #
21
+ # @private
22
+ def [](attribute_name)
23
+ get_attribute(attribute_name) ||
24
+ get_association(attribute_name)
25
+ end
26
+
27
+ # @private
28
+ def singularized_resource_name
29
+ self.class.name.split('::').last.tableize.singularize
30
+ end
31
+ end
32
+ end
33
+ end