jsonapi-consumer 0.1.1 → 1.0.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 +5 -5
- data/.circleci/config.yml +27 -0
- data/.gitignore +1 -0
- data/Gemfile +6 -4
- data/README.md +9 -38
- data/Rakefile +17 -6
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/jsonapi-consumer.gemspec +10 -11
- data/lib/jsonapi/consumer/associations/base_association.rb +26 -0
- data/lib/jsonapi/consumer/associations/belongs_to.rb +30 -0
- data/lib/jsonapi/consumer/associations/has_many.rb +26 -0
- data/lib/jsonapi/consumer/associations/has_one.rb +19 -0
- data/lib/jsonapi/consumer/connection.rb +36 -0
- data/lib/jsonapi/consumer/error_collector.rb +91 -0
- data/lib/jsonapi/consumer/errors.rb +34 -76
- data/lib/jsonapi/consumer/formatter.rb +145 -0
- data/lib/jsonapi/consumer/helpers/callbacks.rb +27 -0
- data/lib/jsonapi/consumer/helpers/dirty.rb +71 -0
- data/lib/jsonapi/consumer/helpers/dynamic_attributes.rb +83 -0
- data/lib/jsonapi/consumer/helpers/uri.rb +9 -0
- data/lib/jsonapi/consumer/implementation.rb +12 -0
- data/lib/jsonapi/consumer/included_data.rb +49 -0
- data/lib/jsonapi/consumer/linking/links.rb +22 -0
- data/lib/jsonapi/consumer/linking/top_level_links.rb +39 -0
- data/lib/jsonapi/consumer/meta_data.rb +19 -0
- data/lib/jsonapi/consumer/middleware/json_request.rb +26 -0
- data/lib/jsonapi/consumer/middleware/parse_json.rb +22 -23
- data/lib/jsonapi/consumer/middleware/status.rb +41 -0
- data/lib/jsonapi/consumer/paginating/paginator.rb +89 -0
- data/lib/jsonapi/consumer/parsers/parser.rb +113 -0
- data/lib/jsonapi/consumer/query/builder.rb +212 -0
- data/lib/jsonapi/consumer/query/requestor.rb +67 -0
- data/lib/jsonapi/consumer/relationships/relations.rb +56 -0
- data/lib/jsonapi/consumer/relationships/top_level_relations.rb +30 -0
- data/lib/jsonapi/consumer/resource.rb +514 -54
- data/lib/jsonapi/consumer/result_set.rb +25 -0
- data/lib/jsonapi/consumer/schema.rb +153 -0
- data/lib/jsonapi/consumer/utils.rb +28 -0
- data/lib/jsonapi/consumer/version.rb +1 -1
- data/lib/jsonapi/consumer.rb +59 -34
- metadata +51 -111
- data/.rspec +0 -2
- data/CHANGELOG.md +0 -36
- data/lib/jsonapi/consumer/middleware/raise_error.rb +0 -21
- data/lib/jsonapi/consumer/middleware/request_headers.rb +0 -20
- data/lib/jsonapi/consumer/middleware/request_timeout.rb +0 -9
- data/lib/jsonapi/consumer/middleware.rb +0 -5
- data/lib/jsonapi/consumer/parser.rb +0 -75
- data/lib/jsonapi/consumer/query/base.rb +0 -34
- data/lib/jsonapi/consumer/query/create.rb +0 -9
- data/lib/jsonapi/consumer/query/delete.rb +0 -10
- data/lib/jsonapi/consumer/query/find.rb +0 -16
- data/lib/jsonapi/consumer/query/new.rb +0 -15
- data/lib/jsonapi/consumer/query/update.rb +0 -11
- data/lib/jsonapi/consumer/query.rb +0 -5
- data/lib/jsonapi/consumer/resource/association_concern.rb +0 -203
- data/lib/jsonapi/consumer/resource/attributes_concern.rb +0 -70
- data/lib/jsonapi/consumer/resource/connection_concern.rb +0 -99
- data/lib/jsonapi/consumer/resource/finders_concern.rb +0 -28
- data/lib/jsonapi/consumer/resource/object_build_concern.rb +0 -28
- data/lib/jsonapi/consumer/resource/serializer_concern.rb +0 -63
- data/spec/fixtures/.gitkeep +0 -0
- data/spec/fixtures/resources.rb +0 -45
- data/spec/fixtures/responses.rb +0 -64
- data/spec/jsonapi/consumer/associations_spec.rb +0 -166
- data/spec/jsonapi/consumer/attributes_spec.rb +0 -27
- data/spec/jsonapi/consumer/connection_spec.rb +0 -147
- data/spec/jsonapi/consumer/error_handling_spec.rb +0 -37
- data/spec/jsonapi/consumer/object_build_spec.rb +0 -20
- data/spec/jsonapi/consumer/parser_spec.rb +0 -39
- data/spec/jsonapi/consumer/resource_spec.rb +0 -62
- data/spec/jsonapi/consumer/serializer_spec.rb +0 -41
- data/spec/spec_helper.rb +0 -97
- data/spec/support/.gitkeep +0 -0
- data/spec/support/load_fixtures.rb +0 -4
@@ -0,0 +1,113 @@
|
|
1
|
+
module JSONAPI::Consumer
|
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
|
+
record_class = choose_model_for(result_set, res)
|
70
|
+
resource = record_class.load(parameters_from_resource(res))
|
71
|
+
resource.last_result_set = result_set
|
72
|
+
resource
|
73
|
+
end
|
74
|
+
result_set.concat(resources)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Accept mixed-content from an endpoint.
|
78
|
+
#
|
79
|
+
# TODO: add ability to configure a model namespace
|
80
|
+
def choose_model_for(result_set, res)
|
81
|
+
return result_set.record_class unless res['type']
|
82
|
+
|
83
|
+
res_type_name = res['type'].underscore.classify
|
84
|
+
(res_type_name.safe_constantize) ? res_type_name.safe_constantize : result_set.record_class
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_errors(result_set, data)
|
88
|
+
result_set.errors = ErrorCollector.new(data.fetch("errors", []))
|
89
|
+
end
|
90
|
+
|
91
|
+
def handle_meta(result_set, data)
|
92
|
+
result_set.meta = MetaData.new(data.fetch("meta", {}), result_set.record_class)
|
93
|
+
end
|
94
|
+
|
95
|
+
def handle_links(result_set, data)
|
96
|
+
result_set.links = Linking::TopLevelLinks.new(result_set.record_class, data.fetch("links", {}))
|
97
|
+
end
|
98
|
+
|
99
|
+
def handle_relationships(result_set, data)
|
100
|
+
result_set.relationships = Relationships::TopLevelRelations.new(result_set.record_class, data.fetch("relationships", {}))
|
101
|
+
end
|
102
|
+
|
103
|
+
def handle_pagination(result_set, data)
|
104
|
+
result_set.pages = result_set.record_class.paginator.new(result_set, data)
|
105
|
+
end
|
106
|
+
|
107
|
+
def handle_included(result_set, data)
|
108
|
+
result_set.included = IncludedData.new(result_set, data.fetch("included", []))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
module JSONAPI::Consumer
|
2
|
+
module Query
|
3
|
+
class Builder
|
4
|
+
|
5
|
+
attr_reader :klass, :requestor
|
6
|
+
delegate :key_formatter, to: :klass
|
7
|
+
|
8
|
+
def initialize(klass, requestor = nil)
|
9
|
+
@klass = klass
|
10
|
+
@requestor = requestor || klass.requestor
|
11
|
+
@primary_key = nil
|
12
|
+
@pagination_params = {}
|
13
|
+
@path_params = {}
|
14
|
+
@additional_params = {}
|
15
|
+
@filters = {}
|
16
|
+
@includes = []
|
17
|
+
@orders = []
|
18
|
+
@fields = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def where(conditions = {})
|
22
|
+
# pull out any path params here
|
23
|
+
@path_params.merge!(conditions.slice(*klass.prefix_params))
|
24
|
+
@filters.merge!(conditions.except(*klass.prefix_params))
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def order(*args)
|
29
|
+
@orders += parse_orders(*args)
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def includes(*tables)
|
34
|
+
@includes += parse_related_links(*tables)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def select(*fields)
|
39
|
+
@fields += parse_fields(*fields)
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def paginate(conditions = {})
|
44
|
+
scope = self
|
45
|
+
scope = scope.page(conditions[:page]) if conditions[:page]
|
46
|
+
scope = scope.per(conditions[:per_page]) if conditions[:per_page]
|
47
|
+
scope
|
48
|
+
end
|
49
|
+
|
50
|
+
def page(number)
|
51
|
+
@pagination_params[ klass.paginator.page_param ] = number
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def per(size)
|
56
|
+
@pagination_params[ klass.paginator.per_page_param ] = size
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def with_params(more_params)
|
61
|
+
@additional_params.merge!(more_params)
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def first
|
66
|
+
paginate(page: 1, per_page: 1).to_a.first
|
67
|
+
end
|
68
|
+
|
69
|
+
def last
|
70
|
+
paginate(page: 1, per_page: 1).pages.last.to_a.last
|
71
|
+
end
|
72
|
+
|
73
|
+
def build
|
74
|
+
klass.new(params)
|
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 ||= find
|
90
|
+
end
|
91
|
+
alias all to_a
|
92
|
+
|
93
|
+
def find(args = {})
|
94
|
+
case args
|
95
|
+
when Hash
|
96
|
+
where(args)
|
97
|
+
else
|
98
|
+
@primary_key = args
|
99
|
+
end
|
100
|
+
|
101
|
+
requestor.get(params)
|
102
|
+
end
|
103
|
+
|
104
|
+
def method_missing(method_name, *args, &block)
|
105
|
+
to_a.send(method_name, *args, &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def path_params
|
111
|
+
@path_params.empty? ? {} : {path: @path_params}
|
112
|
+
end
|
113
|
+
|
114
|
+
def additional_params
|
115
|
+
@additional_params
|
116
|
+
end
|
117
|
+
|
118
|
+
def primary_key_params
|
119
|
+
return {} unless @primary_key
|
120
|
+
|
121
|
+
@primary_key.is_a?(Array) ?
|
122
|
+
{klass.primary_key.to_s.pluralize.to_sym => @primary_key.join(",")} :
|
123
|
+
{klass.primary_key => @primary_key}
|
124
|
+
end
|
125
|
+
|
126
|
+
def pagination_params
|
127
|
+
@pagination_params.empty? ? {} : {page: @pagination_params}
|
128
|
+
end
|
129
|
+
|
130
|
+
def includes_params
|
131
|
+
@includes.empty? ? {} : {include: @includes.join(",")}
|
132
|
+
end
|
133
|
+
|
134
|
+
def filter_params
|
135
|
+
@filters.empty? ? {} : {filter: @filters}
|
136
|
+
end
|
137
|
+
|
138
|
+
def order_params
|
139
|
+
@orders.empty? ? {} : {sort: @orders.join(",")}
|
140
|
+
end
|
141
|
+
|
142
|
+
def select_params
|
143
|
+
if @fields.empty?
|
144
|
+
{}
|
145
|
+
else
|
146
|
+
field_result = Hash.new { |h,k| h[k] = [] }
|
147
|
+
@fields.each do |field|
|
148
|
+
if field.is_a? Hash
|
149
|
+
field.each do |k,v|
|
150
|
+
field_result[k.to_s] << v
|
151
|
+
field_result[k.to_s] = field_result[k.to_s].flatten
|
152
|
+
end
|
153
|
+
else
|
154
|
+
field_result[klass.table_name] << field
|
155
|
+
end
|
156
|
+
end
|
157
|
+
field_result.each { |k,v| field_result[k] = v.join(',') }
|
158
|
+
{fields: field_result}
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def parse_related_links(*tables)
|
163
|
+
tables.map do |table|
|
164
|
+
case table
|
165
|
+
when Hash
|
166
|
+
table.map do |k, v|
|
167
|
+
parse_related_links(*v).map do |sub|
|
168
|
+
"#{k}.#{sub}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
when Array
|
172
|
+
table.map do |v|
|
173
|
+
parse_related_links(*v)
|
174
|
+
end
|
175
|
+
else
|
176
|
+
key_formatter.format(table)
|
177
|
+
end
|
178
|
+
end.flatten
|
179
|
+
end
|
180
|
+
|
181
|
+
def parse_orders(*args)
|
182
|
+
args.map do |arg|
|
183
|
+
case arg
|
184
|
+
when Hash
|
185
|
+
arg.map do |k, v|
|
186
|
+
operator = (v == :desc ? "-" : "")
|
187
|
+
"#{operator}#{k}"
|
188
|
+
end
|
189
|
+
else
|
190
|
+
"#{arg}"
|
191
|
+
end
|
192
|
+
end.flatten
|
193
|
+
end
|
194
|
+
|
195
|
+
def parse_fields(*fields)
|
196
|
+
fields = fields.split(',') if fields.is_a? String
|
197
|
+
fields.map do |field|
|
198
|
+
case field
|
199
|
+
when Hash
|
200
|
+
field.each do |k,v|
|
201
|
+
field[k] = parse_fields(v)
|
202
|
+
end
|
203
|
+
field
|
204
|
+
else
|
205
|
+
Array(field).flatten.map { |i| i.to_s.split(",") }.flatten.map(&:strip)
|
206
|
+
end
|
207
|
+
end.flatten
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module JSONAPI::Consumer
|
2
|
+
module Query
|
3
|
+
class Requestor
|
4
|
+
extend Forwardable
|
5
|
+
include Helpers::URI
|
6
|
+
|
7
|
+
def initialize(klass, path = nil)
|
8
|
+
@klass = klass
|
9
|
+
@path = path
|
10
|
+
end
|
11
|
+
|
12
|
+
# expects a record
|
13
|
+
def create(record)
|
14
|
+
request(:post, klass.path(record.attributes), {
|
15
|
+
data: record.as_json_api
|
16
|
+
})
|
17
|
+
end
|
18
|
+
|
19
|
+
def update(record)
|
20
|
+
request(:patch, resource_path(record.attributes), {
|
21
|
+
data: record.as_json_api
|
22
|
+
})
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(params = {})
|
26
|
+
path = resource_path(params)
|
27
|
+
params.delete(klass.primary_key)
|
28
|
+
request(:get, path, params)
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy(record)
|
32
|
+
request(:delete, resource_path(record.attributes), {})
|
33
|
+
end
|
34
|
+
|
35
|
+
def linked(path)
|
36
|
+
request(:get, path, {})
|
37
|
+
end
|
38
|
+
|
39
|
+
def custom(method_name, options, params)
|
40
|
+
path = resource_path(params)
|
41
|
+
params.delete(klass.primary_key)
|
42
|
+
path = File.join(path, method_name.to_s)
|
43
|
+
|
44
|
+
request(options.fetch(:request_method, :get), path, params)
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
|
49
|
+
attr_reader :klass, :path
|
50
|
+
def_delegators :klass, :connection
|
51
|
+
|
52
|
+
def resource_path(parameters)
|
53
|
+
base_path = path || klass.path(parameters)
|
54
|
+
if resource_id = parameters[klass.primary_key]
|
55
|
+
File.join(base_path, encode_part(resource_id))
|
56
|
+
else
|
57
|
+
base_path
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def request(type, path, params)
|
62
|
+
klass.parser.parse(klass, connection.run(type, path, params, klass.custom_headers))
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module JSONAPI::Consumer
|
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
|
+
clear_changes_information
|
15
|
+
end
|
16
|
+
|
17
|
+
def present?
|
18
|
+
attributes.present?
|
19
|
+
end
|
20
|
+
|
21
|
+
def as_json_api
|
22
|
+
Hash[attributes_for_serialization.map do |k, v|
|
23
|
+
[k, v.slice("data")] if v.has_key?("data")
|
24
|
+
end.compact]
|
25
|
+
end
|
26
|
+
|
27
|
+
def as_json
|
28
|
+
Hash[attributes.map do |k, v|
|
29
|
+
[k, v.slice("data")] if v.has_key?("data")
|
30
|
+
end.compact]
|
31
|
+
end
|
32
|
+
|
33
|
+
def attributes_for_serialization
|
34
|
+
attributes.slice(*changed)
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def set_attribute(name, value)
|
40
|
+
value = case value
|
41
|
+
when JSONAPI::Consumer::Resource
|
42
|
+
{data: value.as_relation}
|
43
|
+
when Array
|
44
|
+
{data: value.map(&:as_relation)}
|
45
|
+
when NilClass
|
46
|
+
{data: nil}
|
47
|
+
else
|
48
|
+
value
|
49
|
+
end
|
50
|
+
attribute_will_change!(name) if value != attributes[name]
|
51
|
+
attributes[name] = value
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module JSONAPI::Consumer
|
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
|