json_api_client 1.5.2 → 1.22.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.
@@ -0,0 +1,88 @@
1
+ module JsonApiClient
2
+ module Helpers
3
+ module Associatable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :associations, instance_accessor: false
8
+ self.associations = []
9
+ attr_accessor :__cached_associations
10
+ attr_accessor :__belongs_to_params
11
+ end
12
+
13
+ module ClassMethods
14
+ def _define_association(attr_name, association_klass, options = {})
15
+ attr_name = attr_name.to_sym
16
+ association = association_klass.new(attr_name, self, options)
17
+ self.associations += [association]
18
+ end
19
+
20
+ def _define_relationship_methods(attr_name)
21
+ attr_name = attr_name.to_sym
22
+
23
+ define_method(attr_name) do
24
+ _cached_relationship(attr_name) do
25
+ relationship_definition = relationship_definition_for(attr_name)
26
+ return unless relationship_definition
27
+ relationship_data_for(attr_name, relationship_definition)
28
+ end
29
+ end
30
+
31
+ define_method("#{attr_name}=") do |value|
32
+ _clear_cached_relationship(attr_name)
33
+ relationships.public_send("#{attr_name}=", value)
34
+ end
35
+ end
36
+
37
+ def belongs_to(attr_name, options = {})
38
+ _define_association(attr_name, JsonApiClient::Associations::BelongsTo::Association, options)
39
+
40
+ param = associations.last.param
41
+ define_method(param) do
42
+ _belongs_to_params[param]
43
+ end
44
+
45
+ define_method(:"#{param}=") do |value|
46
+ _belongs_to_params[param] = value
47
+ end
48
+ end
49
+
50
+ def has_many(attr_name, options = {})
51
+ _define_association(attr_name, JsonApiClient::Associations::HasMany::Association, options)
52
+ _define_relationship_methods(attr_name)
53
+ end
54
+
55
+ def has_one(attr_name, options = {})
56
+ _define_association(attr_name, JsonApiClient::Associations::HasOne::Association, options)
57
+ _define_relationship_methods(attr_name)
58
+ end
59
+ end
60
+
61
+ def _belongs_to_params
62
+ self.__belongs_to_params ||= {}
63
+ end
64
+
65
+ def _clear_belongs_to_params
66
+ self.__belongs_to_params = {}
67
+ end
68
+
69
+ def _cached_associations
70
+ self.__cached_associations ||= {}
71
+ end
72
+
73
+ def _clear_cached_relationships
74
+ self.__cached_associations = {}
75
+ end
76
+
77
+ def _clear_cached_relationship(attr_name)
78
+ _cached_associations.delete(attr_name)
79
+ end
80
+
81
+ def _cached_relationship(attr_name)
82
+ return _cached_associations[attr_name] if _cached_associations.has_key?(attr_name)
83
+ _cached_associations[attr_name] = yield
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -18,6 +18,10 @@ module JsonApiClient
18
18
  @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
19
19
  end
20
20
 
21
+ def forget_change!(attr)
22
+ @changed_attributes.delete(attr.to_s)
23
+ end
24
+
21
25
  def set_all_attributes_dirty
22
26
  attributes.each do |k, v|
23
27
  set_attribute_was(k, v)
@@ -68,4 +72,4 @@ module JsonApiClient
68
72
 
69
73
  end
70
74
  end
71
- end
75
+ end
@@ -24,7 +24,7 @@ module JsonApiClient
24
24
  end
25
25
 
26
26
  def respond_to_missing?(method, include_private = false)
27
- if (method.to_s =~ /^(.*)=$/) || has_attribute?(method)
27
+ if has_attribute?(method) || method.to_s.end_with?('=')
28
28
  true
29
29
  else
30
30
  super
@@ -38,16 +38,14 @@ module JsonApiClient
38
38
  protected
39
39
 
40
40
  def method_missing(method, *args, &block)
41
- normalized_method = if key_formatter
42
- key_formatter.unformat(method.to_s)
43
- else
44
- method.to_s
45
- end
46
-
47
- if normalized_method =~ /^(.*)=$/
48
- set_attribute($1, args.first)
49
- elsif has_attribute?(method)
50
- attributes[method]
41
+ if has_attribute?(method)
42
+ return attributes[method]
43
+ end
44
+
45
+ normalized_method = safe_key_formatter.unformat(method.to_s)
46
+
47
+ if normalized_method.end_with?('=')
48
+ set_attribute(normalized_method[0..-2], args.first)
51
49
  else
52
50
  super
53
51
  end
@@ -61,10 +59,20 @@ module JsonApiClient
61
59
  attributes[name] = value
62
60
  end
63
61
 
62
+ def safe_key_formatter
63
+ @safe_key_formatter ||= (key_formatter || DefaultKeyFormatter.new)
64
+ end
65
+
64
66
  def key_formatter
65
67
  self.class.respond_to?(:key_formatter) && self.class.key_formatter
66
68
  end
67
69
 
70
+ class DefaultKeyFormatter
71
+ def unformat(method)
72
+ method.to_s
73
+ end
74
+ end
75
+
68
76
  end
69
77
  end
70
78
  end
@@ -4,5 +4,6 @@ module JsonApiClient
4
4
  autoload :Dirty, 'json_api_client/helpers/dirty'
5
5
  autoload :DynamicAttributes, 'json_api_client/helpers/dynamic_attributes'
6
6
  autoload :URI, 'json_api_client/helpers/uri'
7
+ autoload :Associatable, 'json_api_client/helpers/associatable'
7
8
  end
8
9
  end
@@ -5,16 +5,29 @@ module JsonApiClient
5
5
  def initialize(result_set, data)
6
6
  record_class = result_set.record_class
7
7
  grouped_data = data.group_by{|datum| datum["type"]}
8
- @data = grouped_data.inject({}) do |h, (type, records)|
8
+ grouped_included_set = grouped_data.each_with_object({}) do |(type, records), h|
9
9
  klass = Utils.compute_type(record_class, record_class.key_formatter.unformat(type).singularize.classify)
10
- h[type] = records.map do |datum|
11
- params = klass.parser.parameters_from_resource(datum)
12
- resource = klass.load(params)
13
- resource.last_result_set = result_set
14
- resource
15
- end.index_by(&:id)
16
- h
10
+ h[type] = records.map do |record|
11
+ params = klass.parser.parameters_from_resource(record)
12
+ klass.load(params).tap do |resource|
13
+ resource.last_result_set = result_set
14
+ end
15
+ end
16
+ end
17
+
18
+ if record_class.search_included_in_result_set
19
+ # deep_merge overrides the nested Arrays o_O
20
+ # {a: [1,2]}.deep_merge(a: [3,4]) # => {a: [3,4]}
21
+ grouped_included_set.merge!(result_set.group_by(&:type)) do |_, resources1, resources2|
22
+ resources1 + resources2
23
+ end
17
24
  end
25
+
26
+ grouped_included_set.each do |type, resources|
27
+ grouped_included_set[type] = resources.index_by { |resource| resource.attributes[:id] }
28
+ end
29
+
30
+ @data = grouped_included_set
18
31
  end
19
32
 
20
33
  def data_for(method_name, definition)
@@ -23,9 +36,7 @@ module JsonApiClient
23
36
 
24
37
  if data.is_a?(Array)
25
38
  # has_many link
26
- data.map do |link_def|
27
- record_for(link_def)
28
- end
39
+ data.map(&method(:record_for)).compact
29
40
  else
30
41
  # has_one link
31
42
  record_for(data)
@@ -40,7 +51,8 @@ module JsonApiClient
40
51
 
41
52
  # should return a resource record of some type for this linked document
42
53
  def record_for(link_def)
43
- data[link_def["type"]][link_def["id"]]
54
+ record = data[link_def["type"]]
55
+ record[link_def["id"]] if record
44
56
  end
45
57
  end
46
58
  end
@@ -2,10 +2,25 @@ module JsonApiClient
2
2
  module Middleware
3
3
  class JsonRequest < Faraday::Middleware
4
4
  def call(environment)
5
+ accept_header = update_accept_header(environment[:request_headers])
6
+
5
7
  environment[:request_headers]["Content-Type"] = 'application/vnd.api+json'
6
- environment[:request_headers]["Accept"] = 'application/vnd.api+json'
8
+ environment[:request_headers]["Accept"] = accept_header
7
9
  @app.call(environment)
8
10
  end
11
+
12
+ private
13
+
14
+ def update_accept_header(headers)
15
+ return 'application/vnd.api+json' if headers["Accept"].nil?
16
+ accept_params = headers["Accept"].split(",")
17
+
18
+ unless accept_params.include?('application/vnd.api+json')
19
+ accept_params.unshift('application/vnd.api+json')
20
+ end
21
+
22
+ accept_params.join(",")
23
+ end
9
24
  end
10
25
  end
11
26
  end
@@ -1,6 +1,11 @@
1
1
  module JsonApiClient
2
2
  module Middleware
3
3
  class Status < Faraday::Middleware
4
+ def initialize(app, options)
5
+ super(app)
6
+ @options = options
7
+ end
8
+
4
9
  def call(environment)
5
10
  @app.call(environment).on_complete do |env|
6
11
  handle_status(env[:status], env)
@@ -11,13 +16,20 @@ module JsonApiClient
11
16
  handle_status(code, env)
12
17
  end
13
18
  end
14
- rescue Faraday::ConnectionFailed, Faraday::TimeoutError
15
- raise Errors::ConnectionError, environment
19
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
20
+ raise Errors::ConnectionError.new environment, e.to_s
16
21
  end
17
22
 
18
- protected
23
+ private
24
+
25
+ def custom_handler_for(code)
26
+ @options.fetch(:custom_handlers, {})[code]
27
+ end
19
28
 
20
29
  def handle_status(code, env)
30
+ custom_handler = custom_handler_for(code)
31
+ return custom_handler.call(env) if custom_handler.present?
32
+
21
33
  case code
22
34
  when 200..399
23
35
  when 401
@@ -25,12 +37,26 @@ module JsonApiClient
25
37
  when 403
26
38
  raise Errors::AccessDenied, env
27
39
  when 404
28
- raise Errors::NotFound, env[:url]
40
+ raise Errors::NotFound, env
41
+ when 408
42
+ raise Errors::RequestTimeout, env
29
43
  when 409
30
44
  raise Errors::Conflict, env
45
+ when 422
46
+ # Allow to proceed as resource errors will be populated
47
+ when 429
48
+ raise Errors::TooManyRequests, env
31
49
  when 400..499
32
- # some other error
33
- when 500..599
50
+ raise Errors::ClientError, env
51
+ when 500
52
+ raise Errors::InternalServerError, env
53
+ when 502
54
+ raise Errors::BadGateway, env
55
+ when 503
56
+ raise Errors::ServiceUnavailable, env
57
+ when 504
58
+ raise Errors::GatewayTimeout, env
59
+ when 501..599
34
60
  raise Errors::ServerError, env
35
61
  else
36
62
  raise Errors::UnexpectedStatus.new(code, env[:url])
@@ -0,0 +1,140 @@
1
+ module JsonApiClient
2
+ module Paginating
3
+ # An alternate, more consistent Paginator that always wraps
4
+ # pagination query string params in a top-level wrapper_name,
5
+ # e.g. page[offset]=2, page[limit]=10.
6
+ class NestedParamPaginator
7
+ DEFAULT_WRAPPER_NAME = "page".freeze
8
+ DEFAULT_PAGE_PARAM = "page".freeze
9
+ DEFAULT_PER_PAGE_PARAM = "per_page".freeze
10
+
11
+ # Define class accessors as methods to enforce standard way
12
+ # of defining pagination related query string params.
13
+ class << self
14
+
15
+ def wrapper_name
16
+ @_wrapper_name ||= DEFAULT_WRAPPER_NAME
17
+ end
18
+
19
+ def wrapper_name=(param = DEFAULT_WRAPPER_NAME)
20
+ raise ArgumentError, "don't wrap wrapper_name" unless valid_param?(param)
21
+
22
+ @_wrapper_name = param.to_s
23
+ end
24
+
25
+ def page_param
26
+ @_page_param ||= DEFAULT_PAGE_PARAM
27
+ "#{wrapper_name}[#{@_page_param}]"
28
+ end
29
+
30
+ def page_param=(param = DEFAULT_PAGE_PARAM)
31
+ raise ArgumentError, "don't wrap page_param" unless valid_param?(param)
32
+
33
+ @_page_param = param.to_s
34
+ end
35
+
36
+ def per_page_param
37
+ @_per_page_param ||= DEFAULT_PER_PAGE_PARAM
38
+ "#{wrapper_name}[#{@_per_page_param}]"
39
+ end
40
+
41
+ def per_page_param=(param = DEFAULT_PER_PAGE_PARAM)
42
+ raise ArgumentError, "don't wrap per_page_param" unless valid_param?(param)
43
+
44
+ @_per_page_param = param
45
+ end
46
+
47
+ private
48
+
49
+ def valid_param?(param)
50
+ !(param.nil? || param.to_s.include?("[") || param.to_s.include?("]"))
51
+ end
52
+
53
+ end
54
+
55
+ attr_reader :params, :result_set, :links
56
+
57
+ def initialize(result_set, data)
58
+ @params = params_for_uri(result_set.uri)
59
+ @result_set = result_set
60
+ @links = data["links"]
61
+ end
62
+
63
+ def next
64
+ result_set.links.fetch_link("next")
65
+ end
66
+
67
+ def prev
68
+ result_set.links.fetch_link("prev")
69
+ end
70
+
71
+ def first
72
+ result_set.links.fetch_link("first")
73
+ end
74
+
75
+ def last
76
+ result_set.links.fetch_link("last")
77
+ end
78
+
79
+ def total_pages
80
+ if links["last"]
81
+ uri = result_set.links.link_url_for("last")
82
+ last_params = params_for_uri(uri)
83
+ last_params.fetch(page_param, &method(:current_page)).to_i
84
+ else
85
+ current_page
86
+ end
87
+ end
88
+
89
+ # this is an estimate, not necessarily an exact count
90
+ def total_entries
91
+ per_page * total_pages
92
+ end
93
+ def total_count; total_entries; end
94
+
95
+ def offset
96
+ per_page * (current_page - 1)
97
+ end
98
+
99
+ def per_page
100
+ params.fetch(per_page_param) do
101
+ result_set.length
102
+ end.to_i
103
+ end
104
+
105
+ def current_page
106
+ params.fetch(page_param, 1).to_i
107
+ end
108
+
109
+ def out_of_bounds?
110
+ current_page > total_pages
111
+ end
112
+
113
+ def previous_page
114
+ current_page > 1 ? (current_page - 1) : nil
115
+ end
116
+
117
+ def next_page
118
+ current_page < total_pages ? (current_page + 1) : nil
119
+ end
120
+
121
+ def page_param
122
+ self.class.page_param
123
+ end
124
+
125
+ def per_page_param
126
+ self.class.per_page_param
127
+ end
128
+
129
+ alias limit_value per_page
130
+
131
+ protected
132
+
133
+ def params_for_uri(uri)
134
+ return {} unless uri
135
+ uri = Addressable::URI.parse(uri)
136
+ ( uri.query_values || {} ).with_indifferent_access
137
+ end
138
+ end
139
+ end
140
+ end
@@ -82,7 +82,7 @@ module JsonApiClient
82
82
  def params_for_uri(uri)
83
83
  return {} unless uri
84
84
  uri = Addressable::URI.parse(uri)
85
- uri.query_values || {}
85
+ ( uri.query_values || {} ).with_indifferent_access
86
86
  end
87
87
  end
88
88
  end
@@ -1,5 +1,6 @@
1
1
  module JsonApiClient
2
2
  module Paginating
3
3
  autoload :Paginator, 'json_api_client/paginating/paginator'
4
+ autoload :NestedParamPaginator, 'json_api_client/paginating/nested_param_paginator'
4
5
  end
5
- end
6
+ end
@@ -1,3 +1,5 @@
1
+ require 'active_support/all'
2
+
1
3
  module JsonApiClient
2
4
  module Query
3
5
  class Builder
@@ -5,60 +7,55 @@ module JsonApiClient
5
7
  attr_reader :klass
6
8
  delegate :key_formatter, to: :klass
7
9
 
8
- def initialize(klass)
9
- @klass = klass
10
- @primary_key = nil
11
- @pagination_params = {}
12
- @path_params = {}
13
- @additional_params = {}
14
- @filters = {}
15
- @includes = []
16
- @orders = []
17
- @fields = []
10
+ def initialize(klass, opts = {})
11
+ @klass = klass
12
+ @primary_key = opts.fetch( :primary_key, nil )
13
+ @pagination_params = opts.fetch( :pagination_params, {} )
14
+ @path_params = opts.fetch( :path_params, {} )
15
+ @additional_params = opts.fetch( :additional_params, {} )
16
+ @filters = opts.fetch( :filters, {} )
17
+ @includes = opts.fetch( :includes, [] )
18
+ @orders = opts.fetch( :orders, [] )
19
+ @fields = opts.fetch( :fields, [] )
18
20
  end
19
21
 
20
22
  def where(conditions = {})
21
23
  # pull out any path params here
22
- @path_params.merge!(conditions.slice(*klass.prefix_params))
23
- @filters.merge!(conditions.except(*klass.prefix_params))
24
- self
24
+ path_conditions = conditions.slice(*klass.prefix_params)
25
+ unpathed_conditions = conditions.except(*klass.prefix_params)
26
+
27
+ _new_scope( path_params: path_conditions, filters: unpathed_conditions )
25
28
  end
26
29
 
27
30
  def order(*args)
28
- @orders += parse_orders(*args)
29
- self
31
+ _new_scope( orders: parse_orders(*args) )
30
32
  end
31
33
 
32
34
  def includes(*tables)
33
- @includes += parse_related_links(*tables)
34
- self
35
+ _new_scope( includes: parse_related_links(*tables) )
35
36
  end
36
37
 
37
38
  def select(*fields)
38
- @fields += parse_fields(*fields)
39
- self
39
+ _new_scope( fields: parse_fields(*fields) )
40
40
  end
41
41
 
42
42
  def paginate(conditions = {})
43
- scope = self
43
+ scope = _new_scope
44
44
  scope = scope.page(conditions[:page]) if conditions[:page]
45
45
  scope = scope.per(conditions[:per_page]) if conditions[:per_page]
46
46
  scope
47
47
  end
48
48
 
49
49
  def page(number)
50
- @pagination_params[:number] = number
51
- self
50
+ _new_scope( pagination_params: { klass.paginator.page_param => number || 1 } )
52
51
  end
53
52
 
54
53
  def per(size)
55
- @pagination_params[:size] = size
56
- self
54
+ _new_scope( pagination_params: { klass.paginator.per_page_param => size } )
57
55
  end
58
56
 
59
57
  def with_params(more_params)
60
- @additional_params.merge!(more_params)
61
- self
58
+ _new_scope( additional_params: more_params )
62
59
  end
63
60
 
64
61
  def first
@@ -69,8 +66,12 @@ module JsonApiClient
69
66
  paginate(page: 1, per_page: 1).pages.last.to_a.last
70
67
  end
71
68
 
72
- def build
73
- klass.new(params)
69
+ def build(attrs = {})
70
+ klass.new @path_params.merge(attrs.with_indifferent_access)
71
+ end
72
+
73
+ def create(attrs = {})
74
+ klass.create @path_params.merge(attrs.with_indifferent_access)
74
75
  end
75
76
 
76
77
  def params
@@ -85,27 +86,63 @@ module JsonApiClient
85
86
  end
86
87
 
87
88
  def to_a
88
- @to_a ||= find
89
+ @to_a ||= _fetch
89
90
  end
90
91
  alias all to_a
91
92
 
92
93
  def find(args = {})
94
+ if klass.raise_on_blank_find_param && args.blank?
95
+ raise Errors::NotFound, nil, 'blank .find param'
96
+ end
97
+
93
98
  case args
94
99
  when Hash
95
- where(args)
100
+ scope = where(args)
96
101
  else
97
- @primary_key = args
102
+ scope = _new_scope( primary_key: args )
98
103
  end
99
104
 
100
- klass.requestor.get(params)
105
+ scope._fetch
101
106
  end
102
107
 
103
108
  def method_missing(method_name, *args, &block)
104
109
  to_a.send(method_name, *args, &block)
105
110
  end
106
111
 
112
+ def hash
113
+ [
114
+ klass,
115
+ params
116
+ ].hash
117
+ end
118
+
119
+ def ==(other)
120
+ return false unless other.is_a?(self.class)
121
+
122
+ hash == other.hash
123
+ end
124
+ alias_method :eql?, :==
125
+
126
+ protected
127
+
128
+ def _fetch
129
+ klass.requestor.get(params)
130
+ end
131
+
107
132
  private
108
133
 
134
+ def _new_scope( opts = {} )
135
+ self.class.new( @klass,
136
+ primary_key: opts.fetch( :primary_key, @primary_key ),
137
+ pagination_params: @pagination_params.merge( opts.fetch( :pagination_params, {} ) ),
138
+ path_params: @path_params.merge( opts.fetch( :path_params, {} ) ),
139
+ additional_params: @additional_params.deep_merge( opts.fetch( :additional_params, {} ) ),
140
+ filters: @filters.merge( opts.fetch( :filters, {} ) ),
141
+ includes: @includes + opts.fetch( :includes, [] ),
142
+ orders: @orders + opts.fetch( :orders, [] ),
143
+ fields: @fields + opts.fetch( :fields, [] ) )
144
+ end
145
+
109
146
  def path_params
110
147
  @path_params.empty? ? {} : {path: @path_params}
111
148
  end
@@ -123,7 +160,13 @@ module JsonApiClient
123
160
  end
124
161
 
125
162
  def pagination_params
126
- @pagination_params.empty? ? {} : {page: @pagination_params}
163
+ if klass.paginator.ancestors.include?(Paginating::Paginator)
164
+ # Original Paginator inconsistently wraps pagination params here. Keeping
165
+ # default behavior for now so as not to break backward compatibility.
166
+ @pagination_params.empty? ? {} : {page: @pagination_params}
167
+ else
168
+ @pagination_params
169
+ end
127
170
  end
128
171
 
129
172
  def includes_params
@@ -159,22 +202,7 @@ module JsonApiClient
159
202
  end
160
203
 
161
204
  def parse_related_links(*tables)
162
- tables.map do |table|
163
- case table
164
- when Hash
165
- table.map do |k, v|
166
- parse_related_links(*v).map do |sub|
167
- "#{k}.#{sub}"
168
- end
169
- end
170
- when Array
171
- table.map do |v|
172
- parse_related_links(*v)
173
- end
174
- else
175
- key_formatter.format(table)
176
- end
177
- end.flatten
205
+ Utils.parse_includes(klass, *tables)
178
206
  end
179
207
 
180
208
  def parse_orders(*args)