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
data/lib/serega/errors.rb
CHANGED
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Serega
|
|
4
|
+
# Raised on invalid definitions and options, and on changes of a locked
|
|
5
|
+
# serializer. Parent class of the other Serega errors.
|
|
4
6
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
+
# See [Errors](https://github.com/andreyruby/serega/blob/master/docs/errors.md).
|
|
7
8
|
class SeregaError < StandardError; end
|
|
8
9
|
|
|
9
|
-
# Raised when
|
|
10
|
+
# Raised when `only:`, `except:` or `with:` names an unknown field
|
|
11
|
+
#
|
|
12
|
+
# ```ruby
|
|
13
|
+
# UserSerializer.to_h(user, only: [:nickname, {friends: :age}])
|
|
14
|
+
# # => Serega::AttributeNotExist: Not existing attributes: nickname, friends(age)
|
|
15
|
+
# ```
|
|
10
16
|
#
|
|
11
|
-
#
|
|
12
|
-
# Serega.new(only: 'FOO')
|
|
13
|
-
# # => Attribute 'FOO' not exists (Serega::AttributeNotExist)
|
|
17
|
+
# See [Unknown fields](https://github.com/andreyruby/serega/blob/master/docs/selecting-fields.md#unknown-fields).
|
|
14
18
|
class AttributeNotExist < SeregaError
|
|
15
|
-
|
|
19
|
+
# Serializer that has no such fields
|
|
20
|
+
attr_reader :serializer
|
|
21
|
+
|
|
22
|
+
# Unknown fields, for example `["nickname", "friends(age)"]`
|
|
23
|
+
attr_reader :attributes
|
|
16
24
|
|
|
17
|
-
def initialize(message = nil, serializer = nil, attributes = nil)
|
|
25
|
+
def initialize(message = nil, serializer = nil, attributes = nil) # :nodoc:
|
|
18
26
|
super(message)
|
|
19
27
|
@serializer = serializer
|
|
20
28
|
@attributes = attributes
|
|
21
29
|
end
|
|
22
30
|
end
|
|
31
|
+
|
|
32
|
+
# Raised when fields nest deeper than `config.depth_limit` of the
|
|
33
|
+
# serializer that starts the serialization
|
|
34
|
+
#
|
|
35
|
+
# ```ruby
|
|
36
|
+
# UserSerializer.to_h(user, with: {friends: :friends})
|
|
37
|
+
# # => Serega::DepthLimitError: Depth limit was exceeded
|
|
38
|
+
# ```
|
|
39
|
+
#
|
|
40
|
+
# See [Depth limit](https://github.com/andreyruby/serega/blob/master/docs/configuration.md#depth-limit).
|
|
41
|
+
class DepthLimitError < SeregaError
|
|
42
|
+
# Path that exceeds the limit, for example
|
|
43
|
+
# `UserSerializer (depth limit: 2) -> friends -> friends`
|
|
44
|
+
attr_reader :details
|
|
45
|
+
|
|
46
|
+
def initialize(message = nil, details = nil) # :nodoc:
|
|
47
|
+
super(message)
|
|
48
|
+
@details = details
|
|
49
|
+
end
|
|
50
|
+
end
|
|
23
51
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Formats attribute values. A formatter is one of:
|
|
6
|
+
# - a callable that gets the value, and the context when it has a second
|
|
7
|
+
# parameter or the :ctx keyword;
|
|
8
|
+
# - a method name, called on the value;
|
|
9
|
+
# - an Array of a method name and its arguments, for example `[:round, 2]`;
|
|
10
|
+
# - a Hash with one of them as `:use` and the `:allow_nil` option.
|
|
11
|
+
#
|
|
12
|
+
# With `allow_nil: true` a nil value stays nil and is not formatted.
|
|
13
|
+
#
|
|
14
|
+
class SeregaFormatter # :nodoc:
|
|
15
|
+
# Callable that formats a value, nil for a method formatter
|
|
16
|
+
attr_reader :callable
|
|
17
|
+
|
|
18
|
+
# Arguments of the method, nil for a callable formatter
|
|
19
|
+
attr_reader :args
|
|
20
|
+
|
|
21
|
+
def initialize(formatter)
|
|
22
|
+
formatter = {use: formatter} unless formatter.is_a?(Hash)
|
|
23
|
+
use = formatter[:use]
|
|
24
|
+
@allow_nil = formatter.fetch(:allow_nil, false)
|
|
25
|
+
|
|
26
|
+
if use.respond_to?(:call)
|
|
27
|
+
@method_name = nil
|
|
28
|
+
@args = nil
|
|
29
|
+
@callable = use
|
|
30
|
+
@signature = SeregaUtils::MethodSignature.call(use, pos_limit: 2, keyword_args: [:ctx])
|
|
31
|
+
else
|
|
32
|
+
method_name, *args = use
|
|
33
|
+
@method_name = method_name
|
|
34
|
+
@args = args
|
|
35
|
+
@callable = nil
|
|
36
|
+
@signature = nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Formats the value
|
|
41
|
+
def call(value, context)
|
|
42
|
+
return if value.nil? && @allow_nil
|
|
43
|
+
return value.public_send(@method_name, *@args) if @method_name
|
|
44
|
+
|
|
45
|
+
case @signature
|
|
46
|
+
when "1" then @callable.call(value)
|
|
47
|
+
when "1_ctx" then @callable.call(value, ctx: context)
|
|
48
|
+
else @callable.call(value, context) # "2"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Ruby code that formats the value of `value_code`. A method is called
|
|
53
|
+
# directly, for example `(source.created_at).iso8601(3)`. Literal
|
|
54
|
+
# arguments are written in the code, other arguments are read from the
|
|
55
|
+
# Array that `args_code` reads. A callable is read with `callable_code`.
|
|
56
|
+
def code(value_code, callable_code, args_code)
|
|
57
|
+
return method_call_code(value_code, args_code) if @method_name
|
|
58
|
+
return unless @signature == "1"
|
|
59
|
+
return "#{callable_code}.call(#{value_code})" unless @allow_nil
|
|
60
|
+
|
|
61
|
+
"(value = #{value_code}).nil? ? nil : #{callable_code}.call(value)"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def method_call_code(value_code, args_code)
|
|
67
|
+
operator = @allow_nil ? "&." : "."
|
|
68
|
+
arguments = @args.each_with_index.map { |arg, index| argument_code(arg, index, args_code) }
|
|
69
|
+
|
|
70
|
+
unless SeregaResultCode::PLAIN_METHOD_NAME.match?(@method_name)
|
|
71
|
+
send_arguments = [@method_name.inspect, *arguments]
|
|
72
|
+
return "(#{value_code})#{operator}public_send(#{send_arguments.join(", ")})"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
return "(#{value_code})#{operator}#{@method_name}" if arguments.empty?
|
|
76
|
+
|
|
77
|
+
"(#{value_code})#{operator}#{@method_name}(#{arguments.join(", ")})"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Literal arguments are written in the code, other arguments are read
|
|
81
|
+
# from the Array of arguments
|
|
82
|
+
def argument_code(arg, index, args_code)
|
|
83
|
+
literal?(arg) ? literal_code(arg) : "#{args_code}[#{index}]"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def literal?(arg)
|
|
87
|
+
case arg
|
|
88
|
+
when Integer, Symbol, String, true, false, nil then true
|
|
89
|
+
when Float then arg.finite?
|
|
90
|
+
else false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def literal_code(arg)
|
|
95
|
+
arg.is_a?(String) ? "#{arg.inspect}.freeze" : arg.inspect
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
class SeregaPlan # :nodoc:
|
|
5
|
+
#
|
|
6
|
+
# Serialization plans of one serializer, cached by modifiers.
|
|
7
|
+
# Validates initiate options before building a plan.
|
|
8
|
+
#
|
|
9
|
+
class Cache # :nodoc:
|
|
10
|
+
# Cached plan, whether its modifiers were validated, and the modifiers
|
|
11
|
+
# as provided
|
|
12
|
+
CachedPlan = Struct.new(:plan, :validated, :only, :with, :except)
|
|
13
|
+
private_constant :CachedPlan
|
|
14
|
+
|
|
15
|
+
# Initializes empty plans cache of the serializer
|
|
16
|
+
def initialize(serializer_class)
|
|
17
|
+
@serializer_class = serializer_class
|
|
18
|
+
@default_plan = nil
|
|
19
|
+
@plans = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns serialization plan for provided modifiers.
|
|
23
|
+
# The plan without modifiers is always cached; plans with modifiers are
|
|
24
|
+
# cached by modifiers as provided when `max_cached_plans_per_serializer_count`
|
|
25
|
+
# is positive. Modifiers are parsed and validated when a plan is built.
|
|
26
|
+
def fetch(only, with, except, check_initiate_params: false)
|
|
27
|
+
return default_plan if only.nil? && with.nil? && except.nil?
|
|
28
|
+
|
|
29
|
+
max_size = @serializer_class.config.max_cached_plans_per_serializer_count
|
|
30
|
+
return build(only, with, except, check_initiate_params) if max_size.zero?
|
|
31
|
+
|
|
32
|
+
cached_plan(only, with, except, max_size, check_initiate_params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def default_plan
|
|
38
|
+
@default_plan ||= SeregaPlan.new(@serializer_class, nil, FROZEN_EMPTY_HASH)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Keeps plans by the hash of the hash values of the modifiers, and
|
|
42
|
+
# compares each modifier with the cached one: an Array of the modifiers
|
|
43
|
+
# as the key hashes and compares nested arrays inside a recursion guard.
|
|
44
|
+
# Modifiers with the hash of other cached modifiers replace them.
|
|
45
|
+
def cached_plan(only, with, except, max_size, check_initiate_params)
|
|
46
|
+
key = [only.hash, with.hash, except.hash].hash # rubocop:disable Security/CompoundHash -- `[only, with, except].hash` hashes nested arrays inside a recursion guard
|
|
47
|
+
cached = @plans[key]
|
|
48
|
+
return reuse(cached, only, with, except, check_initiate_params) if cached && same_modifiers?(cached, only, with, except)
|
|
49
|
+
|
|
50
|
+
plan = build(only, with, except, check_initiate_params)
|
|
51
|
+
@plans[key] = CachedPlan.new(plan, check_initiate_params, *frozen_copy([only, with, except]))
|
|
52
|
+
@plans.shift if @plans.length > max_size
|
|
53
|
+
plan
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def same_modifiers?(cached, only, with, except)
|
|
57
|
+
only.eql?(cached.only) && with.eql?(cached.with) && except.eql?(cached.except)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def reuse(cached, only, with, except, check_initiate_params)
|
|
61
|
+
if check_initiate_params && !cached.validated
|
|
62
|
+
validate(parse_modifiers(only, with, except))
|
|
63
|
+
cached.validated = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
cached.plan
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build(only, with, except, check_initiate_params)
|
|
70
|
+
modifiers = parse_modifiers(only, with, except)
|
|
71
|
+
return default_plan if modifiers[:only].empty? && modifiers[:with].empty? && modifiers[:except].empty?
|
|
72
|
+
|
|
73
|
+
validate(modifiers) if check_initiate_params
|
|
74
|
+
SeregaPlan.new(@serializer_class, nil, modifiers)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_modifiers(only, with, except)
|
|
78
|
+
{only: parse_modifier(only), with: parse_modifier(with), except: parse_modifier(except)}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_modifier(value)
|
|
82
|
+
return SeregaUtils::ParseStringModifiers.parse(value) if value.is_a?(String)
|
|
83
|
+
|
|
84
|
+
SeregaUtils::ToHash.call(value)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def frozen_copy(modifiers)
|
|
88
|
+
SeregaUtils::EnumDeepFreeze.call(SeregaUtils::EnumDeepDup.call(modifiers))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate(modifiers)
|
|
92
|
+
SeregaValidations::Initiate::CheckModifiers.new.call(@serializer_class, modifiers[:only], modifiers[:with], modifiers[:except])
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
class SeregaPlan # :nodoc:
|
|
5
|
+
#
|
|
6
|
+
# Shareable plans cache of a shareable serializer, keeps the plans of each
|
|
7
|
+
# Ractor apart
|
|
8
|
+
#
|
|
9
|
+
class PerRactorCache # :nodoc:
|
|
10
|
+
def initialize(serializer_class)
|
|
11
|
+
@serializer_class = serializer_class
|
|
12
|
+
@key = SeregaUtils::RactorLocal.key(serializer_class, :plan_cache)
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns serialization plan from the plans cache of the current Ractor
|
|
17
|
+
def fetch(only, with, except, check_initiate_params: false)
|
|
18
|
+
plan_cache = SeregaUtils::RactorLocal.fetch(@key) { SeregaPlan::Cache.new(@serializer_class) }
|
|
19
|
+
plan_cache.fetch(only, with, except, check_initiate_params: check_initiate_params)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/serega/plan.rb
CHANGED
|
@@ -5,152 +5,125 @@ class Serega
|
|
|
5
5
|
# Constructs plan - list of serialized attributes.
|
|
6
6
|
# We will traverse this plan to construct serialized response.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
plan
|
|
53
|
-
end
|
|
8
|
+
class SeregaPlan # :nodoc:
|
|
9
|
+
# Serializer of the plan
|
|
10
|
+
attr_reader :serializer_class
|
|
11
|
+
|
|
12
|
+
# Parent plan point
|
|
13
|
+
attr_reader :parent_plan_point
|
|
14
|
+
|
|
15
|
+
# Serialization points
|
|
16
|
+
attr_reader :points
|
|
17
|
+
|
|
18
|
+
# Serialization points with child plans
|
|
19
|
+
attr_reader :relation_points
|
|
20
|
+
|
|
21
|
+
# Whether the root objects of a class are collections
|
|
22
|
+
attr_reader :collection_classes
|
|
23
|
+
|
|
24
|
+
# Presenter class that wraps the sources in the Ractor of the plan, `nil`
|
|
25
|
+
# without presenter methods
|
|
26
|
+
attr_reader :presenter
|
|
27
|
+
|
|
28
|
+
# Instantiate new serialization plan. Raises DepthLimitError when the plan
|
|
29
|
+
# nests deeper than the depth limit of the root serializer.
|
|
30
|
+
def initialize(serializer_class, parent_plan_point, modifiers)
|
|
31
|
+
check_depth_limit(parent_plan_point) if parent_plan_point
|
|
32
|
+
serializer_class.lock
|
|
33
|
+
@serializer_class = serializer_class
|
|
34
|
+
@parent_plan_point = parent_plan_point
|
|
35
|
+
@presenter = serializer_class.local_presenter
|
|
36
|
+
@points = attributes_points(modifiers)
|
|
37
|
+
@relation_points = points.select(&:child_plan).freeze
|
|
38
|
+
@preload_points = points.select(&:preloads).freeze
|
|
39
|
+
@batch_points = points.any? { |point| !point.batch_loaders.empty? }
|
|
40
|
+
@flat = relation_points.empty? && preload_points.empty? && !@batch_points && !@presenter
|
|
41
|
+
@result_builders = {}
|
|
42
|
+
@collection_classes = {}.compare_by_identity
|
|
43
|
+
direct = parent_plan_point.nil? && @flat && !serializer_class.prepare_initial_objects_signature
|
|
44
|
+
@direct_hash_builder = direct ? result_builder(:hash) : nil
|
|
45
|
+
# :nocov:
|
|
46
|
+
rescue *RACTOR_ISOLATION_ERRORS
|
|
47
|
+
raise if serializer_class.shareable?
|
|
48
|
+
|
|
49
|
+
raise SeregaError, "#{serializer_class} can not serialize in other Ractors. Call `make_shareable!` at the end of its definition"
|
|
50
|
+
# :nocov:
|
|
51
|
+
end
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
# Serialization points with preloads
|
|
54
|
+
attr_reader :preload_points
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
# Hash result builder of a flat root plan whose serializer prepares no
|
|
57
|
+
# initial objects. `Serega#call` builds one source with it directly.
|
|
58
|
+
attr_reader :direct_hash_builder
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
end
|
|
60
|
+
# Whether any point uses batch loaders
|
|
61
|
+
def batch_points?
|
|
62
|
+
@batch_points
|
|
63
|
+
end
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
# A flat plan has no relations, preloads, batch loaders and presenter.
|
|
66
|
+
# Its sources need no source groups.
|
|
67
|
+
def flat?
|
|
68
|
+
@flat
|
|
68
69
|
end
|
|
69
70
|
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
module InstanceMethods
|
|
75
|
-
# Parent plan point
|
|
76
|
-
# @return [SeregaPlanPoint, nil]
|
|
77
|
-
attr_reader :parent_plan_point
|
|
78
|
-
|
|
79
|
-
# Serialization points
|
|
80
|
-
# @return [Array<SeregaPlanPoint>] points to serialize
|
|
81
|
-
attr_reader :points
|
|
82
|
-
|
|
83
|
-
# Named serialization points
|
|
84
|
-
# @return [Hash] Named points
|
|
85
|
-
attr_reader :points_hash
|
|
86
|
-
|
|
87
|
-
#
|
|
88
|
-
# Instantiate new serialization plan
|
|
89
|
-
#
|
|
90
|
-
# Patched by
|
|
91
|
-
# - depth_limit plugin, which checks depth limit is not exceeded when adding new plan
|
|
92
|
-
#
|
|
93
|
-
# @param parent_plan_point [SeregaPlanPoint, nil] Parent plan_point
|
|
94
|
-
# @param modifiers [Hash] Serialization parameters
|
|
95
|
-
# @option modifiers [Hash] :only The only attributes to serialize
|
|
96
|
-
# @option modifiers [Hash] :except Attributes to hide
|
|
97
|
-
# @option modifiers [Hash] :with Hidden attributes to serialize additionally
|
|
98
|
-
#
|
|
99
|
-
# @return [SeregaPlan] Serialization plan
|
|
100
|
-
#
|
|
101
|
-
def initialize(parent_plan_point, modifiers)
|
|
102
|
-
@parent_plan_point = parent_plan_point
|
|
103
|
-
@points = attributes_points(modifiers)
|
|
104
|
-
@points_hash = points.to_h { |point| [point.name, point] }
|
|
105
|
-
end
|
|
71
|
+
# Result builder of this plan in the serialization mode
|
|
72
|
+
def result_builder(mode)
|
|
73
|
+
@result_builders[mode] ||= SeregaResultBuilder.new(serializer_class, mode, points)
|
|
74
|
+
end
|
|
106
75
|
|
|
107
|
-
|
|
108
|
-
# Serializer class of current plan
|
|
109
|
-
#
|
|
110
|
-
def serializer_class
|
|
111
|
-
self.class.serializer_class
|
|
112
|
-
end
|
|
76
|
+
private
|
|
113
77
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@data_class ||= self.class.data_class_for(point_names)
|
|
78
|
+
def check_depth_limit(parent_plan_point)
|
|
79
|
+
depth = 2
|
|
80
|
+
root_plan = parent_plan_point.plan
|
|
81
|
+
while (point = root_plan.parent_plan_point)
|
|
82
|
+
depth += 1
|
|
83
|
+
root_plan = point.plan
|
|
121
84
|
end
|
|
122
85
|
|
|
123
|
-
|
|
86
|
+
root_serializer = root_plan.serializer_class
|
|
87
|
+
limit = root_serializer.config.depth_limit
|
|
88
|
+
return if limit.nil? || depth <= limit
|
|
124
89
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
90
|
+
names = [parent_plan_point.name]
|
|
91
|
+
point = parent_plan_point
|
|
92
|
+
names.unshift(point.name) while (point = point.plan.parent_plan_point)
|
|
93
|
+
raise DepthLimitError.new("Depth limit was exceeded", "#{root_serializer} (depth limit: #{limit}) -> #{names.join(" -> ")}")
|
|
94
|
+
end
|
|
130
95
|
|
|
131
|
-
|
|
132
|
-
|
|
96
|
+
def attributes_points(modifiers)
|
|
97
|
+
only = modifiers[:only] || FROZEN_EMPTY_HASH
|
|
98
|
+
except = modifiers[:except] || FROZEN_EMPTY_HASH
|
|
99
|
+
with = modifiers[:with] || FROZEN_EMPTY_HASH
|
|
100
|
+
only_limits = only_limits?(only, except, with)
|
|
101
|
+
points = []
|
|
133
102
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
name = attribute.name
|
|
137
|
-
{only: only[name], with: with[name], except: except[name]}
|
|
138
|
-
end
|
|
103
|
+
serializer_class.attributes.each_value do |attribute|
|
|
104
|
+
next unless attribute.visible?(only, except, with, only_limits)
|
|
139
105
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
106
|
+
child_fields =
|
|
107
|
+
if attribute.relation?
|
|
108
|
+
name = attribute.name
|
|
109
|
+
{only: only[name], with: with[name], except: except[name]}
|
|
110
|
+
end
|
|
143
111
|
|
|
144
|
-
|
|
112
|
+
point = SeregaPlanPoint.new(self, attribute, child_fields)
|
|
113
|
+
points << point.freeze
|
|
145
114
|
end
|
|
146
115
|
|
|
147
|
-
|
|
148
|
-
@point_names ||= points.map(&:name)
|
|
149
|
-
end
|
|
116
|
+
points.freeze
|
|
150
117
|
end
|
|
151
118
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
119
|
+
# `only` hides the fields it does not name when it names a field of
|
|
120
|
+
# this level without nested fields, or when `except` hides no field and
|
|
121
|
+
# `with` adds no field of this level
|
|
122
|
+
def only_limits?(only, except, with)
|
|
123
|
+
return false if only.empty?
|
|
124
|
+
return true if only.each_value.any?(&:empty?)
|
|
125
|
+
|
|
126
|
+
with.empty? && except.each_value.none?(&:empty?)
|
|
127
|
+
end
|
|
155
128
|
end
|
|
156
129
|
end
|