serega 0.38.0 → 0.40.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/README.md +198 -28
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +3 -3
- data/lib/serega/attribute_normalizer.rb +140 -9
- data/lib/serega/attribute_value_resolvers/batch.rb +4 -0
- data/lib/serega/attribute_value_resolvers/hash_access.rb +116 -0
- data/lib/serega/config.rb +107 -1
- data/lib/serega/object_serializer.rb +3 -3
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +2 -0
- data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +3 -2
- data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +6 -4
- data/lib/serega/plugins/presenter/presenter.rb +99 -2
- data/lib/serega/plugins/root/root.rb +1 -1
- data/lib/serega/utils/collection_detector.rb +26 -0
- data/lib/serega/validations/attribute/check_block.rb +20 -55
- data/lib/serega/validations/attribute/check_opt_base_serializer.rb +37 -0
- data/lib/serega/validations/attribute/check_opt_batch.rb +6 -6
- data/lib/serega/validations/attribute/check_opt_const.rb +3 -4
- data/lib/serega/validations/attribute/check_opt_delegate.rb +45 -5
- data/lib/serega/validations/attribute/check_opt_hash_access.rb +78 -0
- data/lib/serega/validations/attribute/check_opt_many.rb +8 -7
- data/lib/serega/validations/attribute/check_opt_method.rb +3 -4
- data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -1
- data/lib/serega/validations/attribute/check_opt_value.rb +3 -4
- data/lib/serega/validations/check_attribute_params.rb +9 -7
- data/lib/serega.rb +22 -6
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 42da0ff66dc5eeb282fbb72144f78bcfc8ded1aa8609122ded10093f6c83dd96
|
|
4
|
+
data.tar.gz: f9fb2a2628756540cd08a5449a21883b0cf28ec45db8f461ee0944fd0f3812e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1355573b7cb8d30ffbf844be46c03c4a5233fa9c190fa5e03fd39c0d1a396b9f7a19e7e2d4cf3afbd0445cd1d8dfc8ebbd1df6d77d91d338c0ede561c836fb99
|
|
7
|
+
data.tar.gz: 90ccd454b970598f9921ec1a6c9feab325a373d5628f884515141d34a3a72e7012273e37aa903046e064876f92bd956783ef3a9b93732ee23a8bcc8bec154ee3
|
data/README.md
CHANGED
|
@@ -26,6 +26,7 @@ It has some great features:
|
|
|
26
26
|
keeping the code dry
|
|
27
27
|
- Conditional attributes - ([if][if] plugin)
|
|
28
28
|
- Auto camelCase keys - [camel_case][camel_case] plugin
|
|
29
|
+
- Serializing Hash records - [hash_access][hash_access] attribute option
|
|
29
30
|
|
|
30
31
|
## Installation
|
|
31
32
|
|
|
@@ -74,12 +75,29 @@ class UserSerializer < Serega
|
|
|
74
75
|
# Regular attribute
|
|
75
76
|
attribute :first_name
|
|
76
77
|
|
|
77
|
-
# Option :method specifies the
|
|
78
|
-
# serialized object
|
|
78
|
+
# Option :method specifies the name used to read the attribute's value —
|
|
79
|
+
# a method called on the serialized object by default, or a Hash key when
|
|
80
|
+
# combined with :hash_access
|
|
79
81
|
attribute :first_name, method: :old_first_name
|
|
80
82
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
+
# Attribute blocks below require a base serializer for nested serializers.
|
|
84
|
+
# See the "Defining a nested serializer with a block" section below.
|
|
85
|
+
config.base_serializer = Serega
|
|
86
|
+
|
|
87
|
+
# Method :itself is handy to serialize the same object with a nested set of
|
|
88
|
+
# attributes. Note: with the :presenter plugin the serialized value will be
|
|
89
|
+
# the Presenter instance itself; use `method: :__getobj__` to serialize the
|
|
90
|
+
# original unwrapped object instead.
|
|
91
|
+
attribute :statistics, method: :itself do
|
|
92
|
+
attribute :likes_count
|
|
93
|
+
attribute :comments_count
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Block defines a nested serializer for the attribute value.
|
|
97
|
+
# See the "Defining a nested serializer with a block" section below.
|
|
98
|
+
attribute :author do
|
|
99
|
+
attribute :name
|
|
100
|
+
end
|
|
83
101
|
|
|
84
102
|
# Option :value can be used with a Proc or callable object to define
|
|
85
103
|
# attribute value
|
|
@@ -114,13 +132,13 @@ class UserSerializer < Serega
|
|
|
114
132
|
|
|
115
133
|
# Option `:many` specifies a has_many relationship. It is optional.
|
|
116
134
|
# If not specified, it is defined during serialization by checking
|
|
117
|
-
# `object.is_a?(Enumerable)`
|
|
135
|
+
# `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`
|
|
118
136
|
# Also the `:many` changes the default value from `nil` to `[]`.
|
|
119
137
|
attribute :posts, serializer: PostSerializer, many: true
|
|
120
138
|
|
|
121
139
|
# Option `:preload` allows to specify associations to preload to
|
|
122
140
|
# attribute value
|
|
123
|
-
attribute
|
|
141
|
+
attribute :email, preload: :emails, value: proc { |user| user.emails.find(&:verified?) }
|
|
124
142
|
|
|
125
143
|
# Options `:if, :unless, :if_value and :unless_value` can be specified
|
|
126
144
|
# when `:if` plugin is enabled. They hide the attribute key and value from the
|
|
@@ -139,6 +157,70 @@ class UserSerializer < Serega
|
|
|
139
157
|
end
|
|
140
158
|
```
|
|
141
159
|
|
|
160
|
+
### Defining a nested serializer with a block
|
|
161
|
+
|
|
162
|
+
An attribute block defines an anonymous nested serializer. The attribute value
|
|
163
|
+
is found as usual — by the attribute name or the `:method`, `:value`,
|
|
164
|
+
`:delegate`, `:batch` option — and is serialized by this nested serializer.
|
|
165
|
+
|
|
166
|
+
The nested serializer is a regular subclass of an explicitly chosen **base
|
|
167
|
+
serializer** — usually a settings-only serializer holding your plugins and
|
|
168
|
+
configuration. Choose it with the `base_serializer:` attribute option or the
|
|
169
|
+
`config.base_serializer` setting (attribute option wins; an error is raised
|
|
170
|
+
when none is set):
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
class AppSerializer < Serega
|
|
174
|
+
plugin :activerecord_preloads
|
|
175
|
+
|
|
176
|
+
# Nested serializers defined with attribute blocks will inherit from
|
|
177
|
+
# AppSerializer
|
|
178
|
+
config.base_serializer = self
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class UserSerializer < AppSerializer
|
|
182
|
+
attribute :first_name
|
|
183
|
+
|
|
184
|
+
# Serializes user.author with a nested serializer inherited from AppSerializer
|
|
185
|
+
attribute :author do
|
|
186
|
+
attribute :name
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Serializes the user itself, exposing only counters
|
|
190
|
+
attribute :statistics, method: :itself, base_serializer: StatsBaseSerializer do
|
|
191
|
+
attribute :likes_count
|
|
192
|
+
attribute :comments_count
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
UserSerializer.to_h(user)
|
|
197
|
+
# => {first_name: "Bruce", author: {name: "Bob"}, statistics: {likes_count: 10, comments_count: 3}}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The nested serializer inherits everything the base serializer has — plugins,
|
|
201
|
+
config, attributes, batch loaders, the preload handler — through the regular
|
|
202
|
+
inheritance mechanism. Any serializer can be used as a base: if it has
|
|
203
|
+
attributes, they are serialized by the nested serializer too, together with
|
|
204
|
+
the attributes defined in the block. Anything the base does not provide can
|
|
205
|
+
be declared inside the block:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
attribute :statistics, method: :itself do
|
|
209
|
+
batch(:stats) { |users| Stats.for_users(users) } # => { user_id => stat }
|
|
210
|
+
|
|
211
|
+
attribute :likes_count, batch: :stats, value: proc { |user, batches:| batches[:stats][user.id].likes }
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Things to keep in mind:
|
|
216
|
+
|
|
217
|
+
- The nested serializer is created at the moment the attribute is defined.
|
|
218
|
+
Changes made to the base serializer later do not affect already defined
|
|
219
|
+
nested serializers.
|
|
220
|
+
- Errors raised while serializing nested attributes are reported with a
|
|
221
|
+
readable serializer label, for example:
|
|
222
|
+
`(when serializing 'comments_count' attribute in UserSerializer.<statistics>)`.
|
|
223
|
+
|
|
142
224
|
### Serializing
|
|
143
225
|
|
|
144
226
|
We can serialize objects using class method `.call` aliased as `.to_h` and
|
|
@@ -183,15 +265,6 @@ serializer.to_h(user1)
|
|
|
183
265
|
serializer.to_h(user2)
|
|
184
266
|
```
|
|
185
267
|
|
|
186
|
-
---
|
|
187
|
-
⚠️ When you serialize the `Struct` object, specify manually `many: false`. As Struct
|
|
188
|
-
is Enumerable and we check `object.is_a?(Enumerable)` to detect if we should
|
|
189
|
-
return array.
|
|
190
|
-
|
|
191
|
-
```ruby
|
|
192
|
-
UserSerializer.to_h(user_struct, many: false)
|
|
193
|
-
```
|
|
194
|
-
|
|
195
268
|
### Selecting Fields
|
|
196
269
|
|
|
197
270
|
By default, all attributes are serialized (except marked as `hide: true`).
|
|
@@ -312,9 +385,9 @@ current_user or any.
|
|
|
312
385
|
|
|
313
386
|
```ruby
|
|
314
387
|
class UserSerializer < Serega
|
|
315
|
-
attribute
|
|
388
|
+
attribute :email, value: proc { |user, ctx|
|
|
316
389
|
user.email if ctx[:current_user] == user
|
|
317
|
-
|
|
390
|
+
}
|
|
318
391
|
end
|
|
319
392
|
|
|
320
393
|
user = OpenStruct.new(email: 'email@example.com')
|
|
@@ -339,15 +412,18 @@ class UserSerializer < Serega
|
|
|
339
412
|
batch :comments_count, ->(users) { Comment.where(user: users).group(:user_id).count }
|
|
340
413
|
batch :comments_count, CommentsCountLoader # Example with callable class
|
|
341
414
|
|
|
342
|
-
#
|
|
343
|
-
attribute :comments_count, batch:
|
|
344
|
-
value: proc { |user, batch:| batch[:comments_count][user.id] }
|
|
415
|
+
# Use a loader by its name
|
|
416
|
+
attribute :comments_count, batch: :comments_count
|
|
345
417
|
|
|
346
|
-
#
|
|
347
|
-
attribute :comments_count, batch:
|
|
418
|
+
# Equivalent — when the attribute name matches the loader name
|
|
419
|
+
attribute :comments_count, batch: true
|
|
348
420
|
|
|
349
|
-
#
|
|
350
|
-
attribute :comments_count, batch:
|
|
421
|
+
# Equivalent — the default value resolution spelled out
|
|
422
|
+
attribute :comments_count, batch: :comments_count,
|
|
423
|
+
value: proc { |user, batches:| batches[:comments_count][user.id] }
|
|
424
|
+
|
|
425
|
+
# Hash form — needed only for sub-options, for example a custom `:id` method
|
|
426
|
+
attribute :comments_count, batch: { use: :comments_count, id: :uuid }
|
|
351
427
|
end
|
|
352
428
|
```
|
|
353
429
|
|
|
@@ -382,7 +458,7 @@ class UserSerializer < Serega
|
|
|
382
458
|
# Summarize likes
|
|
383
459
|
attribute :likes_count,
|
|
384
460
|
batch: { use: [:facebook_likes, :twitter_likes] },
|
|
385
|
-
value: { |user,
|
|
461
|
+
value: proc { |user, batches:| batches[:facebook_likes][user.id] + batches[:twitter_likes][user.id] }
|
|
386
462
|
end
|
|
387
463
|
```
|
|
388
464
|
|
|
@@ -390,6 +466,13 @@ end
|
|
|
390
466
|
|
|
391
467
|
Here are the default options. Other options can be added with plugins.
|
|
392
468
|
|
|
469
|
+
⚠️ Attributes are prepared at the moment they are defined. If a config option
|
|
470
|
+
influences attributes (`auto_preload`, `hide_by_default`, `batch_id_option`,
|
|
471
|
+
formatters, etc.), changing it affects only attributes defined **after** the
|
|
472
|
+
change — including nested serializers defined with attribute blocks, which
|
|
473
|
+
are created at their definition point. Configure the serializer before
|
|
474
|
+
defining attributes.
|
|
475
|
+
|
|
393
476
|
```ruby
|
|
394
477
|
class AppSerializer < Serega
|
|
395
478
|
# With `activerecord_preloads` plugin it automatically adds `preload` option
|
|
@@ -413,6 +496,19 @@ class AppSerializer < Serega
|
|
|
413
496
|
# proc { |object, batches:| batches[:counter][object.id] }
|
|
414
497
|
config.batch_id_option = :id
|
|
415
498
|
|
|
499
|
+
# Defaults for the `hash_access:` attribute option — `default_mode` is
|
|
500
|
+
# what `hash_access: true` (and a Hash form omitting :mode) resolves to.
|
|
501
|
+
# See "Serializing Hash records".
|
|
502
|
+
config.hash_access.default_mode = :symbol # the default
|
|
503
|
+
config.hash_access.default_allow_missing_key = false # the default
|
|
504
|
+
|
|
505
|
+
# Parent class for nested serializers defined with attribute blocks.
|
|
506
|
+
# Usually a settings-only serializer, e.g. `config.base_serializer = self`
|
|
507
|
+
# in an application base serializer class. There is no default — an
|
|
508
|
+
# attribute block raises an error when no base serializer is chosen (it can
|
|
509
|
+
# also be provided per-attribute with the `base_serializer:` option).
|
|
510
|
+
config.base_serializer = self
|
|
511
|
+
|
|
416
512
|
# Disable/enable validation of modifiers (`:with, :except, :only`)
|
|
417
513
|
# By default, this validation is enabled.
|
|
418
514
|
# After disabling, all requested incorrect attributes will be skipped.
|
|
@@ -576,6 +672,69 @@ end
|
|
|
576
672
|
|
|
577
673
|
---
|
|
578
674
|
|
|
675
|
+
## Serializing Hash records
|
|
676
|
+
|
|
677
|
+
(objects should implement `#[]`, `#fetch`, and `#key?` - Hash already has them)
|
|
678
|
+
|
|
679
|
+
The `hash_access: <mode>` attribute option makes an attribute read its value from
|
|
680
|
+
Hash keys instead of calling a method.
|
|
681
|
+
|
|
682
|
+
Allowed `<mode>`:
|
|
683
|
+
|
|
684
|
+
- `:symbol` reads record[:name]
|
|
685
|
+
- `:string` reads record["name"]
|
|
686
|
+
- `true` shorthand for `config.hash_access.default_mode` (`:symbol` by default)
|
|
687
|
+
|
|
688
|
+
By default missing keys will raise `KeyError`.
|
|
689
|
+
|
|
690
|
+
`config.hash_access.default_allow_missing_key = true` will change `KeyError`
|
|
691
|
+
to return `nil` value instead. `config.hash_access.default_mode = <mode>`
|
|
692
|
+
changes what `true` (and a Hash form omitting `:mode`) resolves to.
|
|
693
|
+
|
|
694
|
+
```ruby
|
|
695
|
+
class UserSerializer < Serega
|
|
696
|
+
config.hash_access.default_mode = :symbol # the default
|
|
697
|
+
config.hash_access.default_allow_missing_key = true # false by default
|
|
698
|
+
|
|
699
|
+
attribute :name, hash_access: true # reads record[:name] (config.hash_access.default_mode)
|
|
700
|
+
attribute :name, hash_access: :symbol # reads record[:name]
|
|
701
|
+
attribute :name, hash_access: :string # reads record["name"]
|
|
702
|
+
|
|
703
|
+
# Use long form if you need to override `allow_missing_key`
|
|
704
|
+
attribute :name, hash_access: { allow_missing_key: false }
|
|
705
|
+
attribute :name, hash_access: { mode: :symbol, allow_missing_key: false }
|
|
706
|
+
attribute :name, hash_access: { mode: :string, allow_missing_key: false }
|
|
707
|
+
end
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
Delegated attributes configure hash access **per step**:
|
|
711
|
+
|
|
712
|
+
- `to_hash_access` configures reading `<mode>` for intermediate object
|
|
713
|
+
- `hash_access` configures reading `<mode>` for final key
|
|
714
|
+
|
|
715
|
+
```ruby
|
|
716
|
+
# reads `record.address.city` (no hash access)
|
|
717
|
+
attribute :city, delegate: { to: :address } # record.address.city
|
|
718
|
+
|
|
719
|
+
# reads record.address[:city]
|
|
720
|
+
attribute :city, delegate: { to: :address, hash_access: :symbol }
|
|
721
|
+
|
|
722
|
+
# reads record[:address].city
|
|
723
|
+
attribute :city, delegate: { to: :address, to_hash_access: :symbol }
|
|
724
|
+
|
|
725
|
+
# reads record[:address][:city]
|
|
726
|
+
attribute :city, delegate: { to: :address, to_hash_access: :symbol, hash_access: :symbol }
|
|
727
|
+
|
|
728
|
+
# reads record["address"] and if exists returns record["address"][:city]
|
|
729
|
+
attribute :city,
|
|
730
|
+
delegate: {
|
|
731
|
+
to: :address,
|
|
732
|
+
allow_nil: true,
|
|
733
|
+
to_hash_access: { mode: :string, allow_missing_key: true },
|
|
734
|
+
hash_access: { mode: :symbol, allow_missing_key: false }
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
579
738
|
## Plugins
|
|
580
739
|
|
|
581
740
|
### Plugin :activerecord_preloads
|
|
@@ -793,7 +952,7 @@ class UserSerializer < Serega
|
|
|
793
952
|
end
|
|
794
953
|
```
|
|
795
954
|
|
|
796
|
-
With the plugin, they move into a
|
|
955
|
+
With the plugin, they move into a `presenter do ... end` block:
|
|
797
956
|
|
|
798
957
|
```ruby
|
|
799
958
|
class UserSerializer < Serega
|
|
@@ -802,7 +961,7 @@ class UserSerializer < Serega
|
|
|
802
961
|
attribute :name
|
|
803
962
|
attribute :role
|
|
804
963
|
|
|
805
|
-
|
|
964
|
+
presenter do
|
|
806
965
|
def name
|
|
807
966
|
[first_name, last_name].compact.join(' ')
|
|
808
967
|
end
|
|
@@ -814,6 +973,9 @@ class UserSerializer < Serega
|
|
|
814
973
|
end
|
|
815
974
|
```
|
|
816
975
|
|
|
976
|
+
The block is evaluated inside the serializer's own `Presenter` class, so
|
|
977
|
+
multiple `presenter` blocks accumulate.
|
|
978
|
+
|
|
817
979
|
`Presenter` inherits from `SimpleDelegator`, so every method of the serialized
|
|
818
980
|
object is available directly inside presenter methods. Any method not explicitly
|
|
819
981
|
defined on `Presenter` is resolved via `method_missing` on the first call, which
|
|
@@ -825,6 +987,12 @@ The original wrapped object is accessible via `__getobj__` (standard
|
|
|
825
987
|
|
|
826
988
|
The serialization context is accessible via the private method `__ctx__`.
|
|
827
989
|
|
|
990
|
+
Objects are wrapped in the `Presenter` only when the serializer's `Presenter`
|
|
991
|
+
class (or an inherited one) actually defines custom methods. Loading the
|
|
992
|
+
plugin in a base serializer adds no overhead to serializers that don't
|
|
993
|
+
customize their presenters — their attribute values, batch loaders and value
|
|
994
|
+
callables keep receiving the raw objects.
|
|
995
|
+
|
|
828
996
|
### Plugin :string_modifiers
|
|
829
997
|
|
|
830
998
|
Allows to specify modifiers as strings.
|
|
@@ -973,7 +1141,8 @@ end
|
|
|
973
1141
|
### Plugin :explicit_many_option
|
|
974
1142
|
|
|
975
1143
|
The plugin requires adding a `:many` option when adding relationships
|
|
976
|
-
(attributes with the `:serializer` option
|
|
1144
|
+
(attributes with the `:serializer` option or a block defining a nested
|
|
1145
|
+
serializer).
|
|
977
1146
|
|
|
978
1147
|
Adding this plugin makes it clearer to find if some relationship is an array or
|
|
979
1148
|
a single object.
|
|
@@ -1030,6 +1199,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
1030
1199
|
[context_metadata]: #plugin-context_metadata
|
|
1031
1200
|
[depth_limit]: #plugin-depth_limit
|
|
1032
1201
|
[formatters]: #plugin-formatters
|
|
1202
|
+
[hash_access]: #serializing-hash-records
|
|
1033
1203
|
[metadata]: #plugin-metadata
|
|
1034
1204
|
[preloads]: #preloads
|
|
1035
1205
|
[presenter]: #plugin-presenter
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.40.0
|
data/lib/serega/attribute.rb
CHANGED
|
@@ -45,10 +45,10 @@ class Serega
|
|
|
45
45
|
# @option opts [Symbol] :method Object method name to fetch attribute value
|
|
46
46
|
# @option opts [Hash] :delegate Allows to fetch value from nested object
|
|
47
47
|
# @option opts [Boolean] :hide Specify `true` to not serialize this attribute by default
|
|
48
|
-
# @option opts [Boolean] :many Specifies has_many relationship. By default is detected via object.is_a?(Enumerable)
|
|
48
|
+
# @option opts [Boolean] :many Specifies has_many relationship. By default is detected via object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)
|
|
49
49
|
# @option opts [Proc, #call] :value Custom block or callable to find attribute value
|
|
50
50
|
# @option opts [Serega, Proc] :serializer Relationship serializer class. Use `proc { MySerializer }` if serializers have cross references
|
|
51
|
-
# @param block [Proc]
|
|
51
|
+
# @param block [Proc] Defines attributes of a nested anonymous serializer
|
|
52
52
|
#
|
|
53
53
|
def initialize(name:, opts: {}, block: nil)
|
|
54
54
|
serializer_class = self.class.serializer_class
|
|
@@ -88,7 +88,7 @@ class Serega
|
|
|
88
88
|
# @return [Object] Serialized attribute value
|
|
89
89
|
#
|
|
90
90
|
def value(object, context, batches: nil)
|
|
91
|
-
#
|
|
91
|
+
# Signatures should match allowed signatures in CheckOptValue
|
|
92
92
|
result =
|
|
93
93
|
case value_block_signature
|
|
94
94
|
when "1" then value_block.call(object)
|
|
@@ -133,8 +133,7 @@ class Serega
|
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def prepare_value_block
|
|
136
|
-
|
|
137
|
-
|| init_opts[:value] \
|
|
136
|
+
init_opts[:value] \
|
|
138
137
|
|| prepare_const_block \
|
|
139
138
|
|| prepare_delegate_block \
|
|
140
139
|
|| prepare_batch_loader_block \
|
|
@@ -175,7 +174,73 @@ class Serega
|
|
|
175
174
|
end
|
|
176
175
|
|
|
177
176
|
def prepare_serializer
|
|
178
|
-
|
|
177
|
+
block = init_block
|
|
178
|
+
return init_opts[:serializer] unless block
|
|
179
|
+
|
|
180
|
+
prepare_block_serializer(block)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Builds an anonymous nested serializer from the attribute block.
|
|
184
|
+
#
|
|
185
|
+
# The serializer is a regular subclass of an explicitly chosen base —
|
|
186
|
+
# the :base_serializer attribute option or `config.base_serializer` —
|
|
187
|
+
# so it inherits everything the base has: plugins, config, attributes,
|
|
188
|
+
# batch loaders, the preload handler. Nested values can be any objects,
|
|
189
|
+
# which is why the base is chosen explicitly instead of inheriting from
|
|
190
|
+
# the current serializer. Its `inspect` is overridden to
|
|
191
|
+
# `CurrentSerializer.<attribute_name>` so error messages and debugging
|
|
192
|
+
# output point back to the defining attribute.
|
|
193
|
+
# Guards against cyclic definitions before building: inheriting the base
|
|
194
|
+
# serializer copies its attributes, and copying a block attribute builds
|
|
195
|
+
# its nested serializer — meeting the same block again while it is being
|
|
196
|
+
# built means the base serializer (transitively) contains the block
|
|
197
|
+
# attribute being built, so inheriting from it would recurse forever.
|
|
198
|
+
# The "in progress" mark is kept on the block itself — its identity is
|
|
199
|
+
# what defines the cycle (the same Proc object is shared by all copies
|
|
200
|
+
# of the attribute).
|
|
201
|
+
def prepare_block_serializer(block)
|
|
202
|
+
if block.instance_variable_defined?(:@serega_building_nested_serializer)
|
|
203
|
+
raise SeregaError,
|
|
204
|
+
"Can not define a nested serializer for attribute :#{name} —" \
|
|
205
|
+
" its base serializer #{block_base_serializer.inspect} (transitively)" \
|
|
206
|
+
" contains this same block attribute (cyclic definition)"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
block.instance_variable_set(:@serega_building_nested_serializer, true)
|
|
210
|
+
begin
|
|
211
|
+
build_block_serializer(block)
|
|
212
|
+
ensure
|
|
213
|
+
block.remove_instance_variable(:@serega_building_nested_serializer)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def build_block_serializer(block)
|
|
218
|
+
label = "#{self.class.serializer_class.inspect}.<#{name}>"
|
|
219
|
+
|
|
220
|
+
serializer = Class.new(block_base_serializer) do
|
|
221
|
+
define_singleton_method(:inspect) { label }
|
|
222
|
+
define_singleton_method(:to_s) { label }
|
|
223
|
+
instance_exec(&block)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# An empty nested serializer means the block was intended as an
|
|
227
|
+
# old-style value block — raise the explaining error.
|
|
228
|
+
raise SeregaError, SeregaValidations::Attribute::CheckBlock::ERROR_MESSAGE if serializer.attributes.empty?
|
|
229
|
+
|
|
230
|
+
serializer
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Base class for the nested serializer. Must be chosen explicitly —
|
|
234
|
+
# usually a settings-only serializer holding plugins and configuration
|
|
235
|
+
# (e.g. `config.base_serializer = self` in an application base class).
|
|
236
|
+
def block_base_serializer
|
|
237
|
+
base_serializer = init_opts[:base_serializer] || config.base_serializer
|
|
238
|
+
return base_serializer if base_serializer
|
|
239
|
+
|
|
240
|
+
raise SeregaError,
|
|
241
|
+
"Attribute block requires a base serializer for the nested serializer." \
|
|
242
|
+
" Provide the `base_serializer: <SerializerClass>` attribute option" \
|
|
243
|
+
" or set `config.base_serializer = <SerializerClass>`"
|
|
179
244
|
end
|
|
180
245
|
|
|
181
246
|
def prepare_method_name
|
|
@@ -189,7 +254,10 @@ class Serega
|
|
|
189
254
|
end
|
|
190
255
|
|
|
191
256
|
def prepare_keyword_block
|
|
192
|
-
|
|
257
|
+
mode, allow_nil = hash_access
|
|
258
|
+
return AttributeValueResolvers::KeywordResolver.get(method_name) unless mode
|
|
259
|
+
|
|
260
|
+
AttributeValueResolvers::HashAccessResolver.get(method_name, mode, allow_nil)
|
|
193
261
|
end
|
|
194
262
|
|
|
195
263
|
def prepare_batch_loader_block
|
|
@@ -226,15 +294,68 @@ class Serega
|
|
|
226
294
|
init_opts.fetch(:default) { many ? FROZEN_EMPTY_ARRAY : nil }
|
|
227
295
|
end
|
|
228
296
|
|
|
297
|
+
# `delegate: {to_hash_access: ...}` configures hash access for the
|
|
298
|
+
# intermediate (:to) read, `delegate: {hash_access: ...}` for the
|
|
299
|
+
# final (:method) read. A step without its sub-option keeps a plain
|
|
300
|
+
# method read.
|
|
229
301
|
def prepare_delegate_block
|
|
230
302
|
delegate = init_opts[:delegate]
|
|
231
303
|
return unless delegate
|
|
232
304
|
|
|
233
305
|
key_method_name = delegate[:method] || method_name
|
|
234
306
|
delegate_to = delegate[:to]
|
|
307
|
+
delegate_allow_nil = delegate.fetch(:allow_nil) { config.delegate_default_allow_nil }
|
|
308
|
+
|
|
309
|
+
to_access = delegate[:to_hash_access]
|
|
310
|
+
final_access = delegate[:hash_access]
|
|
311
|
+
unless to_access || final_access
|
|
312
|
+
return AttributeValueResolvers::DelegateResolver.get(delegate_to, key_method_name, delegate_allow_nil)
|
|
313
|
+
end
|
|
235
314
|
|
|
236
|
-
|
|
237
|
-
|
|
315
|
+
to_step = delegate_to_step(delegate_to, to_access)
|
|
316
|
+
final_step = delegate_final_step(key_method_name, final_access)
|
|
317
|
+
AttributeValueResolvers::HashAccessDelegateResolver.get(to_step, final_step, delegate_allow_nil)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def delegate_to_step(delegate_to, access)
|
|
321
|
+
return AttributeValueResolvers::Keyword.new(delegate_to) unless access
|
|
322
|
+
|
|
323
|
+
mode, allow_missing_key = parse_hash_access(access)
|
|
324
|
+
AttributeValueResolvers::HashAccessKeyword.new(delegate_to, mode, allow_missing_key)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def delegate_final_step(key_method_name, access)
|
|
328
|
+
return AttributeValueResolvers::Keyword.new(key_method_name) unless access
|
|
329
|
+
|
|
330
|
+
mode, allow_missing_key = parse_hash_access(access)
|
|
331
|
+
AttributeValueResolvers::HashAccessKeyword.new(key_method_name, mode, allow_missing_key)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Resolves the :hash_access option of plain attributes
|
|
335
|
+
# @return [Array(Symbol, Boolean), nil] mode and allow_missing_key pair,
|
|
336
|
+
# or nil when hash access is not enabled for this attribute
|
|
337
|
+
def hash_access
|
|
338
|
+
option = init_opts[:hash_access]
|
|
339
|
+
return unless option
|
|
340
|
+
|
|
341
|
+
parse_hash_access(option)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Resolves a :hash_access value (`true`, a Symbol mode or a
|
|
345
|
+
# `{mode:, allow_missing_key:}` Hash) into a mode and allow_missing_key
|
|
346
|
+
# pair. `true` and a Hash omitting :mode resolve to
|
|
347
|
+
# `config.hash_access.default_mode`.
|
|
348
|
+
def parse_hash_access(option)
|
|
349
|
+
defaults = config.hash_access
|
|
350
|
+
|
|
351
|
+
case option
|
|
352
|
+
when true then [defaults.default_mode, defaults.default_allow_missing_key]
|
|
353
|
+
when Symbol then [option, defaults.default_allow_missing_key]
|
|
354
|
+
else
|
|
355
|
+
mode = option.fetch(:mode) { defaults.default_mode }
|
|
356
|
+
allow_missing_key = option.fetch(:allow_missing_key) { defaults.default_allow_missing_key }
|
|
357
|
+
[mode, allow_missing_key]
|
|
358
|
+
end
|
|
238
359
|
end
|
|
239
360
|
|
|
240
361
|
# Prepares preloads for this attribute.
|
|
@@ -250,16 +371,26 @@ class Serega
|
|
|
250
371
|
|
|
251
372
|
# Auto-preload the delegated association
|
|
252
373
|
if config.auto_preload.fetch(:has_delegate_option) && init_opts[:delegate]
|
|
253
|
-
return init_opts[:delegate][:to]
|
|
374
|
+
return auto_preload_value(init_opts[:delegate][:to])
|
|
254
375
|
end
|
|
255
376
|
|
|
256
377
|
# Auto-preload the nested serializer's association
|
|
257
|
-
|
|
258
|
-
|
|
378
|
+
# (a block defines a nested serializer, same as the :serializer option)
|
|
379
|
+
if config.auto_preload.fetch(:has_serializer_option) && (init_opts[:serializer] || init_block) && !init_opts.key?(:batch)
|
|
380
|
+
return auto_preload_value(method_name)
|
|
259
381
|
end
|
|
260
382
|
|
|
261
383
|
nil
|
|
262
384
|
end
|
|
385
|
+
|
|
386
|
+
# Skips auto-preloading of methods that return the serialized object
|
|
387
|
+
# itself (:itself by default) — they are not associations,
|
|
388
|
+
# so preloading them would fail or make no sense.
|
|
389
|
+
def auto_preload_value(preload_method)
|
|
390
|
+
return nil if config.auto_preload_excluded_methods.include?(preload_method.to_sym)
|
|
391
|
+
|
|
392
|
+
preload_method
|
|
393
|
+
end
|
|
263
394
|
end
|
|
264
395
|
|
|
265
396
|
extend Serega::SeregaHelpers::SerializerClassHelper
|
|
@@ -18,6 +18,7 @@ class Serega
|
|
|
18
18
|
# It handles this cases:
|
|
19
19
|
# - `attribute :foo, batch: true`
|
|
20
20
|
# - `attribute :foo, batch: FooLoader`
|
|
21
|
+
# - `attribute :foo, batch: :foo_loader`
|
|
21
22
|
# - `attribute :foo, batch: { id: :foo_id }`
|
|
22
23
|
# - `attribute :foo, batch: { use: FooLoader, id: foo_id }`
|
|
23
24
|
# - `attribute :foo, batch: { use: :foo_loader, id: foo_id }`
|
|
@@ -32,6 +33,9 @@ class Serega
|
|
|
32
33
|
serializer_class.batch(attribute_name, batch_opt)
|
|
33
34
|
batch_name = attribute_name
|
|
34
35
|
batch_id_method = default_method
|
|
36
|
+
elsif batch_opt.is_a?(Symbol) || batch_opt.is_a?(String) # ex: `batch: :foo_loader`
|
|
37
|
+
batch_name = batch_opt.to_sym
|
|
38
|
+
batch_id_method = default_method
|
|
35
39
|
else
|
|
36
40
|
use = batch_opt[:use]
|
|
37
41
|
batch_id_method = batch_opt[:id] || default_method
|