jsonapi.rb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.1
7
+ before_install: gem install bundler
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
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jsonapi.gemspec
4
+ gemspec
5
+
6
+ gem 'jsonapi-rspec', github: 'stas/jsonapi-rspec'
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)
@@ -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
@@ -0,0 +1,3 @@
1
+ module JSONAPI
2
+ VERSION = '1.0.0'
3
+ 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: []