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
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'JsonApiClient'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << 'lib'
|
26
|
+
t.libs << 'test'
|
27
|
+
t.pattern = 'test/**/*_test.rb'
|
28
|
+
t.verbose = false
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
3
|
+
require 'json'
|
4
|
+
require 'addressable/uri'
|
5
|
+
require 'json_api_client/formatter'
|
6
|
+
|
7
|
+
module JsonApiClient
|
8
|
+
autoload :Associations, 'json_api_client/associations'
|
9
|
+
autoload :Attributes, 'json_api_client/attributes'
|
10
|
+
autoload :Connection, 'json_api_client/connection'
|
11
|
+
autoload :Errors, 'json_api_client/errors'
|
12
|
+
autoload :ErrorCollector, 'json_api_client/error_collector'
|
13
|
+
autoload :Helpers, 'json_api_client/helpers'
|
14
|
+
autoload :Implementation, 'json_api_client/implementation'
|
15
|
+
autoload :IncludedData, 'json_api_client/included_data'
|
16
|
+
autoload :Linking, 'json_api_client/linking'
|
17
|
+
autoload :Relationships, 'json_api_client/relationships'
|
18
|
+
autoload :LinkDefinition, 'json_api_client/link_definition'
|
19
|
+
autoload :MetaData, 'json_api_client/meta_data'
|
20
|
+
autoload :Middleware, 'json_api_client/middleware'
|
21
|
+
autoload :Paginating, 'json_api_client/paginating'
|
22
|
+
autoload :Parsers, 'json_api_client/parsers'
|
23
|
+
autoload :Query, 'json_api_client/query'
|
24
|
+
autoload :RequestParams, 'json_api_client/request_params'
|
25
|
+
autoload :Resource, 'json_api_client/resource'
|
26
|
+
autoload :ResultSet, 'json_api_client/result_set'
|
27
|
+
autoload :Schema, 'json_api_client/schema'
|
28
|
+
autoload :Utils, 'json_api_client/utils'
|
29
|
+
autoload :VERSION, 'json_api_client/version'
|
30
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Associations
|
3
|
+
autoload :BaseAssociation, 'json_api_client/associations/base_association'
|
4
|
+
autoload :BelongsTo, 'json_api_client/associations/belongs_to'
|
5
|
+
autoload :HasMany, 'json_api_client/associations/has_many'
|
6
|
+
autoload :HasOne, 'json_api_client/associations/has_one'
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Associations
|
3
|
+
class BaseAssociation
|
4
|
+
attr_reader :attr_name, :klass, :options
|
5
|
+
def initialize(attr_name, klass, options = {})
|
6
|
+
@attr_name = attr_name
|
7
|
+
@klass = klass
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def association_class
|
12
|
+
@association_class ||= Utils.compute_type(klass, options.fetch(:class_name) do
|
13
|
+
attr_name.to_s.classify
|
14
|
+
end)
|
15
|
+
end
|
16
|
+
|
17
|
+
def data(url)
|
18
|
+
from_result_set(association_class.requestor.linked(url))
|
19
|
+
end
|
20
|
+
|
21
|
+
def from_result_set(result_set)
|
22
|
+
result_set.to_a
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_records(data)
|
26
|
+
data.map do |d|
|
27
|
+
record_class = Utils.compute_type(klass, klass.key_formatter.unformat(d["type"]).classify)
|
28
|
+
record_class.load id: d["id"]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Associations
|
3
|
+
module BelongsTo
|
4
|
+
class Association < BaseAssociation
|
5
|
+
include Helpers::URI
|
6
|
+
|
7
|
+
attr_reader :param
|
8
|
+
|
9
|
+
def initialize(attr_name, klass, options = {})
|
10
|
+
super
|
11
|
+
@param = options.fetch(:param, :"#{attr_name}_id").to_sym
|
12
|
+
@shallow_path = options.fetch(:shallow_path, false)
|
13
|
+
end
|
14
|
+
|
15
|
+
def shallow_path?
|
16
|
+
@shallow_path
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_prefix_path(formatter)
|
20
|
+
"#{formatter.format(attr_name.to_s.pluralize)}/%{#{param}}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_prefix_path(attrs, formatter)
|
24
|
+
return if shallow_path? && !attrs[param]
|
25
|
+
attrs[param] = encode_part(attrs[param]) if attrs.key?(param)
|
26
|
+
to_prefix_path(formatter) % attrs
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Associations
|
3
|
+
module HasOne
|
4
|
+
class Association < BaseAssociation
|
5
|
+
def from_result_set(result_set)
|
6
|
+
result_set.first
|
7
|
+
end
|
8
|
+
|
9
|
+
def load_records(data)
|
10
|
+
record_class = Utils.compute_type(klass, klass.key_formatter.unformat(data["type"]).classify)
|
11
|
+
record_class.load id: data["id"]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
class Connection
|
3
|
+
|
4
|
+
attr_reader :faraday
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
site = options.fetch(:site)
|
8
|
+
connection_options = options.slice(:proxy, :ssl, :request, :headers, :params)
|
9
|
+
adapter_options = Array(options.fetch(:adapter, Faraday.default_adapter))
|
10
|
+
status_middleware_options = {}
|
11
|
+
status_middleware_options[:custom_handlers] = options[:status_handlers] if options[:status_handlers].present?
|
12
|
+
@faraday = Faraday.new(site, connection_options) do |builder|
|
13
|
+
builder.request :json
|
14
|
+
builder.use Middleware::JsonRequest
|
15
|
+
builder.use Middleware::Status, status_middleware_options
|
16
|
+
builder.use Middleware::ParseJson
|
17
|
+
builder.use ::FaradayMiddleware::Gzip
|
18
|
+
builder.adapter(*adapter_options)
|
19
|
+
end
|
20
|
+
yield(self) if block_given?
|
21
|
+
end
|
22
|
+
|
23
|
+
# insert middleware before ParseJson - middleware executed in reverse order -
|
24
|
+
# inserted middleware will run after json parsed
|
25
|
+
def use(middleware, *args, &block)
|
26
|
+
return if faraday.builder.locked?
|
27
|
+
faraday.builder.insert_before(Middleware::ParseJson, middleware, *args, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete(middleware)
|
31
|
+
faraday.builder.delete(middleware)
|
32
|
+
end
|
33
|
+
|
34
|
+
def run(request_method, path, params: nil, headers: {}, body: nil)
|
35
|
+
faraday.run_request(request_method, path, body, headers) do |request|
|
36
|
+
request.params.update(params) if params
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
class ErrorCollector < Array
|
3
|
+
class Error
|
4
|
+
delegate :[], to: :attrs
|
5
|
+
|
6
|
+
def initialize(attrs = {})
|
7
|
+
@attrs = (attrs || {}).with_indifferent_access
|
8
|
+
end
|
9
|
+
|
10
|
+
def id
|
11
|
+
attrs[:id]
|
12
|
+
end
|
13
|
+
|
14
|
+
def about
|
15
|
+
res = attrs.fetch(:links, {})
|
16
|
+
res ? res[:about] : {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def status
|
20
|
+
attrs[:status]
|
21
|
+
end
|
22
|
+
|
23
|
+
def code
|
24
|
+
attrs[:code]
|
25
|
+
end
|
26
|
+
|
27
|
+
def title
|
28
|
+
attrs[:title]
|
29
|
+
end
|
30
|
+
|
31
|
+
def detail
|
32
|
+
attrs[:detail]
|
33
|
+
end
|
34
|
+
|
35
|
+
def source_parameter
|
36
|
+
source[:parameter]
|
37
|
+
end
|
38
|
+
|
39
|
+
def source_pointer
|
40
|
+
source[:pointer]
|
41
|
+
end
|
42
|
+
|
43
|
+
def error_key
|
44
|
+
if source_pointer && source_pointer != "/data"
|
45
|
+
source_pointer.split("/").last
|
46
|
+
else
|
47
|
+
"base"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def error_msg
|
52
|
+
msg = title || detail || "invalid"
|
53
|
+
if source_parameter
|
54
|
+
"#{source_parameter} #{msg}"
|
55
|
+
else
|
56
|
+
msg
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def source
|
61
|
+
res = attrs.fetch(:source, {})
|
62
|
+
res ? res : {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def meta
|
66
|
+
MetaData.new(attrs.fetch(:meta, {}))
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
attr_reader :attrs
|
72
|
+
end
|
73
|
+
|
74
|
+
def initialize(error_data)
|
75
|
+
super(error_data.map do |data|
|
76
|
+
Error.new(data)
|
77
|
+
end)
|
78
|
+
end
|
79
|
+
|
80
|
+
def full_messages
|
81
|
+
map(&:title)
|
82
|
+
end
|
83
|
+
|
84
|
+
def [](source)
|
85
|
+
map do |error|
|
86
|
+
error.error_key == source
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module JsonApiClient
|
4
|
+
module Errors
|
5
|
+
class ApiError < StandardError
|
6
|
+
attr_reader :env
|
7
|
+
|
8
|
+
def initialize(env, msg = nil)
|
9
|
+
@env = env
|
10
|
+
# Try to fetch json_api errors from response
|
11
|
+
msg = track_json_api_errors(msg)
|
12
|
+
|
13
|
+
super msg
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Try to fetch json_api errors from response
|
19
|
+
def track_json_api_errors(msg)
|
20
|
+
return msg unless env.try(:body).kind_of?(Hash) || env.body.key?('errors')
|
21
|
+
|
22
|
+
errors_msg = env.body['errors'].map { |e| e['title'] }.compact.join('; ').presence
|
23
|
+
return msg unless errors_msg
|
24
|
+
|
25
|
+
msg.nil? ? errors_msg : "#{msg} (#{errors_msg})"
|
26
|
+
# Just to be sure that it is back compatible
|
27
|
+
rescue StandardError
|
28
|
+
msg
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class ClientError < ApiError
|
33
|
+
end
|
34
|
+
|
35
|
+
class ResourceImmutableError < StandardError
|
36
|
+
def initialize(msg = 'Resource immutable')
|
37
|
+
super msg
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class AccessDenied < ClientError
|
42
|
+
end
|
43
|
+
|
44
|
+
class NotAuthorized < ClientError
|
45
|
+
end
|
46
|
+
|
47
|
+
class NotFound < ClientError
|
48
|
+
attr_reader :uri
|
49
|
+
def initialize(uri)
|
50
|
+
@uri = uri
|
51
|
+
|
52
|
+
msg = "Couldn't find resource at: #{uri.to_s}"
|
53
|
+
super nil, msg
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class RequestTimeout < ClientError
|
58
|
+
end
|
59
|
+
|
60
|
+
class Conflict < ClientError
|
61
|
+
def initialize(env, msg = 'Resource already exists')
|
62
|
+
super env, msg
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class TooManyRequests < ClientError
|
67
|
+
end
|
68
|
+
|
69
|
+
class ConnectionError < ApiError
|
70
|
+
end
|
71
|
+
|
72
|
+
class ServerError < ApiError
|
73
|
+
def initialize(env, msg = nil)
|
74
|
+
msg ||= begin
|
75
|
+
status = env.status
|
76
|
+
message = ::Rack::Utils::HTTP_STATUS_CODES[status]
|
77
|
+
"#{status} #{message}"
|
78
|
+
end
|
79
|
+
|
80
|
+
super env, msg
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class InternalServerError < ServerError
|
85
|
+
end
|
86
|
+
|
87
|
+
class BadGateway < ServerError
|
88
|
+
end
|
89
|
+
|
90
|
+
class ServiceUnavailable < ServerError
|
91
|
+
end
|
92
|
+
|
93
|
+
class GatewayTimeout < ServerError
|
94
|
+
end
|
95
|
+
|
96
|
+
class UnexpectedStatus < ServerError
|
97
|
+
attr_reader :code, :uri
|
98
|
+
def initialize(code, uri)
|
99
|
+
@code = code
|
100
|
+
@uri = uri
|
101
|
+
|
102
|
+
msg = "Unexpected response status: #{code} from: #{uri.to_s}"
|
103
|
+
super nil, msg
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# Taken form jsonapi_resources formatter
|
2
|
+
|
3
|
+
require 'active_support/inflector'
|
4
|
+
|
5
|
+
module JsonApiClient
|
6
|
+
class Formatter
|
7
|
+
class << self
|
8
|
+
def format(arg)
|
9
|
+
arg.to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def unformat(arg)
|
13
|
+
# We call to_s() here so that unformat consistently returns a string
|
14
|
+
# (instead of a symbol) regardless which Formatter subclass it is called on
|
15
|
+
arg.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def formatter_for(format)
|
19
|
+
formatter_class_name = "JsonApiClient::#{format.to_s.camelize}Formatter"
|
20
|
+
formatter_class_name.safe_constantize
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class KeyFormatter < Formatter
|
26
|
+
class << self
|
27
|
+
def format(key)
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def format_keys(hash)
|
32
|
+
Hash[
|
33
|
+
hash.map do |key, value|
|
34
|
+
[format(key).to_sym, value]
|
35
|
+
end
|
36
|
+
]
|
37
|
+
end
|
38
|
+
|
39
|
+
def unformat(formatted_key)
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class RouteFormatter < Formatter
|
46
|
+
class << self
|
47
|
+
def format(route)
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
def unformat(formatted_route)
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class ValueFormatter < Formatter
|
58
|
+
class << self
|
59
|
+
def format(raw_value)
|
60
|
+
super(raw_value)
|
61
|
+
end
|
62
|
+
|
63
|
+
def unformat(value)
|
64
|
+
super(value)
|
65
|
+
end
|
66
|
+
|
67
|
+
def value_formatter_for(type)
|
68
|
+
formatter_name = "#{type.to_s.camelize}Value"
|
69
|
+
formatter_for(formatter_name)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class UnderscoredKeyFormatter < KeyFormatter
|
75
|
+
end
|
76
|
+
|
77
|
+
class CamelizedKeyFormatter < KeyFormatter
|
78
|
+
class << self
|
79
|
+
def format(key)
|
80
|
+
super.camelize(:lower)
|
81
|
+
end
|
82
|
+
|
83
|
+
def unformat(formatted_key)
|
84
|
+
formatted_key.to_s.underscore
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class DasherizedKeyFormatter < KeyFormatter
|
90
|
+
class << self
|
91
|
+
def format(key)
|
92
|
+
super.dasherize
|
93
|
+
end
|
94
|
+
|
95
|
+
def unformat(formatted_key)
|
96
|
+
formatted_key.to_s.underscore
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class DefaultValueFormatter < ValueFormatter
|
102
|
+
class << self
|
103
|
+
def format(raw_value)
|
104
|
+
raw_value
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class IdValueFormatter < ValueFormatter
|
110
|
+
class << self
|
111
|
+
def format(raw_value)
|
112
|
+
return if raw_value.nil?
|
113
|
+
raw_value.to_s
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class UnderscoredRouteFormatter < RouteFormatter
|
119
|
+
end
|
120
|
+
|
121
|
+
class CamelizedRouteFormatter < RouteFormatter
|
122
|
+
class << self
|
123
|
+
def format(route)
|
124
|
+
super.camelize(:lower)
|
125
|
+
end
|
126
|
+
|
127
|
+
def unformat(formatted_route)
|
128
|
+
formatted_route.to_s.underscore
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class DasherizedRouteFormatter < RouteFormatter
|
134
|
+
class << self
|
135
|
+
def format(route)
|
136
|
+
super.dasherize
|
137
|
+
end
|
138
|
+
|
139
|
+
def unformat(formatted_route)
|
140
|
+
formatted_route.to_s.underscore
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|