simple_jsonapi 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 +7 -0
  2. data/.gitignore +10 -0
  3. data/.rubocop.yml +131 -0
  4. data/CHANGELOG.md +2 -0
  5. data/Gemfile +5 -0
  6. data/Jenkinsfile +92 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +532 -0
  9. data/Rakefile +10 -0
  10. data/lib/simple_jsonapi.rb +112 -0
  11. data/lib/simple_jsonapi/definition/attribute.rb +45 -0
  12. data/lib/simple_jsonapi/definition/base.rb +50 -0
  13. data/lib/simple_jsonapi/definition/concerns/has_links_object.rb +36 -0
  14. data/lib/simple_jsonapi/definition/concerns/has_meta_object.rb +36 -0
  15. data/lib/simple_jsonapi/definition/error.rb +70 -0
  16. data/lib/simple_jsonapi/definition/error_source.rb +29 -0
  17. data/lib/simple_jsonapi/definition/link.rb +27 -0
  18. data/lib/simple_jsonapi/definition/meta.rb +27 -0
  19. data/lib/simple_jsonapi/definition/relationship.rb +60 -0
  20. data/lib/simple_jsonapi/definition/resource.rb +104 -0
  21. data/lib/simple_jsonapi/error_serializer.rb +76 -0
  22. data/lib/simple_jsonapi/errors/bad_request.rb +11 -0
  23. data/lib/simple_jsonapi/errors/exception_serializer.rb +6 -0
  24. data/lib/simple_jsonapi/errors/wrapped_error.rb +35 -0
  25. data/lib/simple_jsonapi/errors/wrapped_error_serializer.rb +35 -0
  26. data/lib/simple_jsonapi/helpers/exceptions.rb +39 -0
  27. data/lib/simple_jsonapi/helpers/serializer_inferrer.rb +136 -0
  28. data/lib/simple_jsonapi/helpers/serializer_methods.rb +36 -0
  29. data/lib/simple_jsonapi/node/attributes.rb +51 -0
  30. data/lib/simple_jsonapi/node/base.rb +91 -0
  31. data/lib/simple_jsonapi/node/data/collection.rb +25 -0
  32. data/lib/simple_jsonapi/node/data/singular.rb +26 -0
  33. data/lib/simple_jsonapi/node/document/base.rb +62 -0
  34. data/lib/simple_jsonapi/node/document/collection.rb +17 -0
  35. data/lib/simple_jsonapi/node/document/errors.rb +17 -0
  36. data/lib/simple_jsonapi/node/document/singular.rb +17 -0
  37. data/lib/simple_jsonapi/node/error.rb +55 -0
  38. data/lib/simple_jsonapi/node/error_source.rb +40 -0
  39. data/lib/simple_jsonapi/node/errors.rb +28 -0
  40. data/lib/simple_jsonapi/node/included.rb +45 -0
  41. data/lib/simple_jsonapi/node/object_links.rb +40 -0
  42. data/lib/simple_jsonapi/node/object_meta.rb +40 -0
  43. data/lib/simple_jsonapi/node/relationship.rb +79 -0
  44. data/lib/simple_jsonapi/node/relationship_data/base.rb +53 -0
  45. data/lib/simple_jsonapi/node/relationship_data/collection.rb +32 -0
  46. data/lib/simple_jsonapi/node/relationship_data/singular.rb +33 -0
  47. data/lib/simple_jsonapi/node/relationships.rb +60 -0
  48. data/lib/simple_jsonapi/node/resource/base.rb +21 -0
  49. data/lib/simple_jsonapi/node/resource/full.rb +49 -0
  50. data/lib/simple_jsonapi/node/resource/linkage.rb +25 -0
  51. data/lib/simple_jsonapi/parameters/fields_spec.rb +45 -0
  52. data/lib/simple_jsonapi/parameters/include_spec.rb +57 -0
  53. data/lib/simple_jsonapi/parameters/sort_spec.rb +107 -0
  54. data/lib/simple_jsonapi/serializer.rb +89 -0
  55. data/lib/simple_jsonapi/version.rb +3 -0
  56. data/simple_jsonapi.gemspec +29 -0
  57. data/test/errors/bad_request_test.rb +34 -0
  58. data/test/errors/error_serializer_test.rb +229 -0
  59. data/test/errors/exception_serializer_test.rb +25 -0
  60. data/test/errors/wrapped_error_serializer_test.rb +91 -0
  61. data/test/errors/wrapped_error_test.rb +44 -0
  62. data/test/parameters/fields_spec_test.rb +56 -0
  63. data/test/parameters/include_spec_test.rb +58 -0
  64. data/test/parameters/sort_spec_test.rb +65 -0
  65. data/test/resources/attributes_test.rb +109 -0
  66. data/test/resources/extras_test.rb +70 -0
  67. data/test/resources/id_and_type_test.rb +76 -0
  68. data/test/resources/inclusion_test.rb +134 -0
  69. data/test/resources/links_test.rb +63 -0
  70. data/test/resources/meta_test.rb +49 -0
  71. data/test/resources/relationships_test.rb +262 -0
  72. data/test/resources/sorting_test.rb +79 -0
  73. data/test/resources/sparse_fieldset_test.rb +160 -0
  74. data/test/root_objects_test.rb +165 -0
  75. data/test/test_helper.rb +31 -0
  76. metadata +235 -0
@@ -0,0 +1,63 @@
1
+ require 'test_helper'
2
+
3
+ class LinksTest < Minitest::Spec
4
+ class Thing < TestModel
5
+ end
6
+
7
+ class NoLinksSerializer < SimpleJsonapi::Serializer
8
+ end
9
+
10
+ class BlocksSerializer < SimpleJsonapi::Serializer
11
+ link(:self) { |thing| "/api/things/#{thing.id}" }
12
+ end
13
+
14
+ class ProcsSerializer < SimpleJsonapi::Serializer
15
+ link :self, ->(thing) { "/api/things/#{thing.id}" }
16
+ end
17
+
18
+ class ValuesSerializer < SimpleJsonapi::Serializer
19
+ link :index, "/api/things"
20
+ end
21
+
22
+ class LinkObjectSerializer < SimpleJsonapi::Serializer
23
+ link(:self) do |thing|
24
+ { href: "/api/things/#{thing.id}", meta: { count: 1 } }
25
+ end
26
+ end
27
+
28
+ let(:thing) { Thing.new(id: 1) }
29
+
30
+ describe "Resource links object" do
31
+ describe "without links" do
32
+ it "omits the links key" do
33
+ serialized = SimpleJsonapi.render_resource(thing, serializer: NoLinksSerializer)
34
+ refute serialized.key?(:links)
35
+ end
36
+ end
37
+
38
+ describe "rendering string links" do
39
+ it "accepts a block" do
40
+ serialized = SimpleJsonapi.render_resource(thing, serializer: BlocksSerializer)
41
+ assert_equal "/api/things/1", serialized.dig(:data, :links, :self)
42
+ end
43
+
44
+ it "accepts a proc" do
45
+ serialized = SimpleJsonapi.render_resource(thing, serializer: ProcsSerializer)
46
+ assert_equal "/api/things/1", serialized.dig(:data, :links, :self)
47
+ end
48
+
49
+ it "accepts a value" do
50
+ serialized = SimpleJsonapi.render_resource(thing, serializer: ValuesSerializer)
51
+ assert_equal "/api/things", serialized.dig(:data, :links, :index)
52
+ end
53
+ end
54
+
55
+ describe "rendering object links" do
56
+ it "renders an object link" do
57
+ serialized = SimpleJsonapi.render_resource(thing, serializer: LinkObjectSerializer)
58
+ expected_link = { href: "/api/things/1", meta: { count: 1 } }
59
+ assert_equal expected_link, serialized.dig(:data, :links, :self)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,49 @@
1
+ require 'test_helper'
2
+
3
+ class MetaTest < Minitest::Spec
4
+ class Thing < TestModel
5
+ end
6
+
7
+ class NoMetaSerializer < SimpleJsonapi::Serializer
8
+ end
9
+
10
+ class BlocksSerializer < SimpleJsonapi::Serializer
11
+ meta(:scalar) { |t| "the id is #{t.id}" }
12
+ end
13
+
14
+ class ProcsSerializer < SimpleJsonapi::Serializer
15
+ meta :array, ->(_t) { %w[some metadata] }
16
+ end
17
+
18
+ class ValuesSerializer < SimpleJsonapi::Serializer
19
+ meta :scalar, "some metadata"
20
+ end
21
+
22
+ let(:thing) { Thing.new(id: 1) }
23
+
24
+ describe "Resource meta object" do
25
+ describe "without meta information" do
26
+ it "omits the meta key" do
27
+ serialized = SimpleJsonapi.render_resource(thing, serializer: NoMetaSerializer)
28
+ refute serialized.key?(:meta)
29
+ end
30
+ end
31
+
32
+ describe "rendering meta information" do
33
+ it "accepts a block" do
34
+ serialized = SimpleJsonapi.render_resource(thing, serializer: BlocksSerializer)
35
+ assert_equal "the id is 1", serialized.dig(:data, :meta, :scalar)
36
+ end
37
+
38
+ it "accepts a proc" do
39
+ serialized = SimpleJsonapi.render_resource(thing, serializer: ProcsSerializer)
40
+ assert_equal %w[some metadata], serialized.dig(:data, :meta, :array)
41
+ end
42
+
43
+ it "accepts a value" do
44
+ serialized = SimpleJsonapi.render_resource(thing, serializer: ValuesSerializer)
45
+ assert_equal "some metadata", serialized.dig(:data, :meta, :scalar)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,262 @@
1
+ require 'test_helper'
2
+
3
+ class RelationshipsTest < Minitest::Spec
4
+ class Order < TestModel
5
+ attr_accessor :customer
6
+ attr_writer :products
7
+
8
+ def products
9
+ @products ||= []
10
+ end
11
+ end
12
+
13
+ class Customer < TestModel
14
+ attr_accessor :name
15
+ end
16
+
17
+ class Product < TestModel
18
+ attr_accessor :name
19
+ end
20
+
21
+ class OrderSerializer < SimpleJsonapi::Serializer
22
+ has_one :customer
23
+ has_many :products
24
+ end
25
+
26
+ class CustomerSerializer < SimpleJsonapi::Serializer
27
+ attribute :name
28
+ end
29
+
30
+ class ProductSerializer < SimpleJsonapi::Serializer
31
+ attribute :name
32
+ end
33
+
34
+ describe "Relationships" do
35
+ describe "relationship data" do
36
+ describe "for a has_one relationship" do
37
+ it "renders an empty relationship as nil" do
38
+ order = Order.new(id: 1, customer: nil)
39
+ relationship = SimpleJsonapi.render_resource(order).dig(:data, :relationships, :customer)
40
+ assert_includes relationship.keys, :data
41
+ assert_nil relationship[:data]
42
+ end
43
+
44
+ it "renders a non-empty relationship as an object" do
45
+ order = Order.new(id: 1, customer: Customer.new(id: 2, name: "Miguel"))
46
+ relationship = SimpleJsonapi.render_resource(order).dig(:data, :relationships, :customer)
47
+ assert_instance_of Hash, relationship[:data]
48
+ assert_equal "2", relationship[:data][:id]
49
+ end
50
+ end
51
+
52
+ describe "for a has_many relationship" do
53
+ it "renders an empty relationship as an empty array" do
54
+ order = Order.new(id: 1, products: nil)
55
+ relationship = SimpleJsonapi.render_resource(order).dig(:data, :relationships, :products)
56
+ assert_instance_of Array, relationship[:data]
57
+ assert_equal [], relationship[:data]
58
+ end
59
+
60
+ it "renders a non-empty relationship as an array" do
61
+ order = Order.new(id: 1, products: [Product.new(id: 1), Product.new(id: 2)])
62
+ relationship = SimpleJsonapi.render_resource(order).dig(:data, :relationships, :products)
63
+ assert_instance_of Array, relationship[:data]
64
+ assert_equal(["1", "2"], relationship[:data].map { |obj| obj[:id] })
65
+ end
66
+ end
67
+
68
+ describe "related resource documents" do
69
+ it "include only the id and type" do
70
+ order = Order.new(id: 1, customer: Customer.new(id: 2, name: "Andrei"))
71
+ customer_doc = SimpleJsonapi.render_resource(order).dig(:data, :relationships, :customer, :data)
72
+ assert_equal({ id: "2", type: "customers" }, customer_doc.except(:meta))
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "relationship links" do
78
+ class OrderLinksSerializer < SimpleJsonapi::Serializer
79
+ has_many :products do
80
+ link(:self) { |order| "/api/orders/#{order.id}/relationships/products" }
81
+ link(:related) { |order| "/api/orders/#{order.id}/products" }
82
+ end
83
+ end
84
+
85
+ let(:order) { Order.new(id: 1) }
86
+ let(:serialized) { SimpleJsonapi.render_resource(order, serializer: OrderLinksSerializer) }
87
+
88
+ it "renders the links" do
89
+ expected_value = {
90
+ self: "/api/orders/1/relationships/products",
91
+ related: "/api/orders/1/products",
92
+ }
93
+ assert_equal expected_value, serialized.dig(:data, :relationships, :products, :links)
94
+ end
95
+ end
96
+
97
+ describe "conditional relationship links" do
98
+ class ConditionalLinksSerializer < SimpleJsonapi::Serializer
99
+ has_many :products do
100
+ link :if_a, "link if a", if: ->(order) { order.id == "a" }
101
+ link :unless_b, "link unless b", unless: ->(order) { order.id == "b" }
102
+ link :always, "link always"
103
+ end
104
+ end
105
+
106
+ let(:order_a) { Order.new(id: "a") }
107
+ let(:order_b) { Order.new(id: "b") }
108
+
109
+ def links_for(order)
110
+ SimpleJsonapi
111
+ .render_resource(order, serializer: ConditionalLinksSerializer)
112
+ .dig(:data, :relationships, :products, :links)
113
+ end
114
+
115
+ it "renders the links when the 'if' is truthy" do
116
+ assert_equal "link if a", links_for(order_a)[:if_a]
117
+ end
118
+
119
+ it "renders the links when the 'unless' is falsey" do
120
+ assert_equal "link unless b", links_for(order_a)[:unless_b]
121
+ end
122
+
123
+ it "omits the links when the 'if' is falsey" do
124
+ assert_equal [:always], links_for(order_b).keys
125
+ end
126
+
127
+ it "omits the links when the 'unless' is truthy" do
128
+ assert_equal [:always], links_for(order_b).keys
129
+ end
130
+ end
131
+
132
+ describe "relationship meta information" do
133
+ class OrderMetaSerializer < SimpleJsonapi::Serializer
134
+ has_many :products do
135
+ meta(:generated_on) { "2017-01-01" }
136
+ end
137
+ end
138
+
139
+ let(:order) { Order.new(id: 1) }
140
+ let(:serialized) { SimpleJsonapi.render_resource(order, serializer: OrderMetaSerializer) }
141
+ let(:relationship_doc) { serialized.dig(:data, :relationships, :products) }
142
+
143
+ it "renders the meta information" do
144
+ expected_value = { generated_on: "2017-01-01" }
145
+ assert_equal expected_value, relationship_doc[:meta]
146
+ end
147
+ end
148
+
149
+ describe "conditional relationship meta information" do
150
+ class ConditionalMetaSerializer < SimpleJsonapi::Serializer
151
+ has_many :products do
152
+ # data { |order| order.products }
153
+ meta :if_a, "meta if a", if: ->(order) { order.id == "a" }
154
+ meta :unless_b, "meta unless b", unless: ->(order) { order.id == "b" }
155
+ meta :always, "meta always"
156
+ end
157
+ end
158
+
159
+ let(:order_a) { Order.new(id: "a") }
160
+ let(:order_b) { Order.new(id: "b") }
161
+
162
+ def meta_for(order)
163
+ SimpleJsonapi
164
+ .render_resource(order, serializer: ConditionalMetaSerializer)
165
+ .dig(:data, :relationships, :products, :meta)
166
+ end
167
+
168
+ it "renders the meta object when the 'if' is truthy" do
169
+ assert_equal "meta if a", meta_for(order_a)[:if_a]
170
+ end
171
+
172
+ it "renders the meta object when the 'unless' is falsey" do
173
+ assert_equal "meta unless b", meta_for(order_a)[:unless_b]
174
+ end
175
+
176
+ it "omits the meta object when the 'if' is falsey" do
177
+ assert_equal [:always], meta_for(order_b).keys
178
+ end
179
+
180
+ it "omits the meta object when the 'unless' is truthy" do
181
+ assert_equal [:always], meta_for(order_b).keys
182
+ end
183
+ end
184
+
185
+ describe "related resource serializers" do
186
+ class AnotherProductSerializer < ProductSerializer
187
+ type "another_product"
188
+ end
189
+
190
+ class OrderInferringSerializer < SimpleJsonapi::Serializer
191
+ has_many :products
192
+ end
193
+
194
+ class OrderClassSerializer < SimpleJsonapi::Serializer
195
+ has_many :products, serializer: AnotherProductSerializer
196
+ end
197
+
198
+ class OrderStringSerializer < SimpleJsonapi::Serializer
199
+ has_many :products, serializer: AnotherProductSerializer.name
200
+ end
201
+
202
+ let(:order) { Order.new(id: 1, products: [Product.new(id: 2, name: "thing")]) }
203
+
204
+ def data_for(order, serializer_class)
205
+ SimpleJsonapi
206
+ .render_resource(order, serializer: serializer_class)
207
+ .dig(:data, :relationships, :products, :data, 0)
208
+ end
209
+
210
+ it "infers the serializer by default" do
211
+ assert_equal "products", data_for(order, OrderInferringSerializer)[:type]
212
+ end
213
+
214
+ it "accepts a serializer class" do
215
+ assert_equal "another_product", data_for(order, OrderClassSerializer)[:type]
216
+ end
217
+
218
+ it "accepts a serializer class name" do
219
+ assert_equal "another_product", data_for(order, OrderStringSerializer)[:type]
220
+ end
221
+ end
222
+
223
+ describe "tricky situations" do
224
+ it "handles circular relationships" do
225
+ order_1, order_2 = Array.new(2) { Order.new }
226
+ order_1.products = [order_2]
227
+ order_2.products = [order_1]
228
+
229
+ SimpleJsonapi.render_resource(order_1)
230
+ end
231
+ end
232
+
233
+ describe "documentation" do
234
+ class OrderDocSerializer < SimpleJsonapi::Serializer
235
+ has_one :customer
236
+ has_one :recipient, description: "the recipient"
237
+ has_many :products
238
+ has_many :shipments, description: "the shipments"
239
+ end
240
+
241
+ it "builds a has_one relationship without documentation" do
242
+ rel_defn = OrderDocSerializer.definition.relationship_definitions[:customer]
243
+ assert_nil rel_defn.description
244
+ end
245
+
246
+ it "stores the description of a has_one relationship" do
247
+ rel_defn = OrderDocSerializer.definition.relationship_definitions[:recipient]
248
+ assert_equal "the recipient", rel_defn.description
249
+ end
250
+
251
+ it "builds a has_many relationship without documentation" do
252
+ rel_defn = OrderDocSerializer.definition.relationship_definitions[:products]
253
+ assert_nil rel_defn.description
254
+ end
255
+
256
+ it "stores the description of a has_many relationship" do
257
+ rel_defn = OrderDocSerializer.definition.relationship_definitions[:shipments]
258
+ assert_equal "the shipments", rel_defn.description
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,79 @@
1
+ require 'test_helper'
2
+
3
+ class SortingTest < Minitest::Spec
4
+ class Customer < TestModel
5
+ attr_accessor :name, :orders
6
+ end
7
+
8
+ class Order < TestModel
9
+ attr_accessor :products, :order_number
10
+ end
11
+
12
+ class Product < TestModel
13
+ attr_accessor :name
14
+ end
15
+
16
+ class CustomerSerializer < SimpleJsonapi::Serializer
17
+ attribute :name
18
+ has_many :orders do
19
+ data do |customer|
20
+ resources = customer.orders.sort_by { |o| o.send(@sort&.first&.field || :id) }
21
+ resources.reverse! if @sort&.first&.desc?
22
+ resources
23
+ end
24
+ end
25
+ end
26
+
27
+ class OrderSerializer < SimpleJsonapi::Serializer
28
+ attribute :order_number
29
+ has_many :products do
30
+ data do |order|
31
+ order.products.sort_by { |p| p.send(@sort&.first&.field || :id) }
32
+ end
33
+ end
34
+ end
35
+
36
+ class ProductSerializer < SimpleJsonapi::Serializer
37
+ attribute :name
38
+ end
39
+
40
+ let(:customer) { Customer.new(id: 1, name: "John Henry", orders: [order_10, order_11]) }
41
+ let(:order_10) { Order.new(id: 10, order_number: 202, products: [spike, railtie]) }
42
+ let(:order_11) { Order.new(id: 11, order_number: 101, products: [spike, hammer]) }
43
+ let(:spike) { Product.new(id: 21, name: "spike") }
44
+ let(:railtie) { Product.new(id: 22, name: "railtie") }
45
+ let(:hammer) { Product.new(id: 23, name: "hammer") }
46
+
47
+ def render_customer(sort)
48
+ SimpleJsonapi.render_resource(customer, include: "orders,orders.products", sort_related: sort)
49
+ end
50
+
51
+ describe "Sorting top-level relationships" do
52
+ it "provides the relationship with an empty field list" do
53
+ # serializer's default is to sort by id: [10, 11]
54
+ serialized = render_customer(nil)
55
+ assert_equal(["10", "11"], serialized.dig(:data, :relationships, :orders, :data).map { |o| o[:id] })
56
+ end
57
+
58
+ it "provides the relationship with an ascending sort spec" do
59
+ # sort by order number: [101, 202]
60
+ serialized = render_customer(orders: "order_number")
61
+ assert_equal(["11", "10"], serialized.dig(:data, :relationships, :orders, :data).map { |o| o[:id] })
62
+ end
63
+
64
+ it "provides the relationship with a descending sort spec" do
65
+ # sort by order number: [202, 101]
66
+ serialized = render_customer(orders: "-order_number")
67
+ assert_equal(["10", "11"], serialized.dig(:data, :relationships, :orders, :data).map { |o| o[:id] })
68
+ end
69
+ end
70
+
71
+ describe "Sorting lower-level relationships" do
72
+ it "provides a blank sort spec" do
73
+ # products for order 10: [spike, railtie] == [21, 22]
74
+ serialized = render_customer(orders: "id", products: "name")
75
+ included_order = serialized[:included].find { |obj| obj[:type] == "orders" && obj[:id] == "10" }
76
+ assert_equal(["21", "22"], included_order.dig(:relationships, :products, :data).map { |p| p[:id] })
77
+ end
78
+ end
79
+ end