carwow-json_api_client 1.19.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +706 -0
  4. data/Rakefile +32 -0
  5. data/lib/json_api_client.rb +30 -0
  6. data/lib/json_api_client/associations.rb +8 -0
  7. data/lib/json_api_client/associations/base_association.rb +33 -0
  8. data/lib/json_api_client/associations/belongs_to.rb +31 -0
  9. data/lib/json_api_client/associations/has_many.rb +8 -0
  10. data/lib/json_api_client/associations/has_one.rb +16 -0
  11. data/lib/json_api_client/connection.rb +41 -0
  12. data/lib/json_api_client/error_collector.rb +91 -0
  13. data/lib/json_api_client/errors.rb +107 -0
  14. data/lib/json_api_client/formatter.rb +145 -0
  15. data/lib/json_api_client/helpers.rb +9 -0
  16. data/lib/json_api_client/helpers/associatable.rb +88 -0
  17. data/lib/json_api_client/helpers/callbacks.rb +27 -0
  18. data/lib/json_api_client/helpers/dirty.rb +75 -0
  19. data/lib/json_api_client/helpers/dynamic_attributes.rb +78 -0
  20. data/lib/json_api_client/helpers/uri.rb +9 -0
  21. data/lib/json_api_client/implementation.rb +12 -0
  22. data/lib/json_api_client/included_data.rb +58 -0
  23. data/lib/json_api_client/linking.rb +6 -0
  24. data/lib/json_api_client/linking/links.rb +22 -0
  25. data/lib/json_api_client/linking/top_level_links.rb +39 -0
  26. data/lib/json_api_client/meta_data.rb +19 -0
  27. data/lib/json_api_client/middleware.rb +7 -0
  28. data/lib/json_api_client/middleware/json_request.rb +26 -0
  29. data/lib/json_api_client/middleware/parse_json.rb +31 -0
  30. data/lib/json_api_client/middleware/status.rb +67 -0
  31. data/lib/json_api_client/paginating.rb +6 -0
  32. data/lib/json_api_client/paginating/nested_param_paginator.rb +140 -0
  33. data/lib/json_api_client/paginating/paginator.rb +89 -0
  34. data/lib/json_api_client/parsers.rb +5 -0
  35. data/lib/json_api_client/parsers/parser.rb +102 -0
  36. data/lib/json_api_client/query.rb +6 -0
  37. data/lib/json_api_client/query/builder.rb +239 -0
  38. data/lib/json_api_client/query/requestor.rb +73 -0
  39. data/lib/json_api_client/relationships.rb +6 -0
  40. data/lib/json_api_client/relationships/relations.rb +55 -0
  41. data/lib/json_api_client/relationships/top_level_relations.rb +30 -0
  42. data/lib/json_api_client/request_params.rb +57 -0
  43. data/lib/json_api_client/resource.rb +643 -0
  44. data/lib/json_api_client/result_set.rb +25 -0
  45. data/lib/json_api_client/schema.rb +154 -0
  46. data/lib/json_api_client/utils.rb +48 -0
  47. data/lib/json_api_client/version.rb +3 -0
  48. metadata +213 -0
@@ -0,0 +1,7 @@
1
+ module JsonApiClient
2
+ module Middleware
3
+ autoload :JsonRequest, 'json_api_client/middleware/json_request'
4
+ autoload :ParseJson, 'json_api_client/middleware/parse_json'
5
+ autoload :Status, 'json_api_client/middleware/status'
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ module JsonApiClient
2
+ module Middleware
3
+ class JsonRequest < Faraday::Middleware
4
+ def call(environment)
5
+ accept_header = update_accept_header(environment[:request_headers])
6
+
7
+ environment[:request_headers]["Content-Type"] = 'application/vnd.api+json'
8
+ environment[:request_headers]["Accept"] = accept_header
9
+ @app.call(environment)
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
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ module JsonApiClient
2
+ module Middleware
3
+ class ParseJson < Faraday::Middleware
4
+
5
+ def call(environment)
6
+ @app.call(environment).on_complete do |env|
7
+ if process_response_type?(response_type(env))
8
+ env[:raw_body] = env[:body]
9
+ env[:body] = parse(env[:body])
10
+ end
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def parse(body)
17
+ ::JSON.parse(body) unless body.strip.empty?
18
+ end
19
+
20
+ def response_type(env)
21
+ type = env[:response_headers]['Content-Type'].to_s
22
+ type = type.split(';', 2).first if type.index(';')
23
+ type
24
+ end
25
+
26
+ def process_response_type?(type)
27
+ !!type.match(/\bjson$/)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ module JsonApiClient
2
+ module Middleware
3
+ class Status < Faraday::Middleware
4
+ def initialize(app, options)
5
+ super(app)
6
+ @options = options
7
+ end
8
+
9
+ def call(environment)
10
+ @app.call(environment).on_complete do |env|
11
+ handle_status(env[:status], env)
12
+
13
+ # look for meta[:status]
14
+ if env[:body].is_a?(Hash)
15
+ code = env[:body].fetch("meta", {}).fetch("status", 200).to_i
16
+ handle_status(code, env)
17
+ end
18
+ end
19
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
20
+ raise Errors::ConnectionError.new environment, e.to_s
21
+ end
22
+
23
+ private
24
+
25
+ def custom_handler_for(code)
26
+ @options.fetch(:custom_handlers, {})[code]
27
+ end
28
+
29
+ def handle_status(code, env)
30
+ custom_handler = custom_handler_for(code)
31
+ return custom_handler.call(env) if custom_handler.present?
32
+
33
+ case code
34
+ when 200..399
35
+ when 401
36
+ raise Errors::NotAuthorized, env
37
+ when 403
38
+ raise Errors::AccessDenied, env
39
+ when 404
40
+ raise Errors::NotFound, env[:url]
41
+ when 408
42
+ raise Errors::RequestTimeout, env
43
+ when 409
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
49
+ when 400..499
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
60
+ raise Errors::ServerError, env
61
+ else
62
+ raise Errors::UnexpectedStatus.new(code, env[:url])
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,6 @@
1
+ module JsonApiClient
2
+ module Paginating
3
+ autoload :Paginator, 'json_api_client/paginating/paginator'
4
+ autoload :NestedParamPaginator, 'json_api_client/paginating/nested_param_paginator'
5
+ end
6
+ end
@@ -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
@@ -0,0 +1,89 @@
1
+ module JsonApiClient
2
+ module Paginating
3
+ class Paginator
4
+ class_attribute :page_param,
5
+ :per_page_param
6
+
7
+ self.page_param = "page"
8
+ self.per_page_param = "per_page"
9
+
10
+ attr_reader :params, :result_set, :links
11
+
12
+ def initialize(result_set, data)
13
+ @params = params_for_uri(result_set.uri)
14
+ @result_set = result_set
15
+ @links = data["links"]
16
+ end
17
+
18
+ def next
19
+ result_set.links.fetch_link("next")
20
+ end
21
+
22
+ def prev
23
+ result_set.links.fetch_link("prev")
24
+ end
25
+
26
+ def first
27
+ result_set.links.fetch_link("first")
28
+ end
29
+
30
+ def last
31
+ result_set.links.fetch_link("last")
32
+ end
33
+
34
+ def total_pages
35
+ if links["last"]
36
+ uri = result_set.links.link_url_for("last")
37
+ last_params = params_for_uri(uri)
38
+ last_params.fetch(page_param) do
39
+ current_page
40
+ end.to_i
41
+ else
42
+ current_page
43
+ end
44
+ end
45
+
46
+ # this number may be off
47
+ def total_entries
48
+ per_page * total_pages
49
+ end
50
+ def total_count; total_entries; end
51
+
52
+ def offset
53
+ per_page * (current_page - 1)
54
+ end
55
+
56
+ def per_page
57
+ params.fetch(per_page_param) do
58
+ result_set.length
59
+ end.to_i
60
+ end
61
+
62
+ def current_page
63
+ params.fetch(page_param, 1).to_i
64
+ end
65
+
66
+ def out_of_bounds?
67
+ current_page > total_pages
68
+ end
69
+
70
+ def previous_page
71
+ current_page > 1 ? (current_page - 1) : nil
72
+ end
73
+
74
+ def next_page
75
+ current_page < total_pages ? (current_page + 1) : nil
76
+ end
77
+
78
+ alias limit_value per_page
79
+
80
+ protected
81
+
82
+ def params_for_uri(uri)
83
+ return {} unless uri
84
+ uri = Addressable::URI.parse(uri)
85
+ ( uri.query_values || {} ).with_indifferent_access
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,5 @@
1
+ module JsonApiClient
2
+ module Parsers
3
+ autoload :Parser, 'json_api_client/parsers/parser'
4
+ end
5
+ end
@@ -0,0 +1,102 @@
1
+ module JsonApiClient
2
+ module Parsers
3
+ class Parser
4
+ class << self
5
+ def parse(klass, response)
6
+ data = response.body.present? ? response.body : {}
7
+
8
+ ResultSet.new.tap do |result_set|
9
+ result_set.record_class = klass
10
+ result_set.uri = response.env[:url]
11
+ handle_json_api(result_set, data)
12
+ handle_data(result_set, data)
13
+ handle_errors(result_set, data)
14
+ handle_meta(result_set, data)
15
+ handle_links(result_set, data)
16
+ handle_relationships(result_set, data)
17
+ handle_pagination(result_set, data)
18
+ handle_included(result_set, data)
19
+ end
20
+ end
21
+
22
+ #
23
+ # Given a resource hash, returns a Resource.new friendly hash
24
+ # which flattens the attributes in w/ id and type.
25
+ #
26
+ # Example:
27
+ #
28
+ # Given:
29
+ # {
30
+ # id: 1.
31
+ # type: 'person',
32
+ # attributes: {
33
+ # first_name: 'Jeff',
34
+ # last_name: 'Ching'
35
+ # },
36
+ # links: {...},
37
+ # relationships: {...}
38
+ # }
39
+ #
40
+ # Returns:
41
+ # {
42
+ # id: 1,
43
+ # type: 'person',
44
+ # first_name: 'Jeff',
45
+ # last_name: 'Ching'
46
+ # links: {...},
47
+ # relationships: {...}
48
+ # }
49
+ #
50
+ #
51
+ def parameters_from_resource(params)
52
+ attrs = params.slice('id', 'links', 'meta', 'type', 'relationships')
53
+ attrs.merge(params.fetch('attributes', {}))
54
+ end
55
+
56
+ private
57
+
58
+ def handle_json_api(result_set, data)
59
+ result_set.implementation = Implementation.new(data.fetch("jsonapi", {}))
60
+ end
61
+
62
+ def handle_data(result_set, data)
63
+ # all data lives under the "data" attribute
64
+ results = data.fetch("data", [])
65
+
66
+ # we will treat everything as an Array
67
+ results = [results] unless results.is_a?(Array)
68
+ resources = results.compact.map do |res|
69
+ resource = result_set.record_class.load(parameters_from_resource(res))
70
+ resource.last_result_set = result_set
71
+ resource
72
+ end
73
+ result_set.concat(resources)
74
+ end
75
+
76
+ def handle_errors(result_set, data)
77
+ result_set.errors = ErrorCollector.new(data.fetch("errors", []))
78
+ end
79
+
80
+ def handle_meta(result_set, data)
81
+ result_set.meta = MetaData.new(data.fetch("meta", {}), result_set.record_class)
82
+ end
83
+
84
+ def handle_links(result_set, data)
85
+ result_set.links = Linking::TopLevelLinks.new(result_set.record_class, data.fetch("links", {}))
86
+ end
87
+
88
+ def handle_relationships(result_set, data)
89
+ result_set.relationships = Relationships::TopLevelRelations.new(result_set.record_class, data.fetch("relationships", {}))
90
+ end
91
+
92
+ def handle_pagination(result_set, data)
93
+ result_set.pages = result_set.record_class.paginator.new(result_set, data)
94
+ end
95
+
96
+ def handle_included(result_set, data)
97
+ result_set.included = IncludedData.new(result_set, data.fetch("included", []))
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end