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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +706 -0
- data/Rakefile +32 -0
- data/lib/json_api_client.rb +30 -0
- data/lib/json_api_client/associations.rb +8 -0
- data/lib/json_api_client/associations/base_association.rb +33 -0
- data/lib/json_api_client/associations/belongs_to.rb +31 -0
- data/lib/json_api_client/associations/has_many.rb +8 -0
- data/lib/json_api_client/associations/has_one.rb +16 -0
- data/lib/json_api_client/connection.rb +41 -0
- data/lib/json_api_client/error_collector.rb +91 -0
- data/lib/json_api_client/errors.rb +107 -0
- data/lib/json_api_client/formatter.rb +145 -0
- data/lib/json_api_client/helpers.rb +9 -0
- data/lib/json_api_client/helpers/associatable.rb +88 -0
- data/lib/json_api_client/helpers/callbacks.rb +27 -0
- data/lib/json_api_client/helpers/dirty.rb +75 -0
- data/lib/json_api_client/helpers/dynamic_attributes.rb +78 -0
- data/lib/json_api_client/helpers/uri.rb +9 -0
- data/lib/json_api_client/implementation.rb +12 -0
- data/lib/json_api_client/included_data.rb +58 -0
- data/lib/json_api_client/linking.rb +6 -0
- data/lib/json_api_client/linking/links.rb +22 -0
- data/lib/json_api_client/linking/top_level_links.rb +39 -0
- data/lib/json_api_client/meta_data.rb +19 -0
- data/lib/json_api_client/middleware.rb +7 -0
- data/lib/json_api_client/middleware/json_request.rb +26 -0
- data/lib/json_api_client/middleware/parse_json.rb +31 -0
- data/lib/json_api_client/middleware/status.rb +67 -0
- data/lib/json_api_client/paginating.rb +6 -0
- data/lib/json_api_client/paginating/nested_param_paginator.rb +140 -0
- data/lib/json_api_client/paginating/paginator.rb +89 -0
- data/lib/json_api_client/parsers.rb +5 -0
- data/lib/json_api_client/parsers/parser.rb +102 -0
- data/lib/json_api_client/query.rb +6 -0
- data/lib/json_api_client/query/builder.rb +239 -0
- data/lib/json_api_client/query/requestor.rb +73 -0
- data/lib/json_api_client/relationships.rb +6 -0
- data/lib/json_api_client/relationships/relations.rb +55 -0
- data/lib/json_api_client/relationships/top_level_relations.rb +30 -0
- data/lib/json_api_client/request_params.rb +57 -0
- data/lib/json_api_client/resource.rb +643 -0
- data/lib/json_api_client/result_set.rb +25 -0
- data/lib/json_api_client/schema.rb +154 -0
- data/lib/json_api_client/utils.rb +48 -0
- data/lib/json_api_client/version.rb +3 -0
- metadata +213 -0
@@ -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,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,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
|