her 0.8.2 → 0.10.4

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