simple_jsonapi 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 +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