her 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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