her 0.8.2 → 0.10.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -1
- data/.rubocop.yml +1291 -0
- data/.travis.yml +6 -1
- data/README.md +29 -11
- data/her.gemspec +3 -5
- data/lib/her/api.rb +16 -9
- data/lib/her/middleware/json_api_parser.rb +1 -1
- data/lib/her/model/associations/association.rb +32 -5
- data/lib/her/model/associations/association_proxy.rb +1 -1
- data/lib/her/model/associations/belongs_to_association.rb +1 -1
- data/lib/her/model/associations/has_many_association.rb +3 -3
- data/lib/her/model/attributes.rb +105 -75
- data/lib/her/model/http.rb +3 -3
- data/lib/her/model/introspection.rb +1 -1
- data/lib/her/model/orm.rb +96 -19
- data/lib/her/model/parse.rb +27 -17
- data/lib/her/model/relation.rb +46 -2
- data/lib/her/version.rb +1 -1
- data/spec/api_spec.rb +34 -31
- data/spec/collection_spec.rb +25 -10
- data/spec/json_api/model_spec.rb +75 -72
- data/spec/middleware/accept_json_spec.rb +1 -1
- data/spec/middleware/first_level_parse_json_spec.rb +20 -20
- data/spec/middleware/json_api_parser_spec.rb +26 -7
- data/spec/middleware/second_level_parse_json_spec.rb +8 -9
- data/spec/model/associations/association_proxy_spec.rb +2 -5
- data/spec/model/associations_spec.rb +617 -295
- data/spec/model/attributes_spec.rb +114 -107
- data/spec/model/callbacks_spec.rb +59 -27
- data/spec/model/dirty_spec.rb +70 -29
- data/spec/model/http_spec.rb +67 -35
- data/spec/model/introspection_spec.rb +26 -22
- data/spec/model/nested_attributes_spec.rb +31 -31
- data/spec/model/orm_spec.rb +332 -157
- data/spec/model/parse_spec.rb +250 -77
- data/spec/model/paths_spec.rb +109 -109
- data/spec/model/relation_spec.rb +89 -69
- data/spec/model/validations_spec.rb +6 -6
- data/spec/model_spec.rb +17 -17
- data/spec/spec_helper.rb +2 -3
- data/spec/support/macros/model_macros.rb +2 -2
- metadata +36 -63
data/.travis.yml
CHANGED
@@ -3,7 +3,9 @@ language: ruby
|
|
3
3
|
sudo: false
|
4
4
|
|
5
5
|
rvm:
|
6
|
-
- 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
|
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 "/
|
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
|
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", "~>
|
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", "
|
27
|
-
s.add_runtime_dependency "activesupport", ">= 3.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
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
@@ -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
|
-
|
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
|
@@ -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
|
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] =>
|
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.
|
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] =
|
95
|
+
@parent.attributes[@name] = @klass.instantiate_collection(@klass, :data => data)
|
96
96
|
end
|
97
97
|
end
|
98
98
|
end
|
data/lib/her/model/attributes.rb
CHANGED
@@ -16,7 +16,13 @@ module Her
|
|
16
16
|
# include Her::Model
|
17
17
|
# end
|
18
18
|
#
|
19
|
-
#
|
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 =~ /[?=]$/ || @
|
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
|
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
|
-
@
|
72
|
+
@_her_attributes ||= attributes
|
105
73
|
# Use setter methods first
|
106
|
-
unset_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
|
-
|
77
|
+
associations = self.class.parse_associations(unset_attributes)
|
110
78
|
|
111
|
-
# Then merge the
|
112
|
-
@
|
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
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
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) && @
|
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 @
|
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
|
-
@
|
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
|
-
@
|
166
|
-
|
167
|
-
@
|
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
|
-
@
|
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
|
-
|
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
|
-
|
191
|
-
|
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
|
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
|
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 ||=
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
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
|
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
|
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 ||=
|
256
|
-
|
257
|
-
|
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
|
|
data/lib/her/model/http.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
94
|
+
new_from_parsed_data(parsed_data)
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|