simple_jsonapi 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +131 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +5 -0
- data/Jenkinsfile +92 -0
- data/LICENSE.txt +22 -0
- data/README.md +532 -0
- data/Rakefile +10 -0
- data/lib/simple_jsonapi.rb +112 -0
- data/lib/simple_jsonapi/definition/attribute.rb +45 -0
- data/lib/simple_jsonapi/definition/base.rb +50 -0
- data/lib/simple_jsonapi/definition/concerns/has_links_object.rb +36 -0
- data/lib/simple_jsonapi/definition/concerns/has_meta_object.rb +36 -0
- data/lib/simple_jsonapi/definition/error.rb +70 -0
- data/lib/simple_jsonapi/definition/error_source.rb +29 -0
- data/lib/simple_jsonapi/definition/link.rb +27 -0
- data/lib/simple_jsonapi/definition/meta.rb +27 -0
- data/lib/simple_jsonapi/definition/relationship.rb +60 -0
- data/lib/simple_jsonapi/definition/resource.rb +104 -0
- data/lib/simple_jsonapi/error_serializer.rb +76 -0
- data/lib/simple_jsonapi/errors/bad_request.rb +11 -0
- data/lib/simple_jsonapi/errors/exception_serializer.rb +6 -0
- data/lib/simple_jsonapi/errors/wrapped_error.rb +35 -0
- data/lib/simple_jsonapi/errors/wrapped_error_serializer.rb +35 -0
- data/lib/simple_jsonapi/helpers/exceptions.rb +39 -0
- data/lib/simple_jsonapi/helpers/serializer_inferrer.rb +136 -0
- data/lib/simple_jsonapi/helpers/serializer_methods.rb +36 -0
- data/lib/simple_jsonapi/node/attributes.rb +51 -0
- data/lib/simple_jsonapi/node/base.rb +91 -0
- data/lib/simple_jsonapi/node/data/collection.rb +25 -0
- data/lib/simple_jsonapi/node/data/singular.rb +26 -0
- data/lib/simple_jsonapi/node/document/base.rb +62 -0
- data/lib/simple_jsonapi/node/document/collection.rb +17 -0
- data/lib/simple_jsonapi/node/document/errors.rb +17 -0
- data/lib/simple_jsonapi/node/document/singular.rb +17 -0
- data/lib/simple_jsonapi/node/error.rb +55 -0
- data/lib/simple_jsonapi/node/error_source.rb +40 -0
- data/lib/simple_jsonapi/node/errors.rb +28 -0
- data/lib/simple_jsonapi/node/included.rb +45 -0
- data/lib/simple_jsonapi/node/object_links.rb +40 -0
- data/lib/simple_jsonapi/node/object_meta.rb +40 -0
- data/lib/simple_jsonapi/node/relationship.rb +79 -0
- data/lib/simple_jsonapi/node/relationship_data/base.rb +53 -0
- data/lib/simple_jsonapi/node/relationship_data/collection.rb +32 -0
- data/lib/simple_jsonapi/node/relationship_data/singular.rb +33 -0
- data/lib/simple_jsonapi/node/relationships.rb +60 -0
- data/lib/simple_jsonapi/node/resource/base.rb +21 -0
- data/lib/simple_jsonapi/node/resource/full.rb +49 -0
- data/lib/simple_jsonapi/node/resource/linkage.rb +25 -0
- data/lib/simple_jsonapi/parameters/fields_spec.rb +45 -0
- data/lib/simple_jsonapi/parameters/include_spec.rb +57 -0
- data/lib/simple_jsonapi/parameters/sort_spec.rb +107 -0
- data/lib/simple_jsonapi/serializer.rb +89 -0
- data/lib/simple_jsonapi/version.rb +3 -0
- data/simple_jsonapi.gemspec +29 -0
- data/test/errors/bad_request_test.rb +34 -0
- data/test/errors/error_serializer_test.rb +229 -0
- data/test/errors/exception_serializer_test.rb +25 -0
- data/test/errors/wrapped_error_serializer_test.rb +91 -0
- data/test/errors/wrapped_error_test.rb +44 -0
- data/test/parameters/fields_spec_test.rb +56 -0
- data/test/parameters/include_spec_test.rb +58 -0
- data/test/parameters/sort_spec_test.rb +65 -0
- data/test/resources/attributes_test.rb +109 -0
- data/test/resources/extras_test.rb +70 -0
- data/test/resources/id_and_type_test.rb +76 -0
- data/test/resources/inclusion_test.rb +134 -0
- data/test/resources/links_test.rb +63 -0
- data/test/resources/meta_test.rb +49 -0
- data/test/resources/relationships_test.rb +262 -0
- data/test/resources/sorting_test.rb +79 -0
- data/test/resources/sparse_fieldset_test.rb +160 -0
- data/test/root_objects_test.rb +165 -0
- data/test/test_helper.rb +31 -0
- 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
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
data/Gemfile
ADDED
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
|
+
|