alba 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -10
- data/Gemfile +1 -2
- data/README.md +91 -33
- data/benchmark/collection.rb +9 -147
- data/benchmark/prep.rb +82 -0
- data/benchmark/single_resource.rb +3 -154
- data/docs/migrate_from_active_model_serializers.md +2 -2
- data/docs/rails.md +1 -1
- data/lib/alba/association.rb +12 -9
- data/lib/alba/conditional_attribute.rb +7 -7
- data/lib/alba/constants.rb +5 -0
- data/lib/alba/nested_attribute.rb +2 -1
- data/lib/alba/resource.rb +38 -30
- data/lib/alba/version.rb +1 -1
- data/lib/alba.rb +28 -5
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 86253dcf425b1a8bf489db53327a4f9a13c1ae046f87a5f558b75b9c1df18964
|
4
|
+
data.tar.gz: de8b3a29bd9b12f8cd6718a04f6140bec7611bedbf654232e9119f541d7f904e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dea93b6391ceaab7ec2759758d24df0eb14e8c59c52d8a174aeda79373afac23ca9f909a65aae3c2442106dd9d8dc700668eb6b791002ef0f9cf34833f53099a
|
7
|
+
data.tar.gz: 535c33bd2bea00d3029ab07a0848e37e8a30b8c6d66c3a1f894b51ad9b4aae30fc498dd000086c3e6a851c31069e6ab5d5ce11f8d96b4c687c26ae0174d42e54
|
data/CHANGELOG.md
CHANGED
@@ -6,23 +6,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [2.1.0] 2022-12-03
|
10
|
+
|
11
|
+
### Added
|
12
|
+
|
13
|
+
- Add `select` method for filtering attributes [#270](https://github.com/okuramasafumi/alba/pull/270)
|
14
|
+
- Allow ConditionalAttribute with 2-arity proc to reject nil attributes [#273](https://github.com/okuramasafumi/alba/pull/273)
|
15
|
+
|
16
|
+
### Fixed
|
17
|
+
|
18
|
+
- Add support for proc resource in one polymorphic associations [#281](https://github.com/okuramasafumi/alba/pull/281)
|
19
|
+
|
20
|
+
### Deprecated
|
21
|
+
|
22
|
+
- Deprecate `inference` related methods in favor of a unified `inflector` interface.
|
23
|
+
Deprecated methods are: `Alba.enable_inference!`, `Alba.disable_inference!`, and `Alba.inferring`.
|
24
|
+
Use `Alba.inflector = :active_support/:dry` or `Alba.inflector = SomeInflector` to enable.
|
25
|
+
Use `Alba.inflector = nil` to disable.
|
26
|
+
Use `Alba.inflector` to check if enabled.
|
27
|
+
|
28
|
+
## [2.0.1] 2022-11-02
|
29
|
+
|
30
|
+
### Fix
|
31
|
+
|
32
|
+
- the bug including key not in `within` [#262](https://github.com/okuramasafumi/alba/pull/262)
|
33
|
+
- key transformation now cascades multiple levels [#263](https://github.com/okuramasafumi/alba/pull/263)
|
34
|
+
|
9
35
|
## [2.0.0] 2022-10-21
|
10
36
|
|
11
37
|
### Breaking changes
|
12
38
|
|
13
|
-
- All Hash-related methods now return String
|
14
|
-
This affects all users, but you can use `deep_symbolize_keys` in Rails environment if you prefer Symbol keys
|
15
|
-
Some DSLs that take key argument such as `on_nil` and `on_error`, are also affected
|
39
|
+
- All Hash-related methods now return String keys instead of Symbol keys.
|
40
|
+
This affects all users, but you can use `deep_symbolize_keys` in Rails environment if you prefer Symbol keys, or `with_indifferent_access` to support both String and Symbol keys.
|
41
|
+
Some DSLs that take key argument such as `on_nil` and `on_error`, are also affected.
|
42
|
+
- Remove deprecated methods: `Resource#to_hash`, `Resource.ignoring`, `Alba.on_nil`, `Alba.on_error`, `Alba.enable_root_key_transformation!` and `Alba.disable_root_key_transformation!`
|
43
|
+
- If using `transform_keys`, the default inflector is no longer set by default [d02245c8](https://github.com/okuramasafumi/alba/commit/d02245c87e9df303cb20e354a81e5457ea460bdd#diff-ecd8c835d2390b8cb89e7ff75e599f0c15cdbe18c30981d6090f4a515566686f)
|
44
|
+
To retain this functionality in Rails, add an initializer with the following:
|
45
|
+
`Alba.enable_inference!(with: :active_support)`
|
16
46
|
|
17
47
|
### New features
|
18
48
|
|
19
|
-
- Passing an initial object to proc function in associations
|
20
|
-
- Allow association resource to be Proc
|
21
|
-
- `collection_key` to serialize collection into a Hash
|
22
|
-
- params is now overridable
|
23
|
-
- Key transformation now cascades
|
24
|
-
- nested attribute
|
25
|
-
- Implement `as_json`
|
49
|
+
- Passing an initial object to proc function in associations [#209](https://github.com/okuramasafumi/alba/pull/209)
|
50
|
+
- Allow association resource to be Proc [#213](https://github.com/okuramasafumi/alba/pull/213)
|
51
|
+
- `collection_key` to serialize collection into a Hash [#119](https://github.com/okuramasafumi/alba/pull/119)
|
52
|
+
- params is now overridable [#227](https://github.com/okuramasafumi/alba/pull/227)
|
53
|
+
- Key transformation now cascades [#232](https://github.com/okuramasafumi/alba/pull/232)
|
54
|
+
- nested attribute [#237](https://github.com/okuramasafumi/alba/pull/237)
|
55
|
+
- Implement `as_json` [#249](https://github.com/okuramasafumi/alba/pull/249)
|
26
56
|
|
27
57
|
### Bugfix
|
28
58
|
|
data/Gemfile
CHANGED
@@ -6,11 +6,10 @@ gemspec
|
|
6
6
|
gem 'activesupport', require: false # For backend
|
7
7
|
gem 'dry-inflector', require: false # For inflection
|
8
8
|
gem 'ffaker', require: false # For testing
|
9
|
-
gem 'inch', require: false # For inline documents
|
10
9
|
gem 'minitest', '~> 5.14' # For test
|
11
10
|
gem 'rake', '~> 13.0' # For test and automation
|
12
11
|
gem 'rubocop', '>= 0.79.0', require: false # For lint
|
13
|
-
gem 'rubocop-minitest', '~> 0.
|
12
|
+
gem 'rubocop-minitest', '~> 0.24.0', require: false # For lint
|
14
13
|
gem 'rubocop-performance', '~> 1.15.0', require: false # For lint
|
15
14
|
gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
|
16
15
|
gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
|
data/README.md
CHANGED
@@ -37,7 +37,7 @@ Alba is easy to use because there are only a few methods to remember. It's also
|
|
37
37
|
|
38
38
|
### Feature rich
|
39
39
|
|
40
|
-
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), [
|
40
|
+
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).
|
41
41
|
|
42
42
|
## Installation
|
43
43
|
|
@@ -57,7 +57,7 @@ Or install it yourself as:
|
|
57
57
|
|
58
58
|
## Supported Ruby versions
|
59
59
|
|
60
|
-
Alba supports CRuby 2.
|
60
|
+
Alba supports CRuby 2.6 and higher and latest JRuby and TruffleRuby.
|
61
61
|
|
62
62
|
## Documentation
|
63
63
|
|
@@ -68,10 +68,9 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
|
|
68
68
|
* Conditional attributes and associations
|
69
69
|
* Selectable backend
|
70
70
|
* Key transformation
|
71
|
-
* Root key inference
|
71
|
+
* Root key and association resource name inference
|
72
72
|
* Error handling
|
73
73
|
* Nil handling
|
74
|
-
* Resource name inflection based on association name
|
75
74
|
* Circular associations control
|
76
75
|
* [Experimental] Types for validation and conversion
|
77
76
|
* Layout
|
@@ -113,19 +112,33 @@ You can consider setting a backend with Symbol as a shortcut to set encoder.
|
|
113
112
|
|
114
113
|
#### Inference configuration
|
115
114
|
|
116
|
-
You can enable inference feature using `
|
115
|
+
You can enable the inference feature using the `Alba.inflector = SomeInflector` API. For example, in a Rails initializer:
|
117
116
|
|
118
117
|
```ruby
|
119
|
-
Alba.
|
118
|
+
Alba.inflector = :active_support
|
120
119
|
```
|
121
120
|
|
122
|
-
You can choose which inflector Alba uses for inference. Possible
|
121
|
+
You can choose which inflector Alba uses for inference. Possible options are:
|
123
122
|
|
124
123
|
- `:active_support` for `ActiveSupport::Inflector`
|
125
124
|
- `:dry` for `Dry::Inflector`
|
126
|
-
- any object which
|
125
|
+
- any object which conforms to the protocol (see [below](#custom-inflector))
|
127
126
|
|
128
|
-
|
127
|
+
To disable inference, set the `inflector` to `nil`:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
Alba.inflector = nil
|
131
|
+
```
|
132
|
+
|
133
|
+
To check if inference is enabled etc, inspect the return value of `inflector`:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
if Alba.inflector == nil
|
137
|
+
puts "inflector not set"
|
138
|
+
else
|
139
|
+
puts "inflector is set to #{Alba.inflector}"
|
140
|
+
end
|
141
|
+
```
|
129
142
|
|
130
143
|
### Simple serialization with root key
|
131
144
|
|
@@ -157,7 +170,7 @@ end
|
|
157
170
|
|
158
171
|
user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
|
159
172
|
UserResource.new(user).serialize
|
160
|
-
# =>
|
173
|
+
# => '{"user":{"id":1,"name":"Masafumi OKURA","name_with_email":"Masafumi OKURA: masafumi@example.com"}}'
|
161
174
|
```
|
162
175
|
|
163
176
|
You can define instance methods on resources so that you can use it as attribute name in `attributes`.
|
@@ -184,7 +197,7 @@ This even works with users collection.
|
|
184
197
|
user1 = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
|
185
198
|
user2 = User.new(2, 'Test User', 'test@example.com')
|
186
199
|
UserResource.new([user1, user2]).serialize
|
187
|
-
# =>
|
200
|
+
# => '{"users":[{"id":1,"name":"Masafumi OKURA","name_with_email":"Masafumi OKURA: masafumi@example.com"},{"id":2,"name":"Test User","name_with_email":"Test User: test@example.com"}]}'
|
188
201
|
```
|
189
202
|
|
190
203
|
If you have a simple case where you want to change only the name, you can use the Symbol to Proc shortcut:
|
@@ -210,8 +223,8 @@ class UserResource
|
|
210
223
|
end
|
211
224
|
|
212
225
|
user = User.new(1, 'Masa', 'test@example.com')
|
213
|
-
UserResource.new(user).serialize # =>
|
214
|
-
UserResource.new(user, params: {upcase: true}).serialize # =>
|
226
|
+
UserResource.new(user).serialize # => '{"name":"Masa"}'
|
227
|
+
UserResource.new(user, params: {upcase: true}).serialize # => '{"name":"MASA"}'
|
215
228
|
```
|
216
229
|
|
217
230
|
### Serialization with associations
|
@@ -368,9 +381,11 @@ UserResource.new(user).serialize
|
|
368
381
|
# => '{"id":1,"my_articles":[{"title":"Hello World!"}]}'
|
369
382
|
```
|
370
383
|
|
371
|
-
You can omit resource option if you enable Alba's inference feature.
|
384
|
+
You can omit the resource option if you enable Alba's [inference](#inference-configuration) feature.
|
372
385
|
|
373
386
|
```ruby
|
387
|
+
Alba.inflector = :active_support
|
388
|
+
|
374
389
|
class UserResource
|
375
390
|
include Alba::Resource
|
376
391
|
|
@@ -390,7 +405,7 @@ class UserResource
|
|
390
405
|
|
391
406
|
attributes :id
|
392
407
|
|
393
|
-
many :articles, ->(article) { article.with_comment? ? ArticleWithCommentResource : ArticleResource }
|
408
|
+
many :articles, resource: ->(article) { article.with_comment? ? ArticleWithCommentResource : ArticleResource }
|
394
409
|
end
|
395
410
|
```
|
396
411
|
|
@@ -564,13 +579,19 @@ end
|
|
564
579
|
Foo = Struct.new(:bar, :baz)
|
565
580
|
foo = Foo.new(1, 2)
|
566
581
|
FooResource.new(foo).serialize # => '{"foo":{"bar":1}}'
|
567
|
-
ExtendedFooResource.new(foo).serialize # => '{"
|
582
|
+
ExtendedFooResource.new(foo).serialize # => '{"foofoo":{"bar":1,"baz":2}}'
|
568
583
|
```
|
569
584
|
|
570
585
|
In this example we add `baz` attribute and change `root_key`. This way, you can extend existing resource classes just like normal OOP. Don't forget that when your inheritance structure is too deep it'll become difficult to modify existing classes.
|
571
586
|
|
572
587
|
### Filtering attributes
|
573
588
|
|
589
|
+
Filtering attributes can be done in two ways - with `attributes` and `select`. They have different semantics and usage.
|
590
|
+
|
591
|
+
`select` is a new and more intuitive API, so generally it's recommended to use `select`.
|
592
|
+
|
593
|
+
#### Filtering attributes with `attributes`
|
594
|
+
|
574
595
|
You can filter out certain attributes by overriding `attributes` instance method. This is useful when you want to customize existing resource with inheritance.
|
575
596
|
|
576
597
|
You can access raw attributes via `super` call. It returns a Hash whose keys are the name of the attribute and whose values are the body. Usually you need only keys to filter out, like below.
|
@@ -598,19 +619,52 @@ class RestrictedFooResource < GenericFooResource
|
|
598
619
|
end
|
599
620
|
end
|
600
621
|
|
622
|
+
foo = Foo.new(1, 'my foo', 'body')
|
623
|
+
|
601
624
|
RestrictedFooResource.new(foo).serialize
|
602
625
|
# => '{"name":"my foo"}'
|
603
626
|
```
|
604
627
|
|
605
|
-
|
628
|
+
#### Filtering attributes with `select`
|
629
|
+
|
630
|
+
When you want to filter attributes based on more complex logic, you can use `select` instance method. `select` takes two parameters, the name of an attribute and the value of an attribute. If it returns false that attribute is rejected.
|
631
|
+
|
632
|
+
```ruby
|
633
|
+
class Foo
|
634
|
+
attr_accessor :id, :name, :body
|
635
|
+
|
636
|
+
def initialize(id, name, body)
|
637
|
+
@id = id
|
638
|
+
@name = name
|
639
|
+
@body = body
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
class GenericFooResource
|
644
|
+
include Alba::Resource
|
645
|
+
|
646
|
+
attributes :id, :name, :body
|
647
|
+
end
|
648
|
+
|
649
|
+
class RestrictedFooResource < GenericFooResource
|
650
|
+
def select(_key, value)
|
651
|
+
!value.nil?
|
652
|
+
end
|
653
|
+
end
|
606
654
|
|
607
|
-
|
608
|
-
* install it
|
609
|
-
* use a [custom inflector](#custom-inflector)
|
655
|
+
foo = Foo.new(1, nil, 'body')
|
610
656
|
|
611
|
-
|
657
|
+
RestrictedFooResource.new(foo).serialize
|
658
|
+
# => '{"id":1,"body":"body"}'
|
659
|
+
```
|
660
|
+
|
661
|
+
### Key transformation
|
662
|
+
|
663
|
+
If you have [inference](#inference-configuration) enabled, you can use the `transform_keys` DSL to transform attribute keys.
|
612
664
|
|
613
665
|
```ruby
|
666
|
+
Alba.inflector = :active_support
|
667
|
+
|
614
668
|
class User
|
615
669
|
attr_reader :id, :first_name, :last_name
|
616
670
|
|
@@ -646,12 +700,12 @@ Possible values for `transform_keys` argument are:
|
|
646
700
|
|
647
701
|
You can also transform root key when:
|
648
702
|
|
649
|
-
* `Alba.
|
703
|
+
* `Alba.inflector` is set
|
650
704
|
* `root_key!` is called in Resource class
|
651
705
|
* `root` option of `transform_keys` is set to true
|
652
706
|
|
653
707
|
```ruby
|
654
|
-
Alba.
|
708
|
+
Alba.inflector = :active_support
|
655
709
|
|
656
710
|
class BankAccount
|
657
711
|
attr_reader :account_number
|
@@ -675,7 +729,9 @@ BankAccountResource.new(bank_account).serialize
|
|
675
729
|
# => '{"bank-account":{"account-number":123456789}}'
|
676
730
|
```
|
677
731
|
|
678
|
-
This
|
732
|
+
This is the default behavior from version 2.
|
733
|
+
|
734
|
+
Find more details in the [Inference configuration](#inference-configuration) section.
|
679
735
|
|
680
736
|
#### Key transformation cascading
|
681
737
|
|
@@ -687,7 +743,7 @@ You can also turn it off by setting `cascade: false` option to `transform_keys`.
|
|
687
743
|
|
688
744
|
```ruby
|
689
745
|
class User
|
690
|
-
attr_reader :id, :first_name, :last_name
|
746
|
+
attr_reader :id, :first_name, :last_name, :bank_account
|
691
747
|
|
692
748
|
def initialize(id, first_name, last_name)
|
693
749
|
@id = id
|
@@ -746,7 +802,7 @@ module CustomInflector
|
|
746
802
|
end
|
747
803
|
end
|
748
804
|
|
749
|
-
Alba.
|
805
|
+
Alba.inflector = CustomInflector
|
750
806
|
```
|
751
807
|
|
752
808
|
### Conditional attributes
|
@@ -757,7 +813,7 @@ In these cases, conditional attributes works well. We can pass `if` option to `a
|
|
757
813
|
|
758
814
|
```ruby
|
759
815
|
class User
|
760
|
-
attr_accessor :id, :name, :email
|
816
|
+
attr_accessor :id, :name, :email
|
761
817
|
|
762
818
|
def initialize(id, name, email)
|
763
819
|
@id = id
|
@@ -790,12 +846,12 @@ end
|
|
790
846
|
|
791
847
|
We believe this is clearer than using some (not implemented yet) DSL such as `default` because there are some conditions where default values should be applied (`nil`, `blank?`, `empty?` etc.)
|
792
848
|
|
793
|
-
###
|
849
|
+
### Root key and association resource name inference
|
794
850
|
|
795
|
-
|
851
|
+
If [inference](#inference-configuration) is enabled, Alba tries to infer the root key and association resource names.
|
796
852
|
|
797
853
|
```ruby
|
798
|
-
Alba.
|
854
|
+
Alba.inflector = :active_support
|
799
855
|
|
800
856
|
class User
|
801
857
|
attr_reader :id
|
@@ -825,7 +881,7 @@ end
|
|
825
881
|
class UserResource
|
826
882
|
include Alba::Resource
|
827
883
|
|
828
|
-
|
884
|
+
root_key!
|
829
885
|
|
830
886
|
attributes :id
|
831
887
|
|
@@ -843,6 +899,8 @@ This resource automatically sets its root key to either "users" or "user", depen
|
|
843
899
|
|
844
900
|
Also, you don't have to specify which resource class to use with `many`. Alba infers it from association name.
|
845
901
|
|
902
|
+
Find more details in the [Inference configuration](#inference-configuration) section.
|
903
|
+
|
846
904
|
### Error handling
|
847
905
|
|
848
906
|
You can set error handler globally or per resource using `on_error`.
|
@@ -934,7 +992,7 @@ class UserResource
|
|
934
992
|
include Alba::Resource
|
935
993
|
|
936
994
|
on_nil do |object, key|
|
937
|
-
if key == age
|
995
|
+
if key == 'age'
|
938
996
|
20
|
939
997
|
else
|
940
998
|
"User#{object.id}"
|
@@ -990,7 +1048,7 @@ class UserResourceWithoutMeta
|
|
990
1048
|
attributes :id, :name
|
991
1049
|
end
|
992
1050
|
|
993
|
-
|
1051
|
+
UserResourceWithoutMeta.new([user]).serialize(meta: {foo: :bar})
|
994
1052
|
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"foo":"bar"}}'
|
995
1053
|
```
|
996
1054
|
|
data/benchmark/collection.rb
CHANGED
@@ -1,88 +1,7 @@
|
|
1
1
|
# Benchmark script to run varieties of JSON serializers
|
2
2
|
# Fetch Alba from local, otherwise fetch latest from RubyGems
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
require "bundler/inline"
|
7
|
-
|
8
|
-
gemfile(true) do
|
9
|
-
source "https://rubygems.org"
|
10
|
-
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
11
|
-
|
12
|
-
gem "active_model_serializers"
|
13
|
-
gem "activerecord", "6.1.3"
|
14
|
-
gem "alba", path: '../'
|
15
|
-
gem "benchmark-ips"
|
16
|
-
gem "benchmark-memory"
|
17
|
-
gem "blueprinter"
|
18
|
-
gem "fast_serializer_ruby"
|
19
|
-
gem "jbuilder"
|
20
|
-
gem 'turbostreamer'
|
21
|
-
gem "jserializer"
|
22
|
-
gem "multi_json"
|
23
|
-
gem "panko_serializer"
|
24
|
-
gem "pg"
|
25
|
-
gem "primalize"
|
26
|
-
gem "oj"
|
27
|
-
gem "representable"
|
28
|
-
gem "simple_ams"
|
29
|
-
gem "sqlite3"
|
30
|
-
end
|
31
|
-
|
32
|
-
# --- Test data model setup ---
|
33
|
-
|
34
|
-
require "pg"
|
35
|
-
require "active_record"
|
36
|
-
require "active_record/connection_adapters/postgresql_adapter"
|
37
|
-
require "logger"
|
38
|
-
require "oj"
|
39
|
-
require "sqlite3"
|
40
|
-
Oj.optimize_rails
|
41
|
-
|
42
|
-
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
43
|
-
# ActiveRecord::Base.logger = Logger.new($stdout)
|
44
|
-
|
45
|
-
ActiveRecord::Schema.define do
|
46
|
-
create_table :posts, force: true do |t|
|
47
|
-
t.string :body
|
48
|
-
end
|
49
|
-
|
50
|
-
create_table :comments, force: true do |t|
|
51
|
-
t.integer :post_id
|
52
|
-
t.string :body
|
53
|
-
t.integer :commenter_id
|
54
|
-
end
|
55
|
-
|
56
|
-
create_table :users, force: true do |t|
|
57
|
-
t.string :name
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
class Post < ActiveRecord::Base
|
62
|
-
has_many :comments
|
63
|
-
has_many :commenters, through: :comments, class_name: 'User', source: :commenter
|
64
|
-
|
65
|
-
def attributes
|
66
|
-
{id: nil, body: nil, commenter_names: commenter_names}
|
67
|
-
end
|
68
|
-
|
69
|
-
def commenter_names
|
70
|
-
commenters.pluck(:name)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
class Comment < ActiveRecord::Base
|
75
|
-
belongs_to :post
|
76
|
-
belongs_to :commenter, class_name: 'User'
|
77
|
-
|
78
|
-
def attributes
|
79
|
-
{id: nil, body: nil}
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
class User < ActiveRecord::Base
|
84
|
-
has_many :comments
|
85
|
-
end
|
4
|
+
require_relative 'prep'
|
86
5
|
|
87
6
|
# --- Alba serializers ---
|
88
7
|
|
@@ -160,30 +79,6 @@ class FastSerializerPostResource
|
|
160
79
|
has_many :comments, serializer: FastSerializerCommentResource
|
161
80
|
end
|
162
81
|
|
163
|
-
# --- JBuilder serializers ---
|
164
|
-
|
165
|
-
require "jbuilder"
|
166
|
-
|
167
|
-
class Post
|
168
|
-
def to_builder
|
169
|
-
Jbuilder.new do |post|
|
170
|
-
post.call(self, :id, :body, :commenter_names, :comments)
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
def commenter_names
|
175
|
-
commenters.pluck(:name)
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
class Comment
|
180
|
-
def to_builder
|
181
|
-
Jbuilder.new do |comment|
|
182
|
-
comment.call(self, :id, :body)
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
82
|
# --- Jserializer serializers ---
|
188
83
|
|
189
84
|
require 'jserializer'
|
@@ -220,31 +115,6 @@ class PankoPostSerializer < Panko::Serializer
|
|
220
115
|
end
|
221
116
|
end
|
222
117
|
|
223
|
-
# --- Primalize serializers ---
|
224
|
-
#
|
225
|
-
class PrimalizeCommentResource < Primalize::Single
|
226
|
-
attributes id: integer, body: string
|
227
|
-
end
|
228
|
-
|
229
|
-
class PrimalizePostResource < Primalize::Single
|
230
|
-
alias post object
|
231
|
-
|
232
|
-
attributes(
|
233
|
-
id: integer,
|
234
|
-
body: string,
|
235
|
-
comments: array(primalize(PrimalizeCommentResource)),
|
236
|
-
commenter_names: array(string),
|
237
|
-
)
|
238
|
-
|
239
|
-
def commenter_names
|
240
|
-
post.commenters.pluck(:name)
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
class PrimalizePostsResource < Primalize::Many
|
245
|
-
attributes posts: enumerable(PrimalizePostResource)
|
246
|
-
end
|
247
|
-
|
248
118
|
# --- Representable serializers ---
|
249
119
|
|
250
120
|
require "representable"
|
@@ -349,13 +219,6 @@ end
|
|
349
219
|
ams = Proc.new { ActiveModelSerializers::SerializableResource.new(posts, {each_serializer: AMSPostSerializer}).to_json }
|
350
220
|
blueprinter = Proc.new { PostBlueprint.render(posts) }
|
351
221
|
fast_serializer = Proc.new { FastSerializerPostResource.new(posts).to_json }
|
352
|
-
jbuilder = Proc.new do
|
353
|
-
Jbuilder.new do |json|
|
354
|
-
json.array!(posts) do |post|
|
355
|
-
json.post post.to_builder
|
356
|
-
end
|
357
|
-
end.target!
|
358
|
-
end
|
359
222
|
jserializer = Proc.new { JserializerPostSerializer.new(posts, is_collection: true).to_json }
|
360
223
|
panko = proc { Panko::ArraySerializer.new(posts, each_serializer: PankoPostSerializer).to_json }
|
361
224
|
primalize = proc { PrimalizePostsResource.new(posts: posts).to_json }
|
@@ -368,22 +231,25 @@ turbostreamer = Proc.new { TurbostreamerSerializer.new(posts).to_json }
|
|
368
231
|
|
369
232
|
# --- Execute the serializers to check their output ---
|
370
233
|
GC.disable
|
371
|
-
puts "
|
234
|
+
puts "Checking outputs..."
|
235
|
+
correct = alba.call
|
236
|
+
parsed_correct = JSON.parse(correct)
|
372
237
|
{
|
373
|
-
alba: alba,
|
374
238
|
alba_inline: alba_inline,
|
375
239
|
ams: ams,
|
376
240
|
blueprinter: blueprinter,
|
377
241
|
fast_serializer: fast_serializer,
|
378
|
-
jbuilder: jbuilder, # different order
|
379
242
|
jserializer: jserializer,
|
380
243
|
panko: panko,
|
381
|
-
primalize: primalize,
|
382
244
|
rails: rails,
|
383
245
|
representable: representable,
|
384
246
|
simple_ams: simple_ams,
|
385
247
|
turbostreamer: turbostreamer
|
386
|
-
}.each
|
248
|
+
}.each do |name, serializer|
|
249
|
+
result = serializer.call
|
250
|
+
parsed_result = JSON.parse(result)
|
251
|
+
puts "#{name} yields wrong output: #{parsed_result}" unless parsed_result == parsed_correct
|
252
|
+
end
|
387
253
|
|
388
254
|
# --- Run the benchmarks ---
|
389
255
|
|
@@ -394,10 +260,8 @@ Benchmark.ips do |x|
|
|
394
260
|
x.report(:ams, &ams)
|
395
261
|
x.report(:blueprinter, &blueprinter)
|
396
262
|
x.report(:fast_serializer, &fast_serializer)
|
397
|
-
x.report(:jbuilder, &jbuilder)
|
398
263
|
x.report(:jserializer, &jserializer)
|
399
264
|
x.report(:panko, &panko)
|
400
|
-
x.report(:primalize, &primalize)
|
401
265
|
x.report(:rails, &rails)
|
402
266
|
x.report(:representable, &representable)
|
403
267
|
x.report(:simple_ams, &simple_ams)
|
@@ -414,10 +278,8 @@ Benchmark.memory do |x|
|
|
414
278
|
x.report(:ams, &ams)
|
415
279
|
x.report(:blueprinter, &blueprinter)
|
416
280
|
x.report(:fast_serializer, &fast_serializer)
|
417
|
-
x.report(:jbuilder, &jbuilder)
|
418
281
|
x.report(:jserializer, &jserializer)
|
419
282
|
x.report(:panko, &panko)
|
420
|
-
x.report(:primalize, &primalize)
|
421
283
|
x.report(:rails, &rails)
|
422
284
|
x.report(:representable, &representable)
|
423
285
|
x.report(:simple_ams, &simple_ams)
|
data/benchmark/prep.rb
ADDED
@@ -0,0 +1,82 @@
|
|
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
|
+
# --- Test data model setup ---
|
30
|
+
|
31
|
+
require "pg"
|
32
|
+
require "active_record"
|
33
|
+
require "active_record/connection_adapters/postgresql_adapter"
|
34
|
+
require "logger"
|
35
|
+
require "oj"
|
36
|
+
require "sqlite3"
|
37
|
+
Oj.optimize_rails unless ENV['NO_OJ_OPTIMIZE_RAILS']
|
38
|
+
|
39
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
40
|
+
# ActiveRecord::Base.logger = Logger.new($stdout)
|
41
|
+
|
42
|
+
ActiveRecord::Schema.define do
|
43
|
+
create_table :posts, force: true do |t|
|
44
|
+
t.string :body
|
45
|
+
end
|
46
|
+
|
47
|
+
create_table :comments, force: true do |t|
|
48
|
+
t.integer :post_id
|
49
|
+
t.string :body
|
50
|
+
t.integer :commenter_id
|
51
|
+
end
|
52
|
+
|
53
|
+
create_table :users, force: true do |t|
|
54
|
+
t.string :name
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Post < ActiveRecord::Base
|
59
|
+
has_many :comments
|
60
|
+
has_many :commenters, through: :comments, class_name: 'User', source: :commenter
|
61
|
+
|
62
|
+
def attributes
|
63
|
+
{id: nil, body: nil, commenter_names: commenter_names}
|
64
|
+
end
|
65
|
+
|
66
|
+
def commenter_names
|
67
|
+
commenters.pluck(:name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Comment < ActiveRecord::Base
|
72
|
+
belongs_to :post
|
73
|
+
belongs_to :commenter, class_name: 'User'
|
74
|
+
|
75
|
+
def attributes
|
76
|
+
{id: nil, body: nil}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class User < ActiveRecord::Base
|
81
|
+
has_many :comments
|
82
|
+
end
|
@@ -1,88 +1,7 @@
|
|
1
1
|
# Benchmark script to run varieties of JSON serializers
|
2
2
|
# Fetch Alba from local, otherwise fetch latest from RubyGems
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
require "bundler/inline"
|
7
|
-
|
8
|
-
gemfile(true) do
|
9
|
-
source "https://rubygems.org"
|
10
|
-
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
11
|
-
|
12
|
-
gem "active_model_serializers"
|
13
|
-
gem "activerecord", "6.1.3"
|
14
|
-
gem "alba", path: '../'
|
15
|
-
gem "benchmark-ips"
|
16
|
-
gem "benchmark-memory"
|
17
|
-
gem "blueprinter"
|
18
|
-
gem "jbuilder"
|
19
|
-
gem 'turbostreamer'
|
20
|
-
gem "jserializer"
|
21
|
-
gem "jsonapi-serializer" # successor of fast_jsonapi
|
22
|
-
gem "multi_json"
|
23
|
-
gem "panko_serializer"
|
24
|
-
gem "pg"
|
25
|
-
gem "primalize"
|
26
|
-
gem "oj"
|
27
|
-
gem "representable"
|
28
|
-
gem "simple_ams"
|
29
|
-
gem "sqlite3"
|
30
|
-
end
|
31
|
-
|
32
|
-
# --- Test data model setup ---
|
33
|
-
|
34
|
-
require "pg"
|
35
|
-
require "active_record"
|
36
|
-
require "active_record/connection_adapters/postgresql_adapter"
|
37
|
-
require "logger"
|
38
|
-
require "oj"
|
39
|
-
require "sqlite3"
|
40
|
-
Oj.optimize_rails
|
41
|
-
|
42
|
-
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
43
|
-
# ActiveRecord::Base.logger = Logger.new($stdout)
|
44
|
-
|
45
|
-
ActiveRecord::Schema.define do
|
46
|
-
create_table :posts, force: true do |t|
|
47
|
-
t.string :body
|
48
|
-
end
|
49
|
-
|
50
|
-
create_table :comments, force: true do |t|
|
51
|
-
t.integer :post_id
|
52
|
-
t.string :body
|
53
|
-
t.integer :commenter_id
|
54
|
-
end
|
55
|
-
|
56
|
-
create_table :users, force: true do |t|
|
57
|
-
t.string :name
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
class Post < ActiveRecord::Base
|
62
|
-
has_many :comments
|
63
|
-
has_many :commenters, through: :comments, class_name: 'User', source: :commenter
|
64
|
-
|
65
|
-
def attributes
|
66
|
-
{id: nil, body: nil, commenter_names: commenter_names}
|
67
|
-
end
|
68
|
-
|
69
|
-
def commenter_names
|
70
|
-
commenters.pluck(:name)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
class Comment < ActiveRecord::Base
|
75
|
-
belongs_to :post
|
76
|
-
belongs_to :commenter, class_name: 'User'
|
77
|
-
|
78
|
-
def attributes
|
79
|
-
{id: nil, body: nil}
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
class User < ActiveRecord::Base
|
84
|
-
has_many :comments
|
85
|
-
end
|
4
|
+
require_relative 'prep'
|
86
5
|
|
87
6
|
# --- Alba serializers ---
|
88
7
|
|
@@ -177,70 +96,8 @@ class JserializerPostSerializer < Jserializer::Base
|
|
177
96
|
end
|
178
97
|
end
|
179
98
|
|
180
|
-
|
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
99
|
# --- Panko serializers ---
|
243
|
-
|
100
|
+
|
244
101
|
require "panko_serializer"
|
245
102
|
|
246
103
|
class PankoCommentSerializer < Panko::Serializer
|
@@ -259,7 +116,7 @@ class PankoPostSerializer < Panko::Serializer
|
|
259
116
|
end
|
260
117
|
|
261
118
|
# --- Primalize serializers ---
|
262
|
-
|
119
|
+
|
263
120
|
class PrimalizeCommentResource < Primalize::Single
|
264
121
|
attributes id: integer, body: string
|
265
122
|
end
|
@@ -376,8 +233,6 @@ ams = Proc.new { AMSPostSerializer.new(post, {}).to_json }
|
|
376
233
|
blueprinter = Proc.new { PostBlueprint.render(post) }
|
377
234
|
jbuilder = Proc.new { post.to_builder.target! }
|
378
235
|
jserializer = Proc.new { JserializerPostSerializer.new(post).to_json }
|
379
|
-
jsonapi = proc { JsonApiStandardPostSerializer.new(post).to_json }
|
380
|
-
jsonapi_same_format = proc { JsonApiSameFormatPostSerializer.new(post).to_json }
|
381
236
|
panko = proc { PankoPostSerializer.new.serialize_to_json(post) }
|
382
237
|
primalize = proc { PrimalizePostResource.new(post).to_json }
|
383
238
|
rails = Proc.new { ActiveSupport::JSON.encode(post.serializable_hash(include: :comments)) }
|
@@ -395,8 +250,6 @@ puts "Serializer outputs ----------------------------------"
|
|
395
250
|
blueprinter: blueprinter,
|
396
251
|
jbuilder: jbuilder, # different order
|
397
252
|
jserializer: jserializer,
|
398
|
-
jsonapi: jsonapi, # nested JSON:API format
|
399
|
-
jsonapi_same_format: jsonapi_same_format,
|
400
253
|
panko: panko,
|
401
254
|
primalize: primalize,
|
402
255
|
rails: rails,
|
@@ -417,8 +270,6 @@ Benchmark.ips do |x|
|
|
417
270
|
x.report(:blueprinter, &blueprinter)
|
418
271
|
x.report(:jbuilder, &jbuilder)
|
419
272
|
x.report(:jserializer, &jserializer)
|
420
|
-
x.report(:jsonapi, &jsonapi)
|
421
|
-
x.report(:jsonapi_same_format, &jsonapi_same_format)
|
422
273
|
x.report(:panko, &panko)
|
423
274
|
x.report(:primalize, &primalize)
|
424
275
|
x.report(:rails, &rails)
|
@@ -438,8 +289,6 @@ Benchmark.memory do |x|
|
|
438
289
|
x.report(:blueprinter, &blueprinter)
|
439
290
|
x.report(:jbuilder, &jbuilder)
|
440
291
|
x.report(:jserializer, &jserializer)
|
441
|
-
x.report(:jsonapi, &jsonapi)
|
442
|
-
x.report(:jsonapi_same_format, &jsonapi_same_format)
|
443
292
|
x.report(:panko, &panko)
|
444
293
|
x.report(:primalize, &primalize)
|
445
294
|
x.report(:rails, &rails)
|
@@ -12,7 +12,7 @@ This guide is aimed at helping ActiveModelSerializers users transition to Alba,
|
|
12
12
|
|
13
13
|
## Example class
|
14
14
|
|
15
|
-
Example
|
15
|
+
Example class is inherited `ActiveRecord::Base`, because [serializing PORO with AMS is pretty hard](https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/howto/serialize_poro.md).
|
16
16
|
|
17
17
|
```rb
|
18
18
|
class User < ActiveRecord::Base
|
@@ -175,7 +175,7 @@ class UserSerializer < ActiveModel::Serializer
|
|
175
175
|
has_many :articles, serializer: ArticleSerializer # For has_many relation
|
176
176
|
end
|
177
177
|
user = User.create!
|
178
|
-
user.
|
178
|
+
user.create_profile! email: email
|
179
179
|
user.articles.create! title: title, body: body
|
180
180
|
ActiveModelSerializers::SerializableResource.new(
|
181
181
|
user
|
data/docs/rails.md
CHANGED
@@ -14,7 +14,7 @@ You might want to add some configurations to initializer file such as `alba.rb`
|
|
14
14
|
```ruby
|
15
15
|
# alba.rb
|
16
16
|
Alba.backend = :active_support
|
17
|
-
Alba.enable_inference!(:active_support)
|
17
|
+
Alba.enable_inference!(with: :active_support)
|
18
18
|
```
|
19
19
|
|
20
20
|
You can also use `:oj_rails` for backend if you prefer using Oj.
|
data/lib/alba/association.rb
CHANGED
@@ -20,10 +20,9 @@ module Alba
|
|
20
20
|
@condition = condition
|
21
21
|
@resource = resource
|
22
22
|
@params = params
|
23
|
-
@key_transformation = key_transformation
|
24
23
|
return if @resource
|
25
24
|
|
26
|
-
assign_resource(nesting, block)
|
25
|
+
assign_resource(nesting, key_transformation, block)
|
27
26
|
end
|
28
27
|
|
29
28
|
# Recursively converts an object into a Hash
|
@@ -38,8 +37,10 @@ module Alba
|
|
38
37
|
@object = @condition.call(object, params, target) if @condition
|
39
38
|
return if @object.nil?
|
40
39
|
|
41
|
-
if @resource.is_a?(Proc)
|
42
|
-
to_h_with_each_resource(within, params)
|
40
|
+
if @resource.is_a?(Proc)
|
41
|
+
return to_h_with_each_resource(within, params) if @object.is_a?(Enumerable)
|
42
|
+
|
43
|
+
@resource.call(@object).new(@object, within: within, params: params).to_h
|
43
44
|
else
|
44
45
|
to_h_with_constantize_resource(within, params)
|
45
46
|
end
|
@@ -58,13 +59,16 @@ module Alba
|
|
58
59
|
end
|
59
60
|
end
|
60
61
|
|
61
|
-
def assign_resource(nesting, block)
|
62
|
+
def assign_resource(nesting, key_transformation, block)
|
62
63
|
@resource = if block
|
63
|
-
Alba.resource_class
|
64
|
-
|
64
|
+
klass = Alba.resource_class
|
65
|
+
klass.transform_keys(key_transformation)
|
66
|
+
klass.class_eval(&block)
|
67
|
+
klass
|
68
|
+
elsif Alba.inflector
|
65
69
|
Alba.infer_resource_class(@name, nesting: nesting)
|
66
70
|
else
|
67
|
-
raise ArgumentError, 'When Alba.
|
71
|
+
raise ArgumentError, 'When Alba.inflector is nil, either resource or block is required'
|
68
72
|
end
|
69
73
|
end
|
70
74
|
|
@@ -76,7 +80,6 @@ module Alba
|
|
76
80
|
|
77
81
|
def to_h_with_constantize_resource(within, params)
|
78
82
|
@resource = constantize(@resource)
|
79
|
-
@resource.transform_keys(@key_transformation)
|
80
83
|
@resource.new(object, params: params, within: within).to_h
|
81
84
|
end
|
82
85
|
end
|
@@ -1,9 +1,9 @@
|
|
1
|
+
require_relative 'association'
|
2
|
+
require_relative 'constants'
|
3
|
+
|
1
4
|
module Alba
|
2
5
|
# Represents attribute with `if` option
|
3
6
|
class ConditionalAttribute
|
4
|
-
CONDITION_UNMET = Object.new.freeze
|
5
|
-
public_constant :CONDITION_UNMET # It's public for use in `Alba::Resource`
|
6
|
-
|
7
7
|
# @param body [Symbol, Proc, Alba::Association, Alba::TypedAttribute] real attribute wrapped with condition
|
8
8
|
# @param condition [Symbol, Proc] condition to check
|
9
9
|
def initialize(body:, condition:)
|
@@ -15,14 +15,14 @@ module Alba
|
|
15
15
|
#
|
16
16
|
# @param resource [Alba::Resource]
|
17
17
|
# @param object [Object] needed for collection, each object from collection
|
18
|
-
# @return [
|
18
|
+
# @return [Alba::REMOVE_KEY, Object] REMOVE_KEY if condition is unmet, fetched attribute otherwise
|
19
19
|
def with_passing_condition(resource:, object: nil)
|
20
|
-
return
|
20
|
+
return Alba::REMOVE_KEY unless condition_passes?(resource, object)
|
21
21
|
|
22
22
|
fetched_attribute = yield(@body)
|
23
|
-
return fetched_attribute if
|
23
|
+
return fetched_attribute if !with_two_arity_proc_condition
|
24
24
|
|
25
|
-
return
|
25
|
+
return Alba::REMOVE_KEY unless resource.instance_exec(object, attribute_from_association_body_or(fetched_attribute), &@condition)
|
26
26
|
|
27
27
|
fetched_attribute
|
28
28
|
end
|
@@ -10,8 +10,9 @@ module Alba
|
|
10
10
|
|
11
11
|
# @return [Hash]
|
12
12
|
def value(object)
|
13
|
-
resource_class = Alba.resource_class
|
13
|
+
resource_class = Alba.resource_class
|
14
14
|
resource_class.transform_keys(@key_transformation)
|
15
|
+
resource_class.class_eval(&@block)
|
15
16
|
resource_class.new(object).serializable_hash
|
16
17
|
end
|
17
18
|
end
|
data/lib/alba/resource.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require_relative 'association'
|
2
2
|
require_relative 'conditional_attribute'
|
3
|
+
require_relative 'constants'
|
3
4
|
require_relative 'typed_attribute'
|
4
5
|
require_relative 'nested_attribute'
|
5
6
|
require_relative 'deprecation'
|
@@ -40,7 +41,6 @@ module Alba
|
|
40
41
|
@object = object
|
41
42
|
@params = params
|
42
43
|
@within = within
|
43
|
-
@method_existence = {} # Cache for `respond_to?` result
|
44
44
|
DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.__send__(name)) }
|
45
45
|
end
|
46
46
|
|
@@ -148,7 +148,7 @@ module Alba
|
|
148
148
|
end
|
149
149
|
|
150
150
|
def _key_for_collection
|
151
|
-
if Alba.
|
151
|
+
if Alba.inflector
|
152
152
|
@_key_for_collection == true ? resource_name(pluralized: true) : @_key_for_collection.to_s
|
153
153
|
else
|
154
154
|
@_key_for_collection == true ? raise_root_key_inference_error : @_key_for_collection.to_s
|
@@ -157,7 +157,7 @@ module Alba
|
|
157
157
|
|
158
158
|
# @return [String]
|
159
159
|
def _key
|
160
|
-
if Alba.
|
160
|
+
if Alba.inflector
|
161
161
|
@_key == true ? resource_name(pluralized: false) : @_key.to_s
|
162
162
|
else
|
163
163
|
@_key == true ? raise_root_key_inference_error : @_key.to_s
|
@@ -173,7 +173,7 @@ module Alba
|
|
173
173
|
end
|
174
174
|
|
175
175
|
def raise_root_key_inference_error
|
176
|
-
raise Alba::Error, 'You must
|
176
|
+
raise Alba::Error, 'You must set inflector when setting root key as true.'
|
177
177
|
end
|
178
178
|
|
179
179
|
def transforming_root_key?
|
@@ -181,27 +181,27 @@ module Alba
|
|
181
181
|
end
|
182
182
|
|
183
183
|
def converter
|
184
|
-
lambda do |
|
185
|
-
attributes_to_hash(
|
184
|
+
lambda do |obj|
|
185
|
+
attributes_to_hash(obj, {})
|
186
186
|
end
|
187
187
|
end
|
188
188
|
|
189
189
|
def collection_converter
|
190
|
-
lambda do |
|
190
|
+
lambda do |obj, a|
|
191
191
|
a << {}
|
192
192
|
h = a.last
|
193
|
-
attributes_to_hash(
|
193
|
+
attributes_to_hash(obj, h)
|
194
194
|
a
|
195
195
|
end
|
196
196
|
end
|
197
197
|
|
198
|
-
def attributes_to_hash(
|
198
|
+
def attributes_to_hash(obj, hash)
|
199
199
|
attributes.each do |key, attribute|
|
200
|
-
set_key_and_attribute_body_from(
|
200
|
+
set_key_and_attribute_body_from(obj, key, attribute, hash)
|
201
201
|
rescue ::Alba::Error, FrozenError, TypeError
|
202
202
|
raise
|
203
203
|
rescue StandardError => e
|
204
|
-
handle_error(e,
|
204
|
+
handle_error(e, obj, key, attribute, hash)
|
205
205
|
end
|
206
206
|
hash
|
207
207
|
end
|
@@ -212,20 +212,28 @@ module Alba
|
|
212
212
|
@_attributes
|
213
213
|
end
|
214
214
|
|
215
|
-
|
215
|
+
# Default implementation for selecting attributes
|
216
|
+
# Override this method to filter attributes based on key and value
|
217
|
+
def select(_key, _value)
|
218
|
+
true
|
219
|
+
end
|
220
|
+
|
221
|
+
def set_key_and_attribute_body_from(obj, key, attribute, hash)
|
216
222
|
key = transform_key(key)
|
217
|
-
value = fetch_attribute(
|
218
|
-
|
223
|
+
value = fetch_attribute(obj, key, attribute)
|
224
|
+
return unless select(key, value)
|
225
|
+
|
226
|
+
hash[key] = value unless value == Alba::REMOVE_KEY
|
219
227
|
end
|
220
228
|
|
221
|
-
def handle_error(error,
|
229
|
+
def handle_error(error, obj, key, attribute, hash)
|
222
230
|
on_error = @_on_error || :raise
|
223
231
|
case on_error # rubocop:disable Style/MissingElse
|
224
232
|
when :raise, nil then raise(error)
|
225
233
|
when :nullify then hash[key] = nil
|
226
234
|
when :ignore then nil
|
227
235
|
when Proc
|
228
|
-
key, value = on_error.call(error,
|
236
|
+
key, value = on_error.call(error, obj, key, attribute, self.class)
|
229
237
|
hash[key] = value
|
230
238
|
end
|
231
239
|
end
|
@@ -236,7 +244,7 @@ module Alba
|
|
236
244
|
return key if @_transform_type == :none || key.empty? # We can skip transformation
|
237
245
|
|
238
246
|
inflector = Alba.inflector
|
239
|
-
raise Alba::Error, 'Inflector is nil. You
|
247
|
+
raise Alba::Error, 'Inflector is nil. You must set inflector before transforming keys.' unless inflector
|
240
248
|
|
241
249
|
case @_transform_type # rubocop:disable Style/MissingElse
|
242
250
|
when :camel then inflector.camelize(key)
|
@@ -246,23 +254,23 @@ module Alba
|
|
246
254
|
end
|
247
255
|
end
|
248
256
|
|
249
|
-
def fetch_attribute(
|
257
|
+
def fetch_attribute(obj, key, attribute) # rubocop:disable Metrics/CyclomaticComplexity
|
250
258
|
value = case attribute
|
251
|
-
when Symbol then fetch_attribute_from_object_and_resource(
|
252
|
-
when Proc then instance_exec(
|
253
|
-
when Alba::Association then yield_if_within(attribute.name.to_sym) { |within| attribute.to_h(
|
254
|
-
when TypedAttribute, NestedAttribute then attribute.value(
|
255
|
-
when ConditionalAttribute then attribute.with_passing_condition(resource: self, object:
|
259
|
+
when Symbol then fetch_attribute_from_object_and_resource(obj, attribute)
|
260
|
+
when Proc then instance_exec(obj, &attribute)
|
261
|
+
when Alba::Association then yield_if_within(attribute.name.to_sym) { |within| attribute.to_h(obj, params: params, within: within) }
|
262
|
+
when TypedAttribute, NestedAttribute then attribute.value(obj)
|
263
|
+
when ConditionalAttribute then attribute.with_passing_condition(resource: self, object: obj) { |attr| fetch_attribute(obj, key, attr) }
|
256
264
|
else
|
257
265
|
raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
|
258
266
|
end
|
259
|
-
value.nil? && nil_handler ? instance_exec(
|
267
|
+
value.nil? && nil_handler ? instance_exec(obj, key, attribute, &nil_handler) : value
|
260
268
|
end
|
261
269
|
|
262
|
-
def fetch_attribute_from_object_and_resource(
|
263
|
-
|
264
|
-
|
265
|
-
|
270
|
+
def fetch_attribute_from_object_and_resource(obj, attribute)
|
271
|
+
obj.__send__(attribute)
|
272
|
+
rescue NoMethodError
|
273
|
+
__send__(attribute, obj)
|
266
274
|
end
|
267
275
|
|
268
276
|
def nil_handler
|
@@ -271,7 +279,7 @@ module Alba
|
|
271
279
|
|
272
280
|
def yield_if_within(association_name)
|
273
281
|
within = check_within(association_name)
|
274
|
-
yield(within)
|
282
|
+
within ? yield(within) : Alba::REMOVE_KEY
|
275
283
|
end
|
276
284
|
|
277
285
|
def check_within(association_name)
|
@@ -312,7 +320,7 @@ module Alba
|
|
312
320
|
# @param attrs_with_types [Hash<[Symbol, String], [Array<Symbol, Proc>, Symbol]>]
|
313
321
|
# attributes with name in its key and type and optional type converter in its value
|
314
322
|
# @return [void]
|
315
|
-
def attributes(*attrs, if: nil, **attrs_with_types)
|
323
|
+
def attributes(*attrs, if: nil, **attrs_with_types)
|
316
324
|
if_value = binding.local_variable_get(:if)
|
317
325
|
assign_attributes(attrs, if_value)
|
318
326
|
assign_attributes_with_types(attrs_with_types, if_value)
|
data/lib/alba/version.rb
CHANGED
data/lib/alba.rb
CHANGED
@@ -7,10 +7,10 @@ require_relative 'alba/deprecation'
|
|
7
7
|
# Core module
|
8
8
|
module Alba
|
9
9
|
class << self
|
10
|
-
attr_reader :backend, :encoder
|
10
|
+
attr_reader :backend, :encoder
|
11
11
|
|
12
|
-
#
|
13
|
-
|
12
|
+
# Getter for inflector, a module responsible for inflecting strings
|
13
|
+
attr_reader :inflector
|
14
14
|
|
15
15
|
# Set the backend, which actually serializes object into JSON
|
16
16
|
#
|
@@ -54,14 +54,36 @@ module Alba
|
|
54
54
|
# @param with [Symbol, Class, Module] inflector
|
55
55
|
# When it's a Symbol, it sets inflector with given name
|
56
56
|
# When it's a Class or a Module, it sets given object to inflector
|
57
|
+
# @deprecated Use {#inflector=} instead
|
57
58
|
def enable_inference!(with:)
|
59
|
+
Alba::Deprecation.warn('Alba.enable_inference! is deprecated. Use `Alba.inflector=` instead.')
|
58
60
|
@inflector = inflector_from(with)
|
59
61
|
@inferring = true
|
60
62
|
end
|
61
63
|
|
62
64
|
# Disable inference for key and resource name
|
65
|
+
#
|
66
|
+
# @deprecated Use {#inflector=} instead
|
63
67
|
def disable_inference!
|
68
|
+
Alba::Deprecation.warn('Alba.disable_inference! is deprecated. Use `Alba.inflector = nil` instead.')
|
64
69
|
@inferring = false
|
70
|
+
@inflector = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# @deprecated Use {#inflector} instead
|
74
|
+
# @return [Boolean] whether inference is enabled or not
|
75
|
+
def inferring
|
76
|
+
Alba::Deprecation.warn('Alba.inferring is deprecated. Use `Alba.inflector` instead.')
|
77
|
+
@inferring
|
78
|
+
end
|
79
|
+
|
80
|
+
# Set an inflector
|
81
|
+
#
|
82
|
+
# @param inflector [Symbol, Class, Module] inflector
|
83
|
+
# When it's a Symbol, it accepts `:default`, `:active_support` or `:dry`
|
84
|
+
# When it's a Class or a Module, it should have some methods, see {Alba::DefaultInflector}
|
85
|
+
def inflector=(inflector)
|
86
|
+
@inflector = inflector_from(inflector)
|
65
87
|
end
|
66
88
|
|
67
89
|
# @param block [Block] resource body
|
@@ -69,7 +91,7 @@ module Alba
|
|
69
91
|
def resource_class(&block)
|
70
92
|
klass = Class.new
|
71
93
|
klass.include(Alba::Resource)
|
72
|
-
klass.class_eval(&block)
|
94
|
+
klass.class_eval(&block) if block
|
73
95
|
klass
|
74
96
|
end
|
75
97
|
|
@@ -77,7 +99,7 @@ module Alba
|
|
77
99
|
# @param nesting [String, nil] namespace Alba tries to find resource class in
|
78
100
|
# @return [Class<Alba::Resource>] resource class
|
79
101
|
def infer_resource_class(name, nesting: nil)
|
80
|
-
raise Alba::Error, 'Inference is disabled so Alba cannot infer resource name.
|
102
|
+
raise Alba::Error, 'Inference is disabled so Alba cannot infer resource name. Set inflector before use.' unless Alba.inflector
|
81
103
|
|
82
104
|
const_parent = nesting.nil? ? Object : Object.const_get(nesting)
|
83
105
|
const_parent.const_get("#{inflector.classify(name)}Resource")
|
@@ -95,6 +117,7 @@ module Alba
|
|
95
117
|
|
96
118
|
def inflector_from(name_or_module)
|
97
119
|
case name_or_module
|
120
|
+
when nil then nil
|
98
121
|
when :default, :active_support
|
99
122
|
require_relative 'alba/default_inflector'
|
100
123
|
Alba::DefaultInflector
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: alba
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- OKURA Masafumi
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-12-03 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Alba is the fastest JSON serializer for Ruby. It focuses on performance,
|
14
14
|
flexibility and usability.
|
@@ -40,6 +40,7 @@ files:
|
|
40
40
|
- alba.gemspec
|
41
41
|
- benchmark/README.md
|
42
42
|
- benchmark/collection.rb
|
43
|
+
- benchmark/prep.rb
|
43
44
|
- benchmark/single_resource.rb
|
44
45
|
- bin/console
|
45
46
|
- bin/setup
|
@@ -52,6 +53,7 @@ files:
|
|
52
53
|
- lib/alba.rb
|
53
54
|
- lib/alba/association.rb
|
54
55
|
- lib/alba/conditional_attribute.rb
|
56
|
+
- lib/alba/constants.rb
|
55
57
|
- lib/alba/default_inflector.rb
|
56
58
|
- lib/alba/deprecation.rb
|
57
59
|
- lib/alba/errors.rb
|