alba 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|