alba 3.1.0 → 3.3.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/.github/workflows/main.yml +1 -1
- data/.github/workflows/perf.yml +1 -6
- data/.rubocop.yml +10 -4
- data/CHANGELOG.md +14 -1
- data/Gemfile +5 -4
- data/README.md +86 -7
- data/Rakefile +2 -0
- data/benchmark/README.md +78 -44
- data/benchmark/collection.rb +4 -18
- data/benchmark/prep.rb +2 -28
- data/bin/console +1 -0
- data/docs/rails.md +10 -0
- data/gemfiles/without_active_support.gemfile +2 -0
- data/gemfiles/without_oj.gemfile +2 -0
- data/lib/alba/association.rb +17 -6
- data/lib/alba/conditional_attribute.rb +2 -4
- data/lib/alba/constants.rb +2 -0
- data/lib/alba/default_inflector.rb +2 -0
- data/lib/alba/deprecation.rb +2 -0
- data/lib/alba/errors.rb +2 -0
- data/lib/alba/layout.rb +2 -0
- data/lib/alba/nested_attribute.rb +5 -0
- data/lib/alba/railtie.rb +13 -0
- data/lib/alba/resource.rb +27 -21
- data/lib/alba/type.rb +2 -0
- data/lib/alba/typed_attribute.rb +2 -0
- data/lib/alba/version.rb +3 -1
- data/lib/alba.rb +29 -4
- data/script/perf_check.rb +1 -68
- metadata +3 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cabbda58cec9e9d6d1c96149f6d72d589a653f3db8112781a8a5267e4d3a5c25
|
4
|
+
data.tar.gz: b670cb0a6d034da44cab1f267a32e44fe2e99defd3d4318cbe03efc6bf02c8f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 350fd6d656ef37072e22033cb77cf9bb50f613f53392fcb20a0bc229ceeaa4a90f03ad41f75e1176f1adca65bb5a2eeffef13c33e60d288a71600fbaa5fb2572
|
7
|
+
data.tar.gz: 494d0202f252fd01613ba3621284e8d100d25df5d0e73e8732d4aaddc989ecf05282320c6f24c38548cd7ebc1d45920a44da6abaa09fb9efe7b180898d0ffdf3
|
data/.github/workflows/main.yml
CHANGED
@@ -12,7 +12,7 @@ jobs:
|
|
12
12
|
fail-fast: false
|
13
13
|
matrix:
|
14
14
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
15
|
-
ruby: ['3.0', 3.1, 3.2, head, truffleruby]
|
15
|
+
ruby: ['3.0', 3.1, 3.2, 3.3, head, jruby, truffleruby]
|
16
16
|
gemfile: [all, without_active_support, without_oj]
|
17
17
|
exclude:
|
18
18
|
- os: windows-latest
|
data/.github/workflows/perf.yml
CHANGED
@@ -4,18 +4,13 @@ on: [pull_request]
|
|
4
4
|
|
5
5
|
jobs:
|
6
6
|
build:
|
7
|
-
strategy:
|
8
|
-
fail-fast: false
|
9
|
-
matrix:
|
10
|
-
ruby: ['3.0', 3.1, 3.2]
|
11
7
|
runs-on: ubuntu-latest
|
12
8
|
steps:
|
13
9
|
- uses: actions/checkout@v4
|
14
10
|
- name: Set up Ruby
|
15
11
|
uses: ruby/setup-ruby@v1
|
16
12
|
with:
|
17
|
-
ruby-version:
|
18
|
-
bundler-cache: true
|
13
|
+
ruby-version: 3.3
|
19
14
|
- name: Run benchmark
|
20
15
|
run: |
|
21
16
|
ruby script/perf_check.rb
|
data/.rubocop.yml
CHANGED
@@ -88,6 +88,11 @@ Minitest/NoTestCases:
|
|
88
88
|
Exclude:
|
89
89
|
- 'test/dependencies/test_dependencies.rb'
|
90
90
|
|
91
|
+
# We need to use `OpenStruct` to wrap object in ConfitionalAttribute
|
92
|
+
Performance/OpenStruct:
|
93
|
+
Exclude:
|
94
|
+
- 'lib/alba/conditional_attribute.rb'
|
95
|
+
|
91
96
|
# We need to eval resource code to test errors on resource classes
|
92
97
|
Security/Eval:
|
93
98
|
Exclude:
|
@@ -116,10 +121,6 @@ Style/DocumentationMethod:
|
|
116
121
|
Exclude:
|
117
122
|
- 'README.md'
|
118
123
|
|
119
|
-
# This might be true in the future, but not many good things
|
120
|
-
Style/FrozenStringLiteralComment:
|
121
|
-
Enabled: false
|
122
|
-
|
123
124
|
# I don't want to think about error class in example code
|
124
125
|
Style/ImplicitRuntimeError:
|
125
126
|
Exclude:
|
@@ -140,6 +141,11 @@ Style/MethodCallWithArgsParentheses:
|
|
140
141
|
Style/MissingElse:
|
141
142
|
EnforcedStyle: case
|
142
143
|
|
144
|
+
# We need to use `OpenStruct` to wrap object in ConfitionalAttribute
|
145
|
+
Style/OpenStructUse:
|
146
|
+
Exclude:
|
147
|
+
- 'lib/alba/conditional_attribute.rb'
|
148
|
+
|
143
149
|
# It's example code, please forgive us
|
144
150
|
Style/OptionalBooleanParameter:
|
145
151
|
Exclude:
|
data/CHANGELOG.md
CHANGED
@@ -1,11 +1,24 @@
|
|
1
1
|
# Changelog
|
2
2
|
All notable changes to this project will be documented in this file.
|
3
3
|
|
4
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [3.3.0] 2024-10-09
|
10
|
+
|
11
|
+
### Added
|
12
|
+
|
13
|
+
- Add `ArrayOfString` and `ArrayOfInteger` type [#378](https://github.com/okuramasafumi/alba/pull/378)
|
14
|
+
|
15
|
+
## [3.2.0] 2024-06-21
|
16
|
+
|
17
|
+
### Added
|
18
|
+
|
19
|
+
- Rails controller integration [#370](https://github.com/okuramasafumi/alba/pull/370)
|
20
|
+
- Modification API: `transform_keys!` [#372](https://github.com/okuramasafumi/alba/pull/372)
|
21
|
+
|
9
22
|
## [3.1.0] 2024-03-23
|
10
23
|
|
11
24
|
### Added
|
data/Gemfile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
source 'https://rubygems.org'
|
2
4
|
|
3
5
|
# Specify your gem's dependencies in alba.gemspec
|
@@ -9,13 +11,12 @@ gem 'ffaker', require: false # For testing
|
|
9
11
|
gem 'minitest', '~> 5.14' # For test
|
10
12
|
gem 'railties', require: false # For Rails integration testing
|
11
13
|
gem 'rake', '~> 13.0' # For test and automation
|
12
|
-
gem 'rubocop', '~> 1.
|
14
|
+
gem 'rubocop', '~> 1.66.1', require: false # For lint
|
13
15
|
gem 'rubocop-gem_dev', '>= 0.3.0', require: false # For lint
|
14
16
|
gem 'rubocop-md', '~> 1.0', require: false # For lint
|
15
|
-
gem 'rubocop-minitest', '~> 0.
|
16
|
-
gem 'rubocop-performance', '~> 1.
|
17
|
+
gem 'rubocop-minitest', '~> 0.36.0', require: false # For lint
|
18
|
+
gem 'rubocop-performance', '~> 1.22.1', require: false # For lint
|
17
19
|
gem 'rubocop-rake', '~> 0.6.0', require: false # For lint
|
18
|
-
gem 'ruby-lsp', require: false # For language server
|
19
20
|
gem 'simplecov', '~> 0.22.0', require: false # For test coverage
|
20
21
|
gem 'simplecov-cobertura', require: false # For test coverage
|
21
22
|
# gem 'steep', require: false # For language server and typing
|
data/README.md
CHANGED
@@ -92,13 +92,13 @@ Alba is easy to use because there are only a few methods to remember. It's also
|
|
92
92
|
|
93
93
|
### Feature rich
|
94
94
|
|
95
|
-
While Alba's core is simple, it provides additional features when you need them
|
95
|
+
While Alba's core is simple, it provides additional features when you need them. For example, Alba provides [a way to control circular associations](#circular-associations-control), [root key and association resource name inference](#root-key-and-association-resource-name-inference) and [supports layouts](#layout).
|
96
96
|
|
97
97
|
### Other reasons
|
98
98
|
|
99
99
|
- Dependency free, no need to install `oj` or `activesupport` while Alba works well with them
|
100
100
|
- Well tested, the test coverage is 99%
|
101
|
-
- Well maintained,
|
101
|
+
- Well maintained, getting frequent update and new releases (see [version history](https://rubygems.org/gems/alba/versions))
|
102
102
|
|
103
103
|
## Installation
|
104
104
|
|
@@ -317,7 +317,7 @@ end
|
|
317
317
|
FooResource.new(Foo.new).serialize
|
318
318
|
```
|
319
319
|
|
320
|
-
By default, Alba
|
320
|
+
By default, Alba creates the JSON as `'{"bar":"This is FooResource"}'`. This means Alba calls a method on a Resource class and doesn't call a method on a target object. This rule is applied to methods that are explicitly defined on Resource class, so methods that Resource class inherits from `Object` class such as `format` are ignored.
|
321
321
|
|
322
322
|
```ruby
|
323
323
|
class Foo
|
@@ -363,7 +363,7 @@ end
|
|
363
363
|
FooResource.new(Foo.new).serialize
|
364
364
|
# => '{"bar":"This is Foo"}'
|
365
365
|
```
|
366
|
-
|
366
|
+
|
367
367
|
#### Params
|
368
368
|
|
369
369
|
You can pass a Hash to the resource for internal use. It can be used as "flags" to control attribute content.
|
@@ -1023,6 +1023,36 @@ user = User.new(1, nil, nil)
|
|
1023
1023
|
UserResource.new(user).serialize # => '{"id":1}'
|
1024
1024
|
```
|
1025
1025
|
|
1026
|
+
#### Caution for the second parameter in `if` proc
|
1027
|
+
|
1028
|
+
`if` proc takes two parameters. The first one is the target object, `user` in the example above. The second one is `attribute` representing each attribute `if` option affects. Note that it actually calls attribute methods, so you cannot use it to prevent attribute methods called. This means if the target object is an `ActiveRecord::Base` object and using `association` with `if` option, you might want to skip the second parameter so that the SQL query won't be issued.
|
1029
|
+
|
1030
|
+
Example:
|
1031
|
+
|
1032
|
+
```ruby
|
1033
|
+
class User < ApplicationRecord
|
1034
|
+
has_many :posts
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
class Post < ApplicationRecord
|
1038
|
+
belongs_to :user
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
class UserResource
|
1042
|
+
include Alba::Resource
|
1043
|
+
|
1044
|
+
# Since `_posts` parameter exists, `user.posts` are loaded
|
1045
|
+
many :posts, if: proc { |user, _posts| user.admin? }
|
1046
|
+
end
|
1047
|
+
|
1048
|
+
class UserResource2
|
1049
|
+
include Alba::Resource
|
1050
|
+
|
1051
|
+
# Since `_posts` parameter doesn't exist, `user.posts` are NOT loaded
|
1052
|
+
many :posts, if: proc { |user| user.admin? && params[:include_post] }
|
1053
|
+
end
|
1054
|
+
```
|
1055
|
+
|
1026
1056
|
### Default
|
1027
1057
|
|
1028
1058
|
Alba doesn't support default value for attributes, but it's easy to set a default value.
|
@@ -1255,7 +1285,7 @@ UserResourceWithDifferentMetaKey.new([user]).serialize
|
|
1255
1285
|
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"my_meta":{"foo":"bar"}}'
|
1256
1286
|
|
1257
1287
|
UserResourceWithDifferentMetaKey.new([user]).serialize(meta: {extra: 42})
|
1258
|
-
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"
|
1288
|
+
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"my_meta":{"foo":"bar","extra":42}}'
|
1259
1289
|
|
1260
1290
|
class UserResourceChangingMetaKeyOnly
|
1261
1291
|
include Alba::Resource
|
@@ -1379,6 +1409,12 @@ end
|
|
1379
1409
|
|
1380
1410
|
You now get `created_at` attribute with `iso8601` format!
|
1381
1411
|
|
1412
|
+
#### Generating TypeScript types with typelizer gem
|
1413
|
+
|
1414
|
+
We often want TypeScript types corresponding to serializers. That's possible with [typelizer](https://github.com/skryukov/typelizer) gem.
|
1415
|
+
|
1416
|
+
For more information, please read its README.
|
1417
|
+
|
1382
1418
|
### Collection serialization into Hash
|
1383
1419
|
|
1384
1420
|
Sometimes we want to serialize a collection into a Hash, not an Array. It's possible with Alba.
|
@@ -1517,7 +1553,36 @@ end
|
|
1517
1553
|
|
1518
1554
|
Within `helper` block, all methods should be defined without `self.`.
|
1519
1555
|
|
1520
|
-
|
1556
|
+
### Experimental: modification API
|
1557
|
+
|
1558
|
+
Alba now provides an experimental API to modify existing resource class without adding new classes. Currently only `transform_keys!` is implemented.
|
1559
|
+
|
1560
|
+
Modification API returns a new class with given modifications. It's useful when you want lots of resource classes with small changes. See it in action:
|
1561
|
+
|
1562
|
+
```ruby
|
1563
|
+
class FooResource
|
1564
|
+
include Alba::Resource
|
1565
|
+
|
1566
|
+
transform_keys :camel
|
1567
|
+
|
1568
|
+
attributes :id
|
1569
|
+
end
|
1570
|
+
|
1571
|
+
# Rails app
|
1572
|
+
class FoosController < ApplicationController
|
1573
|
+
def index
|
1574
|
+
foos = Foo.where(some: :condition)
|
1575
|
+
key_transformation_type = params[:key_transformation_type] # Say it's "lower_camel"
|
1576
|
+
# When params is absent, do not use modification API since it's slower
|
1577
|
+
resource_class = key_transformation_type ? FooResource.transform_keys!(key_transformation_type) : FooResource
|
1578
|
+
render json: resource_class.new(foos).serialize # The keys are lower_camel
|
1579
|
+
end
|
1580
|
+
end
|
1581
|
+
```
|
1582
|
+
|
1583
|
+
The point is that there's no need to define classes for each key transformation type (dash, camel, lower_camel and snake). This gives even more flexibility.
|
1584
|
+
|
1585
|
+
There are some drawbacks with this approach. For example, it creates an internal, anonymous class when it's called, so there is a performance penalty and debugging difficulty. It's recommended to define classes manually when you don't need high flexibility.
|
1521
1586
|
|
1522
1587
|
### Caching
|
1523
1588
|
|
@@ -1659,6 +1724,20 @@ class BarResource
|
|
1659
1724
|
end
|
1660
1725
|
```
|
1661
1726
|
|
1727
|
+
You can also pass options to your helpers.
|
1728
|
+
|
1729
|
+
```ruby
|
1730
|
+
module AlbaExtension
|
1731
|
+
def time_attributes(*attrs, **options)
|
1732
|
+
attrs.each do |attr|
|
1733
|
+
attribute(attr, **options) do |object|
|
1734
|
+
object.__send__(attr).iso8601
|
1735
|
+
end
|
1736
|
+
end
|
1737
|
+
end
|
1738
|
+
end
|
1739
|
+
```
|
1740
|
+
|
1662
1741
|
### Debugging
|
1663
1742
|
|
1664
1743
|
Debugging is not easy. If you find Alba not working as you expect, there are a few things to do:
|
@@ -1695,7 +1774,7 @@ module Logging
|
|
1695
1774
|
# `...` was added in Ruby 2.7
|
1696
1775
|
def serialize(...)
|
1697
1776
|
puts serializable_hash
|
1698
|
-
super
|
1777
|
+
super
|
1699
1778
|
end
|
1700
1779
|
end
|
1701
1780
|
|
data/Rakefile
CHANGED
data/benchmark/README.md
CHANGED
@@ -10,76 +10,110 @@ Machine spec:
|
|
10
10
|
|
11
11
|
|Key|Value|
|
12
12
|
|---|---|
|
13
|
-
|OS|macOS
|
14
|
-
|CPU|
|
15
|
-
|RAM|
|
16
|
-
|Ruby|ruby 3.
|
13
|
+
|OS|macOS 14.7|
|
14
|
+
|CPU|Apple M1 Pro|
|
15
|
+
|RAM|16GB|
|
16
|
+
|Ruby|ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [arm64-darwin23]|
|
17
17
|
|
18
18
|
Library versions:
|
19
19
|
|
20
20
|
|Library|Version|
|
21
21
|
|---|---|
|
22
|
-
|alba|
|
23
|
-
|blueprinter|
|
22
|
+
|alba|3.2.0|
|
23
|
+
|blueprinter|1.1.0|
|
24
24
|
|fast_serializer_ruby|0.6.9|
|
25
25
|
|jserializer|0.2.1|
|
26
|
-
|oj|3.
|
26
|
+
|oj|3.16.6|
|
27
27
|
|simple_ams|0.2.6|
|
28
28
|
|representable|3.2.0|
|
29
|
-
|turbostreamer|1.
|
30
|
-
|jbuilder|2.
|
31
|
-
|panko_serializer|0.
|
32
|
-
|active_model_serializers|0.10.
|
29
|
+
|turbostreamer|1.11.0|
|
30
|
+
|jbuilder|2.13.0|
|
31
|
+
|panko_serializer|0.8.2|
|
32
|
+
|active_model_serializers|0.10.14|
|
33
33
|
|
34
34
|
`benchmark-ips` with `Oj.optimize_rails`:
|
35
35
|
|
36
36
|
```
|
37
37
|
Comparison:
|
38
|
-
panko:
|
39
|
-
jserializer:
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
fast_serializer:
|
45
|
-
blueprinter:
|
46
|
-
representable:
|
47
|
-
simple_ams:
|
48
|
-
ams:
|
38
|
+
panko: 447.0 i/s
|
39
|
+
jserializer: 168.9 i/s - 2.65x slower
|
40
|
+
alba_inline: 149.4 i/s - 2.99x slower
|
41
|
+
alba: 146.5 i/s - 3.05x slower
|
42
|
+
turbostreamer: 138.7 i/s - 3.22x slower
|
43
|
+
rails: 105.6 i/s - 4.23x slower
|
44
|
+
fast_serializer: 97.6 i/s - 4.58x slower
|
45
|
+
blueprinter: 66.7 i/s - 6.70x slower
|
46
|
+
representable: 50.6 i/s - 8.83x slower
|
47
|
+
simple_ams: 35.5 i/s - 12.57x slower
|
48
|
+
ams: 14.8 i/s - 30.25x slower
|
49
49
|
```
|
50
50
|
|
51
51
|
`benchmark-ips` without `Oj.optimize_rails`:
|
52
52
|
|
53
53
|
```
|
54
54
|
Comparison:
|
55
|
-
panko:
|
56
|
-
|
57
|
-
|
58
|
-
alba_inline:
|
59
|
-
|
60
|
-
fast_serializer:
|
61
|
-
|
62
|
-
|
63
|
-
representable:
|
64
|
-
simple_ams:
|
65
|
-
ams:
|
55
|
+
panko: 457.9 i/s
|
56
|
+
jserializer: 165.9 i/s - 2.76x slower
|
57
|
+
alba: 160.1 i/s - 2.86x slower
|
58
|
+
alba_inline: 158.5 i/s - 2.89x slower
|
59
|
+
turbostreamer: 141.7 i/s - 3.23x slower
|
60
|
+
fast_serializer: 96.2 i/s - 4.76x slower
|
61
|
+
rails: 87.2 i/s - 5.25x slower
|
62
|
+
blueprinter: 67.4 i/s - 6.80x slower
|
63
|
+
representable: 43.4 i/s - 10.55x slower
|
64
|
+
simple_ams: 34.7 i/s - 13.20x slower
|
65
|
+
ams: 14.2 i/s - 32.28x slower
|
66
|
+
```
|
67
|
+
|
68
|
+
`benchmark-ips` with `Oj.optimize_rail` and YJIT:
|
69
|
+
|
70
|
+
```
|
71
|
+
Comparison:
|
72
|
+
panko: 676.6 i/s
|
73
|
+
jserializer: 285.3 i/s - 2.37x slower
|
74
|
+
turbostreamer: 264.2 i/s - 2.56x slower
|
75
|
+
alba: 258.9 i/s - 2.61x slower
|
76
|
+
fast_serializer: 179.0 i/s - 3.78x slower
|
77
|
+
rails: 150.7 i/s - 4.49x slower
|
78
|
+
alba_inline: 131.5 i/s - 5.15x slower
|
79
|
+
blueprinter: 110.0 i/s - 6.15x slower
|
80
|
+
representable: 73.5 i/s - 9.21x slower
|
81
|
+
simple_ams: 62.8 i/s - 10.77x slower
|
82
|
+
ams: 20.4 i/s - 33.10x slower
|
83
|
+
```
|
84
|
+
|
85
|
+
`benchmark-ips` with YJIT and without `Oj.optimize_rail`:
|
86
|
+
|
87
|
+
```
|
88
|
+
Comparison:
|
89
|
+
panko: 701.9 i/s
|
90
|
+
alba: 311.1 i/s - 2.26x slower
|
91
|
+
jserializer: 281.6 i/s - 2.49x slower
|
92
|
+
turbostreamer: 240.4 i/s - 2.92x slower
|
93
|
+
fast_serializer: 180.5 i/s - 3.89x slower
|
94
|
+
alba_inline: 135.6 i/s - 5.18x slower
|
95
|
+
rails: 131.4 i/s - 5.34x slower
|
96
|
+
blueprinter: 110.7 i/s - 6.34x slower
|
97
|
+
representable: 70.5 i/s - 9.96x slower
|
98
|
+
simple_ams: 57.3 i/s - 12.24x slower
|
99
|
+
ams: 20.3 i/s - 34.51x slower
|
66
100
|
```
|
67
101
|
|
68
102
|
`benchmark-memory`:
|
69
103
|
|
70
104
|
```
|
71
105
|
Comparison:
|
72
|
-
panko:
|
73
|
-
turbostreamer:
|
74
|
-
jserializer:
|
75
|
-
alba:
|
76
|
-
alba_inline:
|
77
|
-
fast_serializer:
|
78
|
-
rails:
|
79
|
-
blueprinter:
|
80
|
-
representable:
|
81
|
-
ams:
|
82
|
-
simple_ams:
|
106
|
+
panko: 259178 allocated
|
107
|
+
turbostreamer: 817800 allocated - 3.16x more
|
108
|
+
jserializer: 826425 allocated - 3.19x more
|
109
|
+
alba: 846465 allocated - 3.27x more
|
110
|
+
alba_inline: 867361 allocated - 3.35x more
|
111
|
+
fast_serializer: 1474345 allocated - 5.69x more
|
112
|
+
rails: 2265905 allocated - 8.74x more
|
113
|
+
blueprinter: 2469905 allocated - 9.53x more
|
114
|
+
representable: 4994281 allocated - 19.27x more
|
115
|
+
ams: 5233265 allocated - 20.19x more
|
116
|
+
simple_ams: 9506817 allocated - 36.68x more
|
83
117
|
```
|
84
118
|
|
85
119
|
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, but Alba is slightly slower than other two. With `Oj.optimize_rails`, `jbuilder` and Rails standard serialization are also fast.
|
data/benchmark/collection.rb
CHANGED
@@ -221,7 +221,6 @@ blueprinter = Proc.new { PostBlueprint.render(posts) }
|
|
221
221
|
fast_serializer = Proc.new { FastSerializerPostResource.new(posts).to_json }
|
222
222
|
jserializer = Proc.new { JserializerPostSerializer.new(posts, is_collection: true).to_json }
|
223
223
|
panko = proc { Panko::ArraySerializer.new(posts, each_serializer: PankoPostSerializer).to_json }
|
224
|
-
primalize = proc { PrimalizePostsResource.new(posts: posts).to_json }
|
225
224
|
rails = Proc.new do
|
226
225
|
ActiveSupport::JSON.encode(posts.map{ |post| post.serializable_hash(include: :comments) })
|
227
226
|
end
|
@@ -253,8 +252,7 @@ end
|
|
253
252
|
|
254
253
|
# --- Run the benchmarks ---
|
255
254
|
|
256
|
-
|
257
|
-
Benchmark.ips do |x|
|
255
|
+
benchmark_body = lambda do |x|
|
258
256
|
x.report(:alba, &alba)
|
259
257
|
x.report(:alba_inline, &alba_inline)
|
260
258
|
x.report(:ams, &ams)
|
@@ -270,20 +268,8 @@ Benchmark.ips do |x|
|
|
270
268
|
x.compare!
|
271
269
|
end
|
272
270
|
|
271
|
+
require 'benchmark/ips'
|
272
|
+
Benchmark.ips(&benchmark_body)
|
273
273
|
|
274
274
|
require 'benchmark/memory'
|
275
|
-
Benchmark.memory
|
276
|
-
x.report(:alba, &alba)
|
277
|
-
x.report(:alba_inline, &alba_inline)
|
278
|
-
x.report(:ams, &ams)
|
279
|
-
x.report(:blueprinter, &blueprinter)
|
280
|
-
x.report(:fast_serializer, &fast_serializer)
|
281
|
-
x.report(:jserializer, &jserializer)
|
282
|
-
x.report(:panko, &panko)
|
283
|
-
x.report(:rails, &rails)
|
284
|
-
x.report(:representable, &representable)
|
285
|
-
x.report(:simple_ams, &simple_ams)
|
286
|
-
x.report(:turbostreamer, &turbostreamer)
|
287
|
-
|
288
|
-
x.compare!
|
289
|
-
end
|
275
|
+
Benchmark.memory(&benchmark_body)
|
data/benchmark/prep.rb
CHANGED
@@ -1,33 +1,7 @@
|
|
1
|
-
# --- Bundle dependencies ---
|
2
|
-
|
3
|
-
require "bundler/inline"
|
4
|
-
|
5
|
-
gemfile(true) do
|
6
|
-
source "https://rubygems.org"
|
7
|
-
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
8
|
-
|
9
|
-
gem "active_model_serializers"
|
10
|
-
gem "activerecord", "6.1.3"
|
11
|
-
gem "alba", path: '../'
|
12
|
-
gem "benchmark-ips"
|
13
|
-
gem "benchmark-memory"
|
14
|
-
gem "blueprinter"
|
15
|
-
gem "fast_serializer_ruby"
|
16
|
-
gem "jbuilder"
|
17
|
-
gem 'turbostreamer'
|
18
|
-
gem "jserializer"
|
19
|
-
gem "multi_json"
|
20
|
-
gem "panko_serializer"
|
21
|
-
gem "pg"
|
22
|
-
gem "primalize"
|
23
|
-
gem "oj"
|
24
|
-
gem "representable"
|
25
|
-
gem "simple_ams"
|
26
|
-
gem "sqlite3"
|
27
|
-
end
|
28
|
-
|
29
1
|
# --- Test data model setup ---
|
30
2
|
|
3
|
+
RubyVM::YJIT.enable if ENV["YJIT"]
|
4
|
+
require "csv"
|
31
5
|
require "pg"
|
32
6
|
require "active_record"
|
33
7
|
require "active_record/connection_adapters/postgresql_adapter"
|
data/bin/console
CHANGED
data/docs/rails.md
CHANGED
@@ -44,3 +44,13 @@ render json: FooResource.new(foo), only: [:id]
|
|
44
44
|
# This is OK
|
45
45
|
render json: FooResource.new(foo), status: 200
|
46
46
|
```
|
47
|
+
|
48
|
+
### Shorthand for rendering JSON with Alba
|
49
|
+
|
50
|
+
Now you can render JSON with shorthand.
|
51
|
+
|
52
|
+
First, using `render json: serialize(target)` renders JSON for given target object. You can pass `with: SomeSerializer` option to render with `SomeSerializer` in this case. If you skip `with` option Alba tries to find the correct serialize automatically.
|
53
|
+
|
54
|
+
There is a shorter version: `render_serialized_json(target)`. It also accepts `with` option.
|
55
|
+
|
56
|
+
It's recommended to use `with` option now since it cannot automatically find correct serializers sometimes.
|
data/gemfiles/without_oj.gemfile
CHANGED
data/lib/alba/association.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Alba
|
2
4
|
# Representing association
|
3
5
|
# @api private
|
@@ -29,6 +31,11 @@ module Alba
|
|
29
31
|
assign_resource(nesting, key_transformation, block, helper)
|
30
32
|
end
|
31
33
|
|
34
|
+
# This is the same API in `NestedAttribute`
|
35
|
+
def key_transformation=(type)
|
36
|
+
@resource.transform_keys(type) unless @resource.is_a?(Proc)
|
37
|
+
end
|
38
|
+
|
32
39
|
# Recursively converts an object into a Hash
|
33
40
|
#
|
34
41
|
# @param target [Object] the object having an association method
|
@@ -65,13 +72,9 @@ module Alba
|
|
65
72
|
end
|
66
73
|
end
|
67
74
|
|
68
|
-
def assign_resource(nesting, key_transformation, block, helper)
|
75
|
+
def assign_resource(nesting, key_transformation, block, helper)
|
69
76
|
@resource = if block
|
70
|
-
|
71
|
-
klass.helper(helper) if helper
|
72
|
-
klass.transform_keys(key_transformation)
|
73
|
-
klass.class_eval(&block)
|
74
|
-
klass
|
77
|
+
charged_resource_class(helper, key_transformation, block)
|
75
78
|
elsif Alba.inflector
|
76
79
|
Alba.infer_resource_class(@name, nesting: nesting)
|
77
80
|
else
|
@@ -79,6 +82,14 @@ module Alba
|
|
79
82
|
end
|
80
83
|
end
|
81
84
|
|
85
|
+
def charged_resource_class(helper, key_transformation, block)
|
86
|
+
klass = Alba.resource_class
|
87
|
+
klass.helper(helper) if helper
|
88
|
+
klass.transform_keys(key_transformation)
|
89
|
+
klass.class_eval(&block)
|
90
|
+
klass
|
91
|
+
end
|
92
|
+
|
82
93
|
def to_h_with_each_resource(object, within, params)
|
83
94
|
object.map do |item|
|
84
95
|
@resource.call(item).new(item, within: within, params: params).to_h
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'association'
|
2
4
|
require_relative 'constants'
|
3
5
|
require 'ostruct'
|
@@ -51,8 +53,6 @@ module Alba
|
|
51
53
|
|
52
54
|
# OpenStruct is used as a simple solution for converting Hash or Array of Hash into an object
|
53
55
|
# Using OpenStruct is not good in general, but in this case there's no other solution
|
54
|
-
# rubocop:disable Style/OpenStructUse
|
55
|
-
# rubocop:disable Performance/OpenStruct
|
56
56
|
def objectize(fetched_attribute)
|
57
57
|
return fetched_attribute unless @body.is_a?(Alba::Association)
|
58
58
|
|
@@ -64,7 +64,5 @@ module Alba
|
|
64
64
|
OpenStruct.new(fetched_attribute)
|
65
65
|
end
|
66
66
|
end
|
67
|
-
# rubocop:enable Style/OpenStructUse
|
68
|
-
# rubocop:enable Performance/OpenStruct
|
69
67
|
end
|
70
68
|
end
|
data/lib/alba/constants.rb
CHANGED
data/lib/alba/deprecation.rb
CHANGED
data/lib/alba/errors.rb
CHANGED
data/lib/alba/layout.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Alba
|
2
4
|
# Representing nested attribute
|
3
5
|
# @api private
|
4
6
|
class NestedAttribute
|
7
|
+
# Setter for key_transformation, used when it's changed after class definition
|
8
|
+
attr_writer :key_transformation
|
9
|
+
|
5
10
|
# @param key_transformation [Symbol] determines how to transform keys
|
6
11
|
# @param block [Proc] class body
|
7
12
|
def initialize(key_transformation: :none, &block)
|
data/lib/alba/railtie.rb
CHANGED
@@ -1,8 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Alba
|
2
4
|
# Rails integration
|
3
5
|
class Railtie < Rails::Railtie
|
4
6
|
initializer 'alba.initialize' do
|
5
7
|
Alba.inflector = :active_support
|
8
|
+
|
9
|
+
ActiveSupport.on_load(:action_controller) do
|
10
|
+
ActionController::Base.define_method(:serialize) do |obj, with: nil, &block|
|
11
|
+
with.nil? ? Alba.resource_with(obj, &block) : with.new(obj)
|
12
|
+
end
|
13
|
+
|
14
|
+
ActionController::Base.define_method(:render_serialized_json) do |obj, with: nil, &block|
|
15
|
+
json = with.nil? ? Alba.resource_with(obj, &block) : with.new(obj)
|
16
|
+
render json: json
|
17
|
+
end
|
18
|
+
end
|
6
19
|
end
|
7
20
|
end
|
8
21
|
end
|
data/lib/alba/resource.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'association'
|
2
4
|
require_relative 'conditional_attribute'
|
3
5
|
require_relative 'constants'
|
@@ -22,17 +24,17 @@ module Alba
|
|
22
24
|
# @private
|
23
25
|
def self.included(base) # rubocop:disable Metrics/MethodLength
|
24
26
|
super
|
25
|
-
setup_method_body = +'private def _setup;'
|
26
27
|
base.class_eval do
|
27
28
|
# Initialize
|
29
|
+
setup_method_body = +'private def _setup;'
|
28
30
|
INTERNAL_VARIABLES.each do |name, initial|
|
29
31
|
instance_variable_set(:"@#{name}", initial.dup) unless instance_variable_defined?(:"@#{name}")
|
30
32
|
setup_method_body << "@#{name} = self.class.#{name};"
|
31
33
|
end
|
32
|
-
|
34
|
+
setup_method_body << 'end'
|
35
|
+
class_eval(setup_method_body, __FILE__, __LINE__ + 1)
|
36
|
+
define_method(:encode, Alba.encoder)
|
33
37
|
end
|
34
|
-
setup_method_body << 'end'
|
35
|
-
base.class_eval(setup_method_body, __FILE__, __LINE__ + 1)
|
36
38
|
base.include InstanceMethods
|
37
39
|
base.extend ClassMethods
|
38
40
|
end
|
@@ -234,23 +236,7 @@ module Alba
|
|
234
236
|
def transform_key(key)
|
235
237
|
return Alba.regularize_key(key) if @_transform_type == :none || key.nil? || key.empty? # We can skip transformation
|
236
238
|
|
237
|
-
|
238
|
-
raise Alba::Error, 'Inflector is nil. You must set inflector before transforming keys.' unless inflector
|
239
|
-
|
240
|
-
Alba.regularize_key(_transform_key(inflector, key.to_s))
|
241
|
-
end
|
242
|
-
|
243
|
-
def _transform_key(inflector, key)
|
244
|
-
case @_transform_type
|
245
|
-
when :camel then inflector.camelize(key)
|
246
|
-
when :lower_camel then inflector.camelize_lower(key)
|
247
|
-
when :dash then inflector.dasherize(key)
|
248
|
-
when :snake then inflector.underscore(key)
|
249
|
-
else
|
250
|
-
# :nocov:
|
251
|
-
raise Alba::Error, "Unknown transform type: #{@_transform_type}"
|
252
|
-
# :nocov:
|
253
|
-
end
|
239
|
+
Alba.transform_key(key, transform_type: @_transform_type)
|
254
240
|
end
|
255
241
|
|
256
242
|
def fetch_attribute(obj, key, attribute) # rubocop:disable Metrics/CyclomaticComplexity
|
@@ -486,6 +472,26 @@ module Alba
|
|
486
472
|
@_key_transformation_cascade = cascade
|
487
473
|
end
|
488
474
|
|
475
|
+
# Transform keys as specified type AFTER the class is defined
|
476
|
+
# Note that this is an experimental API and may be removed/changed
|
477
|
+
#
|
478
|
+
# @see #transform_keys
|
479
|
+
def transform_keys!(type)
|
480
|
+
dup.class_eval do
|
481
|
+
transform_keys(type, root: @_transforming_root_key, cascade: @_key_transformation_cascade)
|
482
|
+
|
483
|
+
if @_key_transformation_cascade
|
484
|
+
# We need to update key transformation of associations and nested attributes
|
485
|
+
@_attributes.each_value do |attr|
|
486
|
+
next unless attr.is_a?(Association) || attr.is_a?(NestedAttribute)
|
487
|
+
|
488
|
+
attr.key_transformation = type
|
489
|
+
end
|
490
|
+
end
|
491
|
+
self # Return the new class
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
489
495
|
# Sets key for collection serialization
|
490
496
|
#
|
491
497
|
# @param key [String, Symbol]
|
data/lib/alba/type.rb
CHANGED
data/lib/alba/typed_attribute.rb
CHANGED
data/lib/alba/version.rb
CHANGED
data/lib/alba.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require_relative 'alba/version'
|
3
5
|
require_relative 'alba/errors'
|
@@ -142,6 +144,26 @@ module Alba
|
|
142
144
|
@symbolize_keys ? key.to_sym : key.to_s
|
143
145
|
end
|
144
146
|
|
147
|
+
# Transform a key with given transform_type
|
148
|
+
#
|
149
|
+
# @param key [String] a target key
|
150
|
+
# @param transform_type [Symbol] a transform type, either one of `camel`, `lower_camel`, `dash` or `snake`
|
151
|
+
# @return [String]
|
152
|
+
def transform_key(key, transform_type:)
|
153
|
+
raise Alba::Error, 'Inflector is nil. You must set inflector before transforming keys.' unless inflector
|
154
|
+
|
155
|
+
key = key.to_s
|
156
|
+
|
157
|
+
k = case transform_type
|
158
|
+
when :camel then inflector.camelize(key)
|
159
|
+
when :lower_camel then inflector.camelize_lower(key)
|
160
|
+
when :dash then inflector.dasherize(key)
|
161
|
+
when :snake then inflector.underscore(key)
|
162
|
+
else raise Alba::Error, "Unknown transform type: #{transform_type}"
|
163
|
+
end
|
164
|
+
regularize_key(k)
|
165
|
+
end
|
166
|
+
|
145
167
|
# Register types, used for both builtin and custom types
|
146
168
|
#
|
147
169
|
# @see Alba::Type
|
@@ -170,8 +192,6 @@ module Alba
|
|
170
192
|
register_default_types
|
171
193
|
end
|
172
194
|
|
173
|
-
private
|
174
|
-
|
175
195
|
# This method could be part of public API, but for now it's private
|
176
196
|
def resource_with(object, &block)
|
177
197
|
klass = block ? resource_class(&block) : infer_resource_class(object.class.name)
|
@@ -179,6 +199,8 @@ module Alba
|
|
179
199
|
klass.new(object)
|
180
200
|
end
|
181
201
|
|
202
|
+
private
|
203
|
+
|
182
204
|
def inflector_from(name_or_module)
|
183
205
|
case name_or_module
|
184
206
|
when nil then nil
|
@@ -239,14 +261,17 @@ module Alba
|
|
239
261
|
inflector
|
240
262
|
end
|
241
263
|
|
242
|
-
def register_default_types
|
264
|
+
def register_default_types # rubocop:disable Mertics/AbcSize
|
243
265
|
[String, :String].each do |t|
|
244
|
-
register_type(t, check: ->(obj) { obj.is_a?(String) }, converter:
|
266
|
+
register_type(t, check: ->(obj) { obj.is_a?(String) }, converter: lambda(&:to_s))
|
245
267
|
end
|
246
268
|
[Integer, :Integer].each do |t|
|
247
269
|
register_type(t, check: ->(obj) { obj.is_a?(Integer) }, converter: ->(obj) { Integer(obj) })
|
248
270
|
end
|
249
271
|
register_type(:Boolean, check: ->(obj) { [true, false].include?(obj) }, converter: ->(obj) { !!obj })
|
272
|
+
[String, Integer].each do |t|
|
273
|
+
register_type(:"ArrayOf#{t}", check: ->(d) { d.is_a?(Array) && d.all? { _1.is_a?(t) } })
|
274
|
+
end
|
250
275
|
end
|
251
276
|
end
|
252
277
|
|
data/script/perf_check.rb
CHANGED
@@ -2,74 +2,7 @@
|
|
2
2
|
# Fetch Alba from local, otherwise fetch latest from RubyGems
|
3
3
|
# exit(status)
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
require "bundler/inline"
|
8
|
-
|
9
|
-
gemfile(true) do
|
10
|
-
source "https://rubygems.org"
|
11
|
-
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
12
|
-
|
13
|
-
gem "activerecord", "~> 6.1.3"
|
14
|
-
gem "alba", path: '../'
|
15
|
-
gem "benchmark-ips"
|
16
|
-
gem "blueprinter"
|
17
|
-
gem "jbuilder"
|
18
|
-
gem "multi_json"
|
19
|
-
gem "oj"
|
20
|
-
gem "sqlite3"
|
21
|
-
end
|
22
|
-
|
23
|
-
# --- Test data model setup ---
|
24
|
-
|
25
|
-
require "active_record"
|
26
|
-
require "oj"
|
27
|
-
require "sqlite3"
|
28
|
-
Oj.optimize_rails
|
29
|
-
|
30
|
-
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
31
|
-
|
32
|
-
ActiveRecord::Schema.define do
|
33
|
-
create_table :posts, force: true do |t|
|
34
|
-
t.string :body
|
35
|
-
end
|
36
|
-
|
37
|
-
create_table :comments, force: true do |t|
|
38
|
-
t.integer :post_id
|
39
|
-
t.string :body
|
40
|
-
t.integer :commenter_id
|
41
|
-
end
|
42
|
-
|
43
|
-
create_table :users, force: true do |t|
|
44
|
-
t.string :name
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
class Post < ActiveRecord::Base
|
49
|
-
has_many :comments
|
50
|
-
has_many :commenters, through: :comments, class_name: 'User', source: :commenter
|
51
|
-
|
52
|
-
def attributes
|
53
|
-
{id: nil, body: nil, commenter_names: commenter_names}
|
54
|
-
end
|
55
|
-
|
56
|
-
def commenter_names
|
57
|
-
commenters.pluck(:name)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
class Comment < ActiveRecord::Base
|
62
|
-
belongs_to :post
|
63
|
-
belongs_to :commenter, class_name: 'User'
|
64
|
-
|
65
|
-
def attributes
|
66
|
-
{id: nil, body: nil}
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
class User < ActiveRecord::Base
|
71
|
-
has_many :comments
|
72
|
-
end
|
5
|
+
require_relative '../benchmark/prep'
|
73
6
|
|
74
7
|
# --- Alba serializers ---
|
75
8
|
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: alba
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- OKURA Masafumi
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date: 2024-
|
10
|
+
date: 2024-10-09 00:00:00.000000000 Z
|
12
11
|
dependencies: []
|
13
12
|
description: Alba is the fastest JSON serializer for Ruby. It focuses on performance,
|
14
13
|
flexibility and usability.
|
@@ -79,7 +78,6 @@ metadata:
|
|
79
78
|
documentation_uri: https://rubydoc.info/github/okuramasafumi/alba
|
80
79
|
source_code_uri: https://github.com/okuramasafumi/alba
|
81
80
|
rubygems_mfa_required: 'true'
|
82
|
-
post_install_message:
|
83
81
|
rdoc_options: []
|
84
82
|
require_paths:
|
85
83
|
- lib
|
@@ -94,8 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
92
|
- !ruby/object:Gem::Version
|
95
93
|
version: '0'
|
96
94
|
requirements: []
|
97
|
-
rubygems_version: 3.
|
98
|
-
signing_key:
|
95
|
+
rubygems_version: 3.6.0.dev
|
99
96
|
specification_version: 4
|
100
97
|
summary: Alba is the fastest JSON serializer for Ruby.
|
101
98
|
test_files: []
|