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.
Files changed (54) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +19 -1279
  3. data/.rubocop_todo.yml +232 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +16 -4
  6. data/README.md +25 -1
  7. data/gemfiles/Gemfile.faraday-1.0 +6 -0
  8. data/her.gemspec +4 -3
  9. data/lib/her/api.rb +8 -7
  10. data/lib/her/collection.rb +2 -1
  11. data/lib/her/errors.rb +3 -1
  12. data/lib/her/json_api/model.rb +8 -12
  13. data/lib/her/middleware.rb +1 -1
  14. data/lib/her/middleware/accept_json.rb +1 -0
  15. data/lib/her/middleware/first_level_parse_json.rb +6 -5
  16. data/lib/her/middleware/json_api_parser.rb +6 -5
  17. data/lib/her/middleware/parse_json.rb +2 -1
  18. data/lib/her/middleware/second_level_parse_json.rb +6 -5
  19. data/lib/her/model/associations.rb +7 -7
  20. data/lib/her/model/associations/association.rb +7 -9
  21. data/lib/her/model/associations/association_proxy.rb +2 -3
  22. data/lib/her/model/associations/belongs_to_association.rb +2 -3
  23. data/lib/her/model/attributes.rb +14 -6
  24. data/lib/her/model/base.rb +2 -2
  25. data/lib/her/model/http.rb +7 -2
  26. data/lib/her/model/introspection.rb +5 -3
  27. data/lib/her/model/nested_attributes.rb +1 -1
  28. data/lib/her/model/orm.rb +27 -9
  29. data/lib/her/model/parse.rb +10 -12
  30. data/lib/her/model/paths.rb +3 -4
  31. data/lib/her/model/relation.rb +5 -4
  32. data/lib/her/version.rb +1 -1
  33. data/spec/api_spec.rb +3 -0
  34. data/spec/middleware/accept_json_spec.rb +1 -0
  35. data/spec/middleware/first_level_parse_json_spec.rb +2 -1
  36. data/spec/middleware/json_api_parser_spec.rb +1 -0
  37. data/spec/middleware/second_level_parse_json_spec.rb +1 -0
  38. data/spec/model/associations/association_proxy_spec.rb +1 -0
  39. data/spec/model/associations_spec.rb +98 -14
  40. data/spec/model/attributes_spec.rb +9 -3
  41. data/spec/model/callbacks_spec.rb +14 -15
  42. data/spec/model/dirty_spec.rb +1 -0
  43. data/spec/model/http_spec.rb +29 -18
  44. data/spec/model/introspection_spec.rb +3 -2
  45. data/spec/model/nested_attributes_spec.rb +1 -0
  46. data/spec/model/orm_spec.rb +39 -16
  47. data/spec/model/parse_spec.rb +24 -0
  48. data/spec/model/paths_spec.rb +1 -0
  49. data/spec/model/relation_spec.rb +3 -2
  50. data/spec/model/validations_spec.rb +1 -0
  51. data/spec/model_spec.rb +1 -0
  52. data/spec/support/extensions/array.rb +1 -0
  53. data/spec/support/extensions/hash.rb +1 -0
  54. metadata +15 -19
@@ -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 |*args|
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 { |request_body|
25
- attrs = attributes.dup.symbolize_keys.tap { |filtered_attributes|
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 = changes.symbolize_keys.keys.inject({}) do |hash, attribute|
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
 
@@ -7,6 +7,6 @@ module Her
7
7
  module Middleware
8
8
  DefaultParseJSON = FirstLevelParseJSON
9
9
 
10
- autoload :JsonApiParser, 'her/middleware/json_api_parser'
10
+ autoload :JsonApiParser, 'her/middleware/json_api_parser'
11
11
  end
12
12
  end
@@ -2,6 +2,7 @@ module Her
2
2
  module Middleware
3
3
  # This middleware adds a "Accept: application/json" HTTP header
4
4
  class AcceptJSON < Faraday::Middleware
5
+
5
6
  # @private
6
7
  def add_header(headers)
7
8
  headers.merge! "Accept" => "application/json"
@@ -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
- when 204
29
- parse('{}')
30
- else
31
- parse(env[:body])
32
- end
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
- when 204, 304
29
- parse('{}')
30
- else
31
- parse(env[:body])
32
- end
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) or json.is_a?(Array)
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
- when 204
29
- parse('{}')
30
- else
31
- parse(env[:body])
32
- end
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, (name, details)| memo << details }.flatten.map { |a| a[:name] }
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, (name, details)| memo << details }.flatten.map { |a| a[:data_key] }
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 (defaults to `/{class_name}.pluralize/{id}`)
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 lambda { "#{@parent.request_path(@params)}#{@opts[: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
- begin
59
- instance_exec(&code)
60
- rescue Her::Errors::PathError
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 self.clone.tap { |a| a.params = a.params.merge(params) }
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 lambda { "#{@parent.request_path(@params)}#{@opts[:path]}/#{id}" }
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
- :build, :create, :where, :find, :all, :assign_nested_attributes, :reload
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 :object_id == name # avoid redefining object_id
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 lambda { @klass.build_request_path(path_params) }
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
@@ -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] and record.kind_of?(klass)
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 |record|
165
- record.send :clear_changes_information
166
- record.run_callbacks :find
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
@@ -11,7 +11,7 @@ module Her
11
11
  # @private
12
12
  def has_key?(attribute_name)
13
13
  has_attribute?(attribute_name) ||
14
- has_association?(attribute_name)
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
- get_association(attribute_name)
24
+ get_association(attribute_name)
25
25
  end
26
26
 
27
27
  # @private
@@ -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
- opts = paths.last.is_a?(Hash) ? paths.pop : Hash.new
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 = self.her_containing_module
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 self.name =~ /::/
60
- self.name.split("::")[0..-2].join("::").constantize
61
+ return unless name =~ /::/
62
+ name.split("::")[0..-2].join("::").constantize
61
63
  end
62
64
  end
63
65
  end