her 0.8.2 → 0.10.4

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/.rubocop.yml +1291 -0
  4. data/.travis.yml +6 -1
  5. data/README.md +29 -11
  6. data/her.gemspec +3 -5
  7. data/lib/her/api.rb +16 -9
  8. data/lib/her/middleware/json_api_parser.rb +1 -1
  9. data/lib/her/model/associations/association.rb +32 -5
  10. data/lib/her/model/associations/association_proxy.rb +1 -1
  11. data/lib/her/model/associations/belongs_to_association.rb +1 -1
  12. data/lib/her/model/associations/has_many_association.rb +3 -3
  13. data/lib/her/model/attributes.rb +105 -75
  14. data/lib/her/model/http.rb +3 -3
  15. data/lib/her/model/introspection.rb +1 -1
  16. data/lib/her/model/orm.rb +96 -19
  17. data/lib/her/model/parse.rb +27 -17
  18. data/lib/her/model/relation.rb +46 -2
  19. data/lib/her/version.rb +1 -1
  20. data/spec/api_spec.rb +34 -31
  21. data/spec/collection_spec.rb +25 -10
  22. data/spec/json_api/model_spec.rb +75 -72
  23. data/spec/middleware/accept_json_spec.rb +1 -1
  24. data/spec/middleware/first_level_parse_json_spec.rb +20 -20
  25. data/spec/middleware/json_api_parser_spec.rb +26 -7
  26. data/spec/middleware/second_level_parse_json_spec.rb +8 -9
  27. data/spec/model/associations/association_proxy_spec.rb +2 -5
  28. data/spec/model/associations_spec.rb +617 -295
  29. data/spec/model/attributes_spec.rb +114 -107
  30. data/spec/model/callbacks_spec.rb +59 -27
  31. data/spec/model/dirty_spec.rb +70 -29
  32. data/spec/model/http_spec.rb +67 -35
  33. data/spec/model/introspection_spec.rb +26 -22
  34. data/spec/model/nested_attributes_spec.rb +31 -31
  35. data/spec/model/orm_spec.rb +332 -157
  36. data/spec/model/parse_spec.rb +250 -77
  37. data/spec/model/paths_spec.rb +109 -109
  38. data/spec/model/relation_spec.rb +89 -69
  39. data/spec/model/validations_spec.rb +6 -6
  40. data/spec/model_spec.rb +17 -17
  41. data/spec/spec_helper.rb +2 -3
  42. data/spec/support/macros/model_macros.rb +2 -2
  43. metadata +36 -63
data/.travis.yml CHANGED
@@ -3,7 +3,9 @@ language: ruby
3
3
  sudo: false
4
4
 
5
5
  rvm:
6
- - 2.2.2
6
+ - 2.4.2
7
+ - 2.3.5
8
+ - 2.2.8
7
9
  - 2.1.6
8
10
  - 2.0.0
9
11
  - 1.9.3
@@ -14,4 +16,7 @@ gemfile:
14
16
  - gemfiles/Gemfile.activemodel-4.0
15
17
  - gemfiles/Gemfile.activemodel-3.2.x
16
18
 
19
+ before_install:
20
+ - gem install bundler
21
+
17
22
  script: "echo 'COME ON!' && bundle exec rake spec"
data/README.md CHANGED
@@ -1,8 +1,3 @@
1
- # Maintenance Update 29th Sept 2016
2
- Hi folks, [@edtjones](https://github.com/edtjones) here. Rémi has handed me the keys to Her and [@foxpaul](https://github.com/foxpaul) and I will be trying to do the library justice with the help of the community. There's loads to do; we'll get in touch with everyone who's raised a PR as soon as possible and figure out a plan of action.
3
-
4
- # Rails 5 support
5
- If you need Rails 5 support, version 0.8.2 is for you!
6
1
 
7
2
  <p align="center">
8
3
  <a href="https://github.com/remiprev/her">
@@ -15,6 +10,7 @@ If you need Rails 5 support, version 0.8.2 is for you!
15
10
  <a href="https://codeclimate.com/github/remiprev/her"><img src="http://img.shields.io/codeclimate/github/remiprev/her.svg" /></a>
16
11
  <a href='https://gemnasium.com/remiprev/her'><img src="http://img.shields.io/gemnasium/remiprev/her.svg" /></a>
17
12
  <a href="https://travis-ci.org/remiprev/her"><img src="http://img.shields.io/travis/remiprev/her/master.svg" /></a>
13
+ <a href="https://gitter.im/her-orm/Lobby"><img src="https://badges.gitter.im/her-orm/Lobby.png" alt="Gitter chat" title="" data-pin-nopin="true"></a>
18
14
  </p>
19
15
 
20
16
  ---
@@ -173,6 +169,24 @@ end
173
169
 
174
170
  Now, each HTTP request made by Her will have the `X-API-Token` header.
175
171
 
172
+ ### Basic Http Authentication
173
+ Her can use basic http auth by adding a line to your initializer
174
+
175
+ ```ruby
176
+ # config/initializers/her.rb
177
+ Her::API.setup url: "https://api.example.com" do |c|
178
+ # Request
179
+ c.use Faraday::Request::BasicAuthentication, 'myusername', 'mypassword'
180
+ c.use Faraday::Request::UrlEncoded
181
+
182
+ # Response
183
+ c.use Her::Middleware::DefaultParseJSON
184
+
185
+ # Adapter
186
+ c.use Faraday::Adapter::NetHttp
187
+ end
188
+ ```
189
+
176
190
  ### OAuth
177
191
 
178
192
  Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her.
@@ -214,7 +228,7 @@ end
214
228
  @tweets = Tweet.get("/statuses/home_timeline.json")
215
229
  ```
216
230
 
217
- See the *Authentication* middleware section for an example of how to pass different credentials based on the current user.
231
+ See the [*Authentication middleware section*](#authentication) for an example of how to pass different credentials based on the current user.
218
232
 
219
233
  ### Parsing JSON data
220
234
 
@@ -228,7 +242,7 @@ By default, Her handles JSON data. It expects the resource/collection data to be
228
242
  [{ "id" : 1, "name" : "Tobias Fünke" }]
229
243
  ```
230
244
 
231
- However, if you want Her to be able to parse the data from a single root element (usually based on the model name), you’ll have to use the `parse_root_in_json` method (See the **JSON attributes-wrapping** section).
245
+ However, if you want Her to be able to parse the data from a single root element (usually based on the model name), you’ll have to use the `parse_root_in_json` method (See the [**JSON attributes-wrapping**](#json-attributes-wrapping) section).
232
246
 
233
247
  Also, you can define your own parsing method using a response middleware. The middleware should set `env[:body]` to a hash with three symbol keys: `:data`, `:errors` and `:metadata`. The following code uses a custom middleware to parse the JSON data:
234
248
 
@@ -397,8 +411,8 @@ You can use the association methods to build new objects and save them.
397
411
  @user.comments.build(body: "Just a draft")
398
412
  # => [#<Comment body="Just a draft" user_id=1>]
399
413
 
400
- @user.comments.create(body: "Hello world.")
401
- # POST "/users/1/comments" with `body=Hello+world.`
414
+ @user.comments.create(body: "Hello world.", user_id: 1)
415
+ # POST "/comments" with `body=Hello+world.&user_id=1`
402
416
  # => [#<Comment id=3 body="Hello world." user_id=1>]
403
417
  ```
404
418
 
@@ -432,7 +446,7 @@ class Organization
432
446
  end
433
447
  ```
434
448
 
435
- Her expects all `User` resources to have an `:organization_id` (or `:_organization_id`) attribute. Otherwise, calling mostly all methods, like `User.all`, will thrown an exception like this one:
449
+ Her expects all `User` resources to have an `:organization_id` (or `:_organization_id`) attribute. Otherwise, calling mostly all methods, like `User.all`, will throw an exception like this one:
436
450
 
437
451
  ```ruby
438
452
  Her::Errors::PathError: Missing :_organization_id parameter to build the request path. Path is `organizations/:organization_id/users`. Parameters are `{ … }`.
@@ -593,7 +607,7 @@ users = Users.all
593
607
  #### JSON API support
594
608
 
595
609
  To consume a JSON API 1.0 compliant service, it must return data in accordance with the [JSON API spec](http://jsonapi.org/). The general format
596
- of the data is as follows:
610
+ of the data is as follows:
597
611
 
598
612
  ```json
599
613
  { "data": {
@@ -976,6 +990,7 @@ See the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) for
976
990
  Most projects I know that use Her are internal or private projects but here’s a list of public ones:
977
991
 
978
992
  * [tumbz](https://github.com/remiprev/tumbz)
993
+ * [zoho-ruby](https://github.com/errorstudio/zoho-ruby)
979
994
  * [crowdher](https://github.com/simonprev/crowdher)
980
995
  * [vodka](https://github.com/magnolia-fan/vodka)
981
996
  * [webistrano_cli](https://github.com/chytreg/webistrano_cli)
@@ -987,6 +1002,9 @@ I told myself a few months ago that it would be great to build a gem to replace
987
1002
 
988
1003
  Most of Her’s core concepts were written on a Saturday morning of April 2012 ([first commit](https://github.com/remiprev/her/commit/689d8e88916dc2ad258e69a2a91a283f061cbef2) at 7am!).
989
1004
 
1005
+ ## Maintainers
1006
+ The gem is currently maintained by [@zacharywelch](https://github.com/zacharywelch) and [@edtjones](https://github.com/edtjones).
1007
+
990
1008
  ## Contribute
991
1009
 
992
1010
  Yes please! Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues). There’s no such thing as a bad pull request — even if it’s for a typo, a small improvement to the code or the documentation!
data/her.gemspec CHANGED
@@ -18,13 +18,11 @@ Gem::Specification.new do |s|
18
18
  s.require_paths = ["lib"]
19
19
 
20
20
  s.add_development_dependency "rake", "~> 10.0"
21
- s.add_development_dependency "rspec", "~> 2.13"
22
- s.add_development_dependency "rspec-its", "~> 1.0"
23
- s.add_development_dependency "fivemat", "~> 1.2"
21
+ s.add_development_dependency "rspec", "~> 3.5"
24
22
  s.add_development_dependency "json", "~> 1.8"
25
23
 
26
- s.add_runtime_dependency "activemodel", ">= 3.0.0", "<= 6.0.0"
27
- s.add_runtime_dependency "activesupport", ">= 3.0.0", "<= 6.0.0"
24
+ s.add_runtime_dependency "activemodel", ">= 3.0.0", "< 5.2.0"
25
+ s.add_runtime_dependency "activesupport", ">= 3.0.0", "< 5.2.0"
28
26
  s.add_runtime_dependency "faraday", ">= 0.8", "< 1.0"
29
27
  s.add_runtime_dependency "multi_json", "~> 1.7"
30
28
  end
data/lib/her/api.rb CHANGED
@@ -89,19 +89,26 @@ module Her
89
89
  path = opts.delete(:_path)
90
90
  headers = opts.delete(:_headers)
91
91
  opts.delete_if { |key, value| key.to_s =~ /^_/ } # Remove all internal parameters
92
- response = @connection.send method do |request|
92
+ if method == :options
93
+ # Faraday doesn't support the OPTIONS verb because of a name collision with an internal options method
94
+ # so we need to call run_request directly.
93
95
  request.headers.merge!(headers) if headers
94
- if method == :get
95
- # For GET requests, treat additional parameters as querystring data
96
- request.url path, opts
97
- else
98
- # For POST, PUT and DELETE requests, treat additional parameters as request body
99
- request.url path
100
- request.body = opts
96
+ response = @connection.run_request method, path, opts, headers
97
+ else
98
+ response = @connection.send method do |request|
99
+ request.headers.merge!(headers) if headers
100
+ if method == :get
101
+ # For GET requests, treat additional parameters as querystring data
102
+ request.url path, opts
103
+ else
104
+ # For POST, PUT and DELETE requests, treat additional parameters as request body
105
+ request.url path
106
+ request.body = opts
107
+ end
101
108
  end
102
109
  end
103
-
104
110
  { :parsed_data => response.env[:body], :response => response }
111
+
105
112
  end
106
113
 
107
114
  private
@@ -25,7 +25,7 @@ module Her
25
25
  # @private
26
26
  def on_complete(env)
27
27
  env[:body] = case env[:status]
28
- when 204
28
+ when 204, 304
29
29
  parse('{}')
30
30
  else
31
31
  parse(env[:body])
@@ -26,11 +26,7 @@ module Her
26
26
  return {} unless data[data_key]
27
27
 
28
28
  klass = klass.her_nearby_class(association[:class_name])
29
- if data[data_key].kind_of?(klass)
30
- { association[:name] => data[data_key] }
31
- else
32
- { association[:name] => klass.new(klass.parse(data[data_key])) }
33
- end
29
+ { association[:name] => klass.instantiate_record(klass, data: data[data_key]) }
34
30
  end
35
31
 
36
32
  # @private
@@ -49,6 +45,7 @@ module Her
49
45
 
50
46
  return @cached_result unless @params.any? || @cached_result.nil?
51
47
  return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
48
+ return @opts[:default].try(:dup) if @parent.new?
52
49
 
53
50
  path = build_association_path lambda { "#{@parent.request_path(@params)}#{@opts[:path]}" }
54
51
  @klass.get(path, @params).tap do |result|
@@ -65,6 +62,13 @@ module Her
65
62
  end
66
63
  end
67
64
 
65
+ # @private
66
+ def reset
67
+ @params = {}
68
+ @cached_result = nil
69
+ @parent.attributes.delete(@name)
70
+ end
71
+
68
72
  # Add query parameters to the HTTP request performed to fetch the data
69
73
  #
70
74
  # @example
@@ -97,6 +101,29 @@ module Her
97
101
  @klass.get_resource(path, @params)
98
102
  end
99
103
 
104
+ # Refetches the association and puts the proxy back in its initial state,
105
+ # which is unloaded. Cached associations are cleared.
106
+ #
107
+ # @example
108
+ # class User
109
+ # include Her::Model
110
+ # has_many :comments
111
+ # end
112
+ #
113
+ # class Comment
114
+ # include Her::Model
115
+ # end
116
+ #
117
+ # user = User.find(1)
118
+ # user.comments = [#<Comment(comments/2) id=2 body="Hello!">]
119
+ # user.comments.first.id = "Oops"
120
+ # user.comments.reload # => [#<Comment(comments/2) id=2 body="Hello!">]
121
+ # # Fetched again via GET "/users/1/comments"
122
+ def reload
123
+ reset
124
+ fetch
125
+ end
126
+
100
127
  end
101
128
  end
102
129
  end
@@ -15,7 +15,7 @@ module Her
15
15
  end
16
16
 
17
17
  install_proxy_methods :association,
18
- :build, :create, :where, :find, :all, :assign_nested_attributes
18
+ :build, :create, :where, :find, :all, :assign_nested_attributes, :reload
19
19
 
20
20
  # @private
21
21
  def initialize(association)
@@ -80,7 +80,7 @@ module Her
80
80
  return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
81
81
 
82
82
  path_params = @parent.attributes.merge(@params.merge(@klass.primary_key => foreign_key_value))
83
- path = build_association_path lambda { @klass.build_request_path(path_params) }
83
+ path = build_association_path -> { @klass.build_request_path(@opts[:path], path_params) }
84
84
  @klass.get_resource(path, @params).tap do |result|
85
85
  @cached_result = result if @params.blank?
86
86
  end
@@ -31,7 +31,7 @@ module Her
31
31
  return {} unless data[data_key]
32
32
 
33
33
  klass = klass.her_nearby_class(association[:class_name])
34
- { association[:name] => Her::Model::Attributes.initialize_collection(klass, :data => data[data_key]) }
34
+ { association[:name] => klass.instantiate_collection(klass, :data => data[data_key]) }
35
35
  end
36
36
 
37
37
  # Initialize a new object with a foreign key to the parent
@@ -85,14 +85,14 @@ module Her
85
85
  def fetch
86
86
  super.tap do |o|
87
87
  inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
88
- o.each { |entry| entry.send("#{inverse_of}=", @parent) }
88
+ o.each { |entry| entry.attributes[inverse_of] = @parent }
89
89
  end
90
90
  end
91
91
 
92
92
  # @private
93
93
  def assign_nested_attributes(attributes)
94
94
  data = attributes.is_a?(Hash) ? attributes.values : attributes
95
- @parent.attributes[@name] = Her::Model::Attributes.initialize_collection(@klass, :data => data)
95
+ @parent.attributes[@name] = @klass.instantiate_collection(@klass, :data => data)
96
96
  end
97
97
  end
98
98
  end
@@ -16,7 +16,13 @@ module Her
16
16
  # include Her::Model
17
17
  # end
18
18
  #
19
- # User.new(name: "Tobias") # => #<User name="Tobias">
19
+ # User.new(name: "Tobias")
20
+ # # => #<User name="Tobias">
21
+ #
22
+ # User.new do |u|
23
+ # u.name = "Tobias"
24
+ # end
25
+ # # => #<User name="Tobias">
20
26
  def initialize(attributes={})
21
27
  attributes ||= {}
22
28
  @metadata = attributes.delete(:_metadata) || {}
@@ -25,53 +31,15 @@ module Her
25
31
 
26
32
  attributes = self.class.default_scope.apply_to(attributes)
27
33
  assign_attributes(attributes)
34
+ yield self if block_given?
28
35
  run_callbacks :initialize
29
36
  end
30
37
 
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
38
  # Handles missing methods
71
39
  #
72
40
  # @private
73
41
  def method_missing(method, *args, &blk)
74
- if method.to_s =~ /[?=]$/ || @attributes.include?(method)
42
+ if method.to_s =~ /[?=]$/ || @_her_attributes.include?(method)
75
43
  # Extract the attribute
76
44
  attribute = method.to_s.sub(/[?=]$/, '')
77
45
 
@@ -87,7 +55,7 @@ module Her
87
55
 
88
56
  # @private
89
57
  def respond_to_missing?(method, include_private = false)
90
- method.to_s.end_with?('=') || method.to_s.end_with?('?') || @attributes.include?(method) || super
58
+ method.to_s =~ /[?=]$/ || @_her_attributes.include?(method) || super
91
59
  end
92
60
 
93
61
  # Assign new attributes to a resource
@@ -101,47 +69,55 @@ module Her
101
69
  # user.assign_attributes(name: "Lindsay")
102
70
  # user.changes # => { :name => ["Tobias", "Lindsay"] }
103
71
  def assign_attributes(new_attributes)
104
- @attributes ||= attributes
72
+ @_her_attributes ||= attributes
105
73
  # Use setter methods first
106
- unset_attributes = Her::Model::Attributes.use_setter_methods(self, new_attributes)
74
+ unset_attributes = self.class.use_setter_methods(self, new_attributes)
107
75
 
108
76
  # Then translate attributes of associations into association instances
109
- parsed_attributes = self.class.parse_associations(unset_attributes)
77
+ associations = self.class.parse_associations(unset_attributes)
110
78
 
111
- # Then merge the parsed_data into @attributes.
112
- @attributes.merge!(parsed_attributes)
79
+ # Then merge the associations into @_her_attributes.
80
+ @_her_attributes.merge!(associations)
113
81
  end
114
82
  alias attributes= assign_attributes
115
83
 
116
84
  def attributes
117
- @attributes ||= HashWithIndifferentAccess.new
85
+ # The natural choice of instance variable naming here would be
86
+ # `@attributes`. Unfortunately that causes a naming clash when
87
+ # used with `ActiveModel` version >= 5.2.0.
88
+ # As of v5.2.0 `ActiveModel` checks to see if `ActiveRecord`
89
+ # attributes exist, and assumes that if the instance variable
90
+ # `@attributes` exists on the instance, it is because they are
91
+ # `ActiveRecord` attributes.
92
+ @_her_attributes ||= HashWithIndifferentAccess.new
118
93
  end
119
94
 
120
95
  # Handles returning true for the accessible attributes
121
96
  #
122
97
  # @private
123
98
  def has_attribute?(attribute_name)
124
- @attributes.include?(attribute_name)
99
+ @_her_attributes.include?(attribute_name)
125
100
  end
126
101
 
127
102
  # Handles returning data for a specific attribute
128
103
  #
129
104
  # @private
130
105
  def get_attribute(attribute_name)
131
- @attributes[attribute_name]
106
+ @_her_attributes[attribute_name]
132
107
  end
133
108
  alias attribute get_attribute
134
109
 
135
110
  # Return the value of the model `primary_key` attribute
136
111
  def id
137
- @attributes[self.class.primary_key]
112
+ @_her_attributes[self.class.primary_key]
138
113
  end
139
114
 
140
- # Return `true` if the other object is also a Her::Model and has matching data
115
+ # Return `true` if the other object is also a Her::Model and has matching
116
+ # data
141
117
  #
142
118
  # @private
143
119
  def ==(other)
144
- other.is_a?(Her::Model) && @attributes == other.attributes
120
+ other.is_a?(Her::Model) && @_her_attributes == other.attributes
145
121
  end
146
122
 
147
123
  # Delegate to the == method
@@ -151,47 +127,94 @@ module Her
151
127
  self == other
152
128
  end
153
129
 
154
- # Delegate to @attributes, allowing models to act correctly in code like:
130
+ # Delegate to @_her_attributes, allowing models to act correctly in code like:
155
131
  # [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
156
132
  # @private
157
133
  def hash
158
- @attributes.hash
134
+ @_her_attributes.hash
159
135
  end
160
136
 
161
137
  # Assign attribute value (ActiveModel convention method).
162
138
  #
163
139
  # @private
164
140
  def attribute=(attribute, value)
165
- @attributes[attribute] = nil unless @attributes.include?(attribute)
166
- self.send(:"#{attribute}_will_change!") if @attributes[attribute] != value
167
- @attributes[attribute] = value
141
+ @_her_attributes[attribute] = nil unless @_her_attributes.include?(attribute)
142
+ send("#{attribute}_will_change!") unless value == @_her_attributes[attribute]
143
+ @_her_attributes[attribute] = value
168
144
  end
169
145
 
170
146
  # Check attribute value to be present (ActiveModel convention method).
171
147
  #
172
148
  # @private
173
149
  def attribute?(attribute)
174
- @attributes.include?(attribute) && @attributes[attribute].present?
150
+ @_her_attributes.include?(attribute) && @_her_attributes[attribute].present?
175
151
  end
176
152
 
177
153
  module ClassMethods
154
+
155
+ # Initialize a single resource
156
+ #
157
+ # @private
158
+ def instantiate_record(klass, parsed_data)
159
+ if record = parsed_data[:data] and record.kind_of?(klass)
160
+ record
161
+ else
162
+ attributes = klass.parse(record).merge(_metadata: parsed_data[:metadata],
163
+ _errors: parsed_data[:errors])
164
+ klass.new(attributes).tap do |record|
165
+ record.instance_variable_set(:@changed_attributes, {})
166
+ record.run_callbacks :find
167
+ end
168
+ end
169
+ end
170
+
171
+ # Initialize a collection of resources
172
+ #
173
+ # @private
174
+ def instantiate_collection(klass, parsed_data = {})
175
+ records = klass.extract_array(parsed_data).map do |record|
176
+ instantiate_record(klass, data: record)
177
+ end
178
+ Her::Collection.new(records, parsed_data[:metadata], parsed_data[:errors])
179
+ end
180
+
178
181
  # Initialize a collection of resources with raw data from an HTTP request
179
182
  #
180
183
  # @param [Array] parsed_data
181
184
  # @private
182
185
  def new_collection(parsed_data)
183
- Her::Model::Attributes.initialize_collection(self, parsed_data)
186
+ instantiate_collection(self, parsed_data)
184
187
  end
185
188
 
186
189
  # Initialize a new object with the "raw" parsed_data from the parsing middleware
187
190
  #
188
191
  # @private
189
192
  def new_from_parsed_data(parsed_data)
190
- parsed_data = parsed_data.with_indifferent_access
191
- new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
193
+ instantiate_record(self, parsed_data)
194
+ end
195
+
196
+ # Use setter methods of model for each key / value pair in params
197
+ # Return key / value pairs for which no setter method was defined on the
198
+ # model
199
+ #
200
+ # @private
201
+ def use_setter_methods(model, params = {})
202
+ reserved = [:id, model.class.primary_key, *model.class.association_keys]
203
+ model.class.attributes *params.keys.reject { |k| reserved.include?(k) }
204
+
205
+ setter_method_names = model.class.setter_method_names
206
+ params.each_with_object({}) do |(key, value), memo|
207
+ setter_method = "#{key}="
208
+ if setter_method_names.include?(setter_method)
209
+ model.send setter_method, value
210
+ else
211
+ memo[key.to_sym] = value
212
+ end
213
+ end
192
214
  end
193
215
 
194
- # Define attribute method matchers to automatically define them using ActiveModel's define_attribute_methods.
216
+ # Define attribute method matchers to automatically define them using
217
+ # ActiveModel's define_attribute_methods.
195
218
  #
196
219
  # @private
197
220
  def define_attribute_method_matchers
@@ -199,18 +222,22 @@ module Her
199
222
  attribute_method_suffix '?'
200
223
  end
201
224
 
202
- # Create a mutex for dynamically generated attribute methods or use one defined by ActiveModel.
225
+ # Create a mutex for dynamically generated attribute methods or use one
226
+ # defined by ActiveModel.
203
227
  #
204
228
  # @private
205
229
  def attribute_methods_mutex
206
- @attribute_methods_mutex ||= if generated_attribute_methods.respond_to? :mu_synchronize
207
- generated_attribute_methods
208
- else
209
- Mutex.new
210
- end
230
+ @attribute_methods_mutex ||= begin
231
+ if generated_attribute_methods.respond_to? :mu_synchronize
232
+ generated_attribute_methods
233
+ else
234
+ Mutex.new
235
+ end
236
+ end
211
237
  end
212
238
 
213
- # Define the attributes that will be used to track dirty attributes and validations
239
+ # Define the attributes that will be used to track dirty attributes and
240
+ # validations
214
241
  #
215
242
  # @param [Array] attributes
216
243
  # @example
@@ -224,7 +251,8 @@ module Her
224
251
  end
225
252
  end
226
253
 
227
- # Define the accessor in which the API response errors (obtained from the parsing middleware) will be stored
254
+ # Define the accessor in which the API response errors (obtained from
255
+ # the parsing middleware) will be stored
228
256
  #
229
257
  # @param [Symbol] store_response_errors
230
258
  #
@@ -237,7 +265,8 @@ module Her
237
265
  store_her_data(:response_errors, value)
238
266
  end
239
267
 
240
- # Define the accessor in which the API response metadata (obtained from the parsing middleware) will be stored
268
+ # Define the accessor in which the API response metadata (obtained from
269
+ # the parsing middleware) will be stored
241
270
  #
242
271
  # @param [Symbol] store_metadata
243
272
  #
@@ -252,9 +281,10 @@ module Her
252
281
 
253
282
  # @private
254
283
  def setter_method_names
255
- @_her_setter_method_names ||= instance_methods.inject(Set.new) do |memo, method_name|
256
- memo << method_name.to_s if method_name.to_s.end_with?('=')
257
- memo
284
+ @_her_setter_method_names ||= begin
285
+ instance_methods.each_with_object(Set.new) do |method, memo|
286
+ memo << method.to_s if method.to_s.end_with?('=')
287
+ end
258
288
  end
259
289
  end
260
290
 
@@ -3,7 +3,7 @@ module Her
3
3
  # This module interacts with Her::API to fetch HTTP data
4
4
  module HTTP
5
5
  extend ActiveSupport::Concern
6
- METHODS = [:get, :post, :put, :patch, :delete]
6
+ METHODS = [:get, :post, :put, :patch, :delete, :options]
7
7
 
8
8
  # For each HTTP method, define these class methods:
9
9
  #
@@ -71,7 +71,7 @@ module Her
71
71
  if parsed_data[:data].is_a?(Array) || active_model_serializers_format? || json_api_format?
72
72
  new_collection(parsed_data)
73
73
  else
74
- new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
74
+ new_from_parsed_data(parsed_data)
75
75
  end
76
76
  end
77
77
  end
@@ -91,7 +91,7 @@ module Her
91
91
  def #{method}_resource(path, params={})
92
92
  path = build_request_path_from_string_or_symbol(path, params)
93
93
  send(:"#{method}_raw", path, params) do |parsed_data, response|
94
- new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
94
+ new_from_parsed_data(parsed_data)
95
95
  end
96
96
  end
97
97
 
@@ -38,7 +38,7 @@ module Her
38
38
  #
39
39
  # @private
40
40
  def her_nearby_class(name)
41
- her_sibling_class(name) || name.constantize rescue nil
41
+ her_sibling_class(name) || name.constantize
42
42
  end
43
43
 
44
44
  protected