alba 3.1.0 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0bea04ee2e971c750f9d62eca519a5401e48ab1361199581bad50f3a89e7544
4
- data.tar.gz: 726fc7d31243362d2ef4165a3784a1a36eb8f067af00fc9c25664ff1c15650ac
3
+ metadata.gz: cabbda58cec9e9d6d1c96149f6d72d589a653f3db8112781a8a5267e4d3a5c25
4
+ data.tar.gz: b670cb0a6d034da44cab1f267a32e44fe2e99defd3d4318cbe03efc6bf02c8f4
5
5
  SHA512:
6
- metadata.gz: f249c1974afc00291145059d9fcb61250948138d0ccbf33cadf1b221be1318c3425b24a0a12b16233452363ec7eda292da3a798a3534465eafba9594740e301c
7
- data.tar.gz: 3e5e3e6d6c5889fa26adf5f3164bac67f7904de765e55e67c3290e38024838bbd2439a1e4e69e2b284dd39624eb5094fd8740cc15ca56794ca1d5fdbd76d8288
6
+ metadata.gz: 350fd6d656ef37072e22033cb77cf9bb50f613f53392fcb20a0bc229ceeaa4a90f03ad41f75e1176f1adca65bb5a2eeffef13c33e60d288a71600fbaa5fb2572
7
+ data.tar.gz: 494d0202f252fd01613ba3621284e8d100d25df5d0e73e8732d4aaddc989ecf05282320c6f24c38548cd7ebc1d45920a44da6abaa09fb9efe7b180898d0ffdf3
@@ -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] # TODO: Add jruby after the error is resolved
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
@@ -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: ${{ matrix.ruby }}
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.0.0/),
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.62.0', require: false # For lint
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.35.0', require: false # For lint
16
- gem 'rubocop-performance', '~> 1.20.2', require: false # For lint
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, 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).
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, gettings frequent update and new releases (see [version history](https://rubygems.org/gems/alba/versions))
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 create 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.
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"}],"meta":{"size":1,"extra":42}}'
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
- `helper`
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rake/testtask'
3
5
 
data/benchmark/README.md CHANGED
@@ -10,76 +10,110 @@ Machine spec:
10
10
 
11
11
  |Key|Value|
12
12
  |---|---|
13
- |OS|macOS 13.2.1|
14
- |CPU|Intel Corei7 Quad Core 2.3Ghz|
15
- |RAM|32GB|
16
- |Ruby|ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-darwin21]|
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|2.2.0|
23
- |blueprinter|0.25.3|
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.14.2|
26
+ |oj|3.16.6|
27
27
  |simple_ams|0.2.6|
28
28
  |representable|3.2.0|
29
- |turbostreamer|1.10.0|
30
- |jbuilder|2.11.5|
31
- |panko_serializer|0.7.9|
32
- |active_model_serializers|0.10.13|
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: 310.4 i/s
39
- jserializer: 120.6 i/s - 2.57x slower
40
- turbostreamer: 117.3 i/s - 2.65x slower
41
- rails: 114.0 i/s - 2.72x slower
42
- alba_inline: 99.3 i/s - 3.13x slower
43
- alba: 94.1 i/s - 3.30x slower
44
- fast_serializer: 67.8 i/s - 4.58x slower
45
- blueprinter: 57.6 i/s - 5.39x slower
46
- representable: 36.3 i/s - 8.56x slower
47
- simple_ams: 23.3 i/s - 13.32x slower
48
- ams: 10.9 i/s - 28.53x slower
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: 326.1 i/s
56
- turbostreamer: 120.6 i/s - 2.70x slower
57
- jserializer: 119.2 i/s - 2.74x slower
58
- alba_inline: 104.3 i/s - 3.13x slower
59
- alba: 102.2 i/s - 3.19x slower
60
- fast_serializer: 66.9 i/s - 4.88x slower
61
- blueprinter: 56.7 i/s - 5.75x slower
62
- rails: 33.9 i/s - 9.63x slower
63
- representable: 30.3 i/s - 10.77x slower
64
- simple_ams: 16.4 i/s - 19.84x slower
65
- ams: 9.4 i/s - 34.56x slower
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: 242426 allocated
73
- turbostreamer: 817568 allocated - 3.37x more
74
- jserializer: 831705 allocated - 3.43x more
75
- alba: 1072217 allocated - 4.42x more
76
- alba_inline: 1084889 allocated - 4.48x more
77
- fast_serializer: 1244385 allocated - 5.13x more
78
- rails: 1272761 allocated - 5.25x more
79
- blueprinter: 1680137 allocated - 6.93x more
80
- representable: 2892425 allocated - 11.93x more
81
- ams: 4479569 allocated - 18.48x more
82
- simple_ams: 6957913 allocated - 28.70x more
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.
@@ -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
- require 'benchmark/ips'
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 do |x|
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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'alba'
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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gem 'minitest', '~> 5.14' # For test
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gem 'activesupport', require: false # For backend
@@ -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) # rubocop:disable Metrics/MethodLength
75
+ def assign_resource(nesting, key_transformation, block, helper)
69
76
  @resource = if block
70
- klass = Alba.resource_class
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file includes public constants to prevent circular dependencies.
2
4
  module Alba
3
5
  REMOVE_KEY = Object.new.freeze # A constant to remove key from serialized JSON
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require 'active_support/inflector'
3
5
  require 'active_support/core_ext/module/delegation'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alba
2
4
  # Module for printing deprecation warning
3
5
  # @api private
data/lib/alba/errors.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alba
2
4
  # Base class for Errors
3
5
  class Error < StandardError; end
data/lib/alba/layout.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'erb'
2
4
  require 'forwardable'
3
5
 
@@ -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
- base.define_method(:encode, Alba.encoder)
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
- inflector = Alba.inflector
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alba
2
4
  # Representing type itself, combined with {Alba::TypedAttribute}
3
5
  class Type
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alba
2
4
  # Representing typed attributes to encapsulate logic about types
3
5
  # @api private
data/lib/alba/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alba
2
- VERSION = '3.1.0'.freeze
4
+ VERSION = '3.3.0'
3
5
  end
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: ->(obj) { obj.to_s })
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
- # --- Bundle dependencies ---
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.1.0
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-03-22 00:00:00.000000000 Z
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.5.6
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: []