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.
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