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/plugins.rb
CHANGED
|
@@ -1,58 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Serega
|
|
4
|
-
#
|
|
4
|
+
# Built-in features, turned on with Serega::ClassMethods#plugin.
|
|
5
|
+
#
|
|
6
|
+
# See [Guides](https://github.com/andreyruby/serega/blob/master/docs/README.md).
|
|
5
7
|
module SeregaPlugins
|
|
6
|
-
|
|
8
|
+
# Plugin module names, by plugin name
|
|
9
|
+
MODULES = { # :nodoc:
|
|
10
|
+
root: :Root,
|
|
11
|
+
metadata: :Metadata,
|
|
12
|
+
context_metadata: :ContextMetadata,
|
|
13
|
+
activerecord_preloads: :ActiverecordPreloads
|
|
14
|
+
}.freeze
|
|
7
15
|
|
|
8
16
|
class << self
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# @example Register plugin
|
|
16
|
-
# SeregaPlugins.register_plugin(:plugin_name, PluginModule)
|
|
17
|
-
#
|
|
18
|
-
# @return [void]
|
|
19
|
-
#
|
|
20
|
-
# @private
|
|
21
|
-
def register_plugin(name, mod)
|
|
22
|
-
@plugins[name] = mod
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
# Loads plugin code and returns plugin core module.
|
|
27
|
-
#
|
|
28
|
-
# @param name [Symbol, Module] plugin name or plugin itself
|
|
29
|
-
#
|
|
30
|
-
# @raise [SeregaError] Raises SeregaError when plugin was not found
|
|
31
|
-
#
|
|
32
|
-
# @example Find plugin when providing name
|
|
33
|
-
# SeregaPlugins.find_plugin(:root) # => SeregaPlugins::Root
|
|
34
|
-
#
|
|
35
|
-
# @example Find plugin when providing plugin itself
|
|
36
|
-
# SeregaPlugins.find_plugin(Root) # => Root
|
|
37
|
-
#
|
|
38
|
-
# @return [Class<Module>] Plugin core module
|
|
39
|
-
#
|
|
40
|
-
# @private
|
|
41
|
-
def find_plugin(name)
|
|
42
|
-
return name if name.is_a?(Module)
|
|
43
|
-
return @plugins[name] if @plugins.key?(name)
|
|
44
|
-
|
|
45
|
-
require_plugin(name)
|
|
46
|
-
|
|
47
|
-
@plugins[name] || raise(SeregaError, "Plugin '#{name}' did not register itself correctly")
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def require_plugin(name)
|
|
53
|
-
require "serega/plugins/#{name}/#{name}"
|
|
54
|
-
rescue LoadError
|
|
55
|
-
raise SeregaError, "Plugin '#{name}' does not exist"
|
|
17
|
+
# Loads the code of the plugin and returns its module. Raises
|
|
18
|
+
# SeregaError when the plugin does not exist.
|
|
19
|
+
def find(name) # :nodoc:
|
|
20
|
+
module_name = MODULES.fetch(name) { raise SeregaError, "Plugin #{name.inspect} does not exist" }
|
|
21
|
+
require_relative "plugins/#{name}/#{name}"
|
|
22
|
+
const_get(module_name)
|
|
56
23
|
end
|
|
57
24
|
end
|
|
58
25
|
end
|
data/lib/serega/presenter.rb
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "delegate"
|
|
4
|
-
require "forwardable"
|
|
5
4
|
|
|
6
5
|
class Serega
|
|
7
|
-
#
|
|
8
|
-
# Wraps the serialized object, so computed attribute values can be defined as
|
|
6
|
+
# Wraps the object to serialize, so computed attribute values can be defined as
|
|
9
7
|
# methods instead of `:value` callables.
|
|
10
8
|
#
|
|
11
9
|
# SeregaPresenter inherits from SimpleDelegator:
|
|
12
|
-
# - All methods of the
|
|
10
|
+
# - All methods of the wrapped object are available directly inside presenter methods.
|
|
13
11
|
# - Methods not defined on SeregaPresenter are resolved via method_missing on the first call
|
|
14
12
|
# and then defined as real delegators, so subsequent calls skip method_missing entirely.
|
|
15
13
|
# - The original object is accessible via __getobj__ (standard SimpleDelegator API).
|
|
@@ -18,7 +16,6 @@ class Serega
|
|
|
18
16
|
# The `presenter do ... end` block is evaluated inside the serializer's own
|
|
19
17
|
# SeregaPresenter class, so multiple blocks accumulate.
|
|
20
18
|
#
|
|
21
|
-
# @example
|
|
22
19
|
# class UserSerializer < Serega
|
|
23
20
|
# attribute :name
|
|
24
21
|
# attribute :role
|
|
@@ -33,13 +30,60 @@ class Serega
|
|
|
33
30
|
# end
|
|
34
31
|
# end
|
|
35
32
|
# end
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
class SeregaPresenter < SimpleDelegator # :nodoc:
|
|
34
|
+
# Method names of delegators defined with a compiled `def`
|
|
35
|
+
PLAIN_METHOD_NAME = /\A[a-z_][a-zA-Z0-9_]*[?!]?\z/
|
|
36
|
+
private_constant :PLAIN_METHOD_NAME
|
|
37
|
+
|
|
39
38
|
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
39
|
+
# Delegator to the method of the wrapped object with the name it is
|
|
40
|
+
# called by. Delegators with other names are its aliases.
|
|
42
41
|
#
|
|
42
|
+
module CalleeDelegator # :nodoc:
|
|
43
|
+
#
|
|
44
|
+
# Calls the method of the wrapped object with the name of the alias
|
|
45
|
+
#
|
|
46
|
+
def __serega_delegate__(...)
|
|
47
|
+
__getobj__.public_send(__callee__, ...)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Includes into each presenter class its own `Delegators` module.
|
|
52
|
+
# Delegators to the methods of the wrapped object are defined there after the
|
|
53
|
+
# first #method_missing hit. Presenter methods override them and can call
|
|
54
|
+
# them via `super`.
|
|
55
|
+
def self.inherited(subclass)
|
|
56
|
+
super
|
|
57
|
+
delegators = Module.new
|
|
58
|
+
delegators.include(CalleeDelegator)
|
|
59
|
+
subclass.const_set(:Delegators, delegators)
|
|
60
|
+
subclass.include(delegators)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Defines a method that delegates to the wrapped object in the
|
|
64
|
+
# presenter class `Delegators` module
|
|
65
|
+
def self.define_delegator(name)
|
|
66
|
+
delegators = self::Delegators
|
|
67
|
+
|
|
68
|
+
if PLAIN_METHOD_NAME.match?(name)
|
|
69
|
+
delegators.module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
|
70
|
+
def #{name}(...)
|
|
71
|
+
__getobj__.#{name}(...)
|
|
72
|
+
end
|
|
73
|
+
RUBY
|
|
74
|
+
else
|
|
75
|
+
delegators.alias_method(name, :__serega_delegate__)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Defines delegators for the method names that the presenter class does
|
|
80
|
+
# not define itself, thus presenter methods stay in front of them
|
|
81
|
+
def self.define_delegators(names)
|
|
82
|
+
names.each do |name|
|
|
83
|
+
define_delegator(name) unless method_defined?(name) || private_method_defined?(name)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
43
87
|
def initialize(object, ctx = nil)
|
|
44
88
|
super(object)
|
|
45
89
|
@__ctx__ = ctx
|
|
@@ -50,17 +94,15 @@ class Serega
|
|
|
50
94
|
attr_reader :__ctx__
|
|
51
95
|
|
|
52
96
|
#
|
|
53
|
-
# Delegates all missing methods to
|
|
97
|
+
# Delegates all missing methods to the wrapped object.
|
|
54
98
|
#
|
|
55
|
-
# Creates delegator method
|
|
56
|
-
# performance of following serializations.
|
|
99
|
+
# Creates delegator method for public methods of the wrapped object after
|
|
100
|
+
# first #method_missing hit to improve performance of following serializations.
|
|
57
101
|
#
|
|
58
|
-
def method_missing(name,
|
|
59
|
-
super
|
|
60
|
-
|
|
61
|
-
|
|
102
|
+
def method_missing(name, ...) # rubocop:disable Style/MissingRespondToMissing -- base SimpleDelegator class has this method
|
|
103
|
+
result = super
|
|
104
|
+
self.class.define_delegator(name) if __getobj__.respond_to?(name)
|
|
105
|
+
result
|
|
62
106
|
end
|
|
63
|
-
|
|
64
|
-
extend Forwardable
|
|
65
107
|
end
|
|
66
108
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Builds the serialized objects (Hash, Struct or Data) of one plan in one
|
|
6
|
+
# serialization mode.
|
|
7
|
+
#
|
|
8
|
+
# The builder gets the code of its `#call` and `#call_one` methods from
|
|
9
|
+
# SeregaResultCode. `#call` builds the serialized objects of a source group
|
|
10
|
+
# in one loop.
|
|
11
|
+
#
|
|
12
|
+
class SeregaResultBuilder # :nodoc:
|
|
13
|
+
# Serialization mode
|
|
14
|
+
attr_reader :mode
|
|
15
|
+
|
|
16
|
+
# Makes a result builder and generates its methods:
|
|
17
|
+
#
|
|
18
|
+
# call(sources, context, batches, relations) -> Array<Hash, Struct, Data>
|
|
19
|
+
#
|
|
20
|
+
# - `batches` holds the loaded batches per point, or nil.
|
|
21
|
+
# - `relations` holds the relation values of all sources per relation
|
|
22
|
+
# point, or nil.
|
|
23
|
+
#
|
|
24
|
+
# Points without relations and batch loaders get a `#call_one` method
|
|
25
|
+
# too, which builds the serialized object of one source:
|
|
26
|
+
#
|
|
27
|
+
# call_one(source, context) -> Hash, Struct, Data
|
|
28
|
+
def initialize(serializer_class, mode, points)
|
|
29
|
+
@mode = mode
|
|
30
|
+
@points = points
|
|
31
|
+
@result_class = result_class(serializer_class)
|
|
32
|
+
# The generated code reads the `:const`, `:default` and formatter values from its constants
|
|
33
|
+
singleton_class.const_set(:SeregaAttributeValues, serializer_class::SeregaAttributeValues)
|
|
34
|
+
singleton_class.class_eval(SeregaResultCode.new(mode, points).to_s, SeregaUtils::SerializedAttributeError::GENERATED_CODE_FILE, 1)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def result_class(serializer_class)
|
|
40
|
+
return if mode == :hash
|
|
41
|
+
|
|
42
|
+
initialize_point = @points.find { |point| point.name == :initialize }
|
|
43
|
+
raise_member_error("Struct and Data can not have a member named initialize", initialize_point) if initialize_point
|
|
44
|
+
|
|
45
|
+
member_class(serializer_class, @points.map(&:name))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the Struct or Data class with the members. Raises a
|
|
49
|
+
# SeregaError for an attribute name that Ruby can not make a member.
|
|
50
|
+
def member_class(serializer_class, names)
|
|
51
|
+
(mode == :struct) ? serializer_class.struct_class_for(names) : serializer_class.data_class_for(names)
|
|
52
|
+
rescue ArgumentError, NameError => error
|
|
53
|
+
point = @points.find { |point| !member_name?(point.name) }
|
|
54
|
+
raise_member_error(error.message, point)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def member_name?(name)
|
|
58
|
+
(mode == :struct) ? Struct.new(name) : Data.define(name)
|
|
59
|
+
true
|
|
60
|
+
rescue ArgumentError, NameError
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def raise_member_error(message, point)
|
|
65
|
+
SeregaUtils::SerializedAttributeError.call(SeregaError.new(message), point)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Generates the Ruby code of the `SeregaResultBuilder#call` method of one
|
|
6
|
+
# plan in one serialization mode. The method reads all values of a source,
|
|
7
|
+
# then makes its serialized object in one step: a Hash literal,
|
|
8
|
+
# `Struct.new` or `Data.new`. The code reads plain attributes directly,
|
|
9
|
+
# for example `source.name`.
|
|
10
|
+
#
|
|
11
|
+
# Points without relations and batch loaders get a `#call_one(source,
|
|
12
|
+
# context)` method too. It returns the serialized object of one source,
|
|
13
|
+
# without the Array and the loop of `#call`.
|
|
14
|
+
#
|
|
15
|
+
# Generated code for a plan with `attribute :id` and
|
|
16
|
+
# `attribute :posts, serializer: PostSerializer` in the :hash mode:
|
|
17
|
+
#
|
|
18
|
+
# def call(sources, context, batches, relations)
|
|
19
|
+
# relation_values_1 = relations[@points[1]]
|
|
20
|
+
# size = sources.size
|
|
21
|
+
# serialized = Array.new(size)
|
|
22
|
+
# source_index = 0
|
|
23
|
+
# while source_index < size
|
|
24
|
+
# source = sources[source_index]
|
|
25
|
+
# value_0 =
|
|
26
|
+
# begin
|
|
27
|
+
# source.id
|
|
28
|
+
# rescue => error
|
|
29
|
+
# Serega::SeregaUtils::SerializedAttributeError.call(error, @points[0])
|
|
30
|
+
# end
|
|
31
|
+
# value_1 = relation_values_1[source_index]
|
|
32
|
+
# serialized[source_index] = {:id => value_0, :posts => value_1}
|
|
33
|
+
# source_index += 1
|
|
34
|
+
# end
|
|
35
|
+
# serialized
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
class SeregaResultCode # :nodoc:
|
|
39
|
+
# Method names called directly in generated code
|
|
40
|
+
PLAIN_METHOD_NAME = /\A[a-z_][a-zA-Z0-9_]*[?!]?\z/
|
|
41
|
+
|
|
42
|
+
# Serialization mode
|
|
43
|
+
attr_reader :mode
|
|
44
|
+
|
|
45
|
+
def initialize(mode, points)
|
|
46
|
+
@mode = mode
|
|
47
|
+
@points = points
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the code of the `#call` method, and of the `#call_one`
|
|
51
|
+
# method for points without relations and batch loaders. The methods
|
|
52
|
+
# have local variables for the points, and they have the point index
|
|
53
|
+
# in their names:
|
|
54
|
+
# - `attribute_N` and `batches_N` read values with `SeregaAttribute#value`.
|
|
55
|
+
# - `conditions_N` checks the conditions of a conditional attribute.
|
|
56
|
+
# - `relation_values_N` holds the relation values of all sources.
|
|
57
|
+
# - `value_N` holds the value of the current source.
|
|
58
|
+
def to_s
|
|
59
|
+
variables = variables_code.join("\n ")
|
|
60
|
+
values = values_code
|
|
61
|
+
code = call_code(variables, values)
|
|
62
|
+
return code unless @points.none? { |point| point.child_plan || !point.batch_loaders.empty? }
|
|
63
|
+
|
|
64
|
+
"#{code}\n#{call_one_code(variables, values)}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def call_code(variables, values)
|
|
70
|
+
<<~RUBY
|
|
71
|
+
def call(sources, context, batches, relations)
|
|
72
|
+
#{variables}
|
|
73
|
+
size = sources.size
|
|
74
|
+
serialized = Array.new(size)
|
|
75
|
+
source_index = 0
|
|
76
|
+
while source_index < size
|
|
77
|
+
source = sources[source_index]
|
|
78
|
+
#{[*values, *build_serialized_code("serialized[source_index] = ")].join("\n").gsub("\n", "\n ")}
|
|
79
|
+
source_index += 1
|
|
80
|
+
end
|
|
81
|
+
serialized
|
|
82
|
+
end
|
|
83
|
+
RUBY
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def call_one_code(variables, values)
|
|
87
|
+
<<~RUBY
|
|
88
|
+
def call_one(source, context)
|
|
89
|
+
#{variables}
|
|
90
|
+
#{[*values, *build_serialized_code("")].join("\n").gsub("\n", "\n ")}
|
|
91
|
+
end
|
|
92
|
+
RUBY
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Code that sets the local variables of a method: only the variables
|
|
96
|
+
# that the code of the points reads
|
|
97
|
+
def variables_code
|
|
98
|
+
point_variables = @points.each_with_index.flat_map { |point, index| point_variables_code(point, index) }
|
|
99
|
+
return point_variables if mode == :hash
|
|
100
|
+
|
|
101
|
+
["result_class = @result_class", *point_variables]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def point_variables_code(point, index)
|
|
105
|
+
return ["relation_values_#{index} = relations[@points[#{index}]]"] if point.child_plan
|
|
106
|
+
|
|
107
|
+
variables = []
|
|
108
|
+
variables << "attribute_#{index} = @points[#{index}].attribute" unless point.attribute.value_method
|
|
109
|
+
variables << "batches_#{index} = batches[#{index}]" unless point.batch_loaders.empty?
|
|
110
|
+
variables << "conditions_#{index} = @points[#{index}].attribute.conditions" if point.conditional?
|
|
111
|
+
variables
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Code that reads all values of the source, one entry per point
|
|
115
|
+
def values_code
|
|
116
|
+
@points.each_with_index.map { |point, index| assign_value_code(point, index) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Code that assigns the value of the point to `value_N`. A conditional
|
|
120
|
+
# attribute gets SKIP when it fails its conditions. A conditional
|
|
121
|
+
# relation value is SKIP already.
|
|
122
|
+
def assign_value_code(point, index)
|
|
123
|
+
return "value_#{index} = #{read_value_code(point, index)}" if point.child_plan
|
|
124
|
+
|
|
125
|
+
read_code = rescued_read_code(point, index)
|
|
126
|
+
return "value_#{index} =\n#{read_code.gsub(/^/, " ")}" unless point.conditional?
|
|
127
|
+
|
|
128
|
+
<<~RUBY.chomp
|
|
129
|
+
value_#{index} =
|
|
130
|
+
if conditions_#{index}.satisfy?(source, context)
|
|
131
|
+
value_#{index} =
|
|
132
|
+
#{read_code.gsub(/^/, " ")}
|
|
133
|
+
conditions_#{index}.satisfy_value?(value_#{index}, context) ? value_#{index} : Serega::SeregaEngine::SKIP
|
|
134
|
+
else
|
|
135
|
+
Serega::SeregaEngine::SKIP
|
|
136
|
+
end
|
|
137
|
+
RUBY
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Code that reads the value of the point. An error raised there gets
|
|
141
|
+
# the attribute name. Only the rescue reads the point.
|
|
142
|
+
def rescued_read_code(point, index)
|
|
143
|
+
<<~RUBY.chomp
|
|
144
|
+
begin
|
|
145
|
+
#{read_value_code(point, index)}
|
|
146
|
+
rescue => error
|
|
147
|
+
Serega::SeregaUtils::SerializedAttributeError.call(error, @points[#{index}])
|
|
148
|
+
end
|
|
149
|
+
RUBY
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Code that reads the value of the point. A plain attribute reads its
|
|
153
|
+
# `:const`, `:default` and formatter values from the constants of
|
|
154
|
+
# SeregaAttributeValues.
|
|
155
|
+
def read_value_code(point, index)
|
|
156
|
+
return "relation_values_#{index}[source_index]" if point.child_plan
|
|
157
|
+
|
|
158
|
+
attribute = point.attribute
|
|
159
|
+
return attribute.value_code("source", "SeregaAttributeValues::") if attribute.value_method
|
|
160
|
+
return "attribute_#{index}.value(source, context)" if point.batch_loaders.empty?
|
|
161
|
+
|
|
162
|
+
"attribute_#{index}.value(source, context, batches: batches_#{index})"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Code that builds the serialized object of the source. `assign` is the
|
|
166
|
+
# code that takes the serialized object, empty for the return value.
|
|
167
|
+
def build_serialized_code(assign)
|
|
168
|
+
return build_serialized_hash_code(assign) if mode == :hash
|
|
169
|
+
|
|
170
|
+
arguments = @points.each_index.map { |index| argument_code(index) }
|
|
171
|
+
["#{assign}result_class.new(#{arguments.join(", ")})"]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Code of the Struct or Data argument with the value of the point, nil
|
|
175
|
+
# for a skipped value
|
|
176
|
+
def argument_code(index)
|
|
177
|
+
return "value_#{index}" unless @points[index].conditional?
|
|
178
|
+
|
|
179
|
+
"(Serega::SeregaEngine::SKIP.equal?(value_#{index}) ? nil : value_#{index})"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Code that builds the serialized Hash of the source, after `assign`.
|
|
183
|
+
# A Hash literal holds the values up to the first conditional value. Then
|
|
184
|
+
# the code adds the other values one by one, thus the Hash keys keep the
|
|
185
|
+
# order of the points.
|
|
186
|
+
def build_serialized_hash_code(assign)
|
|
187
|
+
literal_size = @points.index(&:conditional?) || @points.size
|
|
188
|
+
pairs = @points.first(literal_size).each_with_index.map { |point, index| "#{point.name.inspect} => value_#{index}" }
|
|
189
|
+
literal = "{#{pairs.join(", ")}}"
|
|
190
|
+
return ["#{assign}#{literal}"] if literal_size == @points.size
|
|
191
|
+
|
|
192
|
+
assigns = @points.each_with_index.drop(literal_size).map { |point, index| hash_assign_code(point, index) }
|
|
193
|
+
["serialized_hash = #{literal}", *assigns, "#{assign}serialized_hash"]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Code that adds the value of the point to the serialized Hash. It adds
|
|
197
|
+
# no key for a skipped value.
|
|
198
|
+
def hash_assign_code(point, index)
|
|
199
|
+
code = "serialized_hash[#{point.name.inspect}] = value_#{index}"
|
|
200
|
+
point.conditional? ? "#{code} unless Serega::SeregaEngine::SKIP.equal?(value_#{index})" : code
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Makes a serializer shareable between Ractors
|
|
6
|
+
#
|
|
7
|
+
class SeregaShareable # :nodoc:
|
|
8
|
+
class << self
|
|
9
|
+
# Makes the serializer definitions shareable between Ractors, except the
|
|
10
|
+
# plans cache. A definition that can not be shared stays as it is.
|
|
11
|
+
def call(serializer_class)
|
|
12
|
+
reasons = []
|
|
13
|
+
|
|
14
|
+
serializer_class.attributes.each_value do |attribute|
|
|
15
|
+
share(attribute, reasons) do |message|
|
|
16
|
+
"Attribute :#{attribute.name} of #{serializer_class} can not be shared between Ractors (#{attribute.location}): #{message}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
serializer_class.batch_loaders.each_value do |loader|
|
|
21
|
+
share(loader, reasons) { |message| "Batch loader :#{loader.name} of #{serializer_class} can not be shared between Ractors: #{message}" }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
serializer_class.formatters.each do |name, formatter|
|
|
25
|
+
share(formatter, reasons) { |message| "Formatter :#{name} of #{serializer_class} can not be shared between Ractors: #{message}" }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
(serializer_class.instance_variables - [:@plan_cache]).each do |name|
|
|
29
|
+
value = serializer_class.instance_variable_get(name)
|
|
30
|
+
share(value, reasons) { |message| "#{serializer_class} can not be shared between Ractors: #{message}" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
share_constants(serializer_class::SeregaAttributeValues, reasons)
|
|
34
|
+
reasons.first
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def share_constants(values_class, reasons)
|
|
40
|
+
values_class.constants(false).each do |name|
|
|
41
|
+
share(values_class.const_get(name, false), reasons) { |message| "#{values_class} can not be shared between Ractors: #{message}" }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def share(value, reasons)
|
|
46
|
+
Ractor.make_shareable(value)
|
|
47
|
+
rescue Ractor::Error, ArgumentError => error
|
|
48
|
+
reasons << -yield(error.message)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|