adequate_json 0.1.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/.github/workflows/ruby.yml +35 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +7 -0
- data/.ruby-version +1 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +300 -0
- data/Rakefile +8 -0
- data/adequate_json.gemspec +47 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/adequate_json/base.rb +51 -0
- data/lib/adequate_json/collection.rb +51 -0
- data/lib/adequate_json/configuration.rb +32 -0
- data/lib/adequate_json/error.rb +60 -0
- data/lib/adequate_json/hash.rb +39 -0
- data/lib/adequate_json/jsonizer.rb +9 -0
- data/lib/adequate_json/railtie.rb +33 -0
- data/lib/adequate_json/resolver.rb +62 -0
- data/lib/adequate_json/serializer.rb +15 -0
- data/lib/adequate_json/version.rb +5 -0
- data/lib/adequate_json.rb +26 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5b468fd9f03d2c352e7773d349b63961b989d8c6a6f019b97c140f950568bc3f
|
4
|
+
data.tar.gz: 15f7842cd39c754e8a4df4f5509e04152de63aa20a2cf2b34dbd6d773cc54099
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 153f935993fbc16160897afec263173ef90ec68b6e8ae66c32c2f83db74aa4fb6489d917e04ffd294427b8eb7b92c63c9de3dff2dcc00ceadc0bf30dfbcff804
|
7
|
+
data.tar.gz: b789c736fe4610b0e27e0b9bf4dd60e9d333cb456805363e9f45ffe9d1a86247530da0c9f2febcfb61e058f4359f291ffebee2624d3c5f58aa359a509846f272
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
2
|
+
# They are provided by a third-party and are governed by
|
3
|
+
# separate terms of service, privacy policy, and support
|
4
|
+
# documentation.
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
7
|
+
|
8
|
+
name: Ruby
|
9
|
+
|
10
|
+
on:
|
11
|
+
push:
|
12
|
+
branches: [ "main" ]
|
13
|
+
pull_request:
|
14
|
+
branches: [ "main" ]
|
15
|
+
|
16
|
+
permissions:
|
17
|
+
contents: read
|
18
|
+
|
19
|
+
jobs:
|
20
|
+
test:
|
21
|
+
|
22
|
+
runs-on: ubuntu-latest
|
23
|
+
strategy:
|
24
|
+
matrix:
|
25
|
+
ruby-version: ['3.3.1']
|
26
|
+
|
27
|
+
steps:
|
28
|
+
- uses: actions/checkout@v4
|
29
|
+
- name: Set up Ruby
|
30
|
+
uses: ruby/setup-ruby@v1 # v1.146.0
|
31
|
+
with:
|
32
|
+
ruby-version: ${{ matrix.ruby-version }}
|
33
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
34
|
+
- name: Run tests
|
35
|
+
run: bundle exec rspec
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.3.1
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Issam Tribak
|
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,300 @@
|
|
1
|
+
# AdequateJSON
|
2
|
+
|
3
|
+
AdequateJSON is a serialization gem for Ruby on Rails APIs. It is easy to use, versatile,
|
4
|
+
[fast](#performances), and promotes API best practices.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'adequate_json'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install adequate_json
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
If you'd like to tinker with AdequateJSON on your own, see the
|
25
|
+
[sample Rails API](https://github.com/EverestHC-mySofie/adequate_json_sample).
|
26
|
+
|
27
|
+
### At the controller level
|
28
|
+
|
29
|
+
If you're using — and you probably are — a controller inheriting `ActionController::API`,
|
30
|
+
AdequateJSON adds a `render_json` method to controllers, to which you can provide a model
|
31
|
+
instance, or a collection of models:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class CategoriesController < ActionController::API
|
35
|
+
def index
|
36
|
+
render_json Category.order(:name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def show
|
40
|
+
render_json Category.find(params[:id])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
AdequateJSON automatically infers the type of serializer it should use
|
46
|
+
based on the model type, see the [next section](#your-first-serializer).
|
47
|
+
|
48
|
+
### Your first serializer
|
49
|
+
|
50
|
+
When rendering a model, AdequateJSON searches for the corresponding serializing
|
51
|
+
class in the `Serializers` module (or the module you specify using the
|
52
|
+
[configuration](#configuration)).
|
53
|
+
|
54
|
+
Each serializer is a class extending `AdequateJSON::Base`, and defining one or several
|
55
|
+
variants to build (defaulting to `:default` for single objects and `:no_wrapper`
|
56
|
+
for collections):
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# app/serializers/category.rb
|
60
|
+
|
61
|
+
class Serializers::Category < AdequateJSON::Base
|
62
|
+
builder do |json, category| # Same as builder(:default)
|
63
|
+
json.category do
|
64
|
+
json.(category, :id, :name, :created_at, :updated_at)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
builder(:no_wrapper) do |json, category|
|
69
|
+
json.(category, :id, :name)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
AdequateJSON is based on Jbuilder. For all JSON manipulation methods available,
|
75
|
+
have a look at [Jbuilder's DSL documentation](https://github.com/rails/jbuilder).
|
76
|
+
|
77
|
+
### Choosing a variant
|
78
|
+
|
79
|
+
Builder variants default to `:default` for single objects, and `:no_wrapper` for
|
80
|
+
collection items. To use another variant, specify it as a keyword argument:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
class ProductsController < ActionController::API
|
84
|
+
def index
|
85
|
+
render_json Product.order(:name), variant: :header
|
86
|
+
end
|
87
|
+
|
88
|
+
def show
|
89
|
+
render_json Product.find(params[:id]), variant: :full
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
### Reuse and composition
|
95
|
+
|
96
|
+
Each variant can be reused inside other variant using the `serialize` helper
|
97
|
+
method, that runs on the same JSON builder. This allows for reuse of builders
|
98
|
+
as partials, or make a variant "inherit" another one:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class Serializers::Product < AdequateJSON::Base
|
102
|
+
builder do |json, product| # Same as builder(:default)
|
103
|
+
json.product do
|
104
|
+
serialize product, variant: :no_wrapper
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
builder(:full) do |json, product|
|
109
|
+
json.product do
|
110
|
+
serialize product, variant: :no_wrapper
|
111
|
+
serialize product.colors
|
112
|
+
serialize product.sizes
|
113
|
+
serialize product.category
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
builder(:no_wrapper) do |json, product|
|
118
|
+
serialize product, variant: :header
|
119
|
+
json.(product, :description, :created_at, :updated_at)
|
120
|
+
end
|
121
|
+
|
122
|
+
builder(:header) do |json, product|
|
123
|
+
json.(product, :id, :name, :price)
|
124
|
+
serialize product.category
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
### Supported source objects
|
130
|
+
|
131
|
+
AdequateJSON uses built-in serializers for hashes and collections and,
|
132
|
+
when serializing objects, will use the `#model_name` property to retrieve
|
133
|
+
the name of the serializer to search for.
|
134
|
+
|
135
|
+
If you need to change the type of serializer for a given model, you may define
|
136
|
+
the `serializer` method on the model:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class Book < ApplicationRecord
|
140
|
+
# ...
|
141
|
+
|
142
|
+
def serializer = :product
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
In last resort, you may also specify explicitely the serializer class to use:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
render_json Serializers::Product.new(Book.find(params[:id])).to_builder
|
150
|
+
```
|
151
|
+
|
152
|
+
### Serializing multiple objects at once
|
153
|
+
|
154
|
+
To serialize more complex structures, simply use a hash:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
class ProductsController < ActionController::API
|
158
|
+
def show
|
159
|
+
render_json { product: Product.find(params[:id]), categories: Category.order(:name) }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
### Pagination
|
165
|
+
|
166
|
+
As soon as you've added [Kaminari](https://github.com/kaminari/kaminari)
|
167
|
+
to your Gemfile and paginate a collection, AdequateJSON automatically appends
|
168
|
+
the `pagination` property to the JSON output:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
class ProductsController < ActionController::API
|
172
|
+
def index
|
173
|
+
render_json Product.order(:name).page(params[:page]).per(10)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
```
|
179
|
+
{
|
180
|
+
"collection": [
|
181
|
+
{
|
182
|
+
"id": "a9342787-0d24-43cf-8791-3a512f9e9bd4",
|
183
|
+
...
|
184
|
+
},
|
185
|
+
...
|
186
|
+
],
|
187
|
+
"pagination": {
|
188
|
+
"current_page": 1,
|
189
|
+
"total_count": 289,
|
190
|
+
"next_page": 2,
|
191
|
+
"previous_page": null,
|
192
|
+
"total_pages": 29,
|
193
|
+
}
|
194
|
+
}
|
195
|
+
```
|
196
|
+
|
197
|
+
If you'd like AdequateJSON to support other pagination gems, feel
|
198
|
+
free to craft a pull-request or to open an
|
199
|
+
[issue](https://github.com/EverestHC-mySofie/adequate_json/issues),
|
200
|
+
we'd be glad to help.
|
201
|
+
|
202
|
+
### Rendering errors
|
203
|
+
|
204
|
+
Specify an error code, and AdequateJSON will try to localize it
|
205
|
+
with a message found in one of your `locales/*.yml` files. The i18n
|
206
|
+
scope defaults to `api.errors`, you may change it in the
|
207
|
+
[configuration](#configuration). It also automatically renders errors
|
208
|
+
based on ActiveModel messages.
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
class ProductsController < ApplicationController
|
212
|
+
# ...
|
213
|
+
|
214
|
+
def create
|
215
|
+
product = Product.new(product_params)
|
216
|
+
if product.save
|
217
|
+
render_json product
|
218
|
+
else
|
219
|
+
render_error :invalid_model, product
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
Given a locale key `api.errors.invalid_model`, the `render_error`
|
226
|
+
call would produce:
|
227
|
+
|
228
|
+
```json
|
229
|
+
{
|
230
|
+
"error": {
|
231
|
+
"code": "invalid_model",
|
232
|
+
"message": "The object couldn't be saved",
|
233
|
+
"details": {
|
234
|
+
"category": [
|
235
|
+
"must exist"
|
236
|
+
],
|
237
|
+
"name": [
|
238
|
+
"can't be blank"
|
239
|
+
],
|
240
|
+
"description": [
|
241
|
+
"can't be blank"
|
242
|
+
],
|
243
|
+
"price": [
|
244
|
+
"can't be blank",
|
245
|
+
"is not a number"
|
246
|
+
]
|
247
|
+
}
|
248
|
+
}
|
249
|
+
}
|
250
|
+
```
|
251
|
+
|
252
|
+
If you need to return errors for nested models and attributes,
|
253
|
+
you may use the `includes` keyword argument:
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
render_error :invalid_model, product, includes: { category: :shop }
|
257
|
+
```
|
258
|
+
|
259
|
+
### Configuration
|
260
|
+
|
261
|
+
All configuration options are available through a block yielded by
|
262
|
+
the `AdequateJSON.configure` method.
|
263
|
+
|
264
|
+
The configuration code should take place in the `application.rb` file
|
265
|
+
or one of the Rails environment configurations (`production.rb`,
|
266
|
+
`development.rb`, etc.), not in an initializer:
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
module AdequateJsonSample
|
270
|
+
class Application < Rails::Application
|
271
|
+
|
272
|
+
AdequateJson.configure do |c|
|
273
|
+
c.serializers_module :json # defaults to :serializers
|
274
|
+
c.use_model_name_for_collection_key true # defaults to `false`
|
275
|
+
c.collection_key :list # defaults to `collection`
|
276
|
+
c.i18n_errors_scope %i[controllers error] # defaults to `%i[api errors]`
|
277
|
+
end
|
278
|
+
end
|
279
|
+
```
|
280
|
+
|
281
|
+
## Performances
|
282
|
+
|
283
|
+
We've written a small benchmark to see how AdequateJSON performs compared
|
284
|
+
to [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers).
|
285
|
+
The benchmark consists in serializing 10 times a collection of 10 000 objects
|
286
|
+
(the lower the better) and displaying the min, max and average processing times:
|
287
|
+
|
288
|
+
```
|
289
|
+
$ bundle e ruby benchmark.rb
|
290
|
+
AdequateJSON - min: 75.68, max: 95.1 - average: 81.5 ms
|
291
|
+
ActiveModelSerializers - min: 471.04, max: 582.73 - average: 491.63 ms
|
292
|
+
```
|
293
|
+
|
294
|
+
## Contributing
|
295
|
+
|
296
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/EverestHC-mySofie/adequate_json.
|
297
|
+
|
298
|
+
## License
|
299
|
+
|
300
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'adequate_json/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'adequate_json'
|
9
|
+
spec.version = AdequateJson::VERSION
|
10
|
+
spec.authors = ['Jef Mathiot', 'Issam Tribak', 'Wilfried Tacquard']
|
11
|
+
spec.email = ['jeff.mathiot@gmail.com', 'issam.tribak@mysofie.fr', 'wilfried.tacquard@mysofie.fr']
|
12
|
+
|
13
|
+
spec.summary = 'Yet another JSON serialization library'
|
14
|
+
spec.description = 'Adequate Json is a gem that simplifies the process of serializing JSON for API responses.'
|
15
|
+
spec.homepage = 'https://github.com/EverestHC-mySofie/adequate_json'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
19
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
20
|
+
if spec.respond_to?(:metadata)
|
21
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
22
|
+
spec.metadata['source_code_uri'] = 'https://github.com/EverestHC-mySofie/adequate_json'
|
23
|
+
spec.metadata['changelog_uri'] = 'https://github.com/EverestHC-mySofie/adequate_json'
|
24
|
+
else
|
25
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
26
|
+
'public gem pushes.'
|
27
|
+
end
|
28
|
+
|
29
|
+
# Specify which files should be added to the gem when it is released.
|
30
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
31
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
32
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
33
|
+
end
|
34
|
+
spec.bindir = 'exe'
|
35
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
36
|
+
spec.require_paths = ['lib']
|
37
|
+
|
38
|
+
spec.add_dependency 'jbuilder'
|
39
|
+
|
40
|
+
spec.add_development_dependency 'attr_extras'
|
41
|
+
spec.add_development_dependency 'bundler', '~> 2.5'
|
42
|
+
spec.add_development_dependency 'rake', '>= 12.3.3'
|
43
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
44
|
+
spec.add_development_dependency 'rubocop'
|
45
|
+
|
46
|
+
spec.required_ruby_version = '~> 3.0'
|
47
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'adequate_json'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdequateJson
|
4
|
+
class Base
|
5
|
+
include Resolver
|
6
|
+
include Jsonizer
|
7
|
+
|
8
|
+
def initialize(model, json = nil, variant: nil)
|
9
|
+
@model = model
|
10
|
+
@variant = variant
|
11
|
+
@json = json
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_builder
|
15
|
+
variant = @variant || :default
|
16
|
+
builder = self.class.builders[variant]
|
17
|
+
raise "Unknown serializer variant #{variant} for #{self.class.name}" if builder.nil?
|
18
|
+
|
19
|
+
yield_builder builder
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def yield_builder(builder)
|
25
|
+
if @json.nil?
|
26
|
+
Jbuilder.new do |json|
|
27
|
+
@json = json
|
28
|
+
instance_exec json, @model, @variant, &builder
|
29
|
+
end
|
30
|
+
else
|
31
|
+
instance_exec @json, @model, @variant, &builder
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def serialize(model, **options)
|
36
|
+
return if model.nil?
|
37
|
+
|
38
|
+
choose_serializer(model, **options).to_builder
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
def builder(variant = nil, &block)
|
43
|
+
builders[variant || :default] = block
|
44
|
+
end
|
45
|
+
|
46
|
+
def builders
|
47
|
+
@builders ||= {}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'adequate_json/base'
|
4
|
+
module AdequateJson
|
5
|
+
class Collection < AdequateJson::Base
|
6
|
+
def initialize(collection, json = nil, variant: nil)
|
7
|
+
@first_level = true if json.nil?
|
8
|
+
super
|
9
|
+
@variant ||= :no_wrapper
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_builder
|
13
|
+
with_jbuilder do |json|
|
14
|
+
json.set!(collection_key) do
|
15
|
+
json.array! @model do |item|
|
16
|
+
serialize item, variant: @variant
|
17
|
+
end
|
18
|
+
end
|
19
|
+
attach_pagination(json)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def collection_key
|
26
|
+
return @model.model_name.plural if !@first_level || AdequateJson.configuration.use_model_name_for_collection_key
|
27
|
+
|
28
|
+
AdequateJson.configuration.collection_key
|
29
|
+
end
|
30
|
+
|
31
|
+
def attach_pagination(json)
|
32
|
+
return unless @first_level && @model.respond_to?(:current_page)
|
33
|
+
|
34
|
+
json.pagination do
|
35
|
+
json.current_page @model.current_page
|
36
|
+
json.total_count @model.total_count
|
37
|
+
json.next_page @model.next_page
|
38
|
+
json.previous_page @model.prev_page
|
39
|
+
json.total_pages @model.total_pages
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_jbuilder
|
44
|
+
yield @json if @json
|
45
|
+
Jbuilder.new do |json|
|
46
|
+
@json = json
|
47
|
+
yield json
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdequateJson
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :use_model_name_for_collection_key, :collection_key, :serializers_module, :i18n_errors_scope
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@use_model_name_for_collection_key = false
|
9
|
+
@collection_key = :collection
|
10
|
+
@serializers_module = :serializers
|
11
|
+
@i18n_errors_scope = %i[api errors]
|
12
|
+
end
|
13
|
+
|
14
|
+
def serializers_module_const
|
15
|
+
AdequateJson.configuration.serializers_module.to_s.camelcase.constantize
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ConfigurationBuilder
|
20
|
+
def initialize(configuration)
|
21
|
+
@configuration = configuration
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_missing(name, *args, **kwargs)
|
25
|
+
@configuration.send("#{name}=", *args, **kwargs)
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to_missing?(name, _ = false)
|
29
|
+
false unless @configuration.respond_to?(name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdequateJson
|
4
|
+
class Error < Base
|
5
|
+
builder do |json, model, error, message, includes|
|
6
|
+
json.error do
|
7
|
+
json.code error
|
8
|
+
json.message message
|
9
|
+
if model
|
10
|
+
json.details do
|
11
|
+
includes_to_errors(model, includes, model.errors.messages.dup).each do |model_name, errors|
|
12
|
+
json.set! model_name, errors
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(model, error = nil, message = nil, includes = nil)
|
20
|
+
super(model)
|
21
|
+
@error = error
|
22
|
+
@message = message
|
23
|
+
@includes = includes
|
24
|
+
end
|
25
|
+
|
26
|
+
def includes_to_errors(model, includes, attributes = {})
|
27
|
+
unless model.nil?
|
28
|
+
each_inclusion(includes) do |child, value|
|
29
|
+
submodel = model.send(child)
|
30
|
+
subattributes = attributes[submodel.model_name.to_s.underscore] = submodel.errors.messages.dup
|
31
|
+
includes_to_errors(submodel, value, subattributes)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
attributes
|
35
|
+
end
|
36
|
+
|
37
|
+
def each_inclusion(includes, &)
|
38
|
+
unless includes.respond_to?(:keys)
|
39
|
+
includes = [includes].flatten.compact.inject({}) do |hash, key|
|
40
|
+
hash.tap do
|
41
|
+
hash[key] = nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
includes.each(&)
|
46
|
+
end
|
47
|
+
|
48
|
+
def yield_builder(builder)
|
49
|
+
Jbuilder.new do |json|
|
50
|
+
instance_exec json, @model, @error, @message, @includes, &builder
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class << self
|
55
|
+
def for(error, model = nil, includes = nil)
|
56
|
+
new(model, error, I18n.t(error, scope: AdequateJson.configuration.i18n_errors_scope), includes)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdequateJson
|
4
|
+
class Hash
|
5
|
+
include Resolver
|
6
|
+
include Jsonizer
|
7
|
+
|
8
|
+
def initialize(hash, json = nil, variants: {}, **)
|
9
|
+
@hash = hash
|
10
|
+
@json = json
|
11
|
+
@variants = variants
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_builder
|
15
|
+
if @json.nil?
|
16
|
+
Jbuilder.new do |json|
|
17
|
+
@json = json
|
18
|
+
serialize_hash
|
19
|
+
end
|
20
|
+
else
|
21
|
+
serialize_hash
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def serialize_hash
|
28
|
+
@hash.each do |key, value|
|
29
|
+
if value.respond_to?(:to_i) || value.frozen?
|
30
|
+
@json.set!(key, value)
|
31
|
+
else
|
32
|
+
@json.set!(key) do
|
33
|
+
choose_serializer(value, variant: @variants[key] || :no_wrapper).to_builder
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdequateJson
|
4
|
+
module Loader
|
5
|
+
class << self
|
6
|
+
def autoload_serializers(module_name)
|
7
|
+
path = "app/#{module_name}"
|
8
|
+
module_name.to_s.camelize.tap do |type_name|
|
9
|
+
unless Object.const_defined?(type_name)
|
10
|
+
m = Object.const_set(type_name, Module.new)
|
11
|
+
# Listen for Zeitwerk code reloading and clear the serializers cache
|
12
|
+
Rails.autoloaders.main.on_setup do
|
13
|
+
AdequateJson::Resolver::Cache.reset!
|
14
|
+
end
|
15
|
+
Rails.autoloaders.main.push_dir(path, namespace: m)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
if defined?(::Rails::Railtie)
|
23
|
+
class Railtie < ::Rails::Railtie
|
24
|
+
config.before_initialize do
|
25
|
+
Loader.autoload_serializers AdequateJson.configuration.serializers_module
|
26
|
+
end
|
27
|
+
|
28
|
+
config.after_initialize do
|
29
|
+
ActionController::API.include AdequateJson::Serializer
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
|
5
|
+
module AdequateJson
|
6
|
+
module Resolver
|
7
|
+
def choose_serializer(model, **args)
|
8
|
+
if model.respond_to?(:to_hash)
|
9
|
+
AdequateJson::Hash.new(model.to_hash, @json, **args)
|
10
|
+
elsif model.respond_to?(:each)
|
11
|
+
AdequateJson::Collection.new(model, @json, **args)
|
12
|
+
else
|
13
|
+
model_serializer(model, **args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def model_serializer(model, **args)
|
20
|
+
serializer_id = (model.respond_to?(:serializer) && model.serializer) || model.model_name.name
|
21
|
+
klazz = serializer_cache.get(serializer_id) ||
|
22
|
+
serializer_cache.set(serializer_id, resolve_serializer(serializer_id))
|
23
|
+
klazz.new(model, @json, **args)
|
24
|
+
end
|
25
|
+
|
26
|
+
def resolve_serializer(symbol)
|
27
|
+
klazz = symbol.to_s.camelcase
|
28
|
+
return serializers_module.const_get(klazz, false) if serializers_module.const_defined?(klazz, false)
|
29
|
+
|
30
|
+
raise "Unable to find serializer for #{klazz}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def serializers_module
|
34
|
+
AdequateJson.configuration.serializers_module_const
|
35
|
+
end
|
36
|
+
|
37
|
+
def serializer_cache
|
38
|
+
Cache
|
39
|
+
end
|
40
|
+
|
41
|
+
class Cache
|
42
|
+
class << self
|
43
|
+
def get(serializer_id)
|
44
|
+
store[serializer_id]
|
45
|
+
end
|
46
|
+
|
47
|
+
def set(serializer_id, serializer)
|
48
|
+
store[serializer_id] = serializer
|
49
|
+
serializer
|
50
|
+
end
|
51
|
+
|
52
|
+
def store
|
53
|
+
@store ||= {}
|
54
|
+
end
|
55
|
+
|
56
|
+
def reset!
|
57
|
+
@store = {}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdequateJson
|
4
|
+
module Serializer
|
5
|
+
include AdequateJson::Resolver
|
6
|
+
|
7
|
+
def render_json(model, variant: nil, **options)
|
8
|
+
render json: choose_serializer(model, variant: variant), **options
|
9
|
+
end
|
10
|
+
|
11
|
+
def render_error(error, model = nil, includes: nil, **options)
|
12
|
+
render json: Error.for(error, model, includes), **options
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jbuilder'
|
4
|
+
require 'adequate_json/resolver'
|
5
|
+
require 'adequate_json/jsonizer'
|
6
|
+
require 'adequate_json/base'
|
7
|
+
require 'adequate_json/error'
|
8
|
+
require 'adequate_json/hash'
|
9
|
+
require 'adequate_json/collection'
|
10
|
+
require 'adequate_json/configuration'
|
11
|
+
require 'adequate_json/serializer'
|
12
|
+
require 'adequate_json/version'
|
13
|
+
|
14
|
+
module AdequateJson
|
15
|
+
class << self
|
16
|
+
def configure
|
17
|
+
yield ConfigurationBuilder.new(configuration)
|
18
|
+
end
|
19
|
+
|
20
|
+
def configuration
|
21
|
+
@configuration ||= Configuration.new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'adequate_json/railtie' if defined?(::Rails::Railtie)
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: adequate_json
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jef Mathiot
|
8
|
+
- Issam Tribak
|
9
|
+
- Wilfried Tacquard
|
10
|
+
autorequire:
|
11
|
+
bindir: exe
|
12
|
+
cert_chain: []
|
13
|
+
date: 2024-11-20 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: jbuilder
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '0'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: attr_extras
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: bundler
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '2.5'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - "~>"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '2.5'
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: rake
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 12.3.3
|
64
|
+
type: :development
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 12.3.3
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: rspec
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - "~>"
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '3.0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - "~>"
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '3.0'
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: rubocop
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
description: Adequate Json is a gem that simplifies the process of serializing JSON
|
100
|
+
for API responses.
|
101
|
+
email:
|
102
|
+
- jeff.mathiot@gmail.com
|
103
|
+
- issam.tribak@mysofie.fr
|
104
|
+
- wilfried.tacquard@mysofie.fr
|
105
|
+
executables: []
|
106
|
+
extensions: []
|
107
|
+
extra_rdoc_files: []
|
108
|
+
files:
|
109
|
+
- ".github/workflows/ruby.yml"
|
110
|
+
- ".gitignore"
|
111
|
+
- ".rubocop.yml"
|
112
|
+
- ".ruby-version"
|
113
|
+
- Gemfile
|
114
|
+
- LICENSE.txt
|
115
|
+
- README.md
|
116
|
+
- Rakefile
|
117
|
+
- adequate_json.gemspec
|
118
|
+
- bin/console
|
119
|
+
- bin/setup
|
120
|
+
- lib/adequate_json.rb
|
121
|
+
- lib/adequate_json/base.rb
|
122
|
+
- lib/adequate_json/collection.rb
|
123
|
+
- lib/adequate_json/configuration.rb
|
124
|
+
- lib/adequate_json/error.rb
|
125
|
+
- lib/adequate_json/hash.rb
|
126
|
+
- lib/adequate_json/jsonizer.rb
|
127
|
+
- lib/adequate_json/railtie.rb
|
128
|
+
- lib/adequate_json/resolver.rb
|
129
|
+
- lib/adequate_json/serializer.rb
|
130
|
+
- lib/adequate_json/version.rb
|
131
|
+
homepage: https://github.com/EverestHC-mySofie/adequate_json
|
132
|
+
licenses:
|
133
|
+
- MIT
|
134
|
+
metadata:
|
135
|
+
homepage_uri: https://github.com/EverestHC-mySofie/adequate_json
|
136
|
+
source_code_uri: https://github.com/EverestHC-mySofie/adequate_json
|
137
|
+
changelog_uri: https://github.com/EverestHC-mySofie/adequate_json
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - "~>"
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '3.0'
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubygems_version: 3.5.23
|
154
|
+
signing_key:
|
155
|
+
specification_version: 4
|
156
|
+
summary: Yet another JSON serialization library
|
157
|
+
test_files: []
|