serega 0.42.0 → 0.50.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 +124 -1207
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +186 -131
- data/lib/serega/attribute_normalizer.rb +164 -345
- data/lib/serega/attribute_normalizers/batch.rb +45 -0
- data/lib/serega/attribute_normalizers/block.rb +81 -0
- data/lib/serega/attribute_normalizers/delegate.rb +34 -0
- data/lib/serega/attribute_normalizers/hash_access.rb +37 -0
- data/lib/serega/attribute_normalizers/preloads.rb +46 -0
- data/lib/serega/attribute_value_resolvers/batch.rb +5 -54
- data/lib/serega/attribute_value_resolvers/const.rb +2 -24
- data/lib/serega/attribute_value_resolvers/delegate.rb +17 -28
- data/lib/serega/attribute_value_resolvers/hash_access.rb +34 -52
- data/lib/serega/attribute_value_resolvers/keyword.rb +10 -25
- data/lib/serega/attribute_values.rb +46 -0
- data/lib/serega/batch_loader.rb +37 -0
- data/lib/serega/conditions.rb +56 -0
- data/lib/serega/config.rb +105 -144
- data/lib/serega/engine/pulls.rb +64 -0
- data/lib/serega/engine/run.rb +106 -0
- data/lib/serega/errors.rb +36 -8
- data/lib/serega/formatter.rb +98 -0
- data/lib/serega/plan/cache.rb +96 -0
- data/lib/serega/plan/per_ractor_cache.rb +23 -0
- data/lib/serega/plan.rb +101 -128
- data/lib/serega/plan_point.rb +44 -151
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +13 -80
- data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +8 -75
- data/lib/serega/plugins/context_metadata/context_metadata.rb +40 -123
- data/lib/serega/plugins/metadata/check_params.rb +80 -0
- data/lib/serega/plugins/metadata/meta_attribute.rb +40 -79
- data/lib/serega/plugins/metadata/metadata.rb +40 -168
- data/lib/serega/plugins/root/root.rb +91 -235
- data/lib/serega/plugins.rb +16 -49
- data/lib/serega/presenter.rb +61 -19
- data/lib/serega/result_builder.rb +68 -0
- data/lib/serega/result_code.rb +203 -0
- data/lib/serega/shareable.rb +52 -0
- data/lib/serega/source_group.rb +187 -0
- data/lib/serega/utils/collection_detector.rb +19 -13
- data/lib/serega/utils/deep_merge.rb +26 -0
- data/lib/serega/utils/enum_deep_dup.rb +10 -16
- data/lib/serega/utils/enum_deep_freeze.rb +8 -14
- data/lib/serega/utils/method_signature.rb +6 -15
- data/lib/serega/utils/parse_string_modifiers.rb +63 -0
- data/lib/serega/utils/ractor_local.rb +36 -0
- data/lib/serega/utils/serialized_attribute_error.rb +60 -20
- data/lib/serega/utils/to_hash.rb +5 -26
- data/lib/serega/validations/attribute/check_block.rb +3 -15
- data/lib/serega/validations/attribute/check_name.rb +4 -15
- data/lib/serega/validations/attribute/check_opt_base_serializer.rb +3 -15
- data/lib/serega/validations/attribute/check_opt_batch.rb +26 -56
- data/lib/serega/validations/attribute/check_opt_condition.rb +55 -0
- data/lib/serega/validations/attribute/check_opt_delegate.rb +10 -63
- data/lib/serega/validations/attribute/check_opt_format.rb +36 -0
- data/lib/serega/validations/attribute/check_opt_hash_access.rb +11 -59
- data/lib/serega/validations/attribute/check_opt_hide.rb +3 -14
- data/lib/serega/validations/attribute/check_opt_many.rb +18 -20
- data/lib/serega/validations/attribute/check_opt_method.rb +3 -25
- data/lib/serega/validations/attribute/check_opt_preload.rb +3 -26
- data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -18
- data/lib/serega/validations/attribute/check_opt_value.rb +3 -21
- data/lib/serega/validations/check_attribute_params.rb +92 -71
- data/lib/serega/validations/check_batch_loader_params.rb +43 -62
- data/lib/serega/validations/check_formatter.rb +80 -0
- data/lib/serega/validations/check_serialize_keywords.rb +23 -0
- data/lib/serega/validations/initiate/check_modifiers.rb +14 -20
- data/lib/serega/validations/utils/check_allowed_keys.rb +3 -13
- data/lib/serega/validations/utils/check_hash_access_value.rb +44 -0
- data/lib/serega/validations/utils/check_opt_is_bool.rb +3 -13
- data/lib/serega/validations/utils/check_opt_is_hash.rb +3 -13
- data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +3 -13
- data/lib/serega/version.rb +1 -4
- data/lib/serega.rb +459 -388
- data/sig/serega/plugins.rbs +49 -0
- data/sig/serega.rbs +142 -0
- metadata +42 -38
- data/lib/serega/data_builder.rb +0 -54
- data/lib/serega/engine/level.rb +0 -59
- data/lib/serega/engine/level_queue.rb +0 -56
- data/lib/serega/engine/loader.rb +0 -70
- data/lib/serega/helpers/serializer_class_helper.rb +0 -19
- data/lib/serega/object_serializer.rb +0 -125
- data/lib/serega/plugins/camel_case/camel_case.rb +0 -213
- data/lib/serega/plugins/depth_limit/depth_limit.rb +0 -206
- data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +0 -69
- data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +0 -38
- data/lib/serega/plugins/formatters/formatters.rb +0 -322
- data/lib/serega/plugins/if/if.rb +0 -287
- data/lib/serega/plugins/if/validations/check_opt_if.rb +0 -72
- data/lib/serega/plugins/if/validations/check_opt_if_value.rb +0 -77
- data/lib/serega/plugins/if/validations/check_opt_unless.rb +0 -72
- data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +0 -77
- data/lib/serega/plugins/metadata/validations/check_block.rb +0 -47
- data/lib/serega/plugins/metadata/validations/check_opt_const.rb +0 -39
- data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +0 -35
- data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +0 -35
- data/lib/serega/plugins/metadata/validations/check_opt_value.rb +0 -61
- data/lib/serega/plugins/metadata/validations/check_opts.rb +0 -59
- data/lib/serega/plugins/metadata/validations/check_path.rb +0 -50
- data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +0 -111
- data/lib/serega/plugins/string_modifiers/string_modifiers.rb +0 -63
- data/lib/serega/utils/symbol_name.rb +0 -39
- data/lib/serega/validations/attribute/check_opt_const.rb +0 -40
- data/lib/serega/validations/check_initiate_params.rb +0 -57
- data/lib/serega/validations/check_serialize_params.rb +0 -54
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
module AttributeNormalizers # :nodoc:
|
|
5
|
+
#
|
|
6
|
+
# Builds an anonymous nested serializer from the attribute block.
|
|
7
|
+
#
|
|
8
|
+
# The serializer is a regular subclass of an explicitly chosen base —
|
|
9
|
+
# the :base_serializer attribute option or `config.base_serializer` —
|
|
10
|
+
# so it inherits everything the base has: plugins, config, attributes,
|
|
11
|
+
# batch loaders, the preload handler. Nested values can be any objects,
|
|
12
|
+
# which is why the base is chosen explicitly instead of inheriting from
|
|
13
|
+
# the current serializer. Its `inspect` is overridden to
|
|
14
|
+
# `CurrentSerializer.<attribute_name>` so error messages and debugging
|
|
15
|
+
# output point back to the defining attribute.
|
|
16
|
+
#
|
|
17
|
+
class Block # :nodoc:
|
|
18
|
+
class << self
|
|
19
|
+
# Builds the nested serializer of the attribute block. `base_serializer`
|
|
20
|
+
# is the :base_serializer attribute option.
|
|
21
|
+
#
|
|
22
|
+
# Guards against cyclic definitions before building: inheriting the base
|
|
23
|
+
# serializer copies its attributes, and copying a block attribute builds
|
|
24
|
+
# its nested serializer — meeting the same block again while it is being
|
|
25
|
+
# built means the base serializer (transitively) contains the block
|
|
26
|
+
# attribute being built, so inheriting from it would recurse forever.
|
|
27
|
+
# The "in progress" mark is kept on the block itself — its identity is
|
|
28
|
+
# what defines the cycle (the same Proc object is shared by all copies
|
|
29
|
+
# of the attribute).
|
|
30
|
+
def call(serializer_class, attribute_name, block, base_serializer)
|
|
31
|
+
base_serializer = chosen_base_serializer(serializer_class, base_serializer)
|
|
32
|
+
|
|
33
|
+
if block.instance_variable_defined?(:@serega_building_nested_serializer)
|
|
34
|
+
raise SeregaError,
|
|
35
|
+
"Can not define a nested serializer for attribute :#{attribute_name} —" \
|
|
36
|
+
" its base serializer #{base_serializer.inspect} (transitively)" \
|
|
37
|
+
" contains this same block attribute (cyclic definition)"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
block.instance_variable_set(:@serega_building_nested_serializer, true)
|
|
41
|
+
begin
|
|
42
|
+
build(serializer_class, attribute_name, block, base_serializer)
|
|
43
|
+
ensure
|
|
44
|
+
block.remove_instance_variable(:@serega_building_nested_serializer)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def build(serializer_class, attribute_name, block, base_serializer)
|
|
51
|
+
label = "#{serializer_class.inspect}.<#{attribute_name}>"
|
|
52
|
+
|
|
53
|
+
serializer = Class.new(base_serializer) do
|
|
54
|
+
define_singleton_method(:inspect) { label }
|
|
55
|
+
define_singleton_method(:to_s) { label }
|
|
56
|
+
instance_exec(&block)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# An empty nested serializer means the block was intended as an
|
|
60
|
+
# old-style value block — raise the explaining error.
|
|
61
|
+
raise SeregaError, SeregaValidations::Attribute::CheckBlock::ERROR_MESSAGE if serializer.attributes.empty?
|
|
62
|
+
|
|
63
|
+
serializer
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Base class for the nested serializer. Must be chosen explicitly —
|
|
67
|
+
# usually a settings-only serializer holding plugins and configuration
|
|
68
|
+
# (e.g. `config.base_serializer = self` in an application base class).
|
|
69
|
+
def chosen_base_serializer(serializer_class, base_serializer)
|
|
70
|
+
base_serializer ||= serializer_class.config.base_serializer
|
|
71
|
+
return base_serializer if base_serializer
|
|
72
|
+
|
|
73
|
+
raise SeregaError,
|
|
74
|
+
"Attribute block requires a base serializer for the nested serializer." \
|
|
75
|
+
" Provide the `base_serializer: <SerializerClass>` attribute option" \
|
|
76
|
+
" or set `config.base_serializer = <SerializerClass>`"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
module AttributeNormalizers # :nodoc:
|
|
5
|
+
#
|
|
6
|
+
# Builds the value resolver of the attribute :delegate option
|
|
7
|
+
#
|
|
8
|
+
class Delegate # :nodoc:
|
|
9
|
+
class << self
|
|
10
|
+
# Resolver that reads `delegate[:to]` of the object, then
|
|
11
|
+
# `delegate[:method]` (the attribute method by default) of the result.
|
|
12
|
+
# `delegate: {to_hash_access: ...}` configures hash access for the
|
|
13
|
+
# intermediate (:to) read, `delegate: {hash_access: ...}` for the final
|
|
14
|
+
# (:method) read. A step without its sub-option keeps a plain method
|
|
15
|
+
# read.
|
|
16
|
+
def call(delegate, method_name, config)
|
|
17
|
+
key_method_name = delegate[:method] || method_name
|
|
18
|
+
delegate_to = delegate[:to]
|
|
19
|
+
allow_nil = delegate.fetch(:allow_nil) { config.delegate_default_allow_nil }
|
|
20
|
+
|
|
21
|
+
to_access = delegate[:to_hash_access]
|
|
22
|
+
final_access = delegate[:hash_access]
|
|
23
|
+
unless to_access || final_access
|
|
24
|
+
return AttributeValueResolvers::DelegateResolver.get(delegate_to, key_method_name, allow_nil)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
to_step = HashAccess.call(to_access, delegate_to, config.hash_access)
|
|
28
|
+
final_step = HashAccess.call(final_access, key_method_name, config.hash_access)
|
|
29
|
+
AttributeValueResolvers::HashAccessDelegateResolver.get(to_step, final_step, allow_nil)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
module AttributeNormalizers # :nodoc:
|
|
5
|
+
#
|
|
6
|
+
# Builds the value resolver of one read: a method call, or a Hash read when
|
|
7
|
+
# a :hash_access option is given
|
|
8
|
+
#
|
|
9
|
+
class HashAccess # :nodoc:
|
|
10
|
+
class << self
|
|
11
|
+
# Resolver that reads `key` of the object. `option` is a :hash_access
|
|
12
|
+
# value: `true`, a Symbol mode or a `{mode:, allow_missing_key:}` Hash;
|
|
13
|
+
# `true` and a Hash without a key take `config.hash_access` defaults.
|
|
14
|
+
# Without the option the resolver calls the method `key`.
|
|
15
|
+
def call(option, key, defaults)
|
|
16
|
+
return AttributeValueResolvers::Keyword.new(key) unless option
|
|
17
|
+
|
|
18
|
+
mode, allow_missing_key = parse(option, defaults)
|
|
19
|
+
AttributeValueResolvers::HashAccessKeyword.new(key, mode, allow_missing_key)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def parse(option, defaults)
|
|
25
|
+
case option
|
|
26
|
+
when true then [defaults.default_mode, defaults.default_allow_missing_key]
|
|
27
|
+
when Symbol then [option, defaults.default_allow_missing_key]
|
|
28
|
+
else
|
|
29
|
+
mode = option.fetch(:mode) { defaults.default_mode }
|
|
30
|
+
allow_missing_key = option.fetch(:allow_missing_key) { defaults.default_allow_missing_key }
|
|
31
|
+
[mode, allow_missing_key]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
module AttributeNormalizers # :nodoc:
|
|
5
|
+
#
|
|
6
|
+
# Finds the preloads of an attribute
|
|
7
|
+
#
|
|
8
|
+
class Preloads # :nodoc:
|
|
9
|
+
class << self
|
|
10
|
+
# Preloads of the attribute with options `opts` and block `block`.
|
|
11
|
+
#
|
|
12
|
+
# The value is passed through as provided (Symbol, Array, Hash, or any
|
|
13
|
+
# custom value an ORM understands) — it is the data handed to the
|
|
14
|
+
# serializer's `preload_with` handler.
|
|
15
|
+
def call(opts, block, method_name, config)
|
|
16
|
+
# Explicit preload option (false or nil disables preloading)
|
|
17
|
+
return opts[:preload] || nil if opts.key?(:preload)
|
|
18
|
+
|
|
19
|
+
# Auto-preload the delegated association
|
|
20
|
+
if config.auto_preload.fetch(:has_delegate_option) && opts[:delegate]
|
|
21
|
+
return auto_preload_value(opts[:delegate][:to], config)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Auto-preload the nested serializer's association
|
|
25
|
+
# (a block defines a nested serializer, same as the :serializer option)
|
|
26
|
+
if config.auto_preload.fetch(:has_serializer_option) && (opts[:serializer] || block) && !opts.key?(:batch)
|
|
27
|
+
return auto_preload_value(method_name, config)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Skips auto-preloading of methods that return the object
|
|
36
|
+
# itself (:itself by default) — they are not associations,
|
|
37
|
+
# so preloading them would fail or make no sense.
|
|
38
|
+
def auto_preload_value(preload_method, config)
|
|
39
|
+
return nil if config.auto_preload_excluded_methods.include?(preload_method.to_sym)
|
|
40
|
+
|
|
41
|
+
preload_method
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -4,67 +4,18 @@ class Serega
|
|
|
4
4
|
#
|
|
5
5
|
# Attribute value resolvers
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
module AttributeValueResolvers
|
|
7
|
+
module AttributeValueResolvers # :nodoc:
|
|
9
8
|
#
|
|
10
|
-
#
|
|
9
|
+
# Value resolver of attributes with the :batch option and without the
|
|
10
|
+
# :value option. Reads the value of the object from the loaded batch.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
class BatchResolver
|
|
14
|
-
#
|
|
15
|
-
# Generates callable block to find attribute value when attribute with :batch
|
|
16
|
-
# option has no block or manual :value option.
|
|
17
|
-
#
|
|
18
|
-
# In other cases we should never get here as attribute value/block option must be manually defined.
|
|
19
|
-
#
|
|
20
|
-
# It handles this cases:
|
|
21
|
-
# - `attribute :foo, batch: true`
|
|
22
|
-
# - `attribute :foo, batch: FooLoader`
|
|
23
|
-
# - `attribute :foo, batch: :foo_loader`
|
|
24
|
-
# - `attribute :foo, batch: { id: :foo_id }`
|
|
25
|
-
# - `attribute :foo, batch: { use: FooLoader, id: foo_id }`
|
|
26
|
-
# - `attribute :foo, batch: { use: :foo_loader, id: foo_id }`
|
|
27
|
-
#
|
|
28
|
-
def self.get(serializer_class, attribute_name, batch_opt)
|
|
29
|
-
default_method = serializer_class.config.batch_id_option
|
|
30
|
-
|
|
31
|
-
if batch_opt == true # ex: `batch: true`
|
|
32
|
-
batch_name = attribute_name
|
|
33
|
-
batch_id_method = default_method
|
|
34
|
-
elsif batch_opt.respond_to?(:call) # ex: `batch: FooLoader`
|
|
35
|
-
serializer_class.batch(attribute_name, batch_opt)
|
|
36
|
-
batch_name = attribute_name
|
|
37
|
-
batch_id_method = default_method
|
|
38
|
-
elsif batch_opt.is_a?(Symbol) || batch_opt.is_a?(String) # ex: `batch: :foo_loader`
|
|
39
|
-
batch_name = batch_opt.to_sym
|
|
40
|
-
batch_id_method = default_method
|
|
41
|
-
else
|
|
42
|
-
use = batch_opt[:use]
|
|
43
|
-
batch_id_method = batch_opt[:id] || default_method
|
|
44
|
-
|
|
45
|
-
if use.respond_to?(:call) # ex: `batch: { use: FooLoader }`
|
|
46
|
-
batch_name = attribute_name
|
|
47
|
-
serializer_class.batch(batch_name, use)
|
|
48
|
-
else # ex: `batch: { use: :foo }` || batch: { id: :some_id }
|
|
49
|
-
batch_name = use || attribute_name
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
Batch.new(batch_name, batch_id_method)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
# Builds value resolver class for attributes with :batch option
|
|
59
|
-
#
|
|
60
|
-
# @private
|
|
61
|
-
class Batch
|
|
12
|
+
class Batch # :nodoc:
|
|
62
13
|
def initialize(loader_name, id_method)
|
|
63
14
|
@loader_name = loader_name
|
|
64
15
|
@id_method = id_method
|
|
65
16
|
end
|
|
66
17
|
|
|
67
|
-
# Finds object attribute value
|
|
18
|
+
# Finds the object attribute value in the loaded batches
|
|
68
19
|
def call(obj, batches:)
|
|
69
20
|
batches.fetch(@loader_name)[obj.public_send(@id_method)]
|
|
70
21
|
end
|
|
@@ -4,38 +4,16 @@ class Serega
|
|
|
4
4
|
#
|
|
5
5
|
# Attribute value resolvers
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
module AttributeValueResolvers
|
|
9
|
-
#
|
|
10
|
-
# Builds value resolver class for attributes with :const option
|
|
11
|
-
#
|
|
12
|
-
# @private
|
|
13
|
-
class ConstResolver
|
|
14
|
-
#
|
|
15
|
-
# Creates resolver that returns constant value
|
|
16
|
-
#
|
|
17
|
-
# @param const_value [Object] constant value to return
|
|
18
|
-
# @return [Const] resolver instance
|
|
19
|
-
#
|
|
20
|
-
def self.get(const_value)
|
|
21
|
-
Const.new(const_value)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
7
|
+
module AttributeValueResolvers # :nodoc:
|
|
25
8
|
#
|
|
26
9
|
# Value resolver class for attributes with :const option
|
|
27
10
|
#
|
|
28
|
-
#
|
|
29
|
-
class Const
|
|
11
|
+
class Const # :nodoc:
|
|
30
12
|
def initialize(const_value)
|
|
31
13
|
@const_value = const_value
|
|
32
14
|
end
|
|
33
15
|
|
|
34
|
-
#
|
|
35
16
|
# Returns the constant value
|
|
36
|
-
#
|
|
37
|
-
# @return [Object] the constant value
|
|
38
|
-
#
|
|
39
17
|
def call
|
|
40
18
|
@const_value
|
|
41
19
|
end
|
|
@@ -4,21 +4,12 @@ class Serega
|
|
|
4
4
|
#
|
|
5
5
|
# Attribute value resolvers
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
module AttributeValueResolvers
|
|
7
|
+
module AttributeValueResolvers # :nodoc:
|
|
9
8
|
#
|
|
10
9
|
# Builds value resolver class for attributes with :delegate option
|
|
11
10
|
#
|
|
12
|
-
#
|
|
13
|
-
class DelegateResolver
|
|
14
|
-
#
|
|
11
|
+
class DelegateResolver # :nodoc:
|
|
15
12
|
# Creates resolver that delegates method call to another object
|
|
16
|
-
#
|
|
17
|
-
# @param delegate_to [Symbol] method to call on object to get delegated object
|
|
18
|
-
# @param method_name [Symbol] method to call on delegated object
|
|
19
|
-
# @param allow_nil [Boolean] whether to use safe navigation when delegated object is nil
|
|
20
|
-
# @return [Delegate, DelegateAllowNil] resolver instance
|
|
21
|
-
#
|
|
22
13
|
def self.get(delegate_to, method_name, allow_nil)
|
|
23
14
|
allow_nil ? DelegateAllowNil.new(delegate_to, method_name) : Delegate.new(delegate_to, method_name)
|
|
24
15
|
end
|
|
@@ -27,47 +18,45 @@ class Serega
|
|
|
27
18
|
#
|
|
28
19
|
# Value resolver class for attributes with :delegate (with :allow_nil) option
|
|
29
20
|
#
|
|
30
|
-
#
|
|
31
|
-
class DelegateAllowNil
|
|
21
|
+
class DelegateAllowNil # :nodoc:
|
|
32
22
|
def initialize(delegate_to, method_name)
|
|
33
23
|
@delegate_to = delegate_to
|
|
34
24
|
@method_name = method_name
|
|
35
25
|
end
|
|
36
26
|
|
|
37
|
-
#
|
|
38
27
|
# Delegates method call to another object with safe navigation
|
|
39
|
-
#
|
|
40
|
-
# @param object [Object] the object to delegate from
|
|
41
|
-
# @return [Object, nil] result of delegated method call or nil if delegated object is nil
|
|
42
|
-
#
|
|
43
28
|
def call(object)
|
|
44
|
-
object.public_send(delegate_to)&.public_send(method_name)
|
|
29
|
+
object.public_send(@delegate_to)&.public_send(@method_name)
|
|
45
30
|
end
|
|
46
31
|
|
|
47
|
-
|
|
32
|
+
# Ruby code that delegates the method call
|
|
33
|
+
def code(source_variable)
|
|
34
|
+
return unless SeregaResultCode::PLAIN_METHOD_NAME.match?(@delegate_to) && SeregaResultCode::PLAIN_METHOD_NAME.match?(@method_name)
|
|
48
35
|
|
|
49
|
-
|
|
36
|
+
"#{source_variable}.#{@delegate_to}&.#{@method_name}"
|
|
37
|
+
end
|
|
50
38
|
end
|
|
51
39
|
|
|
52
40
|
#
|
|
53
41
|
# Value resolver class for attributes with :delegate (without :allow_nil) option
|
|
54
42
|
#
|
|
55
|
-
#
|
|
56
|
-
class Delegate
|
|
43
|
+
class Delegate # :nodoc:
|
|
57
44
|
def initialize(delegate_to, method_name)
|
|
58
45
|
@delegate_to = delegate_to
|
|
59
46
|
@method_name = method_name
|
|
60
47
|
end
|
|
61
48
|
|
|
62
|
-
#
|
|
63
49
|
# Delegates method call to another object
|
|
64
|
-
#
|
|
65
|
-
# @param object [Object] the object to delegate from
|
|
66
|
-
# @return [Object] result of delegated method call
|
|
67
|
-
#
|
|
68
50
|
def call(object)
|
|
69
51
|
object.public_send(@delegate_to).public_send(@method_name)
|
|
70
52
|
end
|
|
53
|
+
|
|
54
|
+
# Ruby code that delegates the method call
|
|
55
|
+
def code(source_variable)
|
|
56
|
+
return unless SeregaResultCode::PLAIN_METHOD_NAME.match?(@delegate_to) && SeregaResultCode::PLAIN_METHOD_NAME.match?(@method_name)
|
|
57
|
+
|
|
58
|
+
"#{source_variable}.#{@delegate_to}.#{@method_name}"
|
|
59
|
+
end
|
|
71
60
|
end
|
|
72
61
|
end
|
|
73
62
|
end
|
|
@@ -1,45 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Serega
|
|
4
|
-
#
|
|
5
|
-
module AttributeValueResolvers
|
|
4
|
+
module AttributeValueResolvers # :nodoc:
|
|
6
5
|
#
|
|
7
|
-
#
|
|
6
|
+
# Settings of the :hash_access option
|
|
8
7
|
#
|
|
9
|
-
#
|
|
10
|
-
class HashAccessResolver
|
|
8
|
+
class HashAccessResolver # :nodoc:
|
|
11
9
|
# Allowed hash access modes
|
|
12
10
|
MODES = %i[symbol string].freeze
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
# Creates resolver that reads a key from Hash records
|
|
16
|
-
#
|
|
17
|
-
# @param name [Symbol, String] hash key
|
|
18
|
-
# @param mode [Symbol] hash access mode (:symbol, :string)
|
|
19
|
-
# @param allow_missing_key [Boolean] whether a missing key is read via `record[key]` rather than raising
|
|
20
|
-
#
|
|
21
|
-
# @return [HashAccessKeyword] resolver instance
|
|
22
|
-
#
|
|
23
|
-
def self.get(name, mode, allow_missing_key)
|
|
24
|
-
HashAccessKeyword.new(name, mode, allow_missing_key)
|
|
25
|
-
end
|
|
26
11
|
end
|
|
27
12
|
|
|
28
13
|
#
|
|
29
14
|
# Builds value resolver for attributes with the :delegate option using
|
|
30
15
|
# hash access on any of its steps
|
|
31
16
|
#
|
|
32
|
-
#
|
|
33
|
-
class HashAccessDelegateResolver
|
|
34
|
-
#
|
|
17
|
+
class HashAccessDelegateResolver # :nodoc:
|
|
35
18
|
# Creates resolver that delegates through the provided step readers
|
|
36
|
-
#
|
|
37
|
-
# @param to_step [#call] reader of the intermediate object
|
|
38
|
-
# @param final_step [#call] reader of the final value
|
|
39
|
-
# @param delegate_allow_nil [Boolean] whether a nil intermediate object resolves to nil
|
|
40
|
-
#
|
|
41
|
-
# @return [HashAccessDelegate, HashAccessDelegateAllowNil] resolver instance
|
|
42
|
-
#
|
|
43
19
|
def self.get(to_step, final_step, delegate_allow_nil)
|
|
44
20
|
delegate_allow_nil ? HashAccessDelegateAllowNil.new(to_step, final_step) : HashAccessDelegate.new(to_step, final_step)
|
|
45
21
|
end
|
|
@@ -48,75 +24,81 @@ class Serega
|
|
|
48
24
|
#
|
|
49
25
|
# Value resolver for attributes with the :hash_access option
|
|
50
26
|
#
|
|
51
|
-
#
|
|
52
|
-
class HashAccessKeyword
|
|
27
|
+
class HashAccessKeyword # :nodoc:
|
|
53
28
|
def initialize(name, mode, allow_missing_key)
|
|
54
29
|
@key = (mode == :symbol) ? name.to_sym : name.to_s
|
|
55
30
|
@allow_missing_key = allow_missing_key
|
|
56
31
|
end
|
|
57
32
|
|
|
58
|
-
#
|
|
59
33
|
# Reads the key from the record
|
|
60
|
-
#
|
|
61
|
-
# @param object [Object] serialized object or delegation step value
|
|
62
|
-
# @return [Object] the value found
|
|
63
|
-
#
|
|
64
34
|
def call(object)
|
|
65
35
|
return object[@key] if @allow_missing_key
|
|
66
36
|
|
|
67
37
|
object.fetch(@key) do
|
|
68
38
|
default = object[@key]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
raise KeyError.new("key not found: #{@key.inspect}", key: @key, receiver: object)
|
|
39
|
+
default.nil? ? object.fetch(@key) : default
|
|
72
40
|
end
|
|
73
41
|
end
|
|
42
|
+
|
|
43
|
+
# Ruby code that reads the key, the same way as #call
|
|
44
|
+
def code(source_variable)
|
|
45
|
+
key = @key.is_a?(String) ? "#{@key.inspect}.freeze" : @key.inspect
|
|
46
|
+
return "#{source_variable}[#{key}]" if @allow_missing_key
|
|
47
|
+
|
|
48
|
+
"#{source_variable}.fetch(#{key}) { (found = #{source_variable}[#{key}]).nil? ? #{source_variable}.fetch(#{key}) : found }"
|
|
49
|
+
end
|
|
74
50
|
end
|
|
75
51
|
|
|
76
52
|
#
|
|
77
53
|
# Value resolver for attributes with :hash_access and :delegate (without :allow_nil) options
|
|
78
54
|
#
|
|
79
|
-
#
|
|
80
|
-
class HashAccessDelegate
|
|
55
|
+
class HashAccessDelegate # :nodoc:
|
|
81
56
|
def initialize(to_step, final_step)
|
|
82
57
|
@to_step = to_step
|
|
83
58
|
@final_step = final_step
|
|
84
59
|
end
|
|
85
60
|
|
|
86
|
-
#
|
|
87
61
|
# Delegates the value reading through the intermediate object
|
|
88
|
-
#
|
|
89
|
-
# @param object [Object] serialized object
|
|
90
|
-
# @return [Object] the value found
|
|
91
|
-
#
|
|
92
62
|
def call(object)
|
|
93
63
|
@final_step.call(@to_step.call(object))
|
|
94
64
|
end
|
|
65
|
+
|
|
66
|
+
# Ruby code that delegates the value reading, the same way as #call
|
|
67
|
+
def code(source_variable)
|
|
68
|
+
to_code = @to_step.code(source_variable)
|
|
69
|
+
final_code = @final_step.code("intermediate")
|
|
70
|
+
return unless to_code && final_code
|
|
71
|
+
|
|
72
|
+
"(intermediate = #{to_code}; #{final_code})"
|
|
73
|
+
end
|
|
95
74
|
end
|
|
96
75
|
|
|
97
76
|
#
|
|
98
77
|
# Value resolver for attributes with :hash_access and :delegate (with :allow_nil) options
|
|
99
78
|
#
|
|
100
|
-
#
|
|
101
|
-
class HashAccessDelegateAllowNil
|
|
79
|
+
class HashAccessDelegateAllowNil # :nodoc:
|
|
102
80
|
def initialize(to_step, final_step)
|
|
103
81
|
@to_step = to_step
|
|
104
82
|
@final_step = final_step
|
|
105
83
|
end
|
|
106
84
|
|
|
107
|
-
#
|
|
108
85
|
# Delegates the value reading through the intermediate object,
|
|
109
86
|
# resolving a nil intermediate to nil
|
|
110
|
-
#
|
|
111
|
-
# @param object [Object] serialized object
|
|
112
|
-
# @return [Object, nil] the value found
|
|
113
|
-
#
|
|
114
87
|
def call(object)
|
|
115
88
|
intermediate = @to_step.call(object)
|
|
116
89
|
return if intermediate.nil?
|
|
117
90
|
|
|
118
91
|
@final_step.call(intermediate)
|
|
119
92
|
end
|
|
93
|
+
|
|
94
|
+
# Ruby code that delegates the value reading, the same way as #call
|
|
95
|
+
def code(source_variable)
|
|
96
|
+
to_code = @to_step.code(source_variable)
|
|
97
|
+
final_code = @final_step.code("intermediate")
|
|
98
|
+
return unless to_code && final_code
|
|
99
|
+
|
|
100
|
+
"((intermediate = #{to_code}).nil? ? nil : #{final_code})"
|
|
101
|
+
end
|
|
120
102
|
end
|
|
121
103
|
end
|
|
122
104
|
end
|
|
@@ -4,42 +4,27 @@ class Serega
|
|
|
4
4
|
#
|
|
5
5
|
# Attribute value resolvers
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
module AttributeValueResolvers
|
|
9
|
-
#
|
|
10
|
-
# Builds value resolver class for attributes with :keyword option
|
|
11
|
-
#
|
|
12
|
-
# @private
|
|
13
|
-
class KeywordResolver
|
|
14
|
-
#
|
|
15
|
-
# Creates resolver that calls method on object
|
|
16
|
-
#
|
|
17
|
-
# @param keyword [Symbol] method name to call on object
|
|
18
|
-
# @return [Keyword] resolver instance
|
|
19
|
-
#
|
|
20
|
-
def self.get(keyword)
|
|
21
|
-
Keyword.new(keyword)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
7
|
+
module AttributeValueResolvers # :nodoc:
|
|
25
8
|
#
|
|
26
9
|
# Value resolver class for attributes with :keyword option
|
|
27
10
|
#
|
|
28
|
-
#
|
|
29
|
-
class Keyword
|
|
11
|
+
class Keyword # :nodoc:
|
|
30
12
|
def initialize(keyword)
|
|
31
13
|
@keyword = keyword
|
|
32
14
|
end
|
|
33
15
|
|
|
34
|
-
#
|
|
16
|
+
# Name of the method the value comes from
|
|
17
|
+
attr_reader :keyword
|
|
18
|
+
|
|
35
19
|
# Calls the keyword method on the object
|
|
36
|
-
#
|
|
37
|
-
# @param object [Object] the object to call method on
|
|
38
|
-
# @return [Object] result of method call
|
|
39
|
-
#
|
|
40
20
|
def call(object)
|
|
41
21
|
object.public_send(@keyword)
|
|
42
22
|
end
|
|
23
|
+
|
|
24
|
+
# Ruby code that calls the keyword method
|
|
25
|
+
def code(source_variable)
|
|
26
|
+
"#{source_variable}.#{@keyword}" if SeregaResultCode::PLAIN_METHOD_NAME.match?(@keyword)
|
|
27
|
+
end
|
|
43
28
|
end
|
|
44
29
|
end
|
|
45
30
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# One generated method per attribute that is read with plain Ruby code,
|
|
6
|
+
# for example `def read_full_name(source) = source.full_name`. Each method is
|
|
7
|
+
# defined with the file and line of its attribute, thus a backtrace of an
|
|
8
|
+
# error points to the attribute.
|
|
9
|
+
#
|
|
10
|
+
# `CONSTANTS` and `DEFAULTS` hold the `:const` and `:default` values of the
|
|
11
|
+
# attributes, `FORMATTERS` the formatter callables and `FORMATTER_ARGS` the
|
|
12
|
+
# arguments of method formatters, by attribute name.
|
|
13
|
+
#
|
|
14
|
+
# Each method name is `read_` and the attribute name with `_` in place of
|
|
15
|
+
# other characters than letters, digits and `_`, for example
|
|
16
|
+
# `read_first_name` for `first-name`. A name of another attribute gets a
|
|
17
|
+
# number: `read_first_name_2`. Thus attribute names do not clash with
|
|
18
|
+
# methods of Object, and generated code calls the methods directly.
|
|
19
|
+
#
|
|
20
|
+
class SeregaAttributeValues # :nodoc:
|
|
21
|
+
# `:const` values by attribute name
|
|
22
|
+
CONSTANTS = {}
|
|
23
|
+
|
|
24
|
+
# `:default` values by attribute name
|
|
25
|
+
DEFAULTS = {}
|
|
26
|
+
|
|
27
|
+
# Formatter callables by attribute name
|
|
28
|
+
FORMATTERS = {}
|
|
29
|
+
|
|
30
|
+
# Arguments of method formatters by attribute name
|
|
31
|
+
FORMATTER_ARGS = {}
|
|
32
|
+
|
|
33
|
+
# Method names by attribute name
|
|
34
|
+
METHOD_NAMES = {}
|
|
35
|
+
|
|
36
|
+
# Gives each subclass its own values
|
|
37
|
+
def self.inherited(subclass)
|
|
38
|
+
super
|
|
39
|
+
subclass.const_set(:CONSTANTS, {})
|
|
40
|
+
subclass.const_set(:DEFAULTS, {})
|
|
41
|
+
subclass.const_set(:FORMATTERS, {})
|
|
42
|
+
subclass.const_set(:FORMATTER_ARGS, {})
|
|
43
|
+
subclass.const_set(:METHOD_NAMES, {})
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|