her 0.10.0 → 0.10.1
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/.travis.yml +3 -3
- data/README.md +4 -6
- data/her.gemspec +2 -2
- data/lib/her/model/associations/has_many_association.rb +1 -1
- data/lib/her/model/attributes.rb +30 -22
- data/lib/her/model/introspection.rb +1 -1
- data/lib/her/model/orm.rb +56 -52
- data/lib/her/model/parse.rb +17 -4
- data/lib/her/model/relation.rb +1 -1
- data/lib/her/version.rb +1 -1
- data/spec/model/associations_spec.rb +564 -328
- data/spec/model/attributes_spec.rb +8 -8
- data/spec/model/callbacks_spec.rb +1 -1
- data/spec/model/dirty_spec.rb +41 -1
- data/spec/model/introspection_spec.rb +1 -1
- data/spec/model/orm_spec.rb +22 -4
- data/spec/model/parse_spec.rb +151 -0
- data/spec/model/relation_spec.rb +14 -2
- metadata +11 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1d26b46a64420261f94f9394f9552d0f6152d27
|
4
|
+
data.tar.gz: '009e2b3a0c746e2ff24d2c96e7b3e110852b6e5c'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 119265f2cfea3d575768faf7f5b9dbec5e88da8536068dd51957251cdf921b275e4527767ca57f647172951341bfcca6a3d0363ff0091cbb2f40b0ec89c922fc
|
7
|
+
data.tar.gz: d74157011df12022512bc46df0ae02cc48f69bd89753a72eac0e32b88921d9356e2073c8b63e238d925304f3bf4ba07418d7c58898bf87308e8deebbacc46b48
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,3 @@
|
|
1
|
-
# Maintenance Update 29th Sept 2016
|
2
|
-
|
3
|
-
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.
|
4
|
-
|
5
|
-
# Rails 5 support
|
6
|
-
If you need Rails 5 support, version 0.8.2 is for you!
|
7
1
|
|
8
2
|
<p align="center">
|
9
3
|
<a href="https://github.com/remiprev/her">
|
@@ -996,6 +990,7 @@ See the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) for
|
|
996
990
|
Most projects I know that use Her are internal or private projects but here’s a list of public ones:
|
997
991
|
|
998
992
|
* [tumbz](https://github.com/remiprev/tumbz)
|
993
|
+
* [zoho-ruby](https://github.com/errorstudio/zoho-ruby)
|
999
994
|
* [crowdher](https://github.com/simonprev/crowdher)
|
1000
995
|
* [vodka](https://github.com/magnolia-fan/vodka)
|
1001
996
|
* [webistrano_cli](https://github.com/chytreg/webistrano_cli)
|
@@ -1007,6 +1002,9 @@ I told myself a few months ago that it would be great to build a gem to replace
|
|
1007
1002
|
|
1008
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!).
|
1009
1004
|
|
1005
|
+
## Maintainers
|
1006
|
+
The gem is currently maintained by [@zacharywelch](https://github.com/zacharywelch) and [@edtjones](https://github.com/edtjones).
|
1007
|
+
|
1010
1008
|
## Contribute
|
1011
1009
|
|
1012
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
@@ -21,8 +21,8 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.add_development_dependency "rspec", "~> 3.5"
|
22
22
|
s.add_development_dependency "json", "~> 1.8"
|
23
23
|
|
24
|
-
s.add_runtime_dependency "activemodel", ">= 3.0.0", "
|
25
|
-
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"
|
26
26
|
s.add_runtime_dependency "faraday", ">= 0.8", "< 1.0"
|
27
27
|
s.add_runtime_dependency "multi_json", "~> 1.7"
|
28
28
|
end
|
data/lib/her/model/attributes.rb
CHANGED
@@ -39,7 +39,7 @@ module Her
|
|
39
39
|
#
|
40
40
|
# @private
|
41
41
|
def method_missing(method, *args, &blk)
|
42
|
-
if method.to_s =~ /[?=]$/ || @
|
42
|
+
if method.to_s =~ /[?=]$/ || @_her_attributes.include?(method)
|
43
43
|
# Extract the attribute
|
44
44
|
attribute = method.to_s.sub(/[?=]$/, '')
|
45
45
|
|
@@ -55,7 +55,7 @@ module Her
|
|
55
55
|
|
56
56
|
# @private
|
57
57
|
def respond_to_missing?(method, include_private = false)
|
58
|
-
method.to_s =~ /[?=]$/ || @
|
58
|
+
method.to_s =~ /[?=]$/ || @_her_attributes.include?(method) || super
|
59
59
|
end
|
60
60
|
|
61
61
|
# Assign new attributes to a resource
|
@@ -69,40 +69,47 @@ module Her
|
|
69
69
|
# user.assign_attributes(name: "Lindsay")
|
70
70
|
# user.changes # => { :name => ["Tobias", "Lindsay"] }
|
71
71
|
def assign_attributes(new_attributes)
|
72
|
-
@
|
72
|
+
@_her_attributes ||= attributes
|
73
73
|
# Use setter methods first
|
74
74
|
unset_attributes = self.class.use_setter_methods(self, new_attributes)
|
75
75
|
|
76
76
|
# Then translate attributes of associations into association instances
|
77
|
-
|
77
|
+
associations = self.class.parse_associations(unset_attributes)
|
78
78
|
|
79
|
-
# Then merge the
|
80
|
-
@
|
79
|
+
# Then merge the associations into @_her_attributes.
|
80
|
+
@_her_attributes.merge!(associations)
|
81
81
|
end
|
82
82
|
alias attributes= assign_attributes
|
83
83
|
|
84
84
|
def attributes
|
85
|
-
|
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
|
86
93
|
end
|
87
94
|
|
88
95
|
# Handles returning true for the accessible attributes
|
89
96
|
#
|
90
97
|
# @private
|
91
98
|
def has_attribute?(attribute_name)
|
92
|
-
@
|
99
|
+
@_her_attributes.include?(attribute_name)
|
93
100
|
end
|
94
101
|
|
95
102
|
# Handles returning data for a specific attribute
|
96
103
|
#
|
97
104
|
# @private
|
98
105
|
def get_attribute(attribute_name)
|
99
|
-
@
|
106
|
+
@_her_attributes[attribute_name]
|
100
107
|
end
|
101
108
|
alias attribute get_attribute
|
102
109
|
|
103
110
|
# Return the value of the model `primary_key` attribute
|
104
111
|
def id
|
105
|
-
@
|
112
|
+
@_her_attributes[self.class.primary_key]
|
106
113
|
end
|
107
114
|
|
108
115
|
# Return `true` if the other object is also a Her::Model and has matching
|
@@ -110,7 +117,7 @@ module Her
|
|
110
117
|
#
|
111
118
|
# @private
|
112
119
|
def ==(other)
|
113
|
-
other.is_a?(Her::Model) && @
|
120
|
+
other.is_a?(Her::Model) && @_her_attributes == other.attributes
|
114
121
|
end
|
115
122
|
|
116
123
|
# Delegate to the == method
|
@@ -120,27 +127,27 @@ module Her
|
|
120
127
|
self == other
|
121
128
|
end
|
122
129
|
|
123
|
-
# Delegate to @
|
130
|
+
# Delegate to @_her_attributes, allowing models to act correctly in code like:
|
124
131
|
# [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
|
125
132
|
# @private
|
126
133
|
def hash
|
127
|
-
@
|
134
|
+
@_her_attributes.hash
|
128
135
|
end
|
129
136
|
|
130
137
|
# Assign attribute value (ActiveModel convention method).
|
131
138
|
#
|
132
139
|
# @private
|
133
140
|
def attribute=(attribute, value)
|
134
|
-
@
|
135
|
-
|
136
|
-
@
|
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
|
137
144
|
end
|
138
145
|
|
139
146
|
# Check attribute value to be present (ActiveModel convention method).
|
140
147
|
#
|
141
148
|
# @private
|
142
149
|
def attribute?(attribute)
|
143
|
-
@
|
150
|
+
@_her_attributes.include?(attribute) && @_her_attributes[attribute].present?
|
144
151
|
end
|
145
152
|
|
146
153
|
module ClassMethods
|
@@ -155,6 +162,7 @@ module Her
|
|
155
162
|
attributes = klass.parse(record).merge(_metadata: parsed_data[:metadata],
|
156
163
|
_errors: parsed_data[:errors])
|
157
164
|
klass.new(attributes).tap do |record|
|
165
|
+
record.instance_variable_set(:@changed_attributes, {})
|
158
166
|
record.run_callbacks :find
|
159
167
|
end
|
160
168
|
end
|
@@ -164,10 +172,10 @@ module Her
|
|
164
172
|
#
|
165
173
|
# @private
|
166
174
|
def instantiate_collection(klass, parsed_data = {})
|
167
|
-
|
168
|
-
instantiate_record(klass, data:
|
175
|
+
records = klass.extract_array(parsed_data).map do |record|
|
176
|
+
instantiate_record(klass, data: record)
|
169
177
|
end
|
170
|
-
Her::Collection.new(
|
178
|
+
Her::Collection.new(records, parsed_data[:metadata], parsed_data[:errors])
|
171
179
|
end
|
172
180
|
|
173
181
|
# Initialize a collection of resources with raw data from an HTTP request
|
@@ -196,9 +204,9 @@ module Her
|
|
196
204
|
|
197
205
|
setter_method_names = model.class.setter_method_names
|
198
206
|
params.each_with_object({}) do |(key, value), memo|
|
199
|
-
setter_method = key
|
207
|
+
setter_method = "#{key}="
|
200
208
|
if setter_method_names.include?(setter_method)
|
201
|
-
model.send
|
209
|
+
model.send setter_method, value
|
202
210
|
else
|
203
211
|
memo[key.to_sym] = value
|
204
212
|
end
|
data/lib/her/model/orm.rb
CHANGED
@@ -41,12 +41,8 @@ module Her
|
|
41
41
|
|
42
42
|
run_callbacks :save do
|
43
43
|
run_callbacks callback do
|
44
|
-
params = to_params
|
45
44
|
self.class.request(to_params.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
|
46
|
-
|
47
|
-
@metadata = parsed_data[:metadata]
|
48
|
-
@response_errors = parsed_data[:errors]
|
49
|
-
|
45
|
+
load_from_parsed_data(parsed_data)
|
50
46
|
return false if !response.success? || @response_errors.any?
|
51
47
|
if self.changed_attributes.present?
|
52
48
|
@previously_changed = self.changes.clone
|
@@ -77,53 +73,13 @@ module Her
|
|
77
73
|
method = self.class.method_for(:destroy)
|
78
74
|
run_callbacks :destroy do
|
79
75
|
self.class.request(params.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
|
80
|
-
|
81
|
-
@metadata = parsed_data[:metadata]
|
82
|
-
@response_errors = parsed_data[:errors]
|
76
|
+
load_from_parsed_data(parsed_data)
|
83
77
|
@destroyed = response.success?
|
84
78
|
end
|
85
79
|
end
|
86
80
|
self
|
87
81
|
end
|
88
82
|
|
89
|
-
# Refetches the resource
|
90
|
-
#
|
91
|
-
# This method finds the resource by its primary key (which could be
|
92
|
-
# assigned manually) and modifies the object in-place.
|
93
|
-
#
|
94
|
-
# @example
|
95
|
-
# user = User.find(1)
|
96
|
-
# # => #<User(users/1) id=1 name="Tobias Fünke">
|
97
|
-
# user.name = "Oops"
|
98
|
-
# user.reload # Fetched again via GET "/users/1"
|
99
|
-
# # => #<User(users/1) id=1 name="Tobias Fünke">
|
100
|
-
def reload(options = nil)
|
101
|
-
fresh_object = self.class.find(id)
|
102
|
-
assign_attributes(fresh_object.attributes)
|
103
|
-
self
|
104
|
-
end
|
105
|
-
|
106
|
-
# Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
|
107
|
-
# if the predicate returns +true+ the attribute will become +false+. This
|
108
|
-
# method toggles directly the underlying value without calling any setter.
|
109
|
-
# Returns +self+.
|
110
|
-
#
|
111
|
-
# @example
|
112
|
-
# user = User.first
|
113
|
-
# user.admin? # => false
|
114
|
-
# user.toggle(:admin)
|
115
|
-
# user.admin? # => true
|
116
|
-
def toggle(attribute)
|
117
|
-
attributes[attribute] = !public_send("#{attribute}?")
|
118
|
-
self
|
119
|
-
end
|
120
|
-
|
121
|
-
# Wrapper around #toggle that saves the resource. Saving is subjected to
|
122
|
-
# validation checks. Returns +true+ if the record could be saved.
|
123
|
-
def toggle!(attribute)
|
124
|
-
toggle(attribute) && save
|
125
|
-
end
|
126
|
-
|
127
83
|
# Initializes +attribute+ to zero if +nil+ and adds the value passed as
|
128
84
|
# +by+ (default is 1). The increment is performed directly on the
|
129
85
|
# underlying attribute, no setter is invoked. Only makes sense for
|
@@ -155,6 +111,54 @@ module Her
|
|
155
111
|
increment!(attribute, -by)
|
156
112
|
end
|
157
113
|
|
114
|
+
# Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
|
115
|
+
# if the predicate returns +true+ the attribute will become +false+. This
|
116
|
+
# method toggles directly the underlying value without calling any setter.
|
117
|
+
# Returns +self+.
|
118
|
+
#
|
119
|
+
# @example
|
120
|
+
# user = User.first
|
121
|
+
# user.admin? # => false
|
122
|
+
# user.toggle(:admin)
|
123
|
+
# user.admin? # => true
|
124
|
+
def toggle(attribute)
|
125
|
+
attributes[attribute] = !public_send("#{attribute}?")
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
# Wrapper around #toggle that saves the resource. Saving is subjected to
|
130
|
+
# validation checks. Returns +true+ if the record could be saved.
|
131
|
+
def toggle!(attribute)
|
132
|
+
toggle(attribute) && save
|
133
|
+
end
|
134
|
+
|
135
|
+
# Refetches the resource
|
136
|
+
#
|
137
|
+
# This method finds the resource by its primary key (which could be
|
138
|
+
# assigned manually) and modifies the object in-place.
|
139
|
+
#
|
140
|
+
# @example
|
141
|
+
# user = User.find(1)
|
142
|
+
# # => #<User(users/1) id=1 name="Tobias Fünke">
|
143
|
+
# user.name = "Oops"
|
144
|
+
# user.reload # Fetched again via GET "/users/1"
|
145
|
+
# # => #<User(users/1) id=1 name="Tobias Fünke">
|
146
|
+
def reload(options = nil)
|
147
|
+
fresh_object = self.class.find(id)
|
148
|
+
assign_attributes(fresh_object.attributes)
|
149
|
+
self
|
150
|
+
end
|
151
|
+
|
152
|
+
# Uses parsed response to assign attributes and metadata
|
153
|
+
#
|
154
|
+
# @private
|
155
|
+
def load_from_parsed_data(parsed_data)
|
156
|
+
data = parsed_data[:data]
|
157
|
+
assign_attributes(self.class.parse(data)) if data.any?
|
158
|
+
@metadata = parsed_data[:metadata]
|
159
|
+
@response_errors = parsed_data[:errors]
|
160
|
+
end
|
161
|
+
|
158
162
|
module ClassMethods
|
159
163
|
# Create a new chainable scope
|
160
164
|
#
|
@@ -174,10 +178,8 @@ module Her
|
|
174
178
|
instance_exec(*args, &code)
|
175
179
|
end
|
176
180
|
|
177
|
-
# Add the scope method to the
|
178
|
-
|
179
|
-
define_method(name) { |*args| instance_exec(*args, &code) }
|
180
|
-
end
|
181
|
+
# Add the scope method to the default/blank relation
|
182
|
+
scoped.define_singleton_method(name) { |*args| instance_exec(*args, &code) }
|
181
183
|
end
|
182
184
|
|
183
185
|
# @private
|
@@ -233,7 +235,9 @@ module Her
|
|
233
235
|
data = parse(parsed_data[:data])
|
234
236
|
metadata = parsed_data[:metadata]
|
235
237
|
response_errors = parsed_data[:errors]
|
236
|
-
new(data.merge(:_destroyed => response.success?, :metadata => metadata
|
238
|
+
record = new(data.merge(:_destroyed => response.success?, :metadata => metadata))
|
239
|
+
record.response_errors = response_errors
|
240
|
+
record
|
237
241
|
end
|
238
242
|
end
|
239
243
|
|
@@ -269,9 +273,9 @@ module Her
|
|
269
273
|
resource
|
270
274
|
end
|
271
275
|
|
272
|
-
private
|
273
276
|
# @private
|
274
277
|
def blank_relation
|
278
|
+
@blank_relation ||= superclass.blank_relation.clone.tap { |r| r.parent = self } if superclass.respond_to?(:blank_relation)
|
275
279
|
@blank_relation ||= Relation.new(self)
|
276
280
|
end
|
277
281
|
end
|
data/lib/her/model/parse.rb
CHANGED
@@ -32,8 +32,18 @@ module Her
|
|
32
32
|
|
33
33
|
# @private
|
34
34
|
def to_params(attributes, changes={})
|
35
|
-
filtered_attributes = attributes.
|
35
|
+
filtered_attributes = attributes.each_with_object({}) do |(key, value), memo|
|
36
|
+
case value
|
37
|
+
when Her::Model
|
38
|
+
when ActiveModel::Serialization
|
39
|
+
value = value.serializable_hash.symbolize_keys
|
40
|
+
end
|
41
|
+
|
42
|
+
memo[key.to_sym] = value
|
43
|
+
end
|
44
|
+
|
36
45
|
filtered_attributes.merge!(embeded_params(attributes))
|
46
|
+
|
37
47
|
if her_api.options[:send_only_modified_attributes]
|
38
48
|
filtered_attributes = changes.symbolize_keys.keys.inject({}) do |hash, attribute|
|
39
49
|
hash[attribute] = filtered_attributes[attribute]
|
@@ -194,17 +204,20 @@ module Her
|
|
194
204
|
|
195
205
|
# @private
|
196
206
|
def request_new_object_on_build?
|
197
|
-
@_her_request_new_object_on_build
|
207
|
+
return @_her_request_new_object_on_build unless @_her_request_new_object_on_build.nil?
|
208
|
+
superclass.respond_to?(:request_new_object_on_build?) && superclass.request_new_object_on_build?
|
198
209
|
end
|
199
210
|
|
200
211
|
# @private
|
201
212
|
def include_root_in_json?
|
202
|
-
@_her_include_root_in_json
|
213
|
+
return @_her_include_root_in_json unless @_her_include_root_in_json.nil?
|
214
|
+
superclass.respond_to?(:include_root_in_json?) && superclass.include_root_in_json?
|
203
215
|
end
|
204
216
|
|
205
217
|
# @private
|
206
218
|
def parse_root_in_json?
|
207
|
-
@_her_parse_root_in_json
|
219
|
+
return @_her_parse_root_in_json unless @_her_parse_root_in_json.nil?
|
220
|
+
superclass.respond_to?(:parse_root_in_json?) && superclass.parse_root_in_json?
|
208
221
|
end
|
209
222
|
end
|
210
223
|
end
|
data/lib/her/model/relation.rb
CHANGED
@@ -3,6 +3,7 @@ module Her
|
|
3
3
|
class Relation
|
4
4
|
# @private
|
5
5
|
attr_accessor :params
|
6
|
+
attr_writer :parent
|
6
7
|
|
7
8
|
# @private
|
8
9
|
def initialize(parent)
|
@@ -96,7 +97,6 @@ module Her
|
|
96
97
|
@parent.request(request_params) do |parsed_data, response|
|
97
98
|
if response.success?
|
98
99
|
resource = @parent.new_from_parsed_data(parsed_data)
|
99
|
-
resource.instance_variable_set(:@changed_attributes, {})
|
100
100
|
resource.run_callbacks :find
|
101
101
|
else
|
102
102
|
return nil
|