her 1.0.0 → 1.1.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 +5 -5
- data/.rubocop.yml +19 -1279
- data/.rubocop_todo.yml +232 -0
- data/.ruby-version +1 -0
- data/.travis.yml +16 -4
- data/README.md +25 -1
- data/gemfiles/Gemfile.faraday-1.0 +6 -0
- data/her.gemspec +4 -3
- data/lib/her/api.rb +8 -7
- data/lib/her/collection.rb +2 -1
- data/lib/her/errors.rb +3 -1
- data/lib/her/json_api/model.rb +8 -12
- data/lib/her/middleware.rb +1 -1
- data/lib/her/middleware/accept_json.rb +1 -0
- data/lib/her/middleware/first_level_parse_json.rb +6 -5
- data/lib/her/middleware/json_api_parser.rb +6 -5
- data/lib/her/middleware/parse_json.rb +2 -1
- data/lib/her/middleware/second_level_parse_json.rb +6 -5
- data/lib/her/model/associations.rb +7 -7
- data/lib/her/model/associations/association.rb +7 -9
- data/lib/her/model/associations/association_proxy.rb +2 -3
- data/lib/her/model/associations/belongs_to_association.rb +2 -3
- data/lib/her/model/attributes.rb +14 -6
- data/lib/her/model/base.rb +2 -2
- data/lib/her/model/http.rb +7 -2
- data/lib/her/model/introspection.rb +5 -3
- data/lib/her/model/nested_attributes.rb +1 -1
- data/lib/her/model/orm.rb +27 -9
- data/lib/her/model/parse.rb +10 -12
- data/lib/her/model/paths.rb +3 -4
- data/lib/her/model/relation.rb +5 -4
- data/lib/her/version.rb +1 -1
- data/spec/api_spec.rb +3 -0
- data/spec/middleware/accept_json_spec.rb +1 -0
- data/spec/middleware/first_level_parse_json_spec.rb +2 -1
- data/spec/middleware/json_api_parser_spec.rb +1 -0
- data/spec/middleware/second_level_parse_json_spec.rb +1 -0
- data/spec/model/associations/association_proxy_spec.rb +1 -0
- data/spec/model/associations_spec.rb +98 -14
- data/spec/model/attributes_spec.rb +9 -3
- data/spec/model/callbacks_spec.rb +14 -15
- data/spec/model/dirty_spec.rb +1 -0
- data/spec/model/http_spec.rb +29 -18
- data/spec/model/introspection_spec.rb +3 -2
- data/spec/model/nested_attributes_spec.rb +1 -0
- data/spec/model/orm_spec.rb +39 -16
- data/spec/model/parse_spec.rb +24 -0
- data/spec/model/paths_spec.rb +1 -0
- data/spec/model/relation_spec.rb +3 -2
- data/spec/model/validations_spec.rb +1 -0
- data/spec/model_spec.rb +1 -0
- data/spec/support/extensions/array.rb +1 -0
- data/spec/support/extensions/hash.rb +1 -0
- metadata +15 -19
data/lib/her/json_api/model.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
module Her
|
2
2
|
module JsonApi
|
3
3
|
module Model
|
4
|
-
|
5
4
|
def self.included(klass)
|
6
5
|
klass.class_eval do
|
7
6
|
include Her::Model
|
8
7
|
|
9
8
|
[:parse_root_in_json, :include_root_in_json, :root_element, :primary_key].each do |method|
|
10
|
-
define_method method do |*
|
9
|
+
define_method method do |*_|
|
11
10
|
raise NoMethodError, "Her::JsonApi::Model does not support the #{method} configuration option"
|
12
11
|
end
|
13
12
|
end
|
@@ -15,24 +14,21 @@ module Her
|
|
15
14
|
method_for :update, :patch
|
16
15
|
|
17
16
|
@type = name.demodulize.tableize
|
18
|
-
|
17
|
+
|
19
18
|
def self.parse(data)
|
20
19
|
data.fetch(:attributes).merge(data.slice(:id))
|
21
20
|
end
|
22
21
|
|
23
|
-
def self.to_params(attributes, changes={})
|
24
|
-
request_data = { type: @type }.tap
|
25
|
-
attrs = attributes.dup.symbolize_keys.tap
|
22
|
+
def self.to_params(attributes, changes = {})
|
23
|
+
request_data = { type: @type }.tap do |request_body|
|
24
|
+
attrs = attributes.dup.symbolize_keys.tap do |filtered_attributes|
|
26
25
|
if her_api.options[:send_only_modified_attributes]
|
27
|
-
filtered_attributes
|
28
|
-
hash[attribute] = filtered_attributes[attribute]
|
29
|
-
hash
|
30
|
-
end
|
26
|
+
filtered_attributes.slice! *changes.keys.map(&:to_sym)
|
31
27
|
end
|
32
|
-
|
28
|
+
end
|
33
29
|
request_body[:id] = attrs.delete(:id) if attrs[:id]
|
34
30
|
request_body[:attributes] = attrs
|
35
|
-
|
31
|
+
end
|
36
32
|
{ data: request_data }
|
37
33
|
end
|
38
34
|
|
data/lib/her/middleware.rb
CHANGED
@@ -2,6 +2,7 @@ module Her
|
|
2
2
|
module Middleware
|
3
3
|
# This middleware treat the received first-level JSON structure as the resource data.
|
4
4
|
class FirstLevelParseJSON < ParseJSON
|
5
|
+
|
5
6
|
# Parse the response body
|
6
7
|
#
|
7
8
|
# @param [String] body The response body
|
@@ -25,11 +26,11 @@ module Her
|
|
25
26
|
# @private
|
26
27
|
def on_complete(env)
|
27
28
|
env[:body] = case env[:status]
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
when 204
|
30
|
+
parse('{}')
|
31
|
+
else
|
32
|
+
parse(env[:body])
|
33
|
+
end
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
@@ -3,6 +3,7 @@ module Her
|
|
3
3
|
# This middleware expects the resource/collection data to be contained in the `data`
|
4
4
|
# key of the JSON object
|
5
5
|
class JsonApiParser < ParseJSON
|
6
|
+
|
6
7
|
# Parse the response body
|
7
8
|
#
|
8
9
|
# @param [String] body The response body
|
@@ -25,11 +26,11 @@ module Her
|
|
25
26
|
# @private
|
26
27
|
def on_complete(env)
|
27
28
|
env[:body] = case env[:status]
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
when 204, 304
|
30
|
+
parse('{}')
|
31
|
+
else
|
32
|
+
parse(env[:body])
|
33
|
+
end
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Her
|
2
2
|
module Middleware
|
3
3
|
class ParseJSON < Faraday::Response::Middleware
|
4
|
+
|
4
5
|
# @private
|
5
6
|
def parse_json(body = nil)
|
6
7
|
body = '{}' if body.blank?
|
@@ -12,7 +13,7 @@ module Her
|
|
12
13
|
raise Her::Errors::ParseError, message
|
13
14
|
end
|
14
15
|
|
15
|
-
raise Her::Errors::ParseError, message unless json.is_a?(Hash)
|
16
|
+
raise Her::Errors::ParseError, message unless json.is_a?(Hash) || json.is_a?(Array)
|
16
17
|
|
17
18
|
json
|
18
19
|
end
|
@@ -3,6 +3,7 @@ module Her
|
|
3
3
|
# This middleware expects the resource/collection data to be contained in the `data`
|
4
4
|
# key of the JSON object
|
5
5
|
class SecondLevelParseJSON < ParseJSON
|
6
|
+
|
6
7
|
# Parse the response body
|
7
8
|
#
|
8
9
|
# @param [String] body The response body
|
@@ -25,11 +26,11 @@ module Her
|
|
25
26
|
# @private
|
26
27
|
def on_complete(env)
|
27
28
|
env[:body] = case env[:status]
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
when 204
|
30
|
+
parse('{}')
|
31
|
+
else
|
32
|
+
parse(env[:body])
|
33
|
+
end
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
@@ -32,18 +32,18 @@ module Her
|
|
32
32
|
# @private
|
33
33
|
def associations
|
34
34
|
@_her_associations ||= begin
|
35
|
-
superclass.respond_to?(:associations) ? superclass.associations.dup : Hash.new { |h,k| h[k] = [] }
|
35
|
+
superclass.respond_to?(:associations) ? superclass.associations.dup : Hash.new { |h, k| h[k] = [] }
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
39
|
# @private
|
40
40
|
def association_names
|
41
|
-
associations.inject([]) { |memo, (
|
41
|
+
associations.inject([]) { |memo, (_, details)| memo << details }.flatten.map { |a| a[:name] }
|
42
42
|
end
|
43
43
|
|
44
44
|
# @private
|
45
45
|
def association_keys
|
46
|
-
associations.inject([]) { |memo, (
|
46
|
+
associations.inject([]) { |memo, (_, details)| memo << details }.flatten.map { |a| a[:data_key] }
|
47
47
|
end
|
48
48
|
|
49
49
|
# Parse associations data after initializing a new object
|
@@ -81,7 +81,7 @@ module Her
|
|
81
81
|
# @user = User.find(1)
|
82
82
|
# @user.articles # => [#<Article(articles/2) id=2 title="Hello world.">]
|
83
83
|
# # Fetched via GET "/users/1/articles"
|
84
|
-
def has_many(name, opts={})
|
84
|
+
def has_many(name, opts = {})
|
85
85
|
Her::Model::Associations::HasManyAssociation.attach(self, name, opts)
|
86
86
|
end
|
87
87
|
|
@@ -106,7 +106,7 @@ module Her
|
|
106
106
|
# @user = User.find(1)
|
107
107
|
# @user.organization # => #<Organization(organizations/2) id=2 name="Foobar Inc.">
|
108
108
|
# # Fetched via GET "/users/1/organization"
|
109
|
-
def has_one(name, opts={})
|
109
|
+
def has_one(name, opts = {})
|
110
110
|
Her::Model::Associations::HasOneAssociation.attach(self, name, opts)
|
111
111
|
end
|
112
112
|
|
@@ -116,7 +116,7 @@ module Her
|
|
116
116
|
# @param [Hash] opts Options
|
117
117
|
# @option opts [String] :class_name The name of the class to map objects to
|
118
118
|
# @option opts [Symbol] :data_key The attribute where the data is stored
|
119
|
-
# @option opts [Path] :path The relative path where to fetch the data
|
119
|
+
# @option opts [Path] :path The relative path where to fetch the data
|
120
120
|
# @option opts [Symbol] :foreign_key The foreign key used to build the `:id` part of the path (defaults to `{name}_id`)
|
121
121
|
#
|
122
122
|
# @example
|
@@ -132,7 +132,7 @@ module Her
|
|
132
132
|
# @user = User.find(1) # => #<User(users/1) id=1 team_id=2 name="Tobias">
|
133
133
|
# @user.team # => #<Team(teams/2) id=2 name="Developers">
|
134
134
|
# # Fetched via GET "/teams/2"
|
135
|
-
def belongs_to(name, opts={})
|
135
|
+
def belongs_to(name, opts = {})
|
136
136
|
Her::Model::Associations::BelongsToAssociation.attach(self, name, opts)
|
137
137
|
end
|
138
138
|
end
|
@@ -2,6 +2,7 @@ module Her
|
|
2
2
|
module Model
|
3
3
|
module Associations
|
4
4
|
class Association
|
5
|
+
|
5
6
|
# @private
|
6
7
|
attr_accessor :params
|
7
8
|
|
@@ -47,7 +48,7 @@ module Her
|
|
47
48
|
return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
|
48
49
|
return @opts[:default].try(:dup) if @parent.new?
|
49
50
|
|
50
|
-
path = build_association_path
|
51
|
+
path = build_association_path -> { "#{@parent.request_path(@params)}#{@opts[:path]}" }
|
51
52
|
@klass.get(path, @params).tap do |result|
|
52
53
|
@cached_result = result unless @params.any?
|
53
54
|
end
|
@@ -55,11 +56,9 @@ module Her
|
|
55
56
|
|
56
57
|
# @private
|
57
58
|
def build_association_path(code)
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
return nil
|
62
|
-
end
|
59
|
+
instance_exec(&code)
|
60
|
+
rescue Her::Errors::PathError
|
61
|
+
nil
|
63
62
|
end
|
64
63
|
|
65
64
|
# @private
|
@@ -81,7 +80,7 @@ module Her
|
|
81
80
|
# user.comments.where(:approved => 1) # Fetched via GET "/users/1/comments?approved=1
|
82
81
|
def where(params = {})
|
83
82
|
return self if params.blank? && @parent.attributes[@name].blank?
|
84
|
-
AssociationProxy.new
|
83
|
+
AssociationProxy.new clone.tap { |a| a.params = a.params.merge(params) }
|
85
84
|
end
|
86
85
|
alias all where
|
87
86
|
|
@@ -97,7 +96,7 @@ module Her
|
|
97
96
|
# user.comments.find(3) # Fetched via GET "/users/1/comments/3
|
98
97
|
def find(id)
|
99
98
|
return nil if id.blank?
|
100
|
-
path = build_association_path
|
99
|
+
path = build_association_path -> { "#{@parent.request_path(@params)}#{@opts[:path]}/#{id}" }
|
101
100
|
@klass.get_resource(path, @params)
|
102
101
|
end
|
103
102
|
|
@@ -123,7 +122,6 @@ module Her
|
|
123
122
|
reset
|
124
123
|
fetch
|
125
124
|
end
|
126
|
-
|
127
125
|
end
|
128
126
|
end
|
129
127
|
end
|
@@ -15,7 +15,7 @@ module Her
|
|
15
15
|
end
|
16
16
|
|
17
17
|
install_proxy_methods :association,
|
18
|
-
|
18
|
+
:build, :create, :where, :find, :all, :assign_nested_attributes, :reload
|
19
19
|
|
20
20
|
# @private
|
21
21
|
def initialize(association)
|
@@ -28,7 +28,7 @@ module Her
|
|
28
28
|
|
29
29
|
# @private
|
30
30
|
def method_missing(name, *args, &block)
|
31
|
-
if
|
31
|
+
if name == :object_id # avoid redefining object_id
|
32
32
|
return association.fetch.object_id
|
33
33
|
end
|
34
34
|
|
@@ -38,7 +38,6 @@ module Her
|
|
38
38
|
# resend message to fetched object
|
39
39
|
__send__(name, *args, &block)
|
40
40
|
end
|
41
|
-
|
42
41
|
end
|
43
42
|
end
|
44
43
|
end
|
@@ -10,8 +10,7 @@ module Her
|
|
10
10
|
:name => name,
|
11
11
|
:data_key => name,
|
12
12
|
:default => nil,
|
13
|
-
:foreign_key => "#{name}_id"
|
14
|
-
:path => "/#{name.to_s.pluralize}/:id"
|
13
|
+
:foreign_key => "#{name}_id"
|
15
14
|
}.merge(opts)
|
16
15
|
klass.associations[:belongs_to] << opts
|
17
16
|
|
@@ -80,7 +79,7 @@ module Her
|
|
80
79
|
return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
|
81
80
|
|
82
81
|
path_params = @parent.attributes.merge(@params.merge(@klass.primary_key => foreign_key_value))
|
83
|
-
path = build_association_path
|
82
|
+
path = build_association_path -> { @klass.build_request_path(@opts[:path], path_params) }
|
84
83
|
@klass.get_resource(path, @params).tap do |result|
|
85
84
|
@cached_result = result if @params.blank?
|
86
85
|
end
|
data/lib/her/model/attributes.rb
CHANGED
@@ -23,7 +23,7 @@ module Her
|
|
23
23
|
# u.name = "Tobias"
|
24
24
|
# end
|
25
25
|
# # => #<User name="Tobias">
|
26
|
-
def initialize(attributes={})
|
26
|
+
def initialize(attributes = {})
|
27
27
|
attributes ||= {}
|
28
28
|
@metadata = attributes.delete(:_metadata) || {}
|
29
29
|
@response_errors = attributes.delete(:_errors) || {}
|
@@ -69,7 +69,15 @@ module Her
|
|
69
69
|
# user.assign_attributes(name: "Lindsay")
|
70
70
|
# user.changes # => { :name => ["Tobias", "Lindsay"] }
|
71
71
|
def assign_attributes(new_attributes)
|
72
|
+
if !new_attributes.respond_to?(:to_hash)
|
73
|
+
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
|
74
|
+
end
|
75
|
+
|
76
|
+
# Coerce new_attributes to hash in case of strong parameters
|
77
|
+
new_attributes = new_attributes.to_hash
|
78
|
+
|
72
79
|
@_her_attributes ||= attributes
|
80
|
+
|
73
81
|
# Use setter methods first
|
74
82
|
unset_attributes = self.class.use_setter_methods(self, new_attributes)
|
75
83
|
|
@@ -151,19 +159,18 @@ module Her
|
|
151
159
|
end
|
152
160
|
|
153
161
|
module ClassMethods
|
154
|
-
|
155
162
|
# Initialize a single resource
|
156
163
|
#
|
157
164
|
# @private
|
158
165
|
def instantiate_record(klass, parsed_data)
|
159
|
-
if record = parsed_data[:data]
|
166
|
+
if (record = parsed_data[:data]) && record.is_a?(klass)
|
160
167
|
record
|
161
168
|
else
|
162
169
|
attributes = klass.parse(record).merge(_metadata: parsed_data[:metadata],
|
163
170
|
_errors: parsed_data[:errors])
|
164
|
-
klass.new(attributes).tap do |
|
165
|
-
|
166
|
-
|
171
|
+
klass.new(attributes).tap do |record_instance|
|
172
|
+
record_instance.send :clear_changes_information
|
173
|
+
record_instance.run_callbacks :find
|
167
174
|
end
|
168
175
|
end
|
169
176
|
end
|
@@ -289,6 +296,7 @@ module Her
|
|
289
296
|
end
|
290
297
|
|
291
298
|
private
|
299
|
+
|
292
300
|
# @private
|
293
301
|
def store_her_data(name, value)
|
294
302
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
data/lib/her/model/base.rb
CHANGED
@@ -11,7 +11,7 @@ module Her
|
|
11
11
|
# @private
|
12
12
|
def has_key?(attribute_name)
|
13
13
|
has_attribute?(attribute_name) ||
|
14
|
-
|
14
|
+
has_association?(attribute_name)
|
15
15
|
end
|
16
16
|
|
17
17
|
# Returns
|
@@ -21,7 +21,7 @@ module Her
|
|
21
21
|
# @private
|
22
22
|
def [](attribute_name)
|
23
23
|
get_attribute(attribute_name) ||
|
24
|
-
|
24
|
+
get_association(attribute_name)
|
25
25
|
end
|
26
26
|
|
27
27
|
# @private
|
data/lib/her/model/http.rb
CHANGED
@@ -52,7 +52,7 @@ module Her
|
|
52
52
|
# Main request wrapper around Her::API. Used to make custom request to the API.
|
53
53
|
#
|
54
54
|
# @private
|
55
|
-
def request(params={})
|
55
|
+
def request(params = {})
|
56
56
|
request = her_api.request(params)
|
57
57
|
|
58
58
|
if block_given?
|
@@ -97,7 +97,12 @@ module Her
|
|
97
97
|
|
98
98
|
def custom_#{method}(*paths)
|
99
99
|
metaclass = (class << self; self; end)
|
100
|
-
|
100
|
+
|
101
|
+
# TODO: Remove this check after January 2020
|
102
|
+
if paths.last.is_a?(Hash)
|
103
|
+
warn("[DEPRECATION] options for custom request methods are deprecated and will be removed on or after January 2020.")
|
104
|
+
paths.pop
|
105
|
+
end
|
101
106
|
|
102
107
|
paths.each do |path|
|
103
108
|
metaclass.send(:define_method, path) do |*params|
|
@@ -22,6 +22,7 @@ module Her
|
|
22
22
|
end
|
23
23
|
|
24
24
|
private
|
25
|
+
|
25
26
|
def attribute_for_inspect(value)
|
26
27
|
if value.is_a?(String) && value.length > 50
|
27
28
|
"#{value[0..50]}...".inspect
|
@@ -42,11 +43,12 @@ module Her
|
|
42
43
|
end
|
43
44
|
|
44
45
|
protected
|
46
|
+
|
45
47
|
# Looks for a class at the same level as this one with the given name.
|
46
48
|
#
|
47
49
|
# @private
|
48
50
|
def her_sibling_class(name)
|
49
|
-
if mod =
|
51
|
+
if mod = her_containing_module
|
50
52
|
@_her_sibling_class ||= Hash.new { Hash.new }
|
51
53
|
@_her_sibling_class[mod][name] ||= "#{mod.name}::#{name}".constantize rescue nil
|
52
54
|
end
|
@@ -56,8 +58,8 @@ module Her
|
|
56
58
|
#
|
57
59
|
# @private
|
58
60
|
def her_containing_module
|
59
|
-
return unless
|
60
|
-
|
61
|
+
return unless name =~ /::/
|
62
|
+
name.split("::")[0..-2].join("::").constantize
|
61
63
|
end
|
62
64
|
end
|
63
65
|
end
|