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.
- 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
|
|