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.
Files changed (76) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +27 -0
  3. data/.gitignore +1 -0
  4. data/Gemfile +6 -4
  5. data/README.md +9 -38
  6. data/Rakefile +17 -6
  7. data/bin/console +14 -0
  8. data/bin/setup +8 -0
  9. data/jsonapi-consumer.gemspec +10 -11
  10. data/lib/jsonapi/consumer/associations/base_association.rb +26 -0
  11. data/lib/jsonapi/consumer/associations/belongs_to.rb +30 -0
  12. data/lib/jsonapi/consumer/associations/has_many.rb +26 -0
  13. data/lib/jsonapi/consumer/associations/has_one.rb +19 -0
  14. data/lib/jsonapi/consumer/connection.rb +36 -0
  15. data/lib/jsonapi/consumer/error_collector.rb +91 -0
  16. data/lib/jsonapi/consumer/errors.rb +34 -76
  17. data/lib/jsonapi/consumer/formatter.rb +145 -0
  18. data/lib/jsonapi/consumer/helpers/callbacks.rb +27 -0
  19. data/lib/jsonapi/consumer/helpers/dirty.rb +71 -0
  20. data/lib/jsonapi/consumer/helpers/dynamic_attributes.rb +83 -0
  21. data/lib/jsonapi/consumer/helpers/uri.rb +9 -0
  22. data/lib/jsonapi/consumer/implementation.rb +12 -0
  23. data/lib/jsonapi/consumer/included_data.rb +49 -0
  24. data/lib/jsonapi/consumer/linking/links.rb +22 -0
  25. data/lib/jsonapi/consumer/linking/top_level_links.rb +39 -0
  26. data/lib/jsonapi/consumer/meta_data.rb +19 -0
  27. data/lib/jsonapi/consumer/middleware/json_request.rb +26 -0
  28. data/lib/jsonapi/consumer/middleware/parse_json.rb +22 -23
  29. data/lib/jsonapi/consumer/middleware/status.rb +41 -0
  30. data/lib/jsonapi/consumer/paginating/paginator.rb +89 -0
  31. data/lib/jsonapi/consumer/parsers/parser.rb +113 -0
  32. data/lib/jsonapi/consumer/query/builder.rb +212 -0
  33. data/lib/jsonapi/consumer/query/requestor.rb +67 -0
  34. data/lib/jsonapi/consumer/relationships/relations.rb +56 -0
  35. data/lib/jsonapi/consumer/relationships/top_level_relations.rb +30 -0
  36. data/lib/jsonapi/consumer/resource.rb +514 -54
  37. data/lib/jsonapi/consumer/result_set.rb +25 -0
  38. data/lib/jsonapi/consumer/schema.rb +153 -0
  39. data/lib/jsonapi/consumer/utils.rb +28 -0
  40. data/lib/jsonapi/consumer/version.rb +1 -1
  41. data/lib/jsonapi/consumer.rb +59 -34
  42. metadata +51 -111
  43. data/.rspec +0 -2
  44. data/CHANGELOG.md +0 -36
  45. data/lib/jsonapi/consumer/middleware/raise_error.rb +0 -21
  46. data/lib/jsonapi/consumer/middleware/request_headers.rb +0 -20
  47. data/lib/jsonapi/consumer/middleware/request_timeout.rb +0 -9
  48. data/lib/jsonapi/consumer/middleware.rb +0 -5
  49. data/lib/jsonapi/consumer/parser.rb +0 -75
  50. data/lib/jsonapi/consumer/query/base.rb +0 -34
  51. data/lib/jsonapi/consumer/query/create.rb +0 -9
  52. data/lib/jsonapi/consumer/query/delete.rb +0 -10
  53. data/lib/jsonapi/consumer/query/find.rb +0 -16
  54. data/lib/jsonapi/consumer/query/new.rb +0 -15
  55. data/lib/jsonapi/consumer/query/update.rb +0 -11
  56. data/lib/jsonapi/consumer/query.rb +0 -5
  57. data/lib/jsonapi/consumer/resource/association_concern.rb +0 -203
  58. data/lib/jsonapi/consumer/resource/attributes_concern.rb +0 -70
  59. data/lib/jsonapi/consumer/resource/connection_concern.rb +0 -99
  60. data/lib/jsonapi/consumer/resource/finders_concern.rb +0 -28
  61. data/lib/jsonapi/consumer/resource/object_build_concern.rb +0 -28
  62. data/lib/jsonapi/consumer/resource/serializer_concern.rb +0 -63
  63. data/spec/fixtures/.gitkeep +0 -0
  64. data/spec/fixtures/resources.rb +0 -45
  65. data/spec/fixtures/responses.rb +0 -64
  66. data/spec/jsonapi/consumer/associations_spec.rb +0 -166
  67. data/spec/jsonapi/consumer/attributes_spec.rb +0 -27
  68. data/spec/jsonapi/consumer/connection_spec.rb +0 -147
  69. data/spec/jsonapi/consumer/error_handling_spec.rb +0 -37
  70. data/spec/jsonapi/consumer/object_build_spec.rb +0 -20
  71. data/spec/jsonapi/consumer/parser_spec.rb +0 -39
  72. data/spec/jsonapi/consumer/resource_spec.rb +0 -62
  73. data/spec/jsonapi/consumer/serializer_spec.rb +0 -41
  74. data/spec/spec_helper.rb +0 -97
  75. data/spec/support/.gitkeep +0 -0
  76. 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