jsonapi.rb 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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/.travis.yml +7 -0
- data/.yardstick.yml +29 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +204 -0
- data/LICENSE.txt +21 -0
- data/README.md +245 -0
- data/Rakefile +30 -0
- data/jsonapi.rb.gemspec +37 -0
- data/lib/jsonapi.rb +12 -0
- data/lib/jsonapi/active_model_error_serializer.rb +42 -0
- data/lib/jsonapi/error_serializer.rb +25 -0
- data/lib/jsonapi/errors.rb +46 -0
- data/lib/jsonapi/fetching.rb +26 -0
- data/lib/jsonapi/filtering.rb +73 -0
- data/lib/jsonapi/pagination.rb +80 -0
- data/lib/jsonapi/rails.rb +98 -0
- data/lib/jsonapi/version.rb +3 -0
- metadata +231 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2625c5af20d211c8e480235ec9587d648143eda2f76ffc2a5add9bdcc5d5e1d4
|
|
4
|
+
data.tar.gz: e685d519946126bd51b3741b0980edab95d7da4f3bbb9ceca8cb248142eedd65
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fe25509464222844a3511f5cb768b85e3b078939822abe88889bf8f21df91fd8460d52b81d98dadd3e74ae831b7d0c66242353007a44f631560728029d6db231
|
|
7
|
+
data.tar.gz: 789b5919fcc2b8092e9db62ddad3fc420d152b6e9a5542f2daba561a7233d5320e4a672b85bb7640bce31408a2e60788ee1b3ca65e503b766e8d3bfe0b599ee5
|
data/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
coverage
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
inherit_gem:
|
|
2
|
+
rubocop-rails_config:
|
|
3
|
+
- config/rails.yml
|
|
4
|
+
|
|
5
|
+
Rails:
|
|
6
|
+
Enabled: true
|
|
7
|
+
|
|
8
|
+
Style/StringLiterals:
|
|
9
|
+
Enabled: true
|
|
10
|
+
EnforcedStyle: single_quotes
|
|
11
|
+
|
|
12
|
+
Style/FrozenStringLiteralComment:
|
|
13
|
+
Enabled: false
|
|
14
|
+
|
|
15
|
+
Metrics/LineLength:
|
|
16
|
+
Max: 80
|
|
17
|
+
|
|
18
|
+
Layout/IndentationConsistency:
|
|
19
|
+
EnforcedStyle: normal
|
|
20
|
+
|
|
21
|
+
Style/BlockDelimiters:
|
|
22
|
+
Enabled: true
|
data/.travis.yml
ADDED
data/.yardstick.yml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
path: ['lib/**/*.rb']
|
|
3
|
+
threshold: 100
|
|
4
|
+
rules:
|
|
5
|
+
ApiTag::Presence:
|
|
6
|
+
enabled: false
|
|
7
|
+
ApiTag::Inclusion:
|
|
8
|
+
enabled: false
|
|
9
|
+
ApiTag::ProtectedMethod:
|
|
10
|
+
enabled: false
|
|
11
|
+
ApiTag::PrivateMethod:
|
|
12
|
+
enabled: false
|
|
13
|
+
ExampleTag:
|
|
14
|
+
enabled: false
|
|
15
|
+
ReturnTag:
|
|
16
|
+
enabled: true
|
|
17
|
+
exclude: []
|
|
18
|
+
Summary::Presence:
|
|
19
|
+
enabled: true
|
|
20
|
+
exclude: []
|
|
21
|
+
Summary::Length:
|
|
22
|
+
enabled: true
|
|
23
|
+
exclude: []
|
|
24
|
+
Summary::Delimiter:
|
|
25
|
+
enabled: true
|
|
26
|
+
exclude: []
|
|
27
|
+
Summary::SingleLine:
|
|
28
|
+
enabled: true
|
|
29
|
+
exclude: []
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
GIT
|
|
2
|
+
remote: git://github.com/stas/jsonapi-rspec.git
|
|
3
|
+
revision: f223a3d5531cf2c0ce2f90aa8dac2bae46b2d499
|
|
4
|
+
specs:
|
|
5
|
+
jsonapi-rspec (0.0.2)
|
|
6
|
+
rspec-expectations
|
|
7
|
+
|
|
8
|
+
PATH
|
|
9
|
+
remote: .
|
|
10
|
+
specs:
|
|
11
|
+
jsonapi.rb (1.0.0)
|
|
12
|
+
fast_jsonapi (~> 1.5)
|
|
13
|
+
ransack (~> 2.1)
|
|
14
|
+
|
|
15
|
+
GEM
|
|
16
|
+
remote: https://rubygems.org/
|
|
17
|
+
specs:
|
|
18
|
+
actioncable (5.2.2)
|
|
19
|
+
actionpack (= 5.2.2)
|
|
20
|
+
nio4r (~> 2.0)
|
|
21
|
+
websocket-driver (>= 0.6.1)
|
|
22
|
+
actionmailer (5.2.2)
|
|
23
|
+
actionpack (= 5.2.2)
|
|
24
|
+
actionview (= 5.2.2)
|
|
25
|
+
activejob (= 5.2.2)
|
|
26
|
+
mail (~> 2.5, >= 2.5.4)
|
|
27
|
+
rails-dom-testing (~> 2.0)
|
|
28
|
+
actionpack (5.2.2)
|
|
29
|
+
actionview (= 5.2.2)
|
|
30
|
+
activesupport (= 5.2.2)
|
|
31
|
+
rack (~> 2.0)
|
|
32
|
+
rack-test (>= 0.6.3)
|
|
33
|
+
rails-dom-testing (~> 2.0)
|
|
34
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
|
35
|
+
actionview (5.2.2)
|
|
36
|
+
activesupport (= 5.2.2)
|
|
37
|
+
builder (~> 3.1)
|
|
38
|
+
erubi (~> 1.4)
|
|
39
|
+
rails-dom-testing (~> 2.0)
|
|
40
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
|
41
|
+
activejob (5.2.2)
|
|
42
|
+
activesupport (= 5.2.2)
|
|
43
|
+
globalid (>= 0.3.6)
|
|
44
|
+
activemodel (5.2.2)
|
|
45
|
+
activesupport (= 5.2.2)
|
|
46
|
+
activerecord (5.2.2)
|
|
47
|
+
activemodel (= 5.2.2)
|
|
48
|
+
activesupport (= 5.2.2)
|
|
49
|
+
arel (>= 9.0)
|
|
50
|
+
activestorage (5.2.2)
|
|
51
|
+
actionpack (= 5.2.2)
|
|
52
|
+
activerecord (= 5.2.2)
|
|
53
|
+
marcel (~> 0.3.1)
|
|
54
|
+
activesupport (5.2.2)
|
|
55
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
56
|
+
i18n (>= 0.7, < 2)
|
|
57
|
+
minitest (~> 5.1)
|
|
58
|
+
tzinfo (~> 1.1)
|
|
59
|
+
arel (9.0.0)
|
|
60
|
+
ast (2.4.0)
|
|
61
|
+
builder (3.2.3)
|
|
62
|
+
concurrent-ruby (1.1.4)
|
|
63
|
+
crass (1.0.4)
|
|
64
|
+
diff-lcs (1.3)
|
|
65
|
+
docile (1.3.1)
|
|
66
|
+
erubi (1.8.0)
|
|
67
|
+
fast_jsonapi (1.5)
|
|
68
|
+
activesupport (>= 4.2)
|
|
69
|
+
ffaker (2.10.0)
|
|
70
|
+
globalid (0.4.2)
|
|
71
|
+
activesupport (>= 4.2.0)
|
|
72
|
+
i18n (1.5.1)
|
|
73
|
+
concurrent-ruby (~> 1.0)
|
|
74
|
+
jaro_winkler (1.5.2)
|
|
75
|
+
json (2.1.0)
|
|
76
|
+
loofah (2.2.3)
|
|
77
|
+
crass (~> 1.0.2)
|
|
78
|
+
nokogiri (>= 1.5.9)
|
|
79
|
+
mail (2.7.1)
|
|
80
|
+
mini_mime (>= 0.1.1)
|
|
81
|
+
marcel (0.3.3)
|
|
82
|
+
mimemagic (~> 0.3.2)
|
|
83
|
+
method_source (0.9.2)
|
|
84
|
+
mimemagic (0.3.3)
|
|
85
|
+
mini_mime (1.0.1)
|
|
86
|
+
mini_portile2 (2.4.0)
|
|
87
|
+
minitest (5.11.3)
|
|
88
|
+
nio4r (2.3.1)
|
|
89
|
+
nokogiri (1.10.0)
|
|
90
|
+
mini_portile2 (~> 2.4.0)
|
|
91
|
+
parallel (1.12.1)
|
|
92
|
+
parser (2.5.3.0)
|
|
93
|
+
ast (~> 2.4.0)
|
|
94
|
+
powerpack (0.1.2)
|
|
95
|
+
rack (2.0.6)
|
|
96
|
+
rack-test (1.1.0)
|
|
97
|
+
rack (>= 1.0, < 3)
|
|
98
|
+
rails (5.2.2)
|
|
99
|
+
actioncable (= 5.2.2)
|
|
100
|
+
actionmailer (= 5.2.2)
|
|
101
|
+
actionpack (= 5.2.2)
|
|
102
|
+
actionview (= 5.2.2)
|
|
103
|
+
activejob (= 5.2.2)
|
|
104
|
+
activemodel (= 5.2.2)
|
|
105
|
+
activerecord (= 5.2.2)
|
|
106
|
+
activestorage (= 5.2.2)
|
|
107
|
+
activesupport (= 5.2.2)
|
|
108
|
+
bundler (>= 1.3.0)
|
|
109
|
+
railties (= 5.2.2)
|
|
110
|
+
sprockets-rails (>= 2.0.0)
|
|
111
|
+
rails-dom-testing (2.0.3)
|
|
112
|
+
activesupport (>= 4.2.0)
|
|
113
|
+
nokogiri (>= 1.6)
|
|
114
|
+
rails-html-sanitizer (1.0.4)
|
|
115
|
+
loofah (~> 2.2, >= 2.2.2)
|
|
116
|
+
railties (5.2.2)
|
|
117
|
+
actionpack (= 5.2.2)
|
|
118
|
+
activesupport (= 5.2.2)
|
|
119
|
+
method_source
|
|
120
|
+
rake (>= 0.8.7)
|
|
121
|
+
thor (>= 0.19.0, < 2.0)
|
|
122
|
+
rainbow (3.0.0)
|
|
123
|
+
rake (12.3.2)
|
|
124
|
+
ransack (2.1.1)
|
|
125
|
+
actionpack (>= 5.0)
|
|
126
|
+
activerecord (>= 5.0)
|
|
127
|
+
activesupport (>= 5.0)
|
|
128
|
+
i18n
|
|
129
|
+
rspec (3.8.0)
|
|
130
|
+
rspec-core (~> 3.8.0)
|
|
131
|
+
rspec-expectations (~> 3.8.0)
|
|
132
|
+
rspec-mocks (~> 3.8.0)
|
|
133
|
+
rspec-core (3.8.0)
|
|
134
|
+
rspec-support (~> 3.8.0)
|
|
135
|
+
rspec-expectations (3.8.2)
|
|
136
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
137
|
+
rspec-support (~> 3.8.0)
|
|
138
|
+
rspec-mocks (3.8.0)
|
|
139
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
140
|
+
rspec-support (~> 3.8.0)
|
|
141
|
+
rspec-rails (3.8.1)
|
|
142
|
+
actionpack (>= 3.0)
|
|
143
|
+
activesupport (>= 3.0)
|
|
144
|
+
railties (>= 3.0)
|
|
145
|
+
rspec-core (~> 3.8.0)
|
|
146
|
+
rspec-expectations (~> 3.8.0)
|
|
147
|
+
rspec-mocks (~> 3.8.0)
|
|
148
|
+
rspec-support (~> 3.8.0)
|
|
149
|
+
rspec-support (3.8.0)
|
|
150
|
+
rubocop (0.62.0)
|
|
151
|
+
jaro_winkler (~> 1.5.1)
|
|
152
|
+
parallel (~> 1.10)
|
|
153
|
+
parser (>= 2.5, != 2.5.1.1)
|
|
154
|
+
powerpack (~> 0.1)
|
|
155
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
156
|
+
ruby-progressbar (~> 1.7)
|
|
157
|
+
unicode-display_width (~> 1.4.0)
|
|
158
|
+
rubocop-rails_config (0.4.0)
|
|
159
|
+
railties (>= 3.0)
|
|
160
|
+
rubocop (~> 0.58)
|
|
161
|
+
ruby-progressbar (1.10.0)
|
|
162
|
+
simplecov (0.16.1)
|
|
163
|
+
docile (~> 1.1)
|
|
164
|
+
json (>= 1.8, < 3)
|
|
165
|
+
simplecov-html (~> 0.10.0)
|
|
166
|
+
simplecov-html (0.10.2)
|
|
167
|
+
sprockets (3.7.2)
|
|
168
|
+
concurrent-ruby (~> 1.0)
|
|
169
|
+
rack (> 1, < 3)
|
|
170
|
+
sprockets-rails (3.2.1)
|
|
171
|
+
actionpack (>= 4.0)
|
|
172
|
+
activesupport (>= 4.0)
|
|
173
|
+
sprockets (>= 3.0.0)
|
|
174
|
+
sqlite3 (1.3.13)
|
|
175
|
+
thor (0.20.3)
|
|
176
|
+
thread_safe (0.3.6)
|
|
177
|
+
tzinfo (1.2.5)
|
|
178
|
+
thread_safe (~> 0.1)
|
|
179
|
+
unicode-display_width (1.4.1)
|
|
180
|
+
websocket-driver (0.7.0)
|
|
181
|
+
websocket-extensions (>= 0.1.0)
|
|
182
|
+
websocket-extensions (0.1.3)
|
|
183
|
+
yard (0.9.16)
|
|
184
|
+
yardstick (0.9.9)
|
|
185
|
+
yard (~> 0.8, >= 0.8.7.2)
|
|
186
|
+
|
|
187
|
+
PLATFORMS
|
|
188
|
+
ruby
|
|
189
|
+
|
|
190
|
+
DEPENDENCIES
|
|
191
|
+
bundler
|
|
192
|
+
ffaker
|
|
193
|
+
jsonapi-rspec!
|
|
194
|
+
jsonapi.rb!
|
|
195
|
+
rails
|
|
196
|
+
rspec (~> 3.0)
|
|
197
|
+
rspec-rails
|
|
198
|
+
rubocop-rails_config
|
|
199
|
+
simplecov
|
|
200
|
+
sqlite3
|
|
201
|
+
yardstick
|
|
202
|
+
|
|
203
|
+
BUNDLED WITH
|
|
204
|
+
1.16.3
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Stas Suscov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# JSONAPI.rb :electric_plug:
|
|
2
|
+
|
|
3
|
+
So you say you need [JSON:API](https://jsonapi.org/) support in your API...
|
|
4
|
+
|
|
5
|
+
> - hey how did your hackathon go?
|
|
6
|
+
> - not too bad, we got Babel set up
|
|
7
|
+
> - yep…
|
|
8
|
+
> - yep.
|
|
9
|
+
>
|
|
10
|
+
>— [I Am Devloper](https://twitter.com/iamdevloper/status/787969734918668289)
|
|
11
|
+
|
|
12
|
+
Here are some _codes_ to help you build your next JSON:API compliable application
|
|
13
|
+
easier and faster.
|
|
14
|
+
|
|
15
|
+
## But why?
|
|
16
|
+
|
|
17
|
+
It's quite a hassle to setup a Ruby (Rails) web application to use and follow
|
|
18
|
+
the JSON:API specifications.
|
|
19
|
+
|
|
20
|
+
The idea is simple, JSONAPI.rb offers a bunch of modules/mixins/glue,
|
|
21
|
+
add them to your controllers, call some methods, _profit_!
|
|
22
|
+
|
|
23
|
+
Main goals:
|
|
24
|
+
* No _magic_ please
|
|
25
|
+
* No DSLs please
|
|
26
|
+
* Less code, less maintenance
|
|
27
|
+
* Good docs and test coverage
|
|
28
|
+
* Keep it up-to-date (or at least tell people this is for _grabs_)
|
|
29
|
+
|
|
30
|
+
The available features include:
|
|
31
|
+
|
|
32
|
+
* object serialization powered by (Fast JSON API)
|
|
33
|
+
* [error handling](https://jsonapi.org/format/#errors) (parameters,
|
|
34
|
+
validation, generic errors)
|
|
35
|
+
* fetching of the data (support for
|
|
36
|
+
[includes](https://jsonapi.org/format/#fetching-includes) and
|
|
37
|
+
[sparse fields](https://jsonapi.org/format/#fetching-sparse-fieldsets))
|
|
38
|
+
* [filtering](https://jsonapi.org/format/#fetching-filtering) and
|
|
39
|
+
[sorting](https://jsonapi.org/format/#fetching-sorting) of the data
|
|
40
|
+
(powered by Ransack)
|
|
41
|
+
* [pagination](https://jsonapi.org/format/#fetching-pagination) support
|
|
42
|
+
|
|
43
|
+
## But how?
|
|
44
|
+
|
|
45
|
+
Mainly by leveraging [Fast JSON API](https://github.com/Netflix/fast_jsonapi)
|
|
46
|
+
and [Ransack](https://github.com/activerecord-hackery/ransack).
|
|
47
|
+
|
|
48
|
+
Thanks to everyone who worked on these amazing projects!
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
Add this line to your application's Gemfile:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
gem 'jsonapi.rb'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
And then execute:
|
|
59
|
+
|
|
60
|
+
$ bundle
|
|
61
|
+
|
|
62
|
+
Or install it yourself as:
|
|
63
|
+
|
|
64
|
+
$ gem install jsonapi.rb
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
To enable the support for Rails, add this to an initializer:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# config/initializers/jsonapi.rb
|
|
72
|
+
require 'jsonapi'
|
|
73
|
+
|
|
74
|
+
JSONAPI::Rails.install!
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This will register the mime type and the `jsonapi` and `jsonapi_errors`
|
|
78
|
+
renderers.
|
|
79
|
+
|
|
80
|
+
### Object Serialization
|
|
81
|
+
|
|
82
|
+
The `jsonapi` renderer will try to guess and resolve the serializer class based
|
|
83
|
+
on the object class, and if it is a collection, based on the first item in the
|
|
84
|
+
collection.
|
|
85
|
+
|
|
86
|
+
The naming scheme follows the `ModuleName::ClassNameSerializer` for an instance
|
|
87
|
+
of the `ModuleName::ClassName`.
|
|
88
|
+
|
|
89
|
+
Please follow the
|
|
90
|
+
[Fast JSON API guide](https://github.com/Netflix/fast_jsonapi#serializer-definition)
|
|
91
|
+
on how to define a serializer.
|
|
92
|
+
|
|
93
|
+
#### Collection Meta
|
|
94
|
+
|
|
95
|
+
To provide meta information for a collection, provide the `jsonapi_meta`
|
|
96
|
+
controller method.
|
|
97
|
+
|
|
98
|
+
Here's an example:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
class MyController < ActionController::Base
|
|
102
|
+
def index
|
|
103
|
+
render jsonapi: Model.all
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def jsonapi_meta(resources)
|
|
109
|
+
{ total: resources.count } if resources.respond_to?(:count)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Error handling
|
|
115
|
+
|
|
116
|
+
`JSONAPI::Errors` provides a basic error handling. It will generate a valid
|
|
117
|
+
error response on exceptions from strong parameters, on generic errors or
|
|
118
|
+
when a record is not found.
|
|
119
|
+
|
|
120
|
+
To render the validation errors, just pass it to the error renderer.
|
|
121
|
+
|
|
122
|
+
Here's an example:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
class MyController < ActionController::Base
|
|
126
|
+
include JSONAPI::Errors
|
|
127
|
+
|
|
128
|
+
def update
|
|
129
|
+
raise_error! if params[:id] == 'tada'
|
|
130
|
+
|
|
131
|
+
record = Model.find(params[:id])
|
|
132
|
+
|
|
133
|
+
if record.update(params.require(:data).require(:attributes).permit!)
|
|
134
|
+
render jsonapi: record
|
|
135
|
+
else
|
|
136
|
+
render jsonapi_errors: record.errors, status: :unprocessable_entity
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### _Includes_ and sparse fields
|
|
143
|
+
|
|
144
|
+
`JSONAPI::Fetching` provides support on inclusion of related resources and
|
|
145
|
+
serialization of only specific fields.
|
|
146
|
+
|
|
147
|
+
Here's an example:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
class MyController < ActionController::Base
|
|
151
|
+
include JSONAPI::Fetching
|
|
152
|
+
|
|
153
|
+
def index
|
|
154
|
+
render jsonapi: Model.all
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# Overwrite/whitelist the includes
|
|
160
|
+
def jsonapi_include(resources)
|
|
161
|
+
super - [:unwanted_attribute]
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Filtering and sorting
|
|
167
|
+
|
|
168
|
+
`JSONAPI::Filtering` uses the power of
|
|
169
|
+
[Ransack](https://github.com/activerecord-hackery/ransack#search-matchers)
|
|
170
|
+
to filter and sort over a collection of records.
|
|
171
|
+
The support is pretty extended and covers also relationships and composite
|
|
172
|
+
matchers.
|
|
173
|
+
|
|
174
|
+
Here's an example:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class MyController < ActionController::Base
|
|
178
|
+
include JSONAPI::Filtering
|
|
179
|
+
|
|
180
|
+
def index
|
|
181
|
+
allowed = [:model_attr, :relationship_attr]
|
|
182
|
+
|
|
183
|
+
jsonapi_filter(Model.all, allowed) do |filtered|
|
|
184
|
+
render jsonapi: filtered.result
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
This allows you to run queries like:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
$ curl -X GET \
|
|
194
|
+
/api/resources?filter[model_attr_or_relationship_attr_cont_any]=value,name\
|
|
195
|
+
&sort=-model_attr,relationship_attr
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
### Pagination
|
|
200
|
+
|
|
201
|
+
`JSONAPI::Pagination` provides support for paginating model record sets as long
|
|
202
|
+
as enumerables.
|
|
203
|
+
|
|
204
|
+
Here's an example:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
class MyController < ActionController::Base
|
|
208
|
+
include JSONAPI::Pagination
|
|
209
|
+
|
|
210
|
+
def index
|
|
211
|
+
jsonapi_paginate(Model.all) do |paginated|
|
|
212
|
+
render jsonapi: paginated
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This will generate the relevant pagination _links_.
|
|
219
|
+
|
|
220
|
+
## Development
|
|
221
|
+
|
|
222
|
+
After checking out the repo, run `bundle` to install dependencies.
|
|
223
|
+
|
|
224
|
+
Then, run `rake spec` to run the tests.
|
|
225
|
+
|
|
226
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
227
|
+
|
|
228
|
+
To release a new version, update the version number in `version.rb`, and then
|
|
229
|
+
run `bundle exec rake release`, which will create a git tag for the version,
|
|
230
|
+
push git commits and tags, and push the `.gem` file to
|
|
231
|
+
[rubygems.org](https://rubygems.org).
|
|
232
|
+
|
|
233
|
+
## Contributing
|
|
234
|
+
|
|
235
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
236
|
+
https://github.com/stas/jsonapi.rb
|
|
237
|
+
|
|
238
|
+
This project is intended to be a safe, welcoming space for collaboration, and
|
|
239
|
+
contributors are expected to adhere to the
|
|
240
|
+
[Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
The gem is available as open source under the terms of the
|
|
245
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rspec/core/rake_task'
|
|
3
|
+
require 'rubocop/rake_task'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'yardstick'
|
|
6
|
+
|
|
7
|
+
desc('Documentation stats and measurements')
|
|
8
|
+
task('qa:docs') do
|
|
9
|
+
yaml = YAML.load_file(File.expand_path('../.yardstick.yml', __FILE__))
|
|
10
|
+
config = Yardstick::Config.coerce(yaml)
|
|
11
|
+
measure = Yardstick.measure(config)
|
|
12
|
+
measure.puts
|
|
13
|
+
coverage = Yardstick.round_percentage(measure.coverage * 100)
|
|
14
|
+
exit(1) if coverage < config.threshold
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc('Codestyle check and linter')
|
|
18
|
+
RuboCop::RakeTask.new('qa:code') do |task|
|
|
19
|
+
task.fail_on_error = true
|
|
20
|
+
task.patterns = [
|
|
21
|
+
'lib/**/*.rb',
|
|
22
|
+
'spec/**/*.rb'
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc('Run CI QA tasks')
|
|
27
|
+
task(qa: ['qa:docs', 'qa:code'])
|
|
28
|
+
|
|
29
|
+
RSpec::Core::RakeTask.new(spec: :qa)
|
|
30
|
+
task(default: :spec)
|
data/jsonapi.rb.gemspec
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
+
|
|
4
|
+
require 'jsonapi/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = 'jsonapi.rb'
|
|
8
|
+
spec.version = JSONAPI::VERSION
|
|
9
|
+
spec.authors = ['Stas Suscov']
|
|
10
|
+
spec.email = ['stas@nerd.ro']
|
|
11
|
+
|
|
12
|
+
spec.summary = 'So you say you need JSON:API support in your API...'
|
|
13
|
+
spec.description = (
|
|
14
|
+
'JSON:API serialization, error handling, filtering and pagination.'
|
|
15
|
+
)
|
|
16
|
+
spec.homepage = 'https://github.com/stas/jsonapi.rb'
|
|
17
|
+
spec.license = 'MIT'
|
|
18
|
+
|
|
19
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
|
|
21
|
+
end
|
|
22
|
+
spec.require_paths = ['lib']
|
|
23
|
+
|
|
24
|
+
spec.add_dependency 'fast_jsonapi', '~> 1.5'
|
|
25
|
+
spec.add_dependency 'ransack', '~> 2.1'
|
|
26
|
+
|
|
27
|
+
spec.add_development_dependency 'bundler'
|
|
28
|
+
spec.add_development_dependency 'rails'
|
|
29
|
+
spec.add_development_dependency 'sqlite3'
|
|
30
|
+
spec.add_development_dependency 'ffaker'
|
|
31
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
32
|
+
spec.add_development_dependency 'rspec-rails'
|
|
33
|
+
spec.add_development_dependency 'jsonapi-rspec'
|
|
34
|
+
spec.add_development_dependency 'yardstick'
|
|
35
|
+
spec.add_development_dependency 'rubocop-rails_config'
|
|
36
|
+
spec.add_development_dependency 'simplecov'
|
|
37
|
+
end
|
data/lib/jsonapi.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require 'jsonapi/errors'
|
|
2
|
+
require 'jsonapi/fetching'
|
|
3
|
+
require 'jsonapi/filtering'
|
|
4
|
+
require 'jsonapi/pagination'
|
|
5
|
+
require 'jsonapi/rails'
|
|
6
|
+
require 'jsonapi/version'
|
|
7
|
+
|
|
8
|
+
# JSON:API
|
|
9
|
+
module JSONAPI
|
|
10
|
+
# JSONAPI media type.
|
|
11
|
+
MEDIA_TYPE = 'application/vnd.api+json'.freeze
|
|
12
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'jsonapi/error_serializer'
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
# [ActiveModel::Errors] serializer
|
|
5
|
+
class ActiveModelErrorSerializer < ErrorSerializer
|
|
6
|
+
set_id :object_id
|
|
7
|
+
set_type :error
|
|
8
|
+
|
|
9
|
+
attribute :status do
|
|
10
|
+
'422'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attribute :title do
|
|
14
|
+
Net::HTTP::STATUS_CODES[422]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attribute :code do |object|
|
|
18
|
+
_, error_hash = object
|
|
19
|
+
error_hash[:error]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attribute :detail do |object, params|
|
|
23
|
+
error_key, error_hash = object
|
|
24
|
+
errors_object = params[:model].errors
|
|
25
|
+
message = errors_object.generate_message(error_key, error_hash[:error])
|
|
26
|
+
errors_object.full_message(error_key, message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attribute :source do |object, params|
|
|
30
|
+
error_key, _ = object
|
|
31
|
+
model_serializer = params[:model_serializer]
|
|
32
|
+
|
|
33
|
+
if model_serializer.attributes_to_serialize.keys.include?(error_key)
|
|
34
|
+
{ pointer: "/data/attributes/#{error_key}" }
|
|
35
|
+
elsif model_serializer.relationships_to_serialize.keys.include?(error_key)
|
|
36
|
+
{ pointer: "/data/relationships/#{error_key}" }
|
|
37
|
+
else
|
|
38
|
+
{ pointer: '' }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'fast_jsonapi'
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
# A simple error serializer
|
|
5
|
+
class ErrorSerializer
|
|
6
|
+
include FastJsonapi::ObjectSerializer
|
|
7
|
+
|
|
8
|
+
set_id :object_id
|
|
9
|
+
set_type :error
|
|
10
|
+
|
|
11
|
+
# Object/Hash attribute helpers.
|
|
12
|
+
[:status, :source, :title, :detail].each do |attr_name|
|
|
13
|
+
attribute attr_name do |object|
|
|
14
|
+
object.try(attr_name) || object.try(:fetch, attr_name, nil)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Remap the root key to `errors`
|
|
19
|
+
#
|
|
20
|
+
# @return [Hash]
|
|
21
|
+
def serializable_hash
|
|
22
|
+
{ errors: (super[:data] || []).map { |error| error[:attributes] } }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'net/http/status'
|
|
2
|
+
require 'active_support/concern'
|
|
3
|
+
|
|
4
|
+
# Helpers to handle some error responses
|
|
5
|
+
#
|
|
6
|
+
# Most of the exceptions are handled in Rails by [ActionDispatch] middleware
|
|
7
|
+
# See: https://api.rubyonrails.org/classes/ActionDispatch/ExceptionWrapper.html
|
|
8
|
+
module JSONAPI::Errors
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
rescue_from StandardError do |exception|
|
|
13
|
+
error = { status: '500', title: Net::HTTP::STATUS_CODES[500] }
|
|
14
|
+
render jsonapi_errors: [error], status: :internal_server_error
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
[
|
|
18
|
+
ActiveRecord::RecordNotFound
|
|
19
|
+
].each do |exception_class|
|
|
20
|
+
rescue_from exception_class do |exception|
|
|
21
|
+
error = { status: '404', title: Net::HTTP::STATUS_CODES[404] }
|
|
22
|
+
render jsonapi_errors: [error], status: :not_found
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
[
|
|
27
|
+
ActionController::ParameterMissing
|
|
28
|
+
].each do |exception_class|
|
|
29
|
+
rescue_from exception_class do |exception|
|
|
30
|
+
source = { pointer: '' }
|
|
31
|
+
|
|
32
|
+
if !%w{data attributes relationships}.include?(exception.param.to_s)
|
|
33
|
+
source[:pointer] = "/data/attributes/#{exception.param}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
error = {
|
|
37
|
+
status: '422',
|
|
38
|
+
title: Net::HTTP::STATUS_CODES[422],
|
|
39
|
+
source: source
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render jsonapi_errors: [error], status: :unprocessable_entity
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Inclusion and sparse fields support
|
|
2
|
+
module JSONAPI::Fetching
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
# Extracts and formats sparse fieldsets
|
|
6
|
+
#
|
|
7
|
+
# Ex.: `GET /resource?fields[relationship]=id,created_at`
|
|
8
|
+
#
|
|
9
|
+
# @return [Hash]
|
|
10
|
+
def jsonapi_fields
|
|
11
|
+
ActiveSupport::HashWithIndifferentAccess.new.tap do |h|
|
|
12
|
+
(params[:fields] || []).each do |k, v|
|
|
13
|
+
h[k] = v.split(',').map(&:strip).compact
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Extracts and whitelists allowed includes
|
|
19
|
+
#
|
|
20
|
+
# Ex.: `GET /resource?include=relationship,relationship.subrelationship`
|
|
21
|
+
#
|
|
22
|
+
# @return [Array]
|
|
23
|
+
def jsonapi_include
|
|
24
|
+
params['include'].to_s.split(',').map(&:strip).compact
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'ransack/predicate'
|
|
2
|
+
|
|
3
|
+
# Filtering and sorting support
|
|
4
|
+
module JSONAPI::Filtering
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
# Applies filtering and sorting to a set of resources if requested
|
|
8
|
+
#
|
|
9
|
+
# The fields follow [Ransack] specifications.
|
|
10
|
+
# See: https://github.com/activerecord-hackery/ransack#search-matchers
|
|
11
|
+
#
|
|
12
|
+
# Ex.: `GET /resource?filter[region_matches_any]=Lisb%&sort=-created_at,id`
|
|
13
|
+
#
|
|
14
|
+
# @param allowed_fields [Array] a list of allowed fields to be filtered
|
|
15
|
+
# @return [ActiveRecord::Base] a collection of resources
|
|
16
|
+
def jsonapi_filter(resources, allowed_fields)
|
|
17
|
+
extracted_params = jsonapi_filter_params(allowed_fields)
|
|
18
|
+
extracted_params[:sorts] = jsonapi_sort_params(allowed_fields)
|
|
19
|
+
resources = resources.ransack(extracted_params)
|
|
20
|
+
block_given? ? yield(resources) : resources
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Extracts and whitelists allowed fields to be filtered
|
|
24
|
+
#
|
|
25
|
+
# The fields follow [Ransack] specifications.
|
|
26
|
+
# See: https://github.com/activerecord-hackery/ransack#search-matchers
|
|
27
|
+
#
|
|
28
|
+
# @param allowed_fields [Array] a list of allowed fields to be filtered
|
|
29
|
+
# @return [Hash] to be passed to [ActiveRecord::Base#order]
|
|
30
|
+
def jsonapi_filter_params(allowed_fields)
|
|
31
|
+
filtered = {}
|
|
32
|
+
requested = params[:filter] || {}
|
|
33
|
+
allowed_fields = allowed_fields.map(&:to_s)
|
|
34
|
+
|
|
35
|
+
requested.each_pair do |requested_field, to_filter|
|
|
36
|
+
field_name = requested_field.dup
|
|
37
|
+
predicate = Ransack::Predicate.detect_and_strip_from_string!(field_name)
|
|
38
|
+
predicate = Ransack::Predicate.named(predicate)
|
|
39
|
+
|
|
40
|
+
field_names = field_name.split(/_and_|_or_/)
|
|
41
|
+
|
|
42
|
+
if to_filter.is_a?(String) && to_filter.include?(',')
|
|
43
|
+
to_filter = to_filter.split(',')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if predicate && (field_names - allowed_fields).empty?
|
|
47
|
+
filtered[requested_field] = to_filter
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
filtered
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Extracts and whitelists allowed fields to be sorted
|
|
55
|
+
#
|
|
56
|
+
# @param allowed_fields [Array] a list of allowed fields to be sorted
|
|
57
|
+
# @return [Hash] to be passed to [ActiveRecord::Base#order]
|
|
58
|
+
def jsonapi_sort_params(allowed_fields)
|
|
59
|
+
requested = params[:sort].to_s.split(',')
|
|
60
|
+
requested.map! do |requested_field|
|
|
61
|
+
desc = requested_field.to_s.start_with?('-')
|
|
62
|
+
[
|
|
63
|
+
desc ? requested_field[1..-1] : requested_field,
|
|
64
|
+
desc ? 'desc' : 'asc'
|
|
65
|
+
]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Convert to strings instead of hashes to allow joined table columns.
|
|
69
|
+
requested.to_h.slice(*allowed_fields.map(&:to_s)).map do |field, dir|
|
|
70
|
+
[field, dir].join(' ')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Pagination support
|
|
2
|
+
module JSONAPI::Pagination
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
# Default number of items per page.
|
|
6
|
+
JSONAPI_PAGE_SIZE = 30
|
|
7
|
+
|
|
8
|
+
# Applies pagination to a set of resources
|
|
9
|
+
#
|
|
10
|
+
# Ex.: `GET /resource?page[number]=2&page[size]=10`
|
|
11
|
+
#
|
|
12
|
+
# @return [ActiveRecord::Base] a collection of resources
|
|
13
|
+
def jsonapi_paginate(resources)
|
|
14
|
+
offset, limit, _ = jsonapi_pagination_params
|
|
15
|
+
|
|
16
|
+
if resources.respond_to?(:offset)
|
|
17
|
+
resources = resources.offset(offset).limit(limit)
|
|
18
|
+
else
|
|
19
|
+
resources = resources[(offset)..(offset + limit)]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
block_given? ? yield(resources) : resources
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Generates the pagination links
|
|
26
|
+
#
|
|
27
|
+
# @return [Array]
|
|
28
|
+
def jsonapi_pagination(resources)
|
|
29
|
+
links = {
|
|
30
|
+
self: request.base_url + request.original_fullpath
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return links unless resources.respond_to?(:many?)
|
|
34
|
+
|
|
35
|
+
_, limit, page = jsonapi_pagination_params
|
|
36
|
+
|
|
37
|
+
original_params = params.except(
|
|
38
|
+
*request.path_parameters.keys.map(&:to_s)).to_unsafe_h
|
|
39
|
+
original_params[:page] ||= {}
|
|
40
|
+
original_url = request.base_url + request.path + '?'
|
|
41
|
+
|
|
42
|
+
if resources.respond_to?(:unscope)
|
|
43
|
+
total = resources.unscope(:limit, :offset).count()
|
|
44
|
+
else
|
|
45
|
+
total = resources.size
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
last_page = [1, (total.to_f / limit).ceil].max
|
|
49
|
+
|
|
50
|
+
if page > 1
|
|
51
|
+
original_params[:page][:number] = 1
|
|
52
|
+
links[:first] = original_url + CGI.unescape(original_params.to_query)
|
|
53
|
+
original_params[:page][:number] = page - 1
|
|
54
|
+
links[:prev] = original_url + CGI.unescape(original_params.to_query)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if page < last_page
|
|
58
|
+
original_params[:page][:number] = page + 1
|
|
59
|
+
links[:next] = original_url + CGI.unescape(original_params.to_query)
|
|
60
|
+
original_params[:page][:number] = last_page
|
|
61
|
+
links[:last] = original_url + CGI.unescape(original_params.to_query)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
links
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Extracts the pagination params
|
|
68
|
+
#
|
|
69
|
+
# @return [Array] with the offset, limit and the current page number
|
|
70
|
+
def jsonapi_pagination_params
|
|
71
|
+
def_per_page = self.class.const_get(:JSONAPI_PAGE_SIZE).to_i
|
|
72
|
+
|
|
73
|
+
pagination = params[:page].try(:slice, :number, :size) || {}
|
|
74
|
+
per_page = (pagination[:size] || def_per_page).to_f.to_i
|
|
75
|
+
per_page = def_per_page if per_page > def_per_page
|
|
76
|
+
num = [1, pagination[:number].to_f.to_i].max
|
|
77
|
+
|
|
78
|
+
[(num - 1) * per_page, per_page, num]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require 'jsonapi/error_serializer'
|
|
2
|
+
require 'jsonapi/active_model_error_serializer'
|
|
3
|
+
|
|
4
|
+
# Rails integration
|
|
5
|
+
module JSONAPI::Rails
|
|
6
|
+
# Updates the mime types and registers the renderers
|
|
7
|
+
#
|
|
8
|
+
# @return [NilClass]
|
|
9
|
+
def self.install!
|
|
10
|
+
return unless defined?(::Rails)
|
|
11
|
+
|
|
12
|
+
Mime::Type.register JSONAPI::MEDIA_TYPE, :jsonapi
|
|
13
|
+
|
|
14
|
+
# Map the JSON parser to the JSONAPI mime type requests.
|
|
15
|
+
if Rails::VERSION::MAJOR >= 5
|
|
16
|
+
parser = ActionDispatch::Request.parameter_parsers[:json]
|
|
17
|
+
ActionDispatch::Request.parameter_parsers[:jsonapi] = parser
|
|
18
|
+
else
|
|
19
|
+
parser = ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:json]]
|
|
20
|
+
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = parser
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
self.add_renderer!
|
|
24
|
+
self.add_errors_renderer!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Adds the error renderer
|
|
28
|
+
#
|
|
29
|
+
# @return [NilClass]
|
|
30
|
+
def self.add_errors_renderer!
|
|
31
|
+
ActionController::Renderers.add(:jsonapi_errors) do |resource, options|
|
|
32
|
+
self.content_type ||= Mime[:jsonapi]
|
|
33
|
+
|
|
34
|
+
resource = [resource] unless JSONAPI::Rails.is_collection?(resource)
|
|
35
|
+
|
|
36
|
+
return JSONAPI::ErrorSerializer.new(resource, options)
|
|
37
|
+
.serialized_json unless resource.is_a?(ActiveModel::Errors)
|
|
38
|
+
|
|
39
|
+
errors = []
|
|
40
|
+
model = resource.marshal_dump.first
|
|
41
|
+
model_serializer = JSONAPI::Rails.serializer_class(model)
|
|
42
|
+
|
|
43
|
+
resource.details.each do |error_key, error_hashes|
|
|
44
|
+
error_hashes.each do |error_hash|
|
|
45
|
+
errors << [ error_key, error_hash ]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
JSONAPI::ActiveModelErrorSerializer.new(
|
|
50
|
+
errors, params: { model: model, model_serializer: model_serializer }
|
|
51
|
+
).serialized_json
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Adds the default renderer
|
|
56
|
+
#
|
|
57
|
+
# @return [NilClass]
|
|
58
|
+
def self.add_renderer!
|
|
59
|
+
ActionController::Renderers.add(:jsonapi) do |resource, options|
|
|
60
|
+
self.content_type ||= Mime[:jsonapi]
|
|
61
|
+
|
|
62
|
+
options[:meta] ||= (
|
|
63
|
+
jsonapi_meta(resource) if respond_to?(:jsonapi_meta, true))
|
|
64
|
+
options[:links] ||= (
|
|
65
|
+
jsonapi_pagination(resource) if respond_to?(:jsonapi_pagination, true))
|
|
66
|
+
|
|
67
|
+
# If it's an empty collection, return it directly.
|
|
68
|
+
if JSONAPI::Rails.is_collection?(resource) && !resource.any?
|
|
69
|
+
return options.slice(:meta, :links).merge(data: []).to_json
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
options[:fields] ||= jsonapi_fields if respond_to?(:jsonapi_fields, true)
|
|
73
|
+
options[:include] ||= (
|
|
74
|
+
jsonapi_include if respond_to?(:jsonapi_include, true))
|
|
75
|
+
|
|
76
|
+
serializer_class = JSONAPI::Rails.serializer_class(resource)
|
|
77
|
+
serializer_class.new(resource, options).serialized_json
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Checks if an object is a collection
|
|
82
|
+
#
|
|
83
|
+
# @param object [Object] to check
|
|
84
|
+
# @return [TrueClass] upon success
|
|
85
|
+
def self.is_collection?(object)
|
|
86
|
+
object.is_a?(Enumerable) && !object.respond_to?(:each_pair)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Resolves resource serializer class
|
|
90
|
+
#
|
|
91
|
+
# @return [Class]
|
|
92
|
+
def self.serializer_class(resource)
|
|
93
|
+
klass = resource.class
|
|
94
|
+
klass = resource.first.class if self.is_collection?(resource)
|
|
95
|
+
|
|
96
|
+
"#{klass.name}Serializer".constantize
|
|
97
|
+
end
|
|
98
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jsonapi.rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Stas Suscov
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2019-01-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: fast_jsonapi
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.5'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.5'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: ransack
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: bundler
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rails
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: sqlite3
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: ffaker
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rspec
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rspec-rails
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: jsonapi-rspec
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: yardstick
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0'
|
|
153
|
+
- !ruby/object:Gem::Dependency
|
|
154
|
+
name: rubocop-rails_config
|
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - ">="
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '0'
|
|
160
|
+
type: :development
|
|
161
|
+
prerelease: false
|
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - ">="
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '0'
|
|
167
|
+
- !ruby/object:Gem::Dependency
|
|
168
|
+
name: simplecov
|
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: '0'
|
|
174
|
+
type: :development
|
|
175
|
+
prerelease: false
|
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0'
|
|
181
|
+
description: JSON:API serialization, error handling, filtering and pagination.
|
|
182
|
+
email:
|
|
183
|
+
- stas@nerd.ro
|
|
184
|
+
executables: []
|
|
185
|
+
extensions: []
|
|
186
|
+
extra_rdoc_files: []
|
|
187
|
+
files:
|
|
188
|
+
- ".gitignore"
|
|
189
|
+
- ".rspec"
|
|
190
|
+
- ".rubocop.yml"
|
|
191
|
+
- ".travis.yml"
|
|
192
|
+
- ".yardstick.yml"
|
|
193
|
+
- Gemfile
|
|
194
|
+
- Gemfile.lock
|
|
195
|
+
- LICENSE.txt
|
|
196
|
+
- README.md
|
|
197
|
+
- Rakefile
|
|
198
|
+
- jsonapi.rb.gemspec
|
|
199
|
+
- lib/jsonapi.rb
|
|
200
|
+
- lib/jsonapi/active_model_error_serializer.rb
|
|
201
|
+
- lib/jsonapi/error_serializer.rb
|
|
202
|
+
- lib/jsonapi/errors.rb
|
|
203
|
+
- lib/jsonapi/fetching.rb
|
|
204
|
+
- lib/jsonapi/filtering.rb
|
|
205
|
+
- lib/jsonapi/pagination.rb
|
|
206
|
+
- lib/jsonapi/rails.rb
|
|
207
|
+
- lib/jsonapi/version.rb
|
|
208
|
+
homepage: https://github.com/stas/jsonapi.rb
|
|
209
|
+
licenses:
|
|
210
|
+
- MIT
|
|
211
|
+
metadata: {}
|
|
212
|
+
post_install_message:
|
|
213
|
+
rdoc_options: []
|
|
214
|
+
require_paths:
|
|
215
|
+
- lib
|
|
216
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
217
|
+
requirements:
|
|
218
|
+
- - ">="
|
|
219
|
+
- !ruby/object:Gem::Version
|
|
220
|
+
version: '0'
|
|
221
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
222
|
+
requirements:
|
|
223
|
+
- - ">="
|
|
224
|
+
- !ruby/object:Gem::Version
|
|
225
|
+
version: '0'
|
|
226
|
+
requirements: []
|
|
227
|
+
rubygems_version: 3.0.1
|
|
228
|
+
signing_key:
|
|
229
|
+
specification_version: 4
|
|
230
|
+
summary: So you say you need JSON:API support in your API...
|
|
231
|
+
test_files: []
|