her 0.6.3 → 0.6.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 +8 -8
- data/.yardopts +2 -0
- data/README.md +3 -1
- data/her.gemspec +1 -1
- data/lib/her/api.rb +17 -17
- data/lib/her/middleware/accept_json.rb +2 -0
- data/lib/her/middleware/first_level_parse_json.rb +2 -0
- data/lib/her/middleware/parse_json.rb +1 -0
- data/lib/her/middleware/second_level_parse_json.rb +2 -0
- data/lib/her/model.rb +1 -0
- data/lib/her/model/associations.rb +19 -17
- data/lib/her/model/associations/association.rb +54 -5
- data/lib/her/model/associations/belongs_to_association.rb +14 -25
- data/lib/her/model/associations/has_many_association.rb +9 -22
- data/lib/her/model/associations/has_one_association.rb +9 -33
- data/lib/her/model/attributes.rb +55 -58
- data/lib/her/model/http.rb +33 -24
- data/lib/her/model/introspection.rb +1 -0
- data/lib/her/model/orm.rb +2 -2
- data/lib/her/model/parse.rb +2 -1
- data/lib/her/model/paths.rb +7 -11
- data/lib/her/model/relation.rb +27 -24
- data/lib/her/version.rb +1 -1
- data/spec/model/associations_spec.rb +15 -15
- data/spec/model/attributes_spec.rb +81 -0
- data/spec/model/relation_spec.rb +19 -1
- metadata +4 -3
@@ -3,32 +3,29 @@ module Her
|
|
3
3
|
module Associations
|
4
4
|
class HasOneAssociation < Association
|
5
5
|
# @private
|
6
|
-
def self.attach(klass, name,
|
7
|
-
|
6
|
+
def self.attach(klass, name, opts)
|
7
|
+
opts = {
|
8
8
|
:class_name => name.to_s.classify,
|
9
9
|
:name => name,
|
10
10
|
:data_key => name,
|
11
|
+
:default => nil,
|
11
12
|
:path => "/#{name}"
|
12
|
-
}.merge(
|
13
|
-
klass.associations[:has_one] <<
|
13
|
+
}.merge(opts)
|
14
|
+
klass.associations[:has_one] << opts
|
14
15
|
|
15
16
|
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
16
17
|
def #{name}
|
17
18
|
cached_name = :"@_her_association_#{name}"
|
18
19
|
|
19
20
|
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
|
20
|
-
cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasOneAssociation.new(self, #{
|
21
|
+
cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasOneAssociation.new(self, #{opts.inspect}))
|
21
22
|
end
|
22
23
|
RUBY
|
23
24
|
end
|
24
25
|
|
25
26
|
# @private
|
26
|
-
def self.parse(
|
27
|
-
|
28
|
-
return {} unless data[data_key]
|
29
|
-
|
30
|
-
klass = klass.her_nearby_class(association[:class_name])
|
31
|
-
{ association[:name] => klass.new(data[data_key]) }
|
27
|
+
def self.parse(*args)
|
28
|
+
parse_single(*args)
|
32
29
|
end
|
33
30
|
|
34
31
|
# Initialize a new object with a foreign key to the parent
|
@@ -71,30 +68,9 @@ module Her
|
|
71
68
|
resource
|
72
69
|
end
|
73
70
|
|
74
|
-
# @private
|
75
|
-
def fetch
|
76
|
-
return nil if @parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @query_attrs.empty?
|
77
|
-
|
78
|
-
if @parent.attributes[@name].blank? || @query_attrs.any?
|
79
|
-
path = begin
|
80
|
-
@parent.request_path(@query_attrs)
|
81
|
-
rescue Her::Errors::PathError
|
82
|
-
return nil
|
83
|
-
end
|
84
|
-
|
85
|
-
@klass.get_resource("#{path}#{@opts[:path]}", @query_attrs)
|
86
|
-
else
|
87
|
-
@parent.attributes[@name]
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
71
|
# @private
|
92
72
|
def assign_nested_attributes(attributes)
|
93
|
-
|
94
|
-
@parent.attributes[@name] = @klass.new(@klass.parse(attributes))
|
95
|
-
else
|
96
|
-
@parent.attributes[@name].assign_attributes(attributes)
|
97
|
-
end
|
73
|
+
assign_single_nested_attributes(attributes)
|
98
74
|
end
|
99
75
|
end
|
100
76
|
end
|
data/lib/her/model/attributes.rb
CHANGED
@@ -3,7 +3,6 @@ module Her
|
|
3
3
|
# This module handles all methods related to model attributes
|
4
4
|
module Attributes
|
5
5
|
extend ActiveSupport::Concern
|
6
|
-
attr_accessor :attributes
|
7
6
|
|
8
7
|
# Initialize a new object with data
|
9
8
|
#
|
@@ -63,7 +62,7 @@ module Her
|
|
63
62
|
#
|
64
63
|
# @private
|
65
64
|
def method_missing(method, *args, &blk)
|
66
|
-
if method.to_s =~ /[?=]$/ || attributes.include?(method)
|
65
|
+
if method.to_s =~ /[?=]$/ || @attributes.include?(method)
|
67
66
|
# Extract the attribute
|
68
67
|
attribute = method.to_s.sub(/[?=]$/, '')
|
69
68
|
|
@@ -97,41 +96,48 @@ module Her
|
|
97
96
|
# user = User.find(1) # => #<User id=1 name="Tobias">
|
98
97
|
# user.assign_attributes(name: "Lindsay")
|
99
98
|
# user.changes # => { :name => ["Tobias", "Lindsay"] }
|
100
|
-
def assign_attributes(
|
101
|
-
@attributes ||=
|
102
|
-
# Use setter methods first
|
103
|
-
|
104
|
-
|
99
|
+
def assign_attributes(new_attributes)
|
100
|
+
@attributes ||= attributes
|
101
|
+
# Use setter methods first
|
102
|
+
unset_attributes = Her::Model::Attributes.use_setter_methods(self, new_attributes)
|
103
|
+
|
104
|
+
# Then translate attributes of associations into association instances
|
105
105
|
parsed_attributes = self.class.parse_associations(unset_attributes)
|
106
|
-
|
106
|
+
|
107
|
+
# Then merge the parsed_data into @attributes.
|
108
|
+
@attributes.merge!(parsed_attributes)
|
109
|
+
end
|
110
|
+
alias attributes= assign_attributes
|
111
|
+
|
112
|
+
def attributes
|
113
|
+
@attributes ||= HashWithIndifferentAccess.new
|
107
114
|
end
|
108
115
|
|
109
116
|
# Handles returning true for the accessible attributes
|
110
117
|
#
|
111
118
|
# @private
|
112
119
|
def has_attribute?(attribute_name)
|
113
|
-
attributes.include?(attribute_name)
|
120
|
+
@attributes.include?(attribute_name)
|
114
121
|
end
|
115
122
|
|
116
123
|
# Handles returning data for a specific attribute
|
117
124
|
#
|
118
125
|
# @private
|
119
126
|
def get_attribute(attribute_name)
|
120
|
-
attributes[attribute_name]
|
127
|
+
@attributes[attribute_name]
|
121
128
|
end
|
129
|
+
alias attribute get_attribute
|
122
130
|
|
123
|
-
#
|
124
|
-
#
|
125
|
-
# @private
|
131
|
+
# Return the value of the model `primary_key` attribute
|
126
132
|
def id
|
127
|
-
attributes[self.class.primary_key]
|
133
|
+
@attributes[self.class.primary_key]
|
128
134
|
end
|
129
135
|
|
130
136
|
# Return `true` if the other object is also a Her::Model and has matching data
|
131
137
|
#
|
132
138
|
# @private
|
133
139
|
def ==(other)
|
134
|
-
other.is_a?(Her::Model) && attributes == other.attributes
|
140
|
+
other.is_a?(Her::Model) && @attributes == other.attributes
|
135
141
|
end
|
136
142
|
|
137
143
|
# Delegate to the == method
|
@@ -145,13 +151,14 @@ module Her
|
|
145
151
|
# [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
|
146
152
|
# @private
|
147
153
|
def hash
|
148
|
-
attributes.hash
|
154
|
+
@attributes.hash
|
149
155
|
end
|
150
156
|
|
151
157
|
module ClassMethods
|
152
158
|
# Initialize a collection of resources with raw data from an HTTP request
|
153
159
|
#
|
154
160
|
# @param [Array] parsed_data
|
161
|
+
# @private
|
155
162
|
def new_collection(parsed_data)
|
156
163
|
Her::Model::Attributes.initialize_collection(self, parsed_data)
|
157
164
|
end
|
@@ -159,7 +166,6 @@ module Her
|
|
159
166
|
# Define the attributes that will be used to track dirty attributes and validations
|
160
167
|
#
|
161
168
|
# @param [Array] attributes
|
162
|
-
#
|
163
169
|
# @example
|
164
170
|
# class User
|
165
171
|
# include Her::Model
|
@@ -172,17 +178,17 @@ module Her
|
|
172
178
|
attribute = attribute.to_sym
|
173
179
|
|
174
180
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
175
|
-
|
176
|
-
|
181
|
+
unless instance_methods.include?(:'#{attribute}=')
|
182
|
+
def #{attribute}=(value)
|
183
|
+
self.send(:"#{attribute}_will_change!") if @attributes[:'#{attribute}'] != value
|
184
|
+
@attributes[:'#{attribute}'] = value
|
185
|
+
end
|
177
186
|
end
|
178
187
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
def #{attribute}?
|
185
|
-
@attributes.include?(:'#{attribute}') && @attributes[:'#{attribute}'].present?
|
188
|
+
unless instance_methods.include?(:'#{attribute}?')
|
189
|
+
def #{attribute}?
|
190
|
+
@attributes.include?(:'#{attribute}') && @attributes[:'#{attribute}'].present?
|
191
|
+
end
|
186
192
|
end
|
187
193
|
RUBY
|
188
194
|
end
|
@@ -198,22 +204,7 @@ module Her
|
|
198
204
|
# store_response_errors :server_errors
|
199
205
|
# end
|
200
206
|
def store_response_errors(value = nil)
|
201
|
-
|
202
|
-
remove_method @_her_store_response_errors
|
203
|
-
remove_method :"#{@_her_store_response_errors}="
|
204
|
-
end
|
205
|
-
|
206
|
-
@_her_store_response_errors ||= begin
|
207
|
-
superclass.store_response_errors if superclass.respond_to?(:store_response_errors)
|
208
|
-
end
|
209
|
-
|
210
|
-
return @_her_store_response_errors unless value
|
211
|
-
@_her_store_response_errors = value
|
212
|
-
|
213
|
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
214
|
-
def #{@_her_store_response_errors}; @response_errors; end
|
215
|
-
def #{@_her_store_response_errors}=(value); @response_errors = value; end
|
216
|
-
RUBY
|
207
|
+
store_her_data(:response_errors, value)
|
217
208
|
end
|
218
209
|
|
219
210
|
# Define the accessor in which the API response metadata (obtained from the parsing middleware) will be stored
|
@@ -226,22 +217,7 @@ module Her
|
|
226
217
|
# store_metadata :server_data
|
227
218
|
# end
|
228
219
|
def store_metadata(value = nil)
|
229
|
-
|
230
|
-
remove_method @_her_store_metadata
|
231
|
-
remove_method :"#{@_her_store_metadata}="
|
232
|
-
end
|
233
|
-
|
234
|
-
@_her_store_metadata ||= begin
|
235
|
-
superclass.store_metadata if superclass.respond_to?(:store_metadata)
|
236
|
-
end
|
237
|
-
|
238
|
-
return @_her_store_metadata unless value
|
239
|
-
@_her_store_metadata = value
|
240
|
-
|
241
|
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
242
|
-
def #{@_her_store_metadata}; @metadata; end
|
243
|
-
def #{@_her_store_metadata}=(value); @metadata = value; end
|
244
|
-
RUBY
|
220
|
+
store_her_data(:metadata, value)
|
245
221
|
end
|
246
222
|
|
247
223
|
# @private
|
@@ -251,6 +227,27 @@ module Her
|
|
251
227
|
memo
|
252
228
|
end
|
253
229
|
end
|
230
|
+
|
231
|
+
private
|
232
|
+
# @private
|
233
|
+
def store_her_data(name, value)
|
234
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
235
|
+
if @_her_store_#{name} && value.present?
|
236
|
+
remove_method @_her_store_#{name}
|
237
|
+
remove_method @_her_store_#{name}.to_s + '='
|
238
|
+
end
|
239
|
+
|
240
|
+
@_her_store_#{name} ||= begin
|
241
|
+
superclass.store_#{name} if superclass.respond_to?(:store_#{name})
|
242
|
+
end
|
243
|
+
|
244
|
+
return @_her_store_#{name} unless value
|
245
|
+
@_her_store_#{name} = value
|
246
|
+
|
247
|
+
define_method(value) { @#{name} }
|
248
|
+
define_method(value.to_s+'=') { |value| @#{name} = value }
|
249
|
+
RUBY
|
250
|
+
end
|
254
251
|
end
|
255
252
|
end
|
256
253
|
end
|
data/lib/her/model/http.rb
CHANGED
@@ -5,6 +5,22 @@ module Her
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
METHODS = [:get, :post, :put, :patch, :delete]
|
7
7
|
|
8
|
+
# For each HTTP method, define these class methods:
|
9
|
+
#
|
10
|
+
# - <method>(path, params)
|
11
|
+
# - <method>_raw(path, params, &block)
|
12
|
+
# - <method>_collection(path, params, &block)
|
13
|
+
# - <method>_resource(path, params, &block)
|
14
|
+
# - custom_<method>(*paths)
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# class User
|
18
|
+
# include Her::Model
|
19
|
+
# custom_get :active
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# User.get(:popular) # GET "/users/popular"
|
23
|
+
# User.active # GET "/users/active"
|
8
24
|
module ClassMethods
|
9
25
|
# Change which API the model will use to make its HTTP requests
|
10
26
|
#
|
@@ -32,8 +48,8 @@ module Her
|
|
32
48
|
# Main request wrapper around Her::API. Used to make custom request to the API.
|
33
49
|
#
|
34
50
|
# @private
|
35
|
-
def request(
|
36
|
-
request = her_api.request(
|
51
|
+
def request(params={})
|
52
|
+
request = her_api.request(params)
|
37
53
|
|
38
54
|
if block_given?
|
39
55
|
yield request[:parsed_data], request[:response]
|
@@ -42,18 +58,11 @@ module Her
|
|
42
58
|
end
|
43
59
|
end
|
44
60
|
|
45
|
-
# For each HTTP method, define these methods:
|
46
|
-
#
|
47
|
-
# - <method>(path, attrs)
|
48
|
-
# - <method>_raw(path, attrs, &block)
|
49
|
-
# - <method>_collection(path, attrs, &block)
|
50
|
-
# - <method>_resource(path, attrs, &block)
|
51
|
-
# - custom_<method>(path, attrs)
|
52
61
|
METHODS.each do |method|
|
53
62
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
54
|
-
def #{method}(path,
|
55
|
-
path = build_request_path_from_string_or_symbol(path,
|
56
|
-
send(:'#{method}_raw', path,
|
63
|
+
def #{method}(path, params={})
|
64
|
+
path = build_request_path_from_string_or_symbol(path, params)
|
65
|
+
send(:'#{method}_raw', path, params) do |parsed_data, response|
|
57
66
|
if parsed_data[:data].is_a?(Array)
|
58
67
|
new_collection(parsed_data)
|
59
68
|
else
|
@@ -62,21 +71,21 @@ module Her
|
|
62
71
|
end
|
63
72
|
end
|
64
73
|
|
65
|
-
def #{method}_raw(path,
|
66
|
-
path = build_request_path_from_string_or_symbol(path,
|
67
|
-
request(
|
74
|
+
def #{method}_raw(path, params={}, &block)
|
75
|
+
path = build_request_path_from_string_or_symbol(path, params)
|
76
|
+
request(params.merge(:_method => #{method.to_sym.inspect}, :_path => path), &block)
|
68
77
|
end
|
69
78
|
|
70
|
-
def #{method}_collection(path,
|
71
|
-
path = build_request_path_from_string_or_symbol(path,
|
72
|
-
send(:'#{method}_raw', build_request_path_from_string_or_symbol(path,
|
79
|
+
def #{method}_collection(path, params={})
|
80
|
+
path = build_request_path_from_string_or_symbol(path, params)
|
81
|
+
send(:'#{method}_raw', build_request_path_from_string_or_symbol(path, params), params) do |parsed_data, response|
|
73
82
|
new_collection(parsed_data)
|
74
83
|
end
|
75
84
|
end
|
76
85
|
|
77
|
-
def #{method}_resource(path,
|
78
|
-
path = build_request_path_from_string_or_symbol(path,
|
79
|
-
send(:"#{method}_raw", path,
|
86
|
+
def #{method}_resource(path, params={})
|
87
|
+
path = build_request_path_from_string_or_symbol(path, params)
|
88
|
+
send(:"#{method}_raw", path, params) do |parsed_data, response|
|
80
89
|
new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
|
81
90
|
end
|
82
91
|
end
|
@@ -86,9 +95,9 @@ module Her
|
|
86
95
|
opts = paths.last.is_a?(Hash) ? paths.pop : Hash.new
|
87
96
|
|
88
97
|
paths.each do |path|
|
89
|
-
metaclass.send(:define_method, path) do |*
|
90
|
-
|
91
|
-
send(#{method.to_sym.inspect}, path,
|
98
|
+
metaclass.send(:define_method, path) do |*params|
|
99
|
+
params = params.first || Hash.new
|
100
|
+
send(#{method.to_sym.inspect}, path, params)
|
92
101
|
end
|
93
102
|
end
|
94
103
|
end
|
data/lib/her/model/orm.rb
CHANGED
@@ -154,8 +154,8 @@ module Her
|
|
154
154
|
# Delegate the following methods to `scoped`
|
155
155
|
[:all, :where, :create, :build, :first_or_create, :first_or_initialize].each do |method|
|
156
156
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
157
|
-
def #{method}(*
|
158
|
-
scoped.send(#{method.to_sym.inspect}, *
|
157
|
+
def #{method}(*params)
|
158
|
+
scoped.send(#{method.to_sym.inspect}, *params)
|
159
159
|
end
|
160
160
|
RUBY
|
161
161
|
end
|
data/lib/her/model/parse.rb
CHANGED
@@ -10,13 +10,14 @@ module Her
|
|
10
10
|
# @user.to_params
|
11
11
|
# # => { :id => 1, :name => 'John Smith' }
|
12
12
|
def to_params
|
13
|
-
self.class.include_root_in_json? ? { self.class.included_root_element => attributes.dup } : attributes.dup
|
13
|
+
self.class.include_root_in_json? ? { self.class.included_root_element => attributes.dup.symbolize_keys } : attributes.dup.symbolize_keys
|
14
14
|
end
|
15
15
|
|
16
16
|
module ClassMethods
|
17
17
|
# Parse data before assigning it to a resource, based on `parse_root_in_json`.
|
18
18
|
#
|
19
19
|
# @param [Hash] data
|
20
|
+
# @private
|
20
21
|
def parse(data)
|
21
22
|
parse_root_in_json? ? data[parsed_root_element] : data
|
22
23
|
end
|
data/lib/her/model/paths.rb
CHANGED
@@ -87,16 +87,12 @@ module Her
|
|
87
87
|
|
88
88
|
# Return a custom path based on the collection path and variable parameters
|
89
89
|
#
|
90
|
-
# @
|
91
|
-
# class User
|
92
|
-
# include Her::Model
|
93
|
-
# collection_path "/utilisateurs"
|
94
|
-
# end
|
95
|
-
#
|
96
|
-
# User.all # Fetched via GET /utilisateurs
|
90
|
+
# @private
|
97
91
|
def build_request_path(path=nil, parameters={})
|
92
|
+
parameters = parameters.try(:with_indifferent_access)
|
93
|
+
|
98
94
|
unless path.is_a?(String)
|
99
|
-
parameters = path ||
|
95
|
+
parameters = path.try(:with_indifferent_access) || parameters
|
100
96
|
path =
|
101
97
|
if parameters.include?(primary_key) && parameters[primary_key]
|
102
98
|
resource_path.dup
|
@@ -111,13 +107,13 @@ module Her
|
|
111
107
|
path.gsub(/:([\w_]+)/) do
|
112
108
|
# Look for :key or :_key, otherwise raise an exception
|
113
109
|
value = $1.to_sym
|
114
|
-
parameters.delete(value) || parameters.delete(:"_#{value}") || raise(Her::Errors::PathError.new("Missing :_#{$1} parameter to build the request path. Path is `#{path}`. Parameters are `#{parameters.inspect}`.", $1))
|
110
|
+
parameters.delete(value) || parameters.delete(:"_#{value}") || raise(Her::Errors::PathError.new("Missing :_#{$1} parameter to build the request path. Path is `#{path}`. Parameters are `#{parameters.symbolize_keys.inspect}`.", $1))
|
115
111
|
end
|
116
112
|
end
|
117
113
|
|
118
114
|
# @private
|
119
|
-
def build_request_path_from_string_or_symbol(path,
|
120
|
-
path.is_a?(Symbol) ? "#{build_request_path(
|
115
|
+
def build_request_path_from_string_or_symbol(path, params={})
|
116
|
+
path.is_a?(Symbol) ? "#{build_request_path(params)}/#{path}" : path
|
121
117
|
end
|
122
118
|
end
|
123
119
|
end
|