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,239 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
|
3
|
+
module JsonApiClient
|
4
|
+
module Query
|
5
|
+
class Builder
|
6
|
+
|
7
|
+
attr_reader :klass
|
8
|
+
delegate :key_formatter, to: :klass
|
9
|
+
|
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, [] )
|
20
|
+
end
|
21
|
+
|
22
|
+
def where(conditions = {})
|
23
|
+
# pull out any path params here
|
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 )
|
28
|
+
end
|
29
|
+
|
30
|
+
def order(*args)
|
31
|
+
_new_scope( orders: parse_orders(*args) )
|
32
|
+
end
|
33
|
+
|
34
|
+
def includes(*tables)
|
35
|
+
_new_scope( includes: parse_related_links(*tables) )
|
36
|
+
end
|
37
|
+
|
38
|
+
def select(*fields)
|
39
|
+
_new_scope( fields: parse_fields(*fields) )
|
40
|
+
end
|
41
|
+
|
42
|
+
def paginate(conditions = {})
|
43
|
+
scope = _new_scope
|
44
|
+
scope = scope.page(conditions[:page]) if conditions[:page]
|
45
|
+
scope = scope.per(conditions[:per_page]) if conditions[:per_page]
|
46
|
+
scope
|
47
|
+
end
|
48
|
+
|
49
|
+
def page(number)
|
50
|
+
_new_scope( pagination_params: { klass.paginator.page_param => number || 1 } )
|
51
|
+
end
|
52
|
+
|
53
|
+
def per(size)
|
54
|
+
_new_scope( pagination_params: { klass.paginator.per_page_param => size } )
|
55
|
+
end
|
56
|
+
|
57
|
+
def with_params(more_params)
|
58
|
+
_new_scope( additional_params: more_params )
|
59
|
+
end
|
60
|
+
|
61
|
+
def first
|
62
|
+
paginate(page: 1, per_page: 1).to_a.first
|
63
|
+
end
|
64
|
+
|
65
|
+
def last
|
66
|
+
paginate(page: 1, per_page: 1).pages.last.to_a.last
|
67
|
+
end
|
68
|
+
|
69
|
+
def build(attrs = {})
|
70
|
+
klass.new @path_params.merge(attrs.symbolize_keys)
|
71
|
+
end
|
72
|
+
|
73
|
+
def create(attrs = {})
|
74
|
+
klass.create @path_params.merge(attrs.symbolize_keys)
|
75
|
+
end
|
76
|
+
|
77
|
+
def params
|
78
|
+
filter_params
|
79
|
+
.merge(pagination_params)
|
80
|
+
.merge(includes_params)
|
81
|
+
.merge(order_params)
|
82
|
+
.merge(select_params)
|
83
|
+
.merge(primary_key_params)
|
84
|
+
.merge(path_params)
|
85
|
+
.merge(additional_params)
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_a
|
89
|
+
@to_a ||= _fetch
|
90
|
+
end
|
91
|
+
alias all to_a
|
92
|
+
|
93
|
+
def find(args = {})
|
94
|
+
if klass.raise_on_blank_find_param && args.blank?
|
95
|
+
raise Errors::NotFound, 'blank .find param'
|
96
|
+
end
|
97
|
+
|
98
|
+
case args
|
99
|
+
when Hash
|
100
|
+
scope = where(args)
|
101
|
+
else
|
102
|
+
scope = _new_scope( primary_key: args )
|
103
|
+
end
|
104
|
+
|
105
|
+
scope._fetch
|
106
|
+
end
|
107
|
+
|
108
|
+
def method_missing(method_name, *args, &block)
|
109
|
+
to_a.send(method_name, *args, &block)
|
110
|
+
end
|
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
|
+
|
132
|
+
private
|
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.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
|
+
|
146
|
+
def path_params
|
147
|
+
@path_params.empty? ? {} : {path: @path_params}
|
148
|
+
end
|
149
|
+
|
150
|
+
def additional_params
|
151
|
+
@additional_params
|
152
|
+
end
|
153
|
+
|
154
|
+
def primary_key_params
|
155
|
+
return {} unless @primary_key
|
156
|
+
|
157
|
+
@primary_key.is_a?(Array) ?
|
158
|
+
{klass.primary_key.to_s.pluralize.to_sym => @primary_key.join(",")} :
|
159
|
+
{klass.primary_key => @primary_key}
|
160
|
+
end
|
161
|
+
|
162
|
+
def 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
|
170
|
+
end
|
171
|
+
|
172
|
+
def includes_params
|
173
|
+
@includes.empty? ? {} : {include: @includes.join(",")}
|
174
|
+
end
|
175
|
+
|
176
|
+
def filter_params
|
177
|
+
@filters.empty? ? {} : {filter: @filters}
|
178
|
+
end
|
179
|
+
|
180
|
+
def order_params
|
181
|
+
@orders.empty? ? {} : {sort: @orders.join(",")}
|
182
|
+
end
|
183
|
+
|
184
|
+
def select_params
|
185
|
+
if @fields.empty?
|
186
|
+
{}
|
187
|
+
else
|
188
|
+
field_result = Hash.new { |h,k| h[k] = [] }
|
189
|
+
@fields.each do |field|
|
190
|
+
if field.is_a? Hash
|
191
|
+
field.each do |k,v|
|
192
|
+
field_result[k.to_s] << v
|
193
|
+
field_result[k.to_s] = field_result[k.to_s].flatten
|
194
|
+
end
|
195
|
+
else
|
196
|
+
field_result[klass.table_name] << field
|
197
|
+
end
|
198
|
+
end
|
199
|
+
field_result.each { |k,v| field_result[k] = v.join(',') }
|
200
|
+
{fields: field_result}
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def parse_related_links(*tables)
|
205
|
+
Utils.parse_includes(klass, *tables)
|
206
|
+
end
|
207
|
+
|
208
|
+
def parse_orders(*args)
|
209
|
+
args.map do |arg|
|
210
|
+
case arg
|
211
|
+
when Hash
|
212
|
+
arg.map do |k, v|
|
213
|
+
operator = (v == :desc ? "-" : "")
|
214
|
+
"#{operator}#{k}"
|
215
|
+
end
|
216
|
+
else
|
217
|
+
"#{arg}"
|
218
|
+
end
|
219
|
+
end.flatten
|
220
|
+
end
|
221
|
+
|
222
|
+
def parse_fields(*fields)
|
223
|
+
fields = fields.split(',') if fields.is_a? String
|
224
|
+
fields.map do |field|
|
225
|
+
case field
|
226
|
+
when Hash
|
227
|
+
field.each do |k,v|
|
228
|
+
field[k] = parse_fields(v)
|
229
|
+
end
|
230
|
+
field
|
231
|
+
else
|
232
|
+
Array(field).flatten.map { |i| i.to_s.split(",") }.flatten.map(&:strip)
|
233
|
+
end
|
234
|
+
end.flatten
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Query
|
3
|
+
class Requestor
|
4
|
+
extend Forwardable
|
5
|
+
include Helpers::URI
|
6
|
+
|
7
|
+
def initialize(klass)
|
8
|
+
@klass = klass
|
9
|
+
end
|
10
|
+
|
11
|
+
# expects a record
|
12
|
+
def create(record)
|
13
|
+
request(
|
14
|
+
:post,
|
15
|
+
klass.path(record.path_attributes),
|
16
|
+
body: { data: record.as_json_api },
|
17
|
+
params: record.request_params.to_params
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(record)
|
22
|
+
request(
|
23
|
+
:patch,
|
24
|
+
resource_path(record.path_attributes),
|
25
|
+
body: { data: record.as_json_api },
|
26
|
+
params: record.request_params.to_params
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def get(params = {})
|
31
|
+
path = resource_path(params)
|
32
|
+
params.delete(klass.primary_key)
|
33
|
+
request(:get, path, params: params)
|
34
|
+
end
|
35
|
+
|
36
|
+
def destroy(record)
|
37
|
+
request(:delete, resource_path(record.path_attributes))
|
38
|
+
end
|
39
|
+
|
40
|
+
def linked(path)
|
41
|
+
request(:get, path)
|
42
|
+
end
|
43
|
+
|
44
|
+
def custom(method_name, options, params)
|
45
|
+
path = resource_path(params)
|
46
|
+
params.delete(klass.primary_key)
|
47
|
+
path = File.join(path, method_name.to_s)
|
48
|
+
request_method = options.fetch(:request_method, :get).to_sym
|
49
|
+
query_params, body_params = [:get, :delete].include?(request_method) ? [params, nil] : [nil, params]
|
50
|
+
request(request_method, path, params: query_params, body: body_params)
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
attr_reader :klass
|
56
|
+
def_delegators :klass, :connection
|
57
|
+
|
58
|
+
def resource_path(parameters)
|
59
|
+
if resource_id = parameters[klass.primary_key]
|
60
|
+
File.join(klass.path(parameters), encode_part(resource_id))
|
61
|
+
else
|
62
|
+
klass.path(parameters)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def request(type, path, params: nil, body: nil)
|
67
|
+
response = connection.run(type, path, params: params, body: body, headers: klass.custom_headers)
|
68
|
+
klass.parser.parse(klass, response)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Relationships
|
3
|
+
class Relations
|
4
|
+
include Helpers::DynamicAttributes
|
5
|
+
include Helpers::Dirty
|
6
|
+
include ActiveModel::Serialization
|
7
|
+
|
8
|
+
attr_reader :record_class
|
9
|
+
delegate :key_formatter, to: :record_class
|
10
|
+
|
11
|
+
def initialize(record_class, relations)
|
12
|
+
@record_class = record_class
|
13
|
+
self.attributes = relations
|
14
|
+
end
|
15
|
+
|
16
|
+
def present?
|
17
|
+
attributes.present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_json_api
|
21
|
+
Hash[attributes_for_serialization.map do |k, v|
|
22
|
+
[k, v.slice("data")] if v.has_key?("data")
|
23
|
+
end.compact]
|
24
|
+
end
|
25
|
+
|
26
|
+
def as_json
|
27
|
+
Hash[attributes.map do |k, v|
|
28
|
+
[k, v.slice("data")] if v.has_key?("data")
|
29
|
+
end.compact]
|
30
|
+
end
|
31
|
+
|
32
|
+
def attributes_for_serialization
|
33
|
+
attributes.slice(*changed)
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def set_attribute(name, value)
|
39
|
+
value = case value
|
40
|
+
when JsonApiClient::Resource
|
41
|
+
{data: value.as_relation}
|
42
|
+
when Array
|
43
|
+
{data: value.map(&:as_relation)}
|
44
|
+
when NilClass
|
45
|
+
{data: nil}
|
46
|
+
else
|
47
|
+
value
|
48
|
+
end
|
49
|
+
attribute_will_change!(name) if value != attributes[name]
|
50
|
+
attributes[name] = value
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
module Relationships
|
3
|
+
class TopLevelRelations
|
4
|
+
|
5
|
+
attr_reader :relations, :record_class
|
6
|
+
|
7
|
+
def initialize(record_class, relations)
|
8
|
+
@relations = relations
|
9
|
+
@record_class = record_class
|
10
|
+
end
|
11
|
+
|
12
|
+
def respond_to_missing?(method, include_private = false)
|
13
|
+
relations.has_key?(method.to_s) || super
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(method, *args)
|
17
|
+
if respond_to_missing?(method)
|
18
|
+
fetch_relation(method)
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch_relation(relation_name)
|
25
|
+
link_definition = relations.fetch(relation_name.to_s)
|
26
|
+
record_class.requestor.linked(link_definition)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module JsonApiClient
|
2
|
+
class RequestParams
|
3
|
+
attr_reader :klass, :includes, :fields
|
4
|
+
|
5
|
+
def initialize(klass, includes: [], fields: {})
|
6
|
+
@klass = klass
|
7
|
+
@includes = includes
|
8
|
+
@fields = fields
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_includes(includes)
|
12
|
+
Utils.parse_includes(klass, *includes).each do |name|
|
13
|
+
name = name.to_sym
|
14
|
+
self.includes.push(name) unless self.includes.include?(name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def reset_includes!
|
19
|
+
@includes = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_fields(type, field_names)
|
23
|
+
self.fields[type.to_sym] = field_names.map(&:to_sym)
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove_fields(type)
|
27
|
+
self.fields.delete(type.to_sym)
|
28
|
+
end
|
29
|
+
|
30
|
+
def field_types
|
31
|
+
self.fields.keys
|
32
|
+
end
|
33
|
+
|
34
|
+
def clear
|
35
|
+
reset_includes!
|
36
|
+
@fields = {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_params
|
40
|
+
return nil if field_types.empty? && includes.empty?
|
41
|
+
parsed_fields.merge(parsed_includes)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def parsed_includes
|
47
|
+
return {} if includes.empty?
|
48
|
+
{include: includes.join(",")}
|
49
|
+
end
|
50
|
+
|
51
|
+
def parsed_fields
|
52
|
+
return {} if field_types.empty?
|
53
|
+
{fields: fields.map { |type, names| [type, names.join(",")] }.to_h}
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|