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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f10f16e05e855aeb6e4921d0c99a93fbfddf698b
4
+ data.tar.gz: bffd0d8041f0b5d8f4f0258c316391fa53e1cc9a
5
+ SHA512:
6
+ metadata.gz: 8423802544073da058e2f9eec6192b260ed72ad672c8449a9e4dc8690fc4edafe0008e81aa21aa8ffc0169c391dae377aa9c1a57c157aaa8e229b1da4017587c
7
+ data.tar.gz: f05181e2762f5069ed60c4b9d6f4bdc2a238d941c524a0888b3d78a4e5942be8a2cbd80eff5377d1dc0fdc8211d901016e68f7745c99df9014fe651fce8502c9
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /Gemfile.lock
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,131 @@
1
+ AllCops:
2
+ Exclude:
3
+ - '*.gemspec'
4
+ - 'Gemfile*'
5
+ TargetRubyVersion: "2.3"
6
+
7
+ # Do not modify any of these rules without running your changes past the `@techleads` Slack group.
8
+
9
+ # In order to make CodeClimate run in less than 30 minutes (the time-out), we're temporarily turning off
10
+ # some of the low-risk, high-frequency cops. This should be a temporary solution. We should either
11
+ # fix all of these issues across the codebase, or tune the cops to only pull up these issues when they
12
+ # matter (e.g. setting the max line length to 120 instead of 80)
13
+
14
+ Style/StringLiterals:
15
+ Enabled: false # this cop slows rubocop down too much so CodeClimate times out
16
+ EnforcedStyle: double_quotes
17
+
18
+ Style/NumericLiterals:
19
+ Enabled: false # this cop slows rubocop down too much so CodeClimate times out
20
+
21
+ # Permanent rules below
22
+
23
+ Layout/AlignParameters:
24
+ Enabled: false # we haven't reached consensus yet on what this rule should be
25
+
26
+ Layout/CaseIndentation:
27
+ EnforcedStyle: end
28
+ IndentOneStep: false
29
+
30
+ # Layout/ElseAlignment:
31
+ # Enabled: false # we haven't reached consensus yet on what this rule should be
32
+
33
+ # Layout/MultilineMethodCallIndentation:
34
+ # Enabled: false # we haven't reached consensus yet on what this rule should be
35
+
36
+ Layout/MultilineOperationIndentation:
37
+ Enabled: true
38
+ EnforcedStyle: indented
39
+
40
+ Lint/AmbiguousRegexpLiteral:
41
+ Enabled: false # we disagree with this rule
42
+
43
+ # Lint/EndAlignment:
44
+ # EnforcedStyleAlignWith: start_of_line
45
+
46
+ Metrics/AbcSize:
47
+ # There are ~500 methods > 20 as of 3/7/18, vs. ~900 methods > 15 (which is the default)
48
+ Max: 20
49
+
50
+ Metrics/BlockLength:
51
+ Max: 25
52
+ ExcludedMethods: ["class_methods", "describe", "included"]
53
+
54
+ Metrics/ClassLength:
55
+ # nearly all classes over 200 lines are old and crufty and should be split up
56
+ Max: 200
57
+
58
+ Metrics/CyclomaticComplexity:
59
+ # There are ~100 methods > 8 as of 3/7/18, vs. 220 > 6 (which is the default)
60
+ Max: 8
61
+
62
+ Metrics/LineLength:
63
+ # There are 23k lines > 80 chars, 5k > 120, 2k > 140 chars as of 12/21/17
64
+ Max: 120
65
+
66
+ Metrics/MethodLength:
67
+ # There are ~100 methods > 35 lines as of 12/21/17
68
+ Max: 35
69
+
70
+ Metrics/ModuleLength:
71
+ # nearly all modules over 200 lines are old and crufty and should be split up
72
+ Max: 200
73
+
74
+ Metrics/PerceivedComplexity:
75
+ # There are ~100 methods > 8 as of 3/7/18, vs. ~150 methods > 7 (which is the default)
76
+ Max: 8
77
+
78
+ Naming/VariableNumber:
79
+ EnforcedStyle: snake_case # `condition_info_1` is easier to read than `condition_info1`
80
+
81
+ Performance/Casecmp:
82
+ Enabled: false # we generally prefer the readability of downcase over the performance of casecmp
83
+
84
+ Style/Alias:
85
+ EnforcedStyle: prefer_alias_method
86
+
87
+ Style/BracesAroundHashParameters:
88
+ Enabled: false # using braces can be a good choice, e.g., `assert_equal expected_json, { "foo" => "bar" }`
89
+
90
+ Style/ClassAndModuleChildren:
91
+ EnforcedStyle: compact
92
+ Exclude: ["share/**/*"]
93
+
94
+ Style/CollectionMethods:
95
+ Enabled: true
96
+ PreferredMethods:
97
+ collect: map
98
+ collect!: map!
99
+ inject: reduce
100
+ detect: find
101
+ find_all: select
102
+
103
+ Style/Documentation:
104
+ Enabled: false # don't require class and method doc comments
105
+
106
+ Style/EmptyMethod:
107
+ EnforcedStyle: expanded
108
+
109
+ Style/FrozenStringLiteralComment:
110
+ EnforcedStyle: never
111
+
112
+ Style/GuardClause:
113
+ Enabled: false # sometimes guard clauses are appropriate and sometimes they aren't
114
+
115
+ Style/IfUnlessModifier:
116
+ Enabled: false # sometimes its clearer to use traditional if/end conditionals, even if they would fit on one line
117
+
118
+ Style/NumericPredicate:
119
+ Enabled: false # we disagree with this rule
120
+
121
+ Style/SymbolArray:
122
+ Enabled: false # undecided
123
+
124
+ Style/TrailingCommaInArguments:
125
+ EnforcedStyleForMultiline: consistent_comma
126
+
127
+ Style/TrailingCommaInLiteral:
128
+ EnforcedStyleForMultiline: consistent_comma
129
+
130
+ Style/WordArray:
131
+ Enabled: false # sometimes %w makes sense and sometimes it doesn't
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## Version 1.0.0 - April 19, 2018
2
+ * Initial public release
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'rake'
data/Jenkinsfile ADDED
@@ -0,0 +1,92 @@
1
+ pipeline {
2
+ agent none
3
+ options {
4
+ timeout(time: 1, unit: 'HOURS')
5
+ skipDefaultCheckout()
6
+ }
7
+
8
+ stages {
9
+ stage("Build Ruby") {
10
+ agent {
11
+ node {
12
+ label 'docker'
13
+ }
14
+ }
15
+
16
+ steps {
17
+ script {
18
+ with_ruby_build() {
19
+ script {
20
+ uid = sh(returnStdout: true, script: 'stat -c %g .').trim()
21
+ gid = sh(returnStdout: true, script: 'stat -c %u .').trim()
22
+ }
23
+
24
+ sh "chown -R ${uid}:${gid} vendor/bundle/"
25
+ sh "rm -rf vendor/bundle/ruby/2.3.0/cache"
26
+ stash name: 'ruby-bundle', includes: 'vendor/bundle/'
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ stage("Test") {
33
+ steps {
34
+ script {
35
+ node('docker') {
36
+ checkout([
37
+ $class: 'GitSCM',
38
+ branches: scm.branches,
39
+ doGenerateSubmoduleConfigurations: scm.doGenerateSubmoduleConfigurations,
40
+ extensions: scm.extensions + [[$class: 'CloneOption', noTags: true, reference: '', shallow: true]],
41
+ userRemoteConfigs: scm.userRemoteConfigs
42
+ ])
43
+ try {
44
+ docker.image('ruby:2.3.3').inside() {
45
+ sh 'rm -rf vendor/bundle'
46
+ unstash 'ruby-bundle'
47
+ sh 'bundle install --path=vendor/bundle'
48
+
49
+ withEnv([
50
+ 'DISABLE_SPRING=1',
51
+ 'TZ=America/New_York'
52
+ ]) {
53
+ sh 'bundle exec rake test'
54
+ }
55
+ }
56
+ }
57
+ finally {
58
+ junit 'test/reports/'
59
+ cleanWs()
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ post {
68
+ failure {
69
+ script {
70
+ if (env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'current') {
71
+ slackSend (channel: '#plm_website', color: '#FF0000', message: "FAILED ${env.JOB_NAME} [${env.BUILD_NUMBER}] (${env.RUN_DISPLAY_URL})")
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ def with_ruby_build(closure) {
79
+ docker.image('ruby:2.3.3').inside() {
80
+ checkout([
81
+ $class: 'GitSCM',
82
+ branches: scm.branches,
83
+ doGenerateSubmoduleConfigurations: scm.doGenerateSubmoduleConfigurations,
84
+ extensions: scm.extensions + [[$class: 'CloneOption', noTags: true, reference: '', shallow: true]],
85
+ userRemoteConfigs: scm.userRemoteConfigs
86
+ ])
87
+ sh 'rm -rf vendor/bundle'
88
+ sh 'bundle install --path=vendor/bundle'
89
+ closure()
90
+ cleanWs()
91
+ }
92
+ }
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016-2018 PatientsLikeMe
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,532 @@
1
+ # SimpleJsonapi
2
+
3
+ A library for building [JSONAPI](http://jsonapi.org) documents in Ruby. You may also be interested in [simple\_jsonapi\_rails](../simple_jsonapi_rails/README.md), which provides some integrations for using simple_jsonapi in a Rails application.
4
+
5
+ To view this README and more documentation of specific classes and methods, view the YARD documentation by running `yard server --reload` and visiting http://localhost:8808.
6
+
7
+ ### Features
8
+
9
+ SimpleJsonapi supports the following JSONAPI features:
10
+
11
+ * Singular and collection endpoints
12
+ * Attributes and relationships, including nested relationships
13
+ * Sparse fieldsets (**fields** parameter)
14
+ * Inclusion of related resources (**include** parameter & **included** member)
15
+ * Links and meta information on the document root, resources, and relationships
16
+ * Error objects (**errors** member)
17
+
18
+ Other features include:
19
+
20
+ * Serializers that are easy to define
21
+ * Sorting of first-level relationships (**sort_related** parameter)
22
+
23
+ ## What is JSONAPI?
24
+
25
+ A specification for building APIs in JSON. As its creators [write](http://jsonapi.org):
26
+
27
+ > If you’ve ever argued with your team about the way your JSON responses should be formatted, JSON API can be your anti-bikeshedding tool.
28
+
29
+ Here's a sample JSONAPI response that also sets the stage for the examples we'll see below:
30
+
31
+ ```json
32
+ {
33
+ "data": [{
34
+ "type": "orders",
35
+ "id": "1",
36
+ "attributes": {
37
+ "order_date": "2017-10-15",
38
+ "ship_date": null,
39
+ "customer_reference": "ABC123"
40
+ },
41
+ "relationships": {
42
+ "customer": {
43
+ "data": { "type": "customers", "id": "33" },
44
+ "links": {
45
+ "self": "http://example.com/orders/1/relationships/customer",
46
+ "related": "http://example.com/orders/1/customer"
47
+ },
48
+ "meta": { "included": true }
49
+ },
50
+ "products": {
51
+ "data": [
52
+ { "type": "products", "id": "7" },
53
+ { "type": "products", "id": "19" }
54
+ ],
55
+ "links": {
56
+ "self": "http://example.com/orders/1/relationships/products",
57
+ "related": "http://example.com/orders/1/products"
58
+ },
59
+ "meta": { "included": true }
60
+ }
61
+ },
62
+ "links": {
63
+ "self": "http://example.com/orders/1"
64
+ }
65
+ }],
66
+ "included": [{
67
+ "type": "customers",
68
+ "id": "33",
69
+ "attributes": {
70
+ "first_name": "Jane",
71
+ "last_name": "Doe"
72
+ },
73
+ "links": {
74
+ "self": "http://example.com/customers/33"
75
+ }
76
+ }, {
77
+ "type": "products",
78
+ "id": "7",
79
+ "attributes": {
80
+ "name": "Widget"
81
+ },
82
+ "links": {
83
+ "self": "http://example.com/products/7"
84
+ }
85
+ }, {
86
+ "type": "products",
87
+ "id": "19",
88
+ "attributes": {
89
+ "name": "Gadget"
90
+ },
91
+ "links": {
92
+ "self": "http://example.com/products/19"
93
+ }
94
+ }],
95
+ "links": {
96
+ "self": "http://example.com/orders",
97
+ "next": "http://example.com/orders?page[number]=2",
98
+ "last": "http://example.com/orders?page[number]=10"
99
+ },
100
+ "meta": {
101
+ "generated_at": "2017-11-01T12:34:56Z"
102
+ }
103
+ }
104
+ ```
105
+
106
+ ## Installation
107
+
108
+ If you're using Bundler, just add SimpleJsonapi to your Gemfile:
109
+
110
+ ```
111
+ gem 'simple_jsonapi'
112
+ ```
113
+
114
+ Or run `gem install simple_jsonapi` and then add `require 'simple_jsonapi'` to your code.
115
+
116
+ ## Basic usage
117
+
118
+ Suppose we have these resource classes.
119
+
120
+ ```ruby
121
+ class Order
122
+ include ActiveModel::Model # for the intializer
123
+ attr_accessor :id, :order_date, :ship_date, :customer_reference, :customer, :products
124
+ end
125
+
126
+ class Customer
127
+ include ActiveModel::Model
128
+ attr_accessor :id, :first_name, :last_name
129
+ end
130
+
131
+ class Product
132
+ include ActiveModel::Model
133
+ attr_accessor :id, :name
134
+ end
135
+ ```
136
+
137
+ First we define a serializer for each class.
138
+
139
+ ```ruby
140
+ class OrderSerializer < SimpleJsonapi::Serializer
141
+ # `type` and `id` can be inferred from the class name and its `id` method
142
+
143
+ attributes :order_date, :ship_date, :customer_reference
144
+
145
+ has_one :customer, serializer: CustomerSerializer
146
+ has_many :products, serializer: ProductSerializer do
147
+ data { |order| order.products.sort_by(&:name) }
148
+ end
149
+
150
+ link(:self) { |order| "http://example.com/orders/#{order.id}" }
151
+ end
152
+
153
+ class CustomerSerializer < SimpleJsonapi::Serializer
154
+ attributes :first_name, :last_name
155
+ end
156
+
157
+ class ProductSerializer < SimpleJsonapi::Serializer
158
+ attributes :name
159
+ end
160
+ ```
161
+
162
+ Then we can call `SimpleJsonapi.render_resource` to render a single resource.
163
+
164
+ ```ruby
165
+ > order = Order.new(id: 1, order_date: Date.new(2017, 10, 15), customer_reference: "ABC123")
166
+
167
+ > SimpleJsonapi.render_resource(order)
168
+ {
169
+ :data => {
170
+ :id => "1",
171
+ :type => "orders",
172
+ :attributes => {
173
+ :order_date => Sun, 15 Oct 2017,
174
+ :ship_date => nil,
175
+ :customer_reference => "ABC123"
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ And we can call `SimpleJsonapi.render_resources` to render a collection of resources. `render_resources` accepts either a single resource or an `Enumerable` and always renders an array.
182
+
183
+ ```ruby
184
+ > SimpleJsonapi.render_resources([order1, order2])
185
+ {
186
+ :data => [
187
+ {
188
+ :id => "1",
189
+ :type => "orders",
190
+ :attributes => { ... }
191
+ }, {
192
+ :id => "1",
193
+ :type => "orders",
194
+ :attributes => { ... }
195
+ }
196
+ ]
197
+ }
198
+ ```
199
+
200
+ Finally, we can call `SimpleJsonapi.render_errors` to render a document with an `errors` member. Like `render_resources`, `render_errors` accepts either a single error or an `Enumerable` and always renders an array.
201
+
202
+ ```ruby
203
+ > error = StandardError.new("something wicked this way comes")
204
+
205
+ > SimpleJsonapi.render_errors(error)
206
+ {
207
+ :errors => [
208
+ {
209
+ :code => "standard_error",
210
+ :title => "StandardError",
211
+ :detail => "something wicked this way comes"
212
+ }
213
+ ]
214
+ }
215
+ ```
216
+
217
+ ## Advanced usage
218
+
219
+ ### Type and ID
220
+
221
+ The **type** member is inferred from the resource's class name. For example, an `Ordering::LineItem` instance's **type** would be `"line_items"`. A serializer can generate a different **type** by providing a value or a block.
222
+
223
+ ```ruby
224
+ class LineItemSerializer < SimpleJsonapi::Serializer
225
+ type "entries"
226
+ # or
227
+ type { |item| item.class.name.underscore }
228
+ end
229
+ ```
230
+
231
+ The **id** member calls the resource's `id` method. A serializer can override the **id** by providing a value or a block.
232
+
233
+ ```ruby
234
+ class OrderSerializer < SimpleJsonapi::Serializer
235
+ id "3.14"
236
+ # or
237
+ id { |item| item.order_id }
238
+ end
239
+ ```
240
+
241
+ ### Attributes
242
+
243
+ By default, attributes call the method of the same name on the resource. Serializers can provide custom implementations as well.
244
+
245
+ ```ruby
246
+ class OrderSerializer < SimpleJsonapi::Serializer
247
+ attribute :system_version, "1.0"
248
+ attribute(:order_date) { |order| order.created_at.to_date }
249
+ end
250
+ ```
251
+
252
+ Attributes (and relationships) can be conditionally rendered by providing an `if` or `unless` parameter. (`@current_user` is discussed below)
253
+
254
+ ```ruby
255
+ class UserSerializer < SimpleJsonapi::Serializer
256
+ attribute :ssn, if: { @current_user.is_an_admin? }
257
+ attribute :country, unless: { |user| user.hide_demographics? }
258
+ end
259
+ ```
260
+
261
+ Resources can have **links** and **meta** information.
262
+
263
+ ```ruby
264
+ class OrderSerializer < SimpleJsonapi::Serializer
265
+ attributes :order_date, :ship_date, :customer_reference
266
+
267
+ link(:self) { |order| "https://example.com/orders/#{order.id}" }
268
+
269
+ meta(:last_refreshed) { |order| order.updated_at }
270
+ end
271
+ ```
272
+
273
+ ### Relationships
274
+
275
+ Relationships are defined with `has_one` and `has_many`; both take the same parameters.
276
+
277
+ By default, the related resources are retrieved by calling the method of the same name on the resource. Serializers can provide custom implementations as well.
278
+
279
+ ```ruby
280
+ class OrderSerializer < SimpleJsonapi::Serializer
281
+ has_one :customer # calls `order.customer`
282
+
283
+ has_many :products do
284
+ data { |order| order.products.sort_by(&:name) }
285
+ end
286
+ end
287
+ ```
288
+
289
+ By default, a serializer is chosen based on the class of the related resource. Relationships can also specify a serializer to use, or a `SerializerInferrer` that will choose an appropriate serializer for ach resource.
290
+
291
+ ```ruby
292
+ class OrderSerializer < SimpleJsonapi::Serializer
293
+ has_one :customer, serializer: CustomerSerializer
294
+ has_many :products, serializer: ORDERING_SERIALIZER_INFERRER
295
+ end
296
+
297
+ ORDERING_SERIALIZER_INFERRER = SimpleJsonapi::SerializerInferrer.new do |resource|
298
+ "Serializers::#{resource.class.name}".safe_constantize
299
+ end
300
+ ```
301
+
302
+ Like resources, relationships can have **links** and **meta** information.
303
+
304
+ ```ruby
305
+ class OrderSerializer < SimpleJsonapi::Serializer
306
+ has_many :products do
307
+ link(:self) { |order| "https://example.com/orders/#{order.id}/relationship/products" }
308
+ link(:related) { |order| "https://example.com/orders/#{order.id}/products" }
309
+
310
+ meta(:sorted_by) { "product_name" }
311
+ end
312
+ end
313
+ ```
314
+
315
+ Relationships can be conditionally rendered; see the discussion of `if` and `unless` above under "Attributes".
316
+
317
+ ### Sparse fieldsets
318
+
319
+ The `render_resource` and `render_resources` methods accept a `fields` parameter to filter the list of fields in the rendered resources. The parameter is a hash with object **type**s as keys and comma-delimited lists or arrays of fields as values.
320
+
321
+ ```ruby
322
+ > SimpleJsonapi.render_resource(order,
323
+ include: "customer",
324
+ fields: {
325
+ orders: "order_date,ship_date,customer",
326
+ customers: ["last_name", "first_name"],
327
+ }
328
+ ))
329
+ ```
330
+
331
+ Note that if you request a sparse fieldset and an included relationship, the relationship must be in the list of fields.
332
+
333
+ ### Including related resources
334
+
335
+ The `render_resource` and `render_resources` methods accept an `include` parameter to request that specific relationships be rendered under the document's **included** member.
336
+
337
+ ```ruby
338
+ > SimpleJsonapi.render_resource(order, include: "customer,products")
339
+ # or
340
+ > SimpleJsonapi.render_resource(order, include: ["customer", "products"])
341
+ ```
342
+
343
+ Serializers can allow the client to request related resources sorted in a specific order via the `sort_related` parameter. The sort fields are exposed as an instance variable, `@sort`, which is an array of `SortFieldSpec` objects.
344
+
345
+ ```ruby
346
+ > SimpleJsonapi.render_resource(order,
347
+ include: "products",
348
+ sort_related: { products: "-name,id" }
349
+ )
350
+
351
+ class OrderSerializer < SimpleJsonapi::Serializer
352
+ has_many :products do
353
+ data do |order|
354
+ # @sort = [ <SortFieldSpec field=name order=desc>, <SortFieldSpec field=id order=asc> ]
355
+ sort_options = @sort.inject({}) do |hash,spec|
356
+ hash[spec.field] = spec.order
357
+ end
358
+ order.products.order(sort_options)
359
+ end
360
+ end
361
+ end
362
+ ```
363
+
364
+ ### Document links and meta information
365
+
366
+ **Links** and **meta** information can be added to the document root by passing parameters to `render_resource` or `render_resources`. These parameters must be hashes and are passed through to the rendered document verbatim.
367
+
368
+ ```ruby
369
+ > SimpleJsonapi.render_resources(orders,
370
+ links: {
371
+ self: "https://example.com/orders"
372
+ },
373
+ meta: {
374
+ generated_at: Time.now
375
+ })
376
+ ```
377
+
378
+ ### Custom methods and extra context
379
+
380
+ Attributes, relationships, if/unless options, links, meta information, and all other definitions that accept proc evaluate those procs in the context of the serializer instance. This means that any methods defined in the serializer class are also available to the procs.
381
+
382
+ It is also possible to pass in additional variables at render time via the `extras` parameter. Any extra values appear as instance variables on the serializer when the procs are called.
383
+
384
+ ```ruby
385
+ class UserSerializer < SimpleJsonapi::Serializer
386
+ attribute :ssn, if: { @current_user.is_an_admin? }
387
+ relationship :orders do
388
+ data { |user| get_orders_for_user(user) }
389
+ end
390
+
391
+ def get_orders_for_user(user)
392
+ ...
393
+ end
394
+ end
395
+
396
+ > SimpleJsonapi.render_resources(orders, extras: { current_user: user })
397
+ ```
398
+
399
+ ## How it works
400
+
401
+ There are two primary concepts in SimpleJsonapi: serializer definitions and renderer nodes.
402
+
403
+ Oddly enough, the serializer class is **not** the entry point for serializing a document. That's because a document's **data** member may be a collection of resources of different types, each of which may require a different serializer.
404
+
405
+ ### Serializer definitions
406
+
407
+ The definitions for a serializer are listed below.
408
+
409
+ > _NOTE: The `SimpleJsonapi::` prefix is omitted for brevity._
410
+
411
+ ```
412
+ Resource serializer [Serializer]
413
+ └─ resource [Definition::Resource]
414
+ ├─ id [Proc]
415
+ ├─ type [Proc]
416
+ ├─ attributes [Hash]
417
+ │ └─ attribute [Definition::Attribute]
418
+ ├─ relationships [Hash]
419
+ │ └─ relationship [Definition::Relationship]
420
+ │ ├─ related data [Proc]
421
+ │ ├─ links [Hash]
422
+ │ │ └─ link [Definition::ObjectLink]
423
+ │ └─ meta [Hash]
424
+ │ └─ meta member [Definition::ObjectMeta]
425
+ ├─ links [Hash]
426
+ │ └─ link [Definition::ObjectLink]
427
+ └─ meta [Hash]
428
+ └─ meta member [Definition::ObjectMeta]
429
+
430
+ Error serializer [ErrorSerializer]
431
+ └─ error [Definition::Error]
432
+ ├─ id [Proc]
433
+ ├─ status [Proc]
434
+ ├─ code [Proc]
435
+ ├─ title [Proc]
436
+ ├─ detail [Proc]
437
+ ├─ source [Definition::ErrorSource]
438
+ │ ├─ parameter [Proc]
439
+ │ └─ pointer [Proc]
440
+ ├─ links [Hash]
441
+ │ └─ link [Definition::ObjectLink]
442
+ └─ meta [Hash]
443
+ └─ meta member [Definition::ObjectMeta]
444
+ ```
445
+
446
+ ### Renderer nodes
447
+
448
+ The nodes involved in rendering a JSONAPI document are listed below. Entries without a class next to them are rendered by their parent node.
449
+
450
+ > _NOTE: The `SimpleJsonapi::` prefix is omitted for brevity._
451
+
452
+ ```
453
+ Resource document [Node::Document::Singular|Collection|Errors]
454
+ ├─ data [Node::Data::Singular|Collection]
455
+ ├─ resource [Node::Resource::Full]
456
+ │ │ (@serializer is modified here)
457
+ │ ├─ id
458
+ │ ├─ type
459
+ │ ├─ attributes [Node::Attributes]
460
+ │ │ └─ attribute
461
+ │ ├─ relationships [Node::Relationships]
462
+ │ │ └─ relationship [Node::Relationship]
463
+ │ │ ├─ data [Node::RelationshipData::Singular|Collection]
464
+ │ │ │ │ (@include_spec and @serializer_inferrer are modified here)
465
+ │ │ │ ├─ resource linkage [Node::Resource::Linkage]
466
+ │ │ │ │ ├─ id
467
+ │ │ │ │ ├─ type
468
+ │ │ │ │ └─ meta
469
+ │ │ │ │ └─ meta member
470
+ │ │ │ └─ resource (added to `included` node) [Node::Resource::Full]
471
+ │ │ │ └─ (see above for details)
472
+ │ │ ├─ links [Node::ObjectLinks]
473
+ │ │ │ └─ link
474
+ │ │ └─ meta [Node::ObjectMeta]
475
+ │ │ └─ meta member
476
+ │ ├─ links [Node::ObjectLinks]
477
+ │ │ └─ link
478
+ │ └─ meta [Node::ObjectMeta]
479
+ │ └─ meta member
480
+ ├─ included [Node::Included]
481
+ │ └─ (resources may be rendered here by the relationship data nodes)
482
+ ├─ links
483
+ │ └─ link
484
+ └─ meta
485
+ └─ meta member
486
+
487
+ Errors document [Node::Document::Errors]
488
+ ├─ errors [Node::Errors]
489
+ │ ├─ error [Node::Error]
490
+ │ │ ├─ id
491
+ │ │ ├─ status
492
+ │ │ ├─ code
493
+ │ │ ├─ title
494
+ │ │ ├─ detail
495
+ │ │ └─ source [Node::ErrorSource]
496
+ │ │ ├─ parameter
497
+ │ │ └─ pointer
498
+ │ ├─ links [Node::ObjectLinks]
499
+ │ │ └─ link
500
+ │ └─ meta [Node::ObjectMeta]
501
+ │ └─ meta member
502
+ ├─ links
503
+ └─ meta
504
+ ```
505
+
506
+ The following parameters are passed into the top-level document node and through the entire node hierarchy.
507
+
508
+ * **serializer_inferrer** is a `SerializerInferrer` instance, used to choose a serializer for each resource. The serializer_inferrer may be replaced at each relationship data node if that relationship has a `serializer` parameter.
509
+ * **serializer** is a Serializer instance, used when rendering a resource and its child members. The serializer is replaced at each resource node.
510
+ * **include** is an IncludeSpec instance, created from the `include` parameter. It is replaced at each relationship data node.
511
+ * **fields** is a FieldSpec instance, created from the `fields` parameter passed to `serialize_resource(s)` and available throughout the document.
512
+ * **sort_related** is a SortSpec instance, created from the `sort_related` parameter and available throughout the document.
513
+ * **extras** is the hash passed to `serialize_resource(s)`. Its values are available to every block as instance variables on the serializer.
514
+ * **root_node** is a reference to the document node, used to access the **included** node.
515
+
516
+ ## Contributing
517
+
518
+ Running the tests:
519
+
520
+ 1. Change to the gem's directory
521
+ 2. Run `bundle install`
522
+ 3. Run `bundle exec rake test`
523
+
524
+ ## Release Process
525
+ Once pull request is merged to master, on latest master:
526
+ 1. Update CHANGELOG.md. Version: [ major (breaking change: non-backwards
527
+ compatible release) | minor (new features) | patch (bugfixes) ]
528
+ 2. Update version in lib/global_enforcer/version.rb
529
+ 3. Release by running `bundle exec rake release`
530
+
531
+ ## License
532
+