jsonapi-consumer 0.1.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|