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,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Named batch loader. Loads values of many objects in one call.
|
|
6
|
+
#
|
|
7
|
+
class SeregaBatchLoader # :nodoc:
|
|
8
|
+
# Batch loader name
|
|
9
|
+
attr_reader :name
|
|
10
|
+
|
|
11
|
+
# Batch loader block
|
|
12
|
+
attr_reader :block
|
|
13
|
+
|
|
14
|
+
# Initializes new batch loader
|
|
15
|
+
def initialize(name:, block:)
|
|
16
|
+
SeregaValidations::CheckBatchLoaderParams.new(name, block).validate
|
|
17
|
+
|
|
18
|
+
@name = name.to_sym
|
|
19
|
+
@block = block
|
|
20
|
+
@signature = SeregaUtils::MethodSignature.call(block, pos_limit: 2, keyword_args: [:ctx])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Loads values for objects
|
|
24
|
+
def load(objects, context)
|
|
25
|
+
case signature
|
|
26
|
+
when "1" then block.call(objects)
|
|
27
|
+
when "2" then block.call(objects, context)
|
|
28
|
+
else block.call(objects, ctx: context) # "1_ctx"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Batch loader block signature
|
|
35
|
+
attr_reader :signature
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# The :if, :unless, :if_value and :unless_value conditions of one attribute.
|
|
6
|
+
# Each object defines `#satisfy?(object, context)`, which checks :if and
|
|
7
|
+
# :unless, and `#satisfy_value?(value, context)`, which checks :if_value
|
|
8
|
+
# and :unless_value. Each method is one line of code at the file and line
|
|
9
|
+
# of the attribute, and calls the conditions directly, for example:
|
|
10
|
+
#
|
|
11
|
+
# def satisfy?(object, context) = (begin; object.active; rescue => error; ...; end)
|
|
12
|
+
#
|
|
13
|
+
class SeregaConditions # :nodoc:
|
|
14
|
+
def initialize(attribute, conditions)
|
|
15
|
+
@attribute = attribute
|
|
16
|
+
@conditions = conditions
|
|
17
|
+
satisfy_code = "def satisfy?(object, context) = #{check_code(:if, :unless)}"
|
|
18
|
+
satisfy_value_code = "def satisfy_value?(object, context) = #{check_code(:if_value, :unless_value)}"
|
|
19
|
+
attribute.define_methods(singleton_class, "#{satisfy_code}; #{satisfy_value_code}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Code that is true when the object passes the :if and does not match the
|
|
25
|
+
# :unless condition
|
|
26
|
+
def check_code(if_name, unless_name)
|
|
27
|
+
checks = []
|
|
28
|
+
checks << rescued_call_code(if_name) if @conditions.key?(if_name)
|
|
29
|
+
checks << "!#{rescued_call_code(unless_name)}" if @conditions.key?(unless_name)
|
|
30
|
+
checks.empty? ? "true" : checks.join(" && ")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Code that calls the condition. An error raised there gets the condition
|
|
34
|
+
# name.
|
|
35
|
+
def rescued_call_code(name)
|
|
36
|
+
"(begin; #{call_code(name)}; rescue => error; " \
|
|
37
|
+
"Serega::SeregaUtils::SerializedAttributeError.condition(error, @attribute, #{name.inspect}); end)"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Code that calls the condition with the parameters of its signature
|
|
41
|
+
def call_code(name)
|
|
42
|
+
condition = @conditions[name]
|
|
43
|
+
keyword_code = condition.code("object") if condition.is_a?(AttributeValueResolvers::Keyword)
|
|
44
|
+
return keyword_code if keyword_code
|
|
45
|
+
|
|
46
|
+
callable = "@conditions[#{name.inspect}]"
|
|
47
|
+
case SeregaUtils::MethodSignature.call(condition, pos_limit: 2, keyword_args: [:ctx])
|
|
48
|
+
when "1" then "#{callable}.call(object)"
|
|
49
|
+
when "2" then "#{callable}.call(object, context)"
|
|
50
|
+
when "1_ctx" then "#{callable}.call(object, ctx: context)"
|
|
51
|
+
when "2_ctx" then "#{callable}.call(object, context, ctx: context)"
|
|
52
|
+
else "#{callable}.call" # "0"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/serega/config.rb
CHANGED
|
@@ -1,37 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Serega
|
|
4
|
+
# Config of a serializer, `config` in a serializer class. Subclasses copy
|
|
5
|
+
# the config of their parent and can change it.
|
|
4
6
|
#
|
|
5
|
-
#
|
|
7
|
+
# ```ruby
|
|
8
|
+
# class AppSerializer < Serega
|
|
9
|
+
# config.auto_preload = true
|
|
10
|
+
# config.hide_by_default = :auto
|
|
11
|
+
# end
|
|
12
|
+
# ```
|
|
6
13
|
#
|
|
14
|
+
# See [Configuration](https://github.com/andreyruby/serega/blob/master/docs/configuration.md).
|
|
7
15
|
class SeregaConfig
|
|
8
16
|
# :nocov: We can't use both :oj and :json adapters together
|
|
9
17
|
|
|
10
18
|
#
|
|
11
19
|
# Default config options
|
|
12
20
|
#
|
|
13
|
-
DEFAULTS = {
|
|
21
|
+
DEFAULTS = { # :nodoc:
|
|
14
22
|
plugins: [],
|
|
15
|
-
initiate_keys: %i[only with except check_initiate_params].freeze,
|
|
16
|
-
attribute_keys: %i[
|
|
17
|
-
method
|
|
18
|
-
value
|
|
19
|
-
serializer
|
|
20
|
-
many
|
|
21
|
-
hide
|
|
22
|
-
const
|
|
23
|
-
delegate
|
|
24
|
-
default
|
|
25
|
-
preload
|
|
26
|
-
batch
|
|
27
|
-
base_serializer
|
|
28
|
-
hash_access
|
|
29
|
-
].freeze,
|
|
30
23
|
serialize_keys: %i[context many].freeze,
|
|
31
24
|
check_attribute_name: true,
|
|
25
|
+
require_many: false,
|
|
32
26
|
check_initiate_params: true,
|
|
33
27
|
delegate_default_allow_nil: false,
|
|
34
|
-
max_cached_plans_per_serializer_count:
|
|
28
|
+
max_cached_plans_per_serializer_count: 20,
|
|
29
|
+
depth_limit: nil,
|
|
35
30
|
auto_preload: {has_delegate_option: false, has_serializer_option: false},
|
|
36
31
|
auto_preload_excluded_methods: %i[itself __getobj__].freeze,
|
|
37
32
|
hide_by_default: false,
|
|
@@ -41,99 +36,58 @@ class Serega
|
|
|
41
36
|
}.freeze
|
|
42
37
|
# :nocov:
|
|
43
38
|
|
|
44
|
-
#
|
|
39
|
+
# Config options
|
|
45
40
|
module SeregaConfigInstanceMethods
|
|
46
|
-
#
|
|
47
41
|
# Shows current config as Hash
|
|
48
|
-
#
|
|
49
|
-
# @return [Hash] config options
|
|
50
|
-
#
|
|
51
|
-
attr_reader :opts
|
|
42
|
+
attr_reader :opts # :nodoc:
|
|
52
43
|
|
|
53
|
-
#
|
|
54
44
|
# Initializes new config instance.
|
|
55
|
-
#
|
|
56
|
-
# @param opts [Hash] Initial config options
|
|
57
|
-
#
|
|
58
|
-
def initialize(opts = nil)
|
|
45
|
+
def initialize(opts = nil) # :nodoc:
|
|
59
46
|
opts ||= DEFAULTS
|
|
60
47
|
@opts = SeregaUtils::EnumDeepDup.call(opts)
|
|
48
|
+
@hash_access = nil
|
|
49
|
+
@plugin_configs = {}
|
|
61
50
|
end
|
|
62
51
|
|
|
63
|
-
#
|
|
64
52
|
# Shows used plugins
|
|
65
|
-
#
|
|
66
|
-
# @return [Array] Used plugins
|
|
67
|
-
#
|
|
68
|
-
def plugins
|
|
53
|
+
def plugins # :nodoc:
|
|
69
54
|
opts.fetch(:plugins)
|
|
70
55
|
end
|
|
71
56
|
|
|
72
|
-
# Returns options names allowed in `Serega#new` method
|
|
73
|
-
# @return [Array<Symbol>] allowed options keys
|
|
74
|
-
def initiate_keys
|
|
75
|
-
opts.fetch(:initiate_keys)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Returns options names allowed in `Serega.attribute` method
|
|
79
|
-
# @return [Array<Symbol>] Allowed options keys for attribute initialization
|
|
80
|
-
def attribute_keys
|
|
81
|
-
opts.fetch(:attribute_keys)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
57
|
# Returns options names allowed in `call, to_h` methods
|
|
85
|
-
|
|
86
|
-
def serialize_keys
|
|
58
|
+
def serialize_keys # :nodoc:
|
|
87
59
|
opts.fetch(:serialize_keys)
|
|
88
60
|
end
|
|
89
61
|
|
|
90
|
-
#
|
|
91
|
-
#
|
|
62
|
+
# Whether unknown fields of `only:`, `except:` and `with:` raise
|
|
63
|
+
# `Serega::AttributeNotExist`. `true` by default.
|
|
92
64
|
def check_initiate_params
|
|
93
65
|
opts.fetch(:check_initiate_params)
|
|
94
66
|
end
|
|
95
67
|
|
|
96
|
-
# Sets
|
|
97
|
-
#
|
|
98
|
-
# @param value [Boolean] Set :check_initiate_params config option
|
|
99
|
-
#
|
|
100
|
-
# @return [Boolean] :check_initiate_params config option
|
|
68
|
+
# Sets #check_initiate_params
|
|
101
69
|
def check_initiate_params=(value)
|
|
102
|
-
|
|
103
|
-
opts[:check_initiate_params] = value
|
|
70
|
+
opts[:check_initiate_params] = checked_boolean(value)
|
|
104
71
|
end
|
|
105
72
|
|
|
106
|
-
#
|
|
107
|
-
#
|
|
73
|
+
# Default `allow_nil:` of the `:delegate` attribute option. `false` by
|
|
74
|
+
# default.
|
|
108
75
|
def delegate_default_allow_nil
|
|
109
76
|
opts.fetch(:delegate_default_allow_nil)
|
|
110
77
|
end
|
|
111
78
|
|
|
112
|
-
# Sets
|
|
113
|
-
#
|
|
114
|
-
# @param value [Boolean] Set :delegate_default_allow_nil config option
|
|
115
|
-
#
|
|
116
|
-
# @return [Boolean] :delegate_default_allow_nil config option
|
|
79
|
+
# Sets #delegate_default_allow_nil
|
|
117
80
|
def delegate_default_allow_nil=(value)
|
|
118
|
-
|
|
119
|
-
opts[:delegate_default_allow_nil] = value
|
|
81
|
+
opts[:delegate_default_allow_nil] = checked_boolean(value)
|
|
120
82
|
end
|
|
121
83
|
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
# not an association
|
|
125
|
-
# @return [Array<Symbol>] Current :auto_preload_excluded_methods config option
|
|
84
|
+
# Methods that #auto_preload never preloads, as they return the object
|
|
85
|
+
# itself. `[:itself, :__getobj__]` by default.
|
|
126
86
|
def auto_preload_excluded_methods
|
|
127
87
|
opts.fetch(:auto_preload_excluded_methods)
|
|
128
88
|
end
|
|
129
89
|
|
|
130
|
-
# Sets
|
|
131
|
-
# never auto-preloaded. Applies to the `:method` option of attributes
|
|
132
|
-
# with a serializer and to the `delegate: {to: ...}` option.
|
|
133
|
-
#
|
|
134
|
-
# @param value [Array<Symbol>] Method names to skip in auto-preload
|
|
135
|
-
#
|
|
136
|
-
# @return [Array<Symbol>] New :auto_preload_excluded_methods config option
|
|
90
|
+
# Sets #auto_preload_excluded_methods
|
|
137
91
|
def auto_preload_excluded_methods=(value)
|
|
138
92
|
unless value.is_a?(Array) && value.all?(Symbol)
|
|
139
93
|
raise SeregaError, "Must be an Array of Symbols, #{value.inspect} provided"
|
|
@@ -142,41 +96,31 @@ class Serega
|
|
|
142
96
|
opts[:auto_preload_excluded_methods] = value
|
|
143
97
|
end
|
|
144
98
|
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
# @return [Class, nil] Current :base_serializer config option
|
|
99
|
+
# Parent class of nested serializers defined with an attribute block.
|
|
100
|
+
# `nil` by default.
|
|
148
101
|
def base_serializer
|
|
149
102
|
opts.fetch(:base_serializer)
|
|
150
103
|
end
|
|
151
104
|
|
|
152
|
-
# Sets
|
|
153
|
-
# serializers defined with attribute blocks. Usually a settings-only
|
|
154
|
-
# serializer, e.g. `config.base_serializer = self` in an application
|
|
155
|
-
# base serializer class.
|
|
156
|
-
#
|
|
157
|
-
# @param value [Class] Serega or its subclass
|
|
105
|
+
# Sets #base_serializer, a Serega subclass
|
|
158
106
|
#
|
|
159
|
-
#
|
|
107
|
+
# ```ruby
|
|
108
|
+
# config.base_serializer = self
|
|
109
|
+
# ```
|
|
160
110
|
def base_serializer=(value)
|
|
161
111
|
raise SeregaError, "Must be a Serega subclass, #{value.inspect} provided" if !value.is_a?(Class) || !(value <= Serega)
|
|
162
112
|
|
|
163
113
|
opts[:base_serializer] = value
|
|
164
114
|
end
|
|
165
115
|
|
|
166
|
-
#
|
|
167
|
-
#
|
|
116
|
+
# Which attributes are hidden without the `:hide` option: `false` for none
|
|
117
|
+
# (the default), `true` for all, `:auto` for attributes with preloads or
|
|
118
|
+
# batch loaders.
|
|
168
119
|
def hide_by_default
|
|
169
120
|
opts.fetch(:hide_by_default)
|
|
170
121
|
end
|
|
171
122
|
|
|
172
|
-
# Sets
|
|
173
|
-
#
|
|
174
|
-
# @param value [Boolean, Symbol] Accepted values:
|
|
175
|
-
# - false (default) — nothing is hidden by default
|
|
176
|
-
# - true — all attributes are hidden by default
|
|
177
|
-
# - :auto — hides attributes that declare :preload or :batch
|
|
178
|
-
#
|
|
179
|
-
# @return [Boolean, Symbol] New :hide_by_default config option
|
|
123
|
+
# Sets #hide_by_default
|
|
180
124
|
def hide_by_default=(value)
|
|
181
125
|
opts[:hide_by_default] =
|
|
182
126
|
case value
|
|
@@ -187,13 +131,15 @@ class Serega
|
|
|
187
131
|
end
|
|
188
132
|
end
|
|
189
133
|
|
|
190
|
-
#
|
|
134
|
+
# Attributes that get preloads automatically: `:delegate` attributes with
|
|
135
|
+
# `has_delegate_option` and `:serializer` attributes with
|
|
136
|
+
# `has_serializer_option`. Both are `false` by default.
|
|
191
137
|
def auto_preload
|
|
192
138
|
opts.fetch(:auto_preload)
|
|
193
139
|
end
|
|
194
140
|
|
|
195
|
-
#
|
|
196
|
-
#
|
|
141
|
+
# Sets #auto_preload: `true` or `false` for both kinds of attributes, or a
|
|
142
|
+
# Hash with `has_delegate_option` and `has_serializer_option`
|
|
197
143
|
def auto_preload=(value)
|
|
198
144
|
opts[:auto_preload] =
|
|
199
145
|
case value
|
|
@@ -207,85 +153,102 @@ class Serega
|
|
|
207
153
|
end
|
|
208
154
|
end
|
|
209
155
|
|
|
210
|
-
#
|
|
211
|
-
#
|
|
156
|
+
# Builds lazy config objects before freezing, thus a frozen config
|
|
157
|
+
# returns them
|
|
158
|
+
def freeze # :nodoc:
|
|
159
|
+
hash_access
|
|
160
|
+
super
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Defaults of the `hash_access:` attribute option
|
|
212
164
|
def hash_access
|
|
213
165
|
@hash_access ||= HashAccessConfig.new(opts.fetch(:hash_access))
|
|
214
166
|
end
|
|
215
167
|
|
|
216
|
-
#
|
|
217
|
-
#
|
|
168
|
+
# How many serialization plans with field selection the serializer
|
|
169
|
+
# caches, `20` by default. `0` disables the cache.
|
|
218
170
|
def max_cached_plans_per_serializer_count
|
|
219
171
|
opts.fetch(:max_cached_plans_per_serializer_count)
|
|
220
172
|
end
|
|
221
173
|
|
|
222
|
-
# Sets
|
|
223
|
-
#
|
|
224
|
-
# @param value [Boolean] Set :check_initiate_params config option
|
|
225
|
-
#
|
|
226
|
-
# @return [Boolean] New :max_cached_plans_per_serializer_count config option
|
|
174
|
+
# Sets #max_cached_plans_per_serializer_count
|
|
227
175
|
def max_cached_plans_per_serializer_count=(value)
|
|
228
176
|
raise SeregaError, "Must have Integer value, #{value.inspect} provided" unless value.is_a?(Integer)
|
|
229
177
|
opts[:max_cached_plans_per_serializer_count] = value
|
|
230
178
|
end
|
|
231
179
|
|
|
232
|
-
#
|
|
180
|
+
# Deepest nesting of serialized fields, `nil` for no limit (the default).
|
|
181
|
+
# The limit of the serializer that starts the serialization applies.
|
|
182
|
+
def depth_limit
|
|
183
|
+
opts.fetch(:depth_limit)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Sets #depth_limit: a positive Integer or `nil`
|
|
187
|
+
def depth_limit=(value)
|
|
188
|
+
valid = value.nil? || (value.is_a?(Integer) && value.positive?)
|
|
189
|
+
raise SeregaError, "Must have positive Integer or nil value, #{value.inspect} provided" unless valid
|
|
190
|
+
opts[:depth_limit] = value
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Whether attribute names may contain only `a-z`, `A-Z`, `0-9`, `_`, `-`
|
|
194
|
+
# and `~`. `true` by default.
|
|
233
195
|
def check_attribute_name
|
|
234
196
|
opts.fetch(:check_attribute_name)
|
|
235
197
|
end
|
|
236
198
|
|
|
237
|
-
# Sets
|
|
238
|
-
#
|
|
239
|
-
# @param value [Boolean] Set :check_attribute_name config option
|
|
240
|
-
#
|
|
241
|
-
# @return [Boolean] New :check_attribute_name config option
|
|
199
|
+
# Sets #check_attribute_name
|
|
242
200
|
def check_attribute_name=(value)
|
|
243
|
-
|
|
244
|
-
|
|
201
|
+
opts[:check_attribute_name] = checked_boolean(value)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Whether every relation, an attribute with `:serializer` or a block,
|
|
205
|
+
# must have the `:many` option. `false` by default.
|
|
206
|
+
def require_many
|
|
207
|
+
opts.fetch(:require_many)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Sets #require_many
|
|
211
|
+
def require_many=(value)
|
|
212
|
+
opts[:require_many] = checked_boolean(value)
|
|
245
213
|
end
|
|
246
214
|
|
|
247
|
-
#
|
|
215
|
+
# Method that returns the id of an object for batch loaders, `:id` by
|
|
216
|
+
# default
|
|
248
217
|
def batch_id_option
|
|
249
218
|
opts.fetch(:batch_id_option)
|
|
250
219
|
end
|
|
251
220
|
|
|
252
|
-
# Sets
|
|
253
|
-
#
|
|
254
|
-
# @param value [Symbol] Set :batch_id_option config option
|
|
255
|
-
#
|
|
256
|
-
# @return [Symbol] New :check_attribute_name config option
|
|
221
|
+
# Sets #batch_id_option
|
|
257
222
|
def batch_id_option=(value)
|
|
258
223
|
raise SeregaError, "Must have Symbol value, #{value.inspect} provided" unless value.is_a?(Symbol)
|
|
259
224
|
opts[:batch_id_option] = value
|
|
260
225
|
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def checked_boolean(value)
|
|
230
|
+
raise SeregaError, "Must have boolean value, #{value.inspect} provided" unless value == true || value == false
|
|
231
|
+
|
|
232
|
+
value
|
|
233
|
+
end
|
|
261
234
|
end
|
|
262
235
|
|
|
263
|
-
#
|
|
264
|
-
# Config for the `hash_access:` attribute option
|
|
265
|
-
#
|
|
236
|
+
# Defaults of the `hash_access:` attribute option, `config.hash_access`
|
|
266
237
|
class HashAccessConfig
|
|
267
|
-
#
|
|
268
|
-
attr_reader :opts
|
|
238
|
+
# hash_access config options
|
|
239
|
+
attr_reader :opts # :nodoc:
|
|
269
240
|
|
|
270
|
-
#
|
|
271
241
|
# Initializes HashAccessConfig object
|
|
272
|
-
#
|
|
273
|
-
# @param opts [Hash] hash_access config options
|
|
274
|
-
#
|
|
275
|
-
# @return [Serega::SeregaConfig::HashAccessConfig]
|
|
276
|
-
#
|
|
277
|
-
def initialize(opts)
|
|
242
|
+
def initialize(opts) # :nodoc:
|
|
278
243
|
@opts = opts
|
|
279
244
|
end
|
|
280
245
|
|
|
281
|
-
#
|
|
246
|
+
# Mode of `hash_access: true`: `:symbol` (the default) or `:string` keys
|
|
282
247
|
def default_mode
|
|
283
248
|
opts.fetch(:default_mode)
|
|
284
249
|
end
|
|
285
250
|
|
|
286
|
-
# Sets
|
|
287
|
-
# @param value [Symbol] one of :symbol, :string
|
|
288
|
-
# @return [Symbol] new default mode
|
|
251
|
+
# Sets #default_mode
|
|
289
252
|
def default_mode=(value)
|
|
290
253
|
unless AttributeValueResolvers::HashAccessResolver::MODES.include?(value)
|
|
291
254
|
raise SeregaError, "Invalid hash_access default_mode #{value.inspect}. Allowed modes: :symbol, :string"
|
|
@@ -294,14 +257,13 @@ class Serega
|
|
|
294
257
|
opts[:default_mode] = value
|
|
295
258
|
end
|
|
296
259
|
|
|
297
|
-
#
|
|
260
|
+
# Whether a missing Hash key gives `nil` instead of raising `KeyError`.
|
|
261
|
+
# `false` by default.
|
|
298
262
|
def default_allow_missing_key
|
|
299
263
|
opts.fetch(:default_allow_missing_key)
|
|
300
264
|
end
|
|
301
265
|
|
|
302
|
-
# Sets
|
|
303
|
-
# @param value [Boolean]
|
|
304
|
-
# @return [Boolean] new default allow_missing_key
|
|
266
|
+
# Sets #default_allow_missing_key
|
|
305
267
|
def default_allow_missing_key=(value)
|
|
306
268
|
unless value == true || value == false
|
|
307
269
|
raise SeregaError, "Invalid hash_access default_allow_missing_key #{value.inspect}. Must be a Boolean"
|
|
@@ -312,6 +274,5 @@ class Serega
|
|
|
312
274
|
end
|
|
313
275
|
|
|
314
276
|
include SeregaConfigInstanceMethods
|
|
315
|
-
extend Serega::SeregaHelpers::SerializerClassHelper
|
|
316
277
|
end
|
|
317
278
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
module SeregaEngine # :nodoc:
|
|
5
|
+
#
|
|
6
|
+
# Pulls of relation sources. A pull says what to take for one relation
|
|
7
|
+
# source from the serialized objects of its child group:
|
|
8
|
+
# - the count of a collection: an Array of this count of serialized objects
|
|
9
|
+
# - SINGLE_SOURCE for one source: one serialized object
|
|
10
|
+
# - nil for nil: nothing, the relation value is nil
|
|
11
|
+
# - SKIP for a relation skipped by its condition: nothing, the relation
|
|
12
|
+
# value is SKIP
|
|
13
|
+
#
|
|
14
|
+
module Pulls # :nodoc:
|
|
15
|
+
# Pull of one source that is not in a collection. Its relation value is
|
|
16
|
+
# one serialized object, and not an Array.
|
|
17
|
+
SINGLE_SOURCE = Object.new.freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Appends the sources of the relation sources to sources, and returns
|
|
22
|
+
# the pull of each relation source
|
|
23
|
+
def collect(sources, relation_sources, many, known_classes)
|
|
24
|
+
relation_sources.map { |relation_source| append(sources, relation_source, many, known_classes) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Collects like `.collect`, and keeps SKIP of a skipped relation source
|
|
28
|
+
# as its pull
|
|
29
|
+
def collect_conditional(sources, relation_sources, many, known_classes)
|
|
30
|
+
relation_sources.map do |relation_source|
|
|
31
|
+
SKIP.equal?(relation_source) ? relation_source : append(sources, relation_source, many, known_classes)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Appends the source(s) of one relation source to sources, and returns
|
|
36
|
+
# its pull
|
|
37
|
+
def append(sources, relation_source, many, known_classes)
|
|
38
|
+
return if relation_source.nil?
|
|
39
|
+
|
|
40
|
+
if many != false && SeregaUtils::CollectionDetector.call(relation_source, known_classes)
|
|
41
|
+
collection = relation_source.to_a
|
|
42
|
+
sources.concat(collection)
|
|
43
|
+
collection.size
|
|
44
|
+
else
|
|
45
|
+
sources << relation_source
|
|
46
|
+
many ? 1 : SINGLE_SOURCE # `many` on, but a sole source was given — wrap it, don't raise
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Takes the relation value of each pull from the front of the
|
|
51
|
+
# serialized objects. The serialized objects follow the order of the
|
|
52
|
+
# pulls. Empties the serialized objects.
|
|
53
|
+
def take!(serialized, pulls)
|
|
54
|
+
pulls.map do |pull|
|
|
55
|
+
case pull
|
|
56
|
+
when Integer then serialized.shift(pull)
|
|
57
|
+
when SINGLE_SOURCE then serialized.shift
|
|
58
|
+
else pull # nil or SKIP
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Serialization engine
|
|
6
|
+
#
|
|
7
|
+
module SeregaEngine # :nodoc:
|
|
8
|
+
# Value of an attribute skipped by its conditions. It is also the pull of
|
|
9
|
+
# a skipped relation.
|
|
10
|
+
SKIP = Object.new.freeze
|
|
11
|
+
|
|
12
|
+
#
|
|
13
|
+
# One serialization run. Serializes the source(s) with a plan, and all
|
|
14
|
+
# their relation sources. A source is an object to serialize.
|
|
15
|
+
#
|
|
16
|
+
# The run makes source groups. A group holds all sources of one plan, for
|
|
17
|
+
# example the posts of all users, thus batch loaders and preloads run once
|
|
18
|
+
# per group. The run serializes the groups in two passes:
|
|
19
|
+
# - Discover pass, from the root group down. Each group runs its preloads
|
|
20
|
+
# and makes one child group per relation.
|
|
21
|
+
# - Build pass, from the last group up. Each group builds its serialized
|
|
22
|
+
# objects. The serialized related objects are ready at this time.
|
|
23
|
+
#
|
|
24
|
+
class Run # :nodoc:
|
|
25
|
+
class << self
|
|
26
|
+
# Serializes the object(s) with the plan in a new run. A flat plan
|
|
27
|
+
# builds its serialized object(s) with its result builder, without a
|
|
28
|
+
# run, and one source without the sources Array.
|
|
29
|
+
def call(plan, object, many, mode, context)
|
|
30
|
+
return new(mode, context).call(plan, object, many: many) unless plan.flat?
|
|
31
|
+
return if object.nil?
|
|
32
|
+
|
|
33
|
+
builder = plan.result_builder(mode)
|
|
34
|
+
return builder.call_one(object, context) if many == false
|
|
35
|
+
|
|
36
|
+
sources = []
|
|
37
|
+
pull = Pulls.append(sources, object, many, plan.collection_classes)
|
|
38
|
+
Pulls::SINGLE_SOURCE.equal?(pull) ? builder.call_one(object, context) : builder.call(sources, context, nil, nil)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Serialization mode
|
|
43
|
+
attr_reader :mode
|
|
44
|
+
|
|
45
|
+
# Serialization context
|
|
46
|
+
attr_reader :context
|
|
47
|
+
|
|
48
|
+
def initialize(mode, context)
|
|
49
|
+
@mode = mode
|
|
50
|
+
@context = context
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Serializes the object(s) with the plan.
|
|
54
|
+
def call(plan, object, many:)
|
|
55
|
+
return if object.nil?
|
|
56
|
+
|
|
57
|
+
root_group = root_source_group(plan, object, many)
|
|
58
|
+
source_groups = discover(root_group)
|
|
59
|
+
build(source_groups)
|
|
60
|
+
root_value(root_group)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Makes a source group of the plan.
|
|
64
|
+
def new_source_group(plan, sources, pulls)
|
|
65
|
+
SeregaSourceGroup.new(self, plan, sources, pulls)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Makes the root group: the root source(s) with one pull
|
|
71
|
+
def root_source_group(plan, object, many)
|
|
72
|
+
sources = []
|
|
73
|
+
pull = Pulls.append(sources, object, many, plan.collection_classes)
|
|
74
|
+
new_source_group(plan, sources, [pull])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Discovers the root group and its child groups, from the root down.
|
|
78
|
+
# Returns all groups in discover order: a child group comes after its
|
|
79
|
+
# parent group.
|
|
80
|
+
def discover(root_group)
|
|
81
|
+
source_groups = [root_group]
|
|
82
|
+
index = 0
|
|
83
|
+
|
|
84
|
+
while index < source_groups.size
|
|
85
|
+
source_groups.concat(source_groups[index].discover)
|
|
86
|
+
index += 1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
source_groups
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Builds the groups from the last one up, thus a child group is built
|
|
93
|
+
# before its parent group
|
|
94
|
+
def build(source_groups)
|
|
95
|
+
source_groups.reverse_each(&:build)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns the serialized root: one serialized object for SINGLE_SOURCE,
|
|
99
|
+
# or all serialized objects of the root group
|
|
100
|
+
def root_value(root_group)
|
|
101
|
+
serialized = root_group.serialized
|
|
102
|
+
Pulls::SINGLE_SOURCE.equal?(root_group.pulls[0]) ? serialized[0] : serialized
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|