him 0.1.0
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 +7 -0
- data/.github/workflows/ci.yml +40 -0
- data/.gitignore +6 -0
- data/.qlty/qlty.toml +57 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +2 -0
- data/LICENSE +8 -0
- data/README.md +1007 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +101 -0
- data/gemfiles/Gemfile.activemodel-6.1 +6 -0
- data/gemfiles/Gemfile.activemodel-7.0 +6 -0
- data/gemfiles/Gemfile.activemodel-7.1 +6 -0
- data/gemfiles/Gemfile.activemodel-7.2 +6 -0
- data/gemfiles/Gemfile.activemodel-8.0 +6 -0
- data/him.gemspec +28 -0
- data/lib/him/api.rb +121 -0
- data/lib/him/collection.rb +21 -0
- data/lib/him/errors.rb +29 -0
- data/lib/him/json_api/model.rb +42 -0
- data/lib/him/middleware/accept_json.rb +18 -0
- data/lib/him/middleware/first_level_parse_json.rb +37 -0
- data/lib/him/middleware/json_api_parser.rb +65 -0
- data/lib/him/middleware/parse_json.rb +22 -0
- data/lib/him/middleware/second_level_parse_json.rb +37 -0
- data/lib/him/middleware.rb +12 -0
- data/lib/him/model/associations/association.rb +147 -0
- data/lib/him/model/associations/association_proxy.rb +47 -0
- data/lib/him/model/associations/belongs_to_association.rb +95 -0
- data/lib/him/model/associations/has_many_association.rb +113 -0
- data/lib/him/model/associations/has_one_association.rb +79 -0
- data/lib/him/model/associations.rb +141 -0
- data/lib/him/model/attributes.rb +337 -0
- data/lib/him/model/base.rb +33 -0
- data/lib/him/model/http.rb +113 -0
- data/lib/him/model/introspection.rb +77 -0
- data/lib/him/model/nested_attributes.rb +45 -0
- data/lib/him/model/orm.rb +306 -0
- data/lib/him/model/parse.rb +224 -0
- data/lib/him/model/paths.rb +125 -0
- data/lib/him/model/relation.rb +212 -0
- data/lib/him/model.rb +79 -0
- data/lib/him/version.rb +3 -0
- data/lib/him.rb +22 -0
- data/spec/api_spec.rb +120 -0
- data/spec/collection_spec.rb +70 -0
- data/spec/json_api/model_spec.rb +260 -0
- data/spec/middleware/accept_json_spec.rb +11 -0
- data/spec/middleware/first_level_parse_json_spec.rb +63 -0
- data/spec/middleware/json_api_parser_spec.rb +52 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations/association_proxy_spec.rb +29 -0
- data/spec/model/associations_spec.rb +1010 -0
- data/spec/model/attributes_spec.rb +384 -0
- data/spec/model/callbacks_spec.rb +194 -0
- data/spec/model/dirty_spec.rb +133 -0
- data/spec/model/http_spec.rb +187 -0
- data/spec/model/introspection_spec.rb +110 -0
- data/spec/model/nested_attributes_spec.rb +135 -0
- data/spec/model/orm_spec.rb +717 -0
- data/spec/model/parse_spec.rb +619 -0
- data/spec/model/paths_spec.rb +348 -0
- data/spec/model/relation_spec.rb +255 -0
- data/spec/model/validations_spec.rb +45 -0
- data/spec/model_spec.rb +55 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/extensions/array.rb +6 -0
- data/spec/support/extensions/hash.rb +6 -0
- data/spec/support/macros/her_macros.rb +17 -0
- data/spec/support/macros/model_macros.rb +36 -0
- data/spec/support/macros/request_macros.rb +27 -0
- metadata +201 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
module Him
|
|
2
|
+
module Model
|
|
3
|
+
# This module handles all methods related to model attributes
|
|
4
|
+
module Attributes
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
# Initialize a new object with data
|
|
8
|
+
#
|
|
9
|
+
# @param [Hash] attributes The attributes to initialize the object with
|
|
10
|
+
# @option attributes [Hash,Array] :_metadata
|
|
11
|
+
# @option attributes [Hash,Array] :_errors
|
|
12
|
+
# @option attributes [Boolean] :_destroyed
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# class User
|
|
16
|
+
# include Him::Model
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
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">
|
|
26
|
+
def initialize(attributes = {})
|
|
27
|
+
attributes ||= {}
|
|
28
|
+
@metadata = attributes.delete(:_metadata) || {}
|
|
29
|
+
@response_errors = attributes.delete(:_errors) || {}
|
|
30
|
+
@destroyed = attributes.delete(:_destroyed) || false
|
|
31
|
+
@_her_new_record = attributes.delete(:_new_record) { true }
|
|
32
|
+
|
|
33
|
+
attributes = self.class.default_scope.apply_to(attributes)
|
|
34
|
+
assign_attributes(attributes)
|
|
35
|
+
yield self if block_given?
|
|
36
|
+
run_callbacks :initialize
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Ensure dup/clone creates a deep copy of mutable state
|
|
40
|
+
#
|
|
41
|
+
# @private
|
|
42
|
+
def initialize_copy(other)
|
|
43
|
+
super
|
|
44
|
+
@_her_attributes = other.attributes.dup
|
|
45
|
+
@metadata = other.metadata.dup if other.metadata
|
|
46
|
+
@response_errors = other.response_errors.dup if other.response_errors
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Handles missing methods
|
|
50
|
+
#
|
|
51
|
+
# @private
|
|
52
|
+
def method_missing(method, *args, &blk)
|
|
53
|
+
if method.to_s =~ /[?=]$/ || @_her_attributes.include?(method)
|
|
54
|
+
# Extract the attribute
|
|
55
|
+
attribute = method.to_s.sub(/[?=]$/, '')
|
|
56
|
+
|
|
57
|
+
# Create a new `attribute` methods set
|
|
58
|
+
self.class.attributes(*attribute)
|
|
59
|
+
|
|
60
|
+
# Resend the method!
|
|
61
|
+
send(method, *args, &blk)
|
|
62
|
+
else
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @private
|
|
68
|
+
def respond_to_missing?(method, include_private = false)
|
|
69
|
+
return false if Thread.current[:her_respond_to_missing]
|
|
70
|
+
method.to_s =~ /[?=]$/ || @_her_attributes.include?(method) || super
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Check if the model responds to a method without considering
|
|
74
|
+
# method_missing-based dynamic attributes.
|
|
75
|
+
def respond_to_without_missing?(method, include_private = false)
|
|
76
|
+
Thread.current[:her_respond_to_missing] = true
|
|
77
|
+
respond_to?(method, include_private)
|
|
78
|
+
ensure
|
|
79
|
+
Thread.current[:her_respond_to_missing] = false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Assign new attributes to a resource
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# class User
|
|
86
|
+
# include Him::Model
|
|
87
|
+
# end
|
|
88
|
+
#
|
|
89
|
+
# user = User.find(1) # => #<User id=1 name="Tobias">
|
|
90
|
+
# user.assign_attributes(name: "Lindsay")
|
|
91
|
+
# user.changes # => { :name => ["Tobias", "Lindsay"] }
|
|
92
|
+
def assign_attributes(new_attributes)
|
|
93
|
+
if !new_attributes.respond_to?(:to_hash)
|
|
94
|
+
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Coerce new_attributes to hash in case of strong parameters
|
|
98
|
+
new_attributes = new_attributes.to_hash
|
|
99
|
+
|
|
100
|
+
@_her_attributes ||= attributes
|
|
101
|
+
|
|
102
|
+
# Use setter methods first
|
|
103
|
+
unset_attributes = self.class.use_setter_methods(self, new_attributes)
|
|
104
|
+
|
|
105
|
+
# Then translate attributes of associations into association instances
|
|
106
|
+
associations = self.class.parse_associations(unset_attributes)
|
|
107
|
+
|
|
108
|
+
# Then merge the associations into @_her_attributes.
|
|
109
|
+
@_her_attributes.merge!(associations)
|
|
110
|
+
end
|
|
111
|
+
alias attributes= assign_attributes
|
|
112
|
+
|
|
113
|
+
def attributes
|
|
114
|
+
# The natural choice of instance variable naming here would be
|
|
115
|
+
# `@attributes`. Unfortunately that causes a naming clash when
|
|
116
|
+
# used with `ActiveModel` version >= 5.2.0.
|
|
117
|
+
# As of v5.2.0 `ActiveModel` checks to see if `ActiveRecord`
|
|
118
|
+
# attributes exist, and assumes that if the instance variable
|
|
119
|
+
# `@attributes` exists on the instance, it is because they are
|
|
120
|
+
# `ActiveRecord` attributes.
|
|
121
|
+
@_her_attributes ||= HashWithIndifferentAccess.new
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Handles returning true for the accessible attributes
|
|
125
|
+
#
|
|
126
|
+
# @private
|
|
127
|
+
def has_attribute?(attribute_name)
|
|
128
|
+
@_her_attributes.include?(attribute_name)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Handles returning data for a specific attribute
|
|
132
|
+
#
|
|
133
|
+
# @private
|
|
134
|
+
def get_attribute(attribute_name)
|
|
135
|
+
@_her_attributes[attribute_name]
|
|
136
|
+
end
|
|
137
|
+
alias attribute get_attribute
|
|
138
|
+
|
|
139
|
+
# Return the value of the model `primary_key` attribute
|
|
140
|
+
def id
|
|
141
|
+
@_her_attributes[self.class.primary_key]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Return `true` if the other object is also a Him::Model and has matching
|
|
145
|
+
# data
|
|
146
|
+
#
|
|
147
|
+
# @private
|
|
148
|
+
def ==(other)
|
|
149
|
+
other.is_a?(Him::Model) && @_her_attributes == other.attributes
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Delegate to the == method
|
|
153
|
+
#
|
|
154
|
+
# @private
|
|
155
|
+
def eql?(other)
|
|
156
|
+
self == other
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Delegate to @_her_attributes, allowing models to act correctly in code like:
|
|
160
|
+
# [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
|
|
161
|
+
# @private
|
|
162
|
+
def hash
|
|
163
|
+
@_her_attributes.hash
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Assign attribute value (ActiveModel convention method).
|
|
167
|
+
#
|
|
168
|
+
# @private
|
|
169
|
+
def attribute=(attribute, value)
|
|
170
|
+
@_her_attributes[attribute] = nil unless @_her_attributes.include?(attribute)
|
|
171
|
+
send("#{attribute}_will_change!") unless value == @_her_attributes[attribute]
|
|
172
|
+
@_her_attributes[attribute] = value
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Check attribute value to be present (ActiveModel convention method).
|
|
176
|
+
#
|
|
177
|
+
# @private
|
|
178
|
+
def attribute?(attribute)
|
|
179
|
+
@_her_attributes.include?(attribute) && @_her_attributes[attribute].present?
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
module ClassMethods
|
|
183
|
+
# Initialize a single resource
|
|
184
|
+
#
|
|
185
|
+
# @private
|
|
186
|
+
def instantiate_record(klass, parsed_data)
|
|
187
|
+
if (record = parsed_data[:data]) && record.is_a?(klass)
|
|
188
|
+
record
|
|
189
|
+
else
|
|
190
|
+
attributes = klass.parse(record).merge(_metadata: parsed_data[:metadata],
|
|
191
|
+
_errors: parsed_data[:errors],
|
|
192
|
+
_new_record: false)
|
|
193
|
+
klass.new(attributes).tap do |record_instance|
|
|
194
|
+
record_instance.send :clear_changes_information
|
|
195
|
+
record_instance.run_callbacks :find
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Initialize a collection of resources
|
|
201
|
+
#
|
|
202
|
+
# @private
|
|
203
|
+
def instantiate_collection(klass, parsed_data = {})
|
|
204
|
+
raw_data = klass.extract_array(parsed_data)
|
|
205
|
+
raw_data = [] if raw_data.blank?
|
|
206
|
+
records = raw_data.map do |record|
|
|
207
|
+
instantiate_record(klass, data: record)
|
|
208
|
+
end
|
|
209
|
+
Him::Collection.new(records, parsed_data[:metadata], parsed_data[:errors])
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Initialize a collection of resources with raw data from an HTTP request
|
|
213
|
+
#
|
|
214
|
+
# @param [Array] parsed_data
|
|
215
|
+
# @private
|
|
216
|
+
def new_collection(parsed_data)
|
|
217
|
+
instantiate_collection(self, parsed_data)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Initialize a new object with the "raw" parsed_data from the parsing middleware
|
|
221
|
+
#
|
|
222
|
+
# @private
|
|
223
|
+
def new_from_parsed_data(parsed_data)
|
|
224
|
+
instantiate_record(self, parsed_data)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Use setter methods of model for each key / value pair in params
|
|
228
|
+
# Return key / value pairs for which no setter method was defined on the
|
|
229
|
+
# model
|
|
230
|
+
#
|
|
231
|
+
# @private
|
|
232
|
+
def use_setter_methods(model, params = {})
|
|
233
|
+
reserved = [:id, :class, :attributes, model.class.primary_key, *model.class.association_keys]
|
|
234
|
+
settable_params = params.reject { |k, _| reserved.include?(k.to_sym) }
|
|
235
|
+
model.class.attributes *settable_params.keys
|
|
236
|
+
|
|
237
|
+
settable_params.each_with_object(params.slice(*reserved)) do |(key, value), memo|
|
|
238
|
+
setter_method = "#{key}="
|
|
239
|
+
if model.respond_to_without_missing?(setter_method)
|
|
240
|
+
model.send setter_method, value
|
|
241
|
+
else
|
|
242
|
+
memo[key.to_sym] = value
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Define attribute method matchers to automatically define them using
|
|
248
|
+
# ActiveModel's define_attribute_methods.
|
|
249
|
+
#
|
|
250
|
+
# @private
|
|
251
|
+
def define_attribute_method_matchers
|
|
252
|
+
attribute_method_suffix '='
|
|
253
|
+
attribute_method_suffix '?'
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Create a mutex for dynamically generated attribute methods or use one
|
|
257
|
+
# defined by ActiveModel.
|
|
258
|
+
#
|
|
259
|
+
# @private
|
|
260
|
+
def attribute_methods_mutex
|
|
261
|
+
@attribute_methods_mutex ||= begin
|
|
262
|
+
if generated_attribute_methods.respond_to? :mu_synchronize
|
|
263
|
+
generated_attribute_methods
|
|
264
|
+
else
|
|
265
|
+
Mutex.new
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Define the attributes that will be used to track dirty attributes and
|
|
271
|
+
# validations
|
|
272
|
+
#
|
|
273
|
+
# @param [Array] attributes
|
|
274
|
+
# @example
|
|
275
|
+
# class User
|
|
276
|
+
# include Him::Model
|
|
277
|
+
# attributes :name, :email
|
|
278
|
+
# end
|
|
279
|
+
def attributes(*attributes)
|
|
280
|
+
attribute_methods_mutex.synchronize do
|
|
281
|
+
define_attribute_methods attributes
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Define the accessor in which the API response errors (obtained from
|
|
286
|
+
# the parsing middleware) will be stored
|
|
287
|
+
#
|
|
288
|
+
# @param [Symbol] store_response_errors
|
|
289
|
+
#
|
|
290
|
+
# @example
|
|
291
|
+
# class User
|
|
292
|
+
# include Him::Model
|
|
293
|
+
# store_response_errors :server_errors
|
|
294
|
+
# end
|
|
295
|
+
def store_response_errors(value = nil)
|
|
296
|
+
store_her_data(:response_errors, value)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Define the accessor in which the API response metadata (obtained from
|
|
300
|
+
# the parsing middleware) will be stored
|
|
301
|
+
#
|
|
302
|
+
# @param [Symbol] store_metadata
|
|
303
|
+
#
|
|
304
|
+
# @example
|
|
305
|
+
# class User
|
|
306
|
+
# include Him::Model
|
|
307
|
+
# store_metadata :server_data
|
|
308
|
+
# end
|
|
309
|
+
def store_metadata(value = nil)
|
|
310
|
+
store_her_data(:metadata, value)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
private
|
|
314
|
+
|
|
315
|
+
# @private
|
|
316
|
+
def store_her_data(name, value)
|
|
317
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
318
|
+
if @_her_store_#{name} && value.present?
|
|
319
|
+
remove_method @_her_store_#{name}.to_sym
|
|
320
|
+
remove_method @_her_store_#{name}.to_s + '='
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
@_her_store_#{name} ||= begin
|
|
324
|
+
superclass.store_#{name} if superclass.respond_to?(:store_#{name})
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
return @_her_store_#{name} unless value
|
|
328
|
+
@_her_store_#{name} = value
|
|
329
|
+
|
|
330
|
+
define_method(value) { @#{name} }
|
|
331
|
+
define_method(value.to_s+'=') { |value| @#{name} = value }
|
|
332
|
+
RUBY
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Him
|
|
2
|
+
module Model
|
|
3
|
+
# This module includes basic functionality to Him::Model
|
|
4
|
+
module Base
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
# Returns true if attribute_name is
|
|
8
|
+
# * in resource attributes
|
|
9
|
+
# * an association
|
|
10
|
+
#
|
|
11
|
+
# @private
|
|
12
|
+
def has_key?(attribute_name)
|
|
13
|
+
has_attribute?(attribute_name) ||
|
|
14
|
+
has_association?(attribute_name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Returns
|
|
18
|
+
# * the value of the attribute_name attribute if it's in orm data
|
|
19
|
+
# * the resource/collection corresponding to attribute_name if it's an association
|
|
20
|
+
#
|
|
21
|
+
# @private
|
|
22
|
+
def [](attribute_name)
|
|
23
|
+
get_attribute(attribute_name) ||
|
|
24
|
+
get_association(attribute_name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @private
|
|
28
|
+
def singularized_resource_name
|
|
29
|
+
self.class.name.split('::').last.tableize.singularize
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Him
|
|
2
|
+
module Model
|
|
3
|
+
# This module interacts with Him::API to fetch HTTP data
|
|
4
|
+
module HTTP
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
METHODS = [:get, :post, :put, :patch, :delete, :options]
|
|
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 Him::Model
|
|
19
|
+
# custom_get :active
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# User.get(:popular) # GET "/users/popular"
|
|
23
|
+
# User.active # GET "/users/active"
|
|
24
|
+
module ClassMethods
|
|
25
|
+
# Change which API the model will use to make its HTTP requests
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# secondary_api = Him::API.new :url => "https://api.example" do |connection|
|
|
29
|
+
# connection.use Faraday::Request::UrlEncoded
|
|
30
|
+
# connection.use Him::Middleware::DefaultParseJSON
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# class User
|
|
34
|
+
# include Him::Model
|
|
35
|
+
# use_api secondary_api
|
|
36
|
+
# end
|
|
37
|
+
def use_api(value = nil)
|
|
38
|
+
@_her_use_api ||= begin
|
|
39
|
+
superclass.use_api if superclass.respond_to?(:use_api)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
unless value
|
|
43
|
+
return (@_her_use_api.respond_to? :call) ? @_her_use_api.call : @_her_use_api
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@_her_use_api = value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
alias her_api use_api
|
|
50
|
+
alias uses_api use_api
|
|
51
|
+
|
|
52
|
+
# Main request wrapper around Him::API. Used to make custom request to the API.
|
|
53
|
+
#
|
|
54
|
+
# @private
|
|
55
|
+
def request(params = {})
|
|
56
|
+
request = her_api.request(params)
|
|
57
|
+
parsed_data = request[:parsed_data]
|
|
58
|
+
parsed_data = parsed_data.symbolize_keys if parsed_data.respond_to?(:symbolize_keys)
|
|
59
|
+
|
|
60
|
+
if block_given?
|
|
61
|
+
yield parsed_data, request[:response]
|
|
62
|
+
else
|
|
63
|
+
{ parsed_data: parsed_data, response: request[:response] }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
METHODS.each do |method|
|
|
68
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
69
|
+
def #{method}(path, params={})
|
|
70
|
+
path = build_request_path_from_string_or_symbol(path, params)
|
|
71
|
+
params = to_params(params) unless #{method.to_sym.inspect} == :get
|
|
72
|
+
send(:'#{method}_raw', path, params) do |parsed_data, response|
|
|
73
|
+
if parsed_data[:data].is_a?(Array) || json_api_format? || (active_model_serializers_format? && parsed_data[:data].is_a?(Hash) && parsed_data[:data].key?(pluralized_parsed_root_element))
|
|
74
|
+
new_collection(parsed_data)
|
|
75
|
+
else
|
|
76
|
+
new_from_parsed_data(parsed_data)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def #{method}_raw(path, params={}, &block)
|
|
82
|
+
path = build_request_path_from_string_or_symbol(path, params)
|
|
83
|
+
request(params.merge(:_method => #{method.to_sym.inspect}, :_path => path), &block)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def #{method}_collection(path, params={})
|
|
87
|
+
path = build_request_path_from_string_or_symbol(path, params)
|
|
88
|
+
send(:'#{method}_raw', build_request_path_from_string_or_symbol(path, params), params) do |parsed_data, response|
|
|
89
|
+
new_collection(parsed_data)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def #{method}_resource(path, params={})
|
|
94
|
+
path = build_request_path_from_string_or_symbol(path, params)
|
|
95
|
+
send(:"#{method}_raw", path, params) do |parsed_data, response|
|
|
96
|
+
new_from_parsed_data(parsed_data)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def custom_#{method}(*paths)
|
|
101
|
+
paths.each do |path|
|
|
102
|
+
singleton_class.send(:define_method, path) do |*params|
|
|
103
|
+
params = params.first || {}
|
|
104
|
+
send(#{method.to_sym.inspect}, path, params)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
RUBY
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module Him
|
|
2
|
+
module Model
|
|
3
|
+
module Introspection
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
# Inspect an element, returns it for introspection.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class User
|
|
9
|
+
# include Him::Model
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# @user = User.find(1)
|
|
13
|
+
# p @user # => #<User(/users/1) id=1 name="Tobias Fünke">
|
|
14
|
+
def inspect
|
|
15
|
+
first = Thread.current[:her_inspect_objects].nil?
|
|
16
|
+
Thread.current[:her_inspect_objects] = [] if first
|
|
17
|
+
|
|
18
|
+
resource_path = begin
|
|
19
|
+
request_path
|
|
20
|
+
rescue Him::Errors::PathError => e
|
|
21
|
+
"<unknown path, missing `#{e.missing_parameter}`>"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if Thread.current[:her_inspect_objects].include?(object_id)
|
|
25
|
+
"#<#{self.class}(#{resource_path}) ...>"
|
|
26
|
+
else
|
|
27
|
+
Thread.current[:her_inspect_objects] << object_id
|
|
28
|
+
"#<#{self.class}(#{resource_path}) #{attributes.keys.map { |k| "#{k}=#{attribute_for_inspect(send(k))}" }.join(" ")}>"
|
|
29
|
+
end
|
|
30
|
+
ensure
|
|
31
|
+
Thread.current[:her_inspect_objects] = nil if first
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def attribute_for_inspect(value)
|
|
37
|
+
if value.is_a?(String) && value.length > 50
|
|
38
|
+
"#{value[0..50]}...".inspect
|
|
39
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
|
40
|
+
%("#{value}")
|
|
41
|
+
else
|
|
42
|
+
value.inspect
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @private
|
|
47
|
+
module ClassMethods
|
|
48
|
+
# Finds a class at the same level as this one or at the global level.
|
|
49
|
+
#
|
|
50
|
+
# @private
|
|
51
|
+
def her_nearby_class(name)
|
|
52
|
+
her_sibling_class(name) || name.constantize
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
protected
|
|
56
|
+
|
|
57
|
+
# Looks for a class at the same level as this one with the given name.
|
|
58
|
+
#
|
|
59
|
+
# @private
|
|
60
|
+
def her_sibling_class(name)
|
|
61
|
+
if mod = her_containing_module
|
|
62
|
+
@_her_sibling_class ||= Hash.new { Hash.new }
|
|
63
|
+
@_her_sibling_class[mod][name] ||= "#{mod.name}::#{name}".constantize rescue nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# If available, returns the containing Module for this class.
|
|
68
|
+
#
|
|
69
|
+
# @private
|
|
70
|
+
def her_containing_module
|
|
71
|
+
return unless name =~ /::/
|
|
72
|
+
name.split("::")[0..-2].join("::").constantize
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Him
|
|
2
|
+
module Model
|
|
3
|
+
module NestedAttributes
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
module ClassMethods
|
|
7
|
+
# Allow nested attributes for an association
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# class User
|
|
11
|
+
# include Him::Model
|
|
12
|
+
#
|
|
13
|
+
# has_one :role
|
|
14
|
+
# accepts_nested_attributes_for :role
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# class Role
|
|
18
|
+
# include Him::Model
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# user = User.new(name: "Tobias", role_attributes: { title: "moderator" })
|
|
22
|
+
# user.role # => #<Role title="moderator">
|
|
23
|
+
def accepts_nested_attributes_for(*associations)
|
|
24
|
+
allowed_association_names = association_names
|
|
25
|
+
|
|
26
|
+
associations.each do |association_name|
|
|
27
|
+
unless allowed_association_names.include?(association_name)
|
|
28
|
+
raise Him::Errors::AssociationUnknownError, "Unknown association name :#{association_name}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
32
|
+
if method_defined?(:#{association_name}_attributes=)
|
|
33
|
+
remove_method(:#{association_name}_attributes=)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def #{association_name}_attributes=(attributes)
|
|
37
|
+
self.#{association_name}.assign_nested_attributes(attributes)
|
|
38
|
+
end
|
|
39
|
+
RUBY
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|