alba 1.5.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +11 -0
- data/.github/dependabot.yml +4 -18
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/main.yml +4 -4
- data/.github/workflows/perf.yml +2 -2
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +34 -0
- data/CONTRIBUTING.md +30 -0
- data/Gemfile +6 -2
- data/HACKING.md +41 -0
- data/README.md +599 -128
- data/Rakefile +2 -2
- data/alba.gemspec +8 -4
- data/benchmark/README.md +81 -0
- data/benchmark/collection.rb +60 -74
- data/benchmark/single_resource.rb +33 -2
- data/docs/migrate_from_jbuilder.md +18 -4
- data/docs/rails.md +44 -0
- data/lib/alba/association.rb +49 -9
- data/lib/alba/conditional_attribute.rb +54 -0
- data/lib/alba/default_inflector.rb +13 -24
- data/lib/alba/errors.rb +10 -0
- data/lib/alba/layout.rb +67 -0
- data/lib/alba/nested_attribute.rb +18 -0
- data/lib/alba/resource.rb +240 -156
- data/lib/alba/typed_attribute.rb +1 -1
- data/lib/alba/version.rb +1 -1
- data/lib/alba.rb +43 -58
- data/logo/alba-card.png +0 -0
- data/logo/alba-sign.png +0 -0
- data/logo/alba-typography.png +0 -0
- metadata +21 -11
- data/gemfiles/all.gemfile +0 -19
- data/lib/alba/key_transform_factory.rb +0 -33
- data/lib/alba/many.rb +0 -21
- data/lib/alba/one.rb +0 -21
- data/sider.yml +0 -60
data/Rakefile
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
3
|
|
4
|
-
ENV["BUNDLE_GEMFILE"] = File.expand_path("
|
4
|
+
ENV["BUNDLE_GEMFILE"] = File.expand_path("Gemfile") if ENV["BUNDLE_GEMFILE"] == File.expand_path("Gemfile") || ENV["BUNDLE_GEMFILE"].empty? || ENV["BUNDLE_GEMFILE"].nil?
|
5
5
|
|
6
6
|
Rake::TestTask.new(:test) do |t|
|
7
7
|
t.libs << "test"
|
8
8
|
t.libs << "lib"
|
9
|
-
file_list = ENV["BUNDLE_GEMFILE"] == File.expand_path("
|
9
|
+
file_list = ENV["BUNDLE_GEMFILE"] == File.expand_path("Gemfile") ? FileList["test/**/*_test.rb"] : FileList["test/dependencies/test_dependencies.rb"]
|
10
10
|
t.test_files = file_list
|
11
11
|
end
|
12
12
|
|
data/alba.gemspec
CHANGED
@@ -10,11 +10,15 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.description = "Alba is the fastest JSON serializer for Ruby. It focuses on performance, flexibility and usability."
|
11
11
|
spec.homepage = 'https://github.com/okuramasafumi/alba'
|
12
12
|
spec.license = 'MIT'
|
13
|
-
spec.required_ruby_version = Gem::Requirement.new('>= 2.
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
|
14
14
|
|
15
|
-
spec.metadata
|
16
|
-
|
17
|
-
|
15
|
+
spec.metadata = {
|
16
|
+
'bug_tracker_uri' => 'https://github.com/okuramasafumi/issues',
|
17
|
+
'changelog_uri' => 'https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md',
|
18
|
+
'documentation_uri' => 'https://rubydoc.info/github/okuramasafumi/alba',
|
19
|
+
'source_code_uri' => 'https://github.com/okuramasafumi/alba',
|
20
|
+
'rubygems_mfa_required' => 'true'
|
21
|
+
}
|
18
22
|
|
19
23
|
# Specify which files should be added to the gem when it is released.
|
20
24
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
data/benchmark/README.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
## Benchmark for json serializers
|
2
|
+
|
3
|
+
This directory contains a few different benchmark scripts. They all use inline Bundler definitions so you can run them by `ruby benchmark/collection.rb` for instance.
|
4
|
+
|
5
|
+
## Result
|
6
|
+
|
7
|
+
As a reference, here's the benchmark result run in my (@okuramasafumi) machine.
|
8
|
+
|
9
|
+
Machine spec:
|
10
|
+
|
11
|
+
|Key|Value|
|
12
|
+
|---|---|
|
13
|
+
|OS|macOS 12.2.1|
|
14
|
+
|CPU|Intel Corei7 Quad Core 2.3Ghz|
|
15
|
+
|RAM|32GB|
|
16
|
+
|Ruby|ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin19]|
|
17
|
+
|
18
|
+
`benchmark-ips` with `Oj.optimize_rails`:
|
19
|
+
|
20
|
+
```
|
21
|
+
Comparison:
|
22
|
+
panko: 267.6 i/s
|
23
|
+
rails: 111.2 i/s - 2.41x (± 0.00) slower
|
24
|
+
jserializer: 106.2 i/s - 2.52x (± 0.00) slower
|
25
|
+
alba: 102.8 i/s - 2.60x (± 0.00) slower
|
26
|
+
turbostreamer: 99.9 i/s - 2.68x (± 0.00) slower
|
27
|
+
jbuilder: 90.7 i/s - 2.95x (± 0.00) slower
|
28
|
+
alba_inline: 90.0 i/s - 2.97x (± 0.00) slower
|
29
|
+
primalize: 82.1 i/s - 3.26x (± 0.00) slower
|
30
|
+
fast_serializer: 62.7 i/s - 4.27x (± 0.00) slower
|
31
|
+
jsonapi_same_format: 59.5 i/s - 4.50x (± 0.00) slower
|
32
|
+
jsonapi: 55.3 i/s - 4.84x (± 0.00) slower
|
33
|
+
blueprinter: 54.1 i/s - 4.95x (± 0.00) slower
|
34
|
+
representable: 35.9 i/s - 7.46x (± 0.00) slower
|
35
|
+
simple_ams: 25.4 i/s - 10.53x (± 0.00) slower
|
36
|
+
ams: 9.1 i/s - 29.39x (± 0.00) slower
|
37
|
+
```
|
38
|
+
|
39
|
+
`benchmark-ips` without `Oj.optimize_rails`:
|
40
|
+
|
41
|
+
```
|
42
|
+
Comparison:
|
43
|
+
panko: 283.8 i/s
|
44
|
+
turbostreamer: 102.9 i/s - 2.76x (± 0.00) slower
|
45
|
+
alba: 102.4 i/s - 2.77x (± 0.00) slower
|
46
|
+
alba_inline: 98.7 i/s - 2.87x (± 0.00) slower
|
47
|
+
jserializer: 93.3 i/s - 3.04x (± 0.00) slower
|
48
|
+
fast_serializer: 60.1 i/s - 4.73x (± 0.00) slower
|
49
|
+
blueprinter: 53.8 i/s - 5.28x (± 0.00) slower
|
50
|
+
rails: 37.1 i/s - 7.65x (± 0.00) slower
|
51
|
+
jbuilder: 37.1 i/s - 7.66x (± 0.00) slower
|
52
|
+
primalize: 31.2 i/s - 9.08x (± 0.00) slower
|
53
|
+
jsonapi_same_format: 28.3 i/s - 10.03x (± 0.00) slower
|
54
|
+
representable: 27.8 i/s - 10.23x (± 0.00) slower
|
55
|
+
jsonapi: 27.5 i/s - 10.34x (± 0.00) slower
|
56
|
+
simple_ams: 16.9 i/s - 16.75x (± 0.00) slower
|
57
|
+
ams: 8.3 i/s - 34.36x (± 0.00) slower
|
58
|
+
```
|
59
|
+
|
60
|
+
`benchmark-memory`:
|
61
|
+
|
62
|
+
```
|
63
|
+
Comparison:
|
64
|
+
panko: 230418 allocated
|
65
|
+
alba: 733217 allocated - 3.18x more
|
66
|
+
alba_inline: 748297 allocated - 3.25x more
|
67
|
+
turbostreamer: 781008 allocated - 3.39x more
|
68
|
+
jserializer: 819705 allocated - 3.56x more
|
69
|
+
primalize: 1195163 allocated - 5.19x more
|
70
|
+
fast_serializer: 1232385 allocated - 5.35x more
|
71
|
+
rails: 1236761 allocated - 5.37x more
|
72
|
+
blueprinter: 1588937 allocated - 6.90x more
|
73
|
+
jbuilder: 1774157 allocated - 7.70x more
|
74
|
+
jsonapi_same_format: 2132489 allocated - 9.25x more
|
75
|
+
jsonapi: 2279958 allocated - 9.89x more
|
76
|
+
representable: 2869166 allocated - 12.45x more
|
77
|
+
ams: 4473161 allocated - 19.41x more
|
78
|
+
simple_ams: 7868345 allocated - 34.15x more
|
79
|
+
```
|
80
|
+
|
81
|
+
Conclusion: panko is extremely fast but it's a C extension gem. As pure Ruby gems, Alba, turbostreamer and jserializer are notably faster than others. With `Oj.optimize_rails` jbuilder and Rails standard serialization are also fast.
|
data/benchmark/collection.rb
CHANGED
@@ -15,9 +15,10 @@ gemfile(true) do
|
|
15
15
|
gem "benchmark-ips"
|
16
16
|
gem "benchmark-memory"
|
17
17
|
gem "blueprinter"
|
18
|
+
gem "fast_serializer_ruby"
|
18
19
|
gem "jbuilder"
|
20
|
+
gem 'turbostreamer'
|
19
21
|
gem "jserializer"
|
20
|
-
gem "jsonapi-serializer" # successor of fast_jsonapi
|
21
22
|
gem "multi_json"
|
22
23
|
gem "panko_serializer"
|
23
24
|
gem "pg"
|
@@ -138,6 +139,27 @@ class PostBlueprint < Blueprinter::Base
|
|
138
139
|
end
|
139
140
|
end
|
140
141
|
|
142
|
+
# --- Fast Serializer Ruby
|
143
|
+
|
144
|
+
require "fast_serializer"
|
145
|
+
|
146
|
+
class FastSerializerCommentResource
|
147
|
+
include ::FastSerializer::Schema::Mixin
|
148
|
+
attributes :id, :body
|
149
|
+
end
|
150
|
+
|
151
|
+
class FastSerializerPostResource
|
152
|
+
include ::FastSerializer::Schema::Mixin
|
153
|
+
|
154
|
+
attributes :id, :body
|
155
|
+
|
156
|
+
attribute :commenter_names do
|
157
|
+
object.commenters.pluck(:name)
|
158
|
+
end
|
159
|
+
|
160
|
+
has_many :comments, serializer: FastSerializerCommentResource
|
161
|
+
end
|
162
|
+
|
141
163
|
# --- JBuilder serializers ---
|
142
164
|
|
143
165
|
require "jbuilder"
|
@@ -178,67 +200,6 @@ class JserializerPostSerializer < Jserializer::Base
|
|
178
200
|
end
|
179
201
|
end
|
180
202
|
|
181
|
-
# --- JSONAPI:Serializer serializers / (successor of fast_jsonapi) ---
|
182
|
-
|
183
|
-
class JsonApiStandardCommentSerializer
|
184
|
-
include JSONAPI::Serializer
|
185
|
-
|
186
|
-
attribute :id
|
187
|
-
attribute :body
|
188
|
-
end
|
189
|
-
|
190
|
-
class JsonApiStandardPostSerializer
|
191
|
-
include JSONAPI::Serializer
|
192
|
-
|
193
|
-
# set_type :post # optional
|
194
|
-
attribute :id
|
195
|
-
attribute :body
|
196
|
-
attribute :commenter_names
|
197
|
-
|
198
|
-
attribute :comments do |post|
|
199
|
-
post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
# --- JSONAPI:Serializer serializers that format the code the same flat way as the other gems here ---
|
204
|
-
|
205
|
-
# code to convert from JSON:API output to "flat" JSON, like the other serializers build
|
206
|
-
class JsonApiSameFormatSerializer
|
207
|
-
include JSONAPI::Serializer
|
208
|
-
|
209
|
-
def as_json(*_options)
|
210
|
-
hash = serializable_hash
|
211
|
-
|
212
|
-
if hash[:data].is_a? Hash
|
213
|
-
hash[:data][:attributes]
|
214
|
-
|
215
|
-
elsif hash[:data].is_a? Array
|
216
|
-
hash[:data].pluck(:attributes)
|
217
|
-
|
218
|
-
elsif hash[:data].nil?
|
219
|
-
{ }
|
220
|
-
|
221
|
-
else
|
222
|
-
raise "unexpected data type #{hash[:data].class}"
|
223
|
-
end
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
class JsonApiSameFormatCommentSerializer < JsonApiSameFormatSerializer
|
228
|
-
attribute :id
|
229
|
-
attribute :body
|
230
|
-
end
|
231
|
-
|
232
|
-
class JsonApiSameFormatPostSerializer < JsonApiSameFormatSerializer
|
233
|
-
attribute :id
|
234
|
-
attribute :body
|
235
|
-
attribute :commenter_names
|
236
|
-
|
237
|
-
attribute :comments do |post|
|
238
|
-
post.comments.map { |comment| JsonApiSameFormatCommentSerializer.new(comment) }
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
203
|
# --- Panko serializers ---
|
243
204
|
#
|
244
205
|
|
@@ -255,7 +216,7 @@ class PankoPostSerializer < Panko::Serializer
|
|
255
216
|
has_many :comments, serializer: PankoCommentSerializer
|
256
217
|
|
257
218
|
def commenter_names
|
258
|
-
object.
|
219
|
+
object.commenters.pluck(:name)
|
259
220
|
end
|
260
221
|
end
|
261
222
|
|
@@ -332,6 +293,31 @@ class SimpleAMSPostSerializer
|
|
332
293
|
end
|
333
294
|
end
|
334
295
|
|
296
|
+
require 'turbostreamer'
|
297
|
+
TurboStreamer.set_default_encoder(:json, :oj)
|
298
|
+
|
299
|
+
class TurbostreamerSerializer
|
300
|
+
def initialize(posts)
|
301
|
+
@posts = posts
|
302
|
+
end
|
303
|
+
|
304
|
+
def to_json
|
305
|
+
TurboStreamer.encode do |json|
|
306
|
+
json.array! @posts do |post|
|
307
|
+
json.object! do
|
308
|
+
json.extract! post, :id, :body, :commenter_names
|
309
|
+
|
310
|
+
json.comments post.comments do |comment|
|
311
|
+
json.object! do
|
312
|
+
json.extract! comment, :id, :body
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
335
321
|
# --- Test data creation ---
|
336
322
|
|
337
323
|
100.times do |i|
|
@@ -344,7 +330,7 @@ end
|
|
344
330
|
end
|
345
331
|
end
|
346
332
|
|
347
|
-
posts = Post.all.
|
333
|
+
posts = Post.all.includes(:comments, :commenters)
|
348
334
|
|
349
335
|
# --- Store the serializers in procs ---
|
350
336
|
|
@@ -360,8 +346,9 @@ alba_inline = Proc.new do
|
|
360
346
|
end
|
361
347
|
end
|
362
348
|
end
|
363
|
-
ams = Proc.new { ActiveModelSerializers::SerializableResource.new(posts, {}).
|
349
|
+
ams = Proc.new { ActiveModelSerializers::SerializableResource.new(posts, {each_serializer: AMSPostSerializer}).to_json }
|
364
350
|
blueprinter = Proc.new { PostBlueprint.render(posts) }
|
351
|
+
fast_serializer = Proc.new { FastSerializerPostResource.new(posts).to_json }
|
365
352
|
jbuilder = Proc.new do
|
366
353
|
Jbuilder.new do |json|
|
367
354
|
json.array!(posts) do |post|
|
@@ -370,8 +357,6 @@ jbuilder = Proc.new do
|
|
370
357
|
end.target!
|
371
358
|
end
|
372
359
|
jserializer = Proc.new { JserializerPostSerializer.new(posts, is_collection: true).to_json }
|
373
|
-
jsonapi = proc { JsonApiStandardPostSerializer.new(posts).to_json }
|
374
|
-
jsonapi_same_format = proc { JsonApiSameFormatPostSerializer.new(posts).to_json }
|
375
360
|
panko = proc { Panko::ArraySerializer.new(posts, each_serializer: PankoPostSerializer).to_json }
|
376
361
|
primalize = proc { PrimalizePostsResource.new(posts: posts).to_json }
|
377
362
|
rails = Proc.new do
|
@@ -379,24 +364,25 @@ rails = Proc.new do
|
|
379
364
|
end
|
380
365
|
representable = Proc.new { PostsRepresenter.new(posts).to_json }
|
381
366
|
simple_ams = Proc.new { SimpleAMS::Renderer::Collection.new(posts, serializer: SimpleAMSPostSerializer).to_json }
|
367
|
+
turbostreamer = Proc.new { TurbostreamerSerializer.new(posts).to_json }
|
382
368
|
|
383
369
|
# --- Execute the serializers to check their output ---
|
384
|
-
|
370
|
+
GC.disable
|
385
371
|
puts "Serializer outputs ----------------------------------"
|
386
372
|
{
|
387
373
|
alba: alba,
|
388
374
|
alba_inline: alba_inline,
|
389
375
|
ams: ams,
|
390
376
|
blueprinter: blueprinter,
|
377
|
+
fast_serializer: fast_serializer,
|
391
378
|
jbuilder: jbuilder, # different order
|
392
379
|
jserializer: jserializer,
|
393
|
-
jsonapi: jsonapi, # nested JSON:API format
|
394
|
-
jsonapi_same_format: jsonapi_same_format,
|
395
380
|
panko: panko,
|
396
381
|
primalize: primalize,
|
397
382
|
rails: rails,
|
398
383
|
representable: representable,
|
399
384
|
simple_ams: simple_ams,
|
385
|
+
turbostreamer: turbostreamer
|
400
386
|
}.each { |name, serializer| puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}" }
|
401
387
|
|
402
388
|
# --- Run the benchmarks ---
|
@@ -407,15 +393,15 @@ Benchmark.ips do |x|
|
|
407
393
|
x.report(:alba_inline, &alba_inline)
|
408
394
|
x.report(:ams, &ams)
|
409
395
|
x.report(:blueprinter, &blueprinter)
|
396
|
+
x.report(:fast_serializer, &fast_serializer)
|
410
397
|
x.report(:jbuilder, &jbuilder)
|
411
398
|
x.report(:jserializer, &jserializer)
|
412
|
-
x.report(:jsonapi, &jsonapi)
|
413
|
-
x.report(:jsonapi_same_format, &jsonapi_same_format)
|
414
399
|
x.report(:panko, &panko)
|
415
400
|
x.report(:primalize, &primalize)
|
416
401
|
x.report(:rails, &rails)
|
417
402
|
x.report(:representable, &representable)
|
418
403
|
x.report(:simple_ams, &simple_ams)
|
404
|
+
x.report(:turbostreamer, &turbostreamer)
|
419
405
|
|
420
406
|
x.compare!
|
421
407
|
end
|
@@ -427,15 +413,15 @@ Benchmark.memory do |x|
|
|
427
413
|
x.report(:alba_inline, &alba_inline)
|
428
414
|
x.report(:ams, &ams)
|
429
415
|
x.report(:blueprinter, &blueprinter)
|
416
|
+
x.report(:fast_serializer, &fast_serializer)
|
430
417
|
x.report(:jbuilder, &jbuilder)
|
431
418
|
x.report(:jserializer, &jserializer)
|
432
|
-
x.report(:jsonapi, &jsonapi)
|
433
|
-
x.report(:jsonapi_same_format, &jsonapi_same_format)
|
434
419
|
x.report(:panko, &panko)
|
435
420
|
x.report(:primalize, &primalize)
|
436
421
|
x.report(:rails, &rails)
|
437
422
|
x.report(:representable, &representable)
|
438
423
|
x.report(:simple_ams, &simple_ams)
|
424
|
+
x.report(:turbostreamer, &turbostreamer)
|
439
425
|
|
440
426
|
x.compare!
|
441
427
|
end
|
@@ -16,6 +16,7 @@ gemfile(true) do
|
|
16
16
|
gem "benchmark-memory"
|
17
17
|
gem "blueprinter"
|
18
18
|
gem "jbuilder"
|
19
|
+
gem 'turbostreamer'
|
19
20
|
gem "jserializer"
|
20
21
|
gem "jsonapi-serializer" # successor of fast_jsonapi
|
21
22
|
gem "multi_json"
|
@@ -253,7 +254,7 @@ class PankoPostSerializer < Panko::Serializer
|
|
253
254
|
has_many :comments, serializer: PankoCommentSerializer
|
254
255
|
|
255
256
|
def commenter_names
|
256
|
-
object.
|
257
|
+
object.commenters.pluck(:name)
|
257
258
|
end
|
258
259
|
end
|
259
260
|
|
@@ -324,6 +325,29 @@ class SimpleAMSPostSerializer
|
|
324
325
|
end
|
325
326
|
end
|
326
327
|
|
328
|
+
require 'turbostreamer'
|
329
|
+
TurboStreamer.set_default_encoder(:json, :oj)
|
330
|
+
|
331
|
+
class TurbostreamerSerializer
|
332
|
+
def initialize(post)
|
333
|
+
@post = post
|
334
|
+
end
|
335
|
+
|
336
|
+
def to_json
|
337
|
+
TurboStreamer.encode do |json|
|
338
|
+
json.object! do
|
339
|
+
json.extract! @post, :id, :body, :commenter_names
|
340
|
+
|
341
|
+
json.comments @post.comments do |comment|
|
342
|
+
json.object! do
|
343
|
+
json.extract! comment, :id, :body
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
327
351
|
# --- Test data creation ---
|
328
352
|
|
329
353
|
post = Post.create!(body: 'post')
|
@@ -347,6 +371,7 @@ alba_inline = Proc.new do
|
|
347
371
|
end
|
348
372
|
end
|
349
373
|
end
|
374
|
+
|
350
375
|
ams = Proc.new { AMSPostSerializer.new(post, {}).to_json }
|
351
376
|
blueprinter = Proc.new { PostBlueprint.render(post) }
|
352
377
|
jbuilder = Proc.new { post.to_builder.target! }
|
@@ -358,6 +383,7 @@ primalize = proc { PrimalizePostResource.new(post).to_json }
|
|
358
383
|
rails = Proc.new { ActiveSupport::JSON.encode(post.serializable_hash(include: :comments)) }
|
359
384
|
representable = Proc.new { PostRepresenter.new(post).to_json }
|
360
385
|
simple_ams = Proc.new { SimpleAMS::Renderer.new(post, serializer: SimpleAMSPostSerializer).to_json }
|
386
|
+
turbostreamer = Proc.new { TurbostreamerSerializer.new(post).to_json }
|
361
387
|
|
362
388
|
# --- Execute the serializers to check their output ---
|
363
389
|
|
@@ -376,7 +402,10 @@ puts "Serializer outputs ----------------------------------"
|
|
376
402
|
rails: rails,
|
377
403
|
representable: representable,
|
378
404
|
simple_ams: simple_ams,
|
379
|
-
|
405
|
+
turbostreamer: turbostreamer
|
406
|
+
}.each do |name, serializer|
|
407
|
+
puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}"
|
408
|
+
end
|
380
409
|
|
381
410
|
# --- Run the benchmarks ---
|
382
411
|
|
@@ -395,6 +424,7 @@ Benchmark.ips do |x|
|
|
395
424
|
x.report(:rails, &rails)
|
396
425
|
x.report(:representable, &representable)
|
397
426
|
x.report(:simple_ams, &simple_ams)
|
427
|
+
x.report(:turbostreamer, &turbostreamer)
|
398
428
|
|
399
429
|
x.compare!
|
400
430
|
end
|
@@ -415,6 +445,7 @@ Benchmark.memory do |x|
|
|
415
445
|
x.report(:rails, &rails)
|
416
446
|
x.report(:representable, &representable)
|
417
447
|
x.report(:simple_ams, &simple_ams)
|
448
|
+
x.report(:turbostreamer, &turbostreamer)
|
418
449
|
|
419
450
|
x.compare!
|
420
451
|
end
|
@@ -4,11 +4,12 @@ title: Upgrading from Jbuilder
|
|
4
4
|
|
5
5
|
<!-- @format -->
|
6
6
|
|
7
|
-
This guide is aimed at helping Jbuilder users transition to Alba, and it consists of
|
7
|
+
This guide is aimed at helping Jbuilder users transition to Alba, and it consists of four parts:
|
8
8
|
|
9
9
|
1. Basic serialization
|
10
10
|
2. Complex serialization
|
11
|
-
3.
|
11
|
+
3. Key transformation
|
12
|
+
4. Unsupported features
|
12
13
|
|
13
14
|
## Example class
|
14
15
|
|
@@ -216,8 +217,21 @@ UserResource.new(user).serialize
|
|
216
217
|
# => '{"user":{"id":id, "created_at": created_at, "updated_at": updated_at, "profile": {"email": email}, articles: [{"title": title, "body": body}]}'
|
217
218
|
```
|
218
219
|
|
219
|
-
## 3.
|
220
|
+
## 3. Key transformation
|
221
|
+
|
222
|
+
See the README for more information, but it's possible to migrate the `Jbuilder.key_format!` behavior with the `transform_keys` macro.
|
223
|
+
|
224
|
+
```rb
|
225
|
+
class UserResource
|
226
|
+
include Alba::Resource
|
227
|
+
root_key :user
|
228
|
+
attributes :id, :created_at, :updated_at
|
229
|
+
|
230
|
+
transform_keys :lower_camel
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
## 4. Unsupported features
|
220
235
|
|
221
236
|
- Jbuilder#ignore_nil!
|
222
237
|
- Jbuilder#cache!
|
223
|
-
- Jbuilder.key_format! and Jbuilder.deep_format_keys!
|
data/docs/rails.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
---
|
2
|
+
title: Alba for Rails
|
3
|
+
author: OKURA Masafumi
|
4
|
+
---
|
5
|
+
|
6
|
+
# Alba for Rails
|
7
|
+
|
8
|
+
While Alba is NOT designed for Rails specifically, you can definitely use Alba with Rails. This document describes in detail how to use Alba with Rails to be more productive.
|
9
|
+
|
10
|
+
## Initializer
|
11
|
+
|
12
|
+
You might want to add some configurations to initializer file such as `alba.rb` with something like below:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
# alba.rb
|
16
|
+
Alba.backend = :active_support
|
17
|
+
Alba.enable_inference!(:active_support)
|
18
|
+
```
|
19
|
+
|
20
|
+
You can also use `:oj_rails` for backend if you prefer using Oj.
|
21
|
+
|
22
|
+
## Rendering JSON
|
23
|
+
|
24
|
+
You can render JSON with Rails in two ways. One way is to pass JSON String.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
render json: FooResource.new(foo).serialize
|
28
|
+
```
|
29
|
+
|
30
|
+
But you can also render JSON passing `Alba::Resource` object. Rails automatically calls `to_json` on a resource.
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
render json: FooResource.new(foo)
|
34
|
+
```
|
35
|
+
|
36
|
+
Note that almost all options given to this `render` are ignored. The only exceptions are `layout`, `prefixes`, `template` and `status`.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
# This `only` option is ignored
|
40
|
+
render json: FooResource.new(foo), only: [:id]
|
41
|
+
|
42
|
+
# This is OK
|
43
|
+
render json: FooResource.new(foo), status: 200
|
44
|
+
```
|
data/lib/alba/association.rb
CHANGED
@@ -1,22 +1,48 @@
|
|
1
1
|
module Alba
|
2
|
-
#
|
3
|
-
# Child class should implement `to_hash` method
|
2
|
+
# Representing association
|
4
3
|
class Association
|
4
|
+
@const_cache = {}
|
5
|
+
class << self
|
6
|
+
attr_reader :const_cache
|
7
|
+
end
|
8
|
+
|
5
9
|
attr_reader :object, :name
|
6
10
|
|
7
11
|
# @param name [Symbol, String] name of the method to fetch association
|
8
12
|
# @param condition [Proc, nil] a proc filtering data
|
9
13
|
# @param resource [Class<Alba::Resource>, nil] a resource class for the association
|
14
|
+
# @param params [Hash] params override for the association
|
10
15
|
# @param nesting [String] a namespace where source class is inferred with
|
16
|
+
# @param key_transformation [Symbol] key transformation type
|
11
17
|
# @param block [Block] used to define resource when resource arg is absent
|
12
|
-
def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
|
18
|
+
def initialize(name:, condition: nil, resource: nil, params: {}, nesting: nil, key_transformation: :none, &block)
|
13
19
|
@name = name
|
14
20
|
@condition = condition
|
15
|
-
@block = block
|
16
21
|
@resource = resource
|
22
|
+
@params = params
|
23
|
+
@key_transformation = key_transformation
|
17
24
|
return if @resource
|
18
25
|
|
19
|
-
assign_resource(nesting)
|
26
|
+
assign_resource(nesting, block)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Recursively converts an object into a Hash
|
30
|
+
#
|
31
|
+
# @param target [Object] the object having an association method
|
32
|
+
# @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
|
33
|
+
# @param params [Hash] user-given Hash for arbitrary data
|
34
|
+
# @return [Hash]
|
35
|
+
def to_h(target, within: nil, params: {})
|
36
|
+
params = params.merge(@params) unless @params.empty?
|
37
|
+
@object = target.__send__(@name)
|
38
|
+
@object = @condition.call(object, params, target) if @condition
|
39
|
+
return if @object.nil?
|
40
|
+
|
41
|
+
if @resource.is_a?(Proc) && @object.is_a?(Enumerable)
|
42
|
+
to_h_with_each_resource(within, params)
|
43
|
+
else
|
44
|
+
to_h_with_constantize_resource(within, params)
|
45
|
+
end
|
20
46
|
end
|
21
47
|
|
22
48
|
private
|
@@ -26,18 +52,32 @@ module Alba
|
|
26
52
|
when Class
|
27
53
|
resource
|
28
54
|
when Symbol, String
|
29
|
-
|
55
|
+
self.class.const_cache.fetch(resource) do
|
56
|
+
self.class.const_cache[resource] = Object.const_get(resource)
|
57
|
+
end
|
30
58
|
end
|
31
59
|
end
|
32
60
|
|
33
|
-
def assign_resource(nesting)
|
34
|
-
@resource = if
|
35
|
-
Alba.resource_class(
|
61
|
+
def assign_resource(nesting, block)
|
62
|
+
@resource = if block
|
63
|
+
Alba.resource_class(&block)
|
36
64
|
elsif Alba.inferring
|
37
65
|
Alba.infer_resource_class(@name, nesting: nesting)
|
38
66
|
else
|
39
67
|
raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
|
40
68
|
end
|
41
69
|
end
|
70
|
+
|
71
|
+
def to_h_with_each_resource(within, params)
|
72
|
+
@object.map do |item|
|
73
|
+
@resource.call(item).new(item, within: within, params: params).to_h
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_h_with_constantize_resource(within, params)
|
78
|
+
@resource = constantize(@resource)
|
79
|
+
@resource.transform_keys(@key_transformation)
|
80
|
+
@resource.new(object, params: params, within: within).to_h
|
81
|
+
end
|
42
82
|
end
|
43
83
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Alba
|
2
|
+
# Represents attribute with `if` option
|
3
|
+
class ConditionalAttribute
|
4
|
+
CONDITION_UNMET = Object.new.freeze
|
5
|
+
public_constant :CONDITION_UNMET # It's public for use in `Alba::Resource`
|
6
|
+
|
7
|
+
# @param body [Symbol, Proc, Alba::Association, Alba::TypedAttribute] real attribute wrapped with condition
|
8
|
+
# @param condition [Symbol, Proc] condition to check
|
9
|
+
def initialize(body:, condition:)
|
10
|
+
@body = body
|
11
|
+
@condition = condition
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns attribute body if condition passes
|
15
|
+
#
|
16
|
+
# @param resource [Alba::Resource]
|
17
|
+
# @param object [Object] needed for collection, each object from collection
|
18
|
+
# @return [ConditionalAttribute::CONDITION_UNMET, Object] CONDITION_UNMET if condition is unmet, fetched attribute otherwise
|
19
|
+
def with_passing_condition(resource:, object: nil)
|
20
|
+
return CONDITION_UNMET unless condition_passes?(resource, object)
|
21
|
+
|
22
|
+
fetched_attribute = yield(@body)
|
23
|
+
return fetched_attribute if fetched_attribute.nil? || !with_two_arity_proc_condition
|
24
|
+
|
25
|
+
return CONDITION_UNMET unless resource.instance_exec(object, attribute_from_association_body_or(fetched_attribute), &@condition)
|
26
|
+
|
27
|
+
fetched_attribute
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def condition_passes?(resource, object)
|
33
|
+
if @condition.is_a?(Proc)
|
34
|
+
arity = @condition.arity
|
35
|
+
# We can return early to skip fetch_attribute if arity is 1
|
36
|
+
# When arity is 2, we check the condition later
|
37
|
+
return true if arity >= 2
|
38
|
+
return false if arity <= 1 && !resource.instance_exec(object, &@condition)
|
39
|
+
|
40
|
+
true
|
41
|
+
else # Symbol
|
42
|
+
resource.__send__(@condition)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def with_two_arity_proc_condition
|
47
|
+
@condition.is_a?(Proc) && @condition.arity >= 2
|
48
|
+
end
|
49
|
+
|
50
|
+
def attribute_from_association_body_or(fetched_attribute)
|
51
|
+
@body.is_a?(Alba::Association) ? @body.object : fetched_attribute
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|