serega 0.41.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 +123 -1205
- 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 +106 -145
- 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 -145
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +14 -100
- 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 +108 -0
- 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 +480 -372
- data/sig/serega/plugins.rbs +49 -0
- data/sig/serega.rbs +142 -0
- metadata +43 -39
- 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 -118
- 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/presenter/presenter.rb +0 -242
- 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(:presenter) # => SeregaPlugins::Presenter
|
|
34
|
-
#
|
|
35
|
-
# @example Find plugin when providing plugin itself
|
|
36
|
-
# SeregaPlugins.find_plugin(Presenter) # => Presenter
|
|
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
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
class Serega
|
|
6
|
+
# Wraps the object to serialize, so computed attribute values can be defined as
|
|
7
|
+
# methods instead of `:value` callables.
|
|
8
|
+
#
|
|
9
|
+
# SeregaPresenter inherits from SimpleDelegator:
|
|
10
|
+
# - All methods of the wrapped object are available directly inside presenter methods.
|
|
11
|
+
# - Methods not defined on SeregaPresenter are resolved via method_missing on the first call
|
|
12
|
+
# and then defined as real delegators, so subsequent calls skip method_missing entirely.
|
|
13
|
+
# - The original object is accessible via __getobj__ (standard SimpleDelegator API).
|
|
14
|
+
# - The serialization context is accessible via the private method __ctx__.
|
|
15
|
+
#
|
|
16
|
+
# The `presenter do ... end` block is evaluated inside the serializer's own
|
|
17
|
+
# SeregaPresenter class, so multiple blocks accumulate.
|
|
18
|
+
#
|
|
19
|
+
# class UserSerializer < Serega
|
|
20
|
+
# attribute :name
|
|
21
|
+
# attribute :role
|
|
22
|
+
#
|
|
23
|
+
# presenter do
|
|
24
|
+
# def name
|
|
25
|
+
# [first_name, last_name].compact.join(' ') # first_name/last_name delegated to object
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# def role
|
|
29
|
+
# id == __ctx__[:current_user_id] ? :self : :other
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
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
|
+
|
|
38
|
+
#
|
|
39
|
+
# Delegator to the method of the wrapped object with the name it is
|
|
40
|
+
# called by. Delegators with other names are its aliases.
|
|
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
|
+
|
|
87
|
+
def initialize(object, ctx = nil)
|
|
88
|
+
super(object)
|
|
89
|
+
@__ctx__ = ctx
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
attr_reader :__ctx__
|
|
95
|
+
|
|
96
|
+
#
|
|
97
|
+
# Delegates all missing methods to the wrapped object.
|
|
98
|
+
#
|
|
99
|
+
# Creates delegator method for public methods of the wrapped object after
|
|
100
|
+
# first #method_missing hit to improve performance of following serializations.
|
|
101
|
+
#
|
|
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
|
|
106
|
+
end
|
|
107
|
+
end
|
|
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
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# All sources that one plan serializes in one run, for example the posts of
|
|
6
|
+
# all users. A source is an object to serialize. A serialized object is its
|
|
7
|
+
# output: a Hash, Struct or Data object.
|
|
8
|
+
#
|
|
9
|
+
# The group serializes its sources in two steps:
|
|
10
|
+
# - `#discover` runs the preloads, reads the relation sources and adds one
|
|
11
|
+
# child group per relation.
|
|
12
|
+
# - `#build` builds the serialized objects. It runs after the child groups
|
|
13
|
+
# are built, thus the relation values are ready.
|
|
14
|
+
#
|
|
15
|
+
class SeregaSourceGroup # :nodoc:
|
|
16
|
+
# Serialization run
|
|
17
|
+
attr_reader :run
|
|
18
|
+
|
|
19
|
+
# Serialization plan
|
|
20
|
+
attr_reader :plan
|
|
21
|
+
|
|
22
|
+
# Serialization context
|
|
23
|
+
attr_reader :context
|
|
24
|
+
|
|
25
|
+
# Sources (or their presenters)
|
|
26
|
+
attr_reader :sources
|
|
27
|
+
|
|
28
|
+
# Serialized object of each
|
|
29
|
+
# source, after #build
|
|
30
|
+
attr_reader :serialized
|
|
31
|
+
|
|
32
|
+
# Pulls from
|
|
33
|
+
# `SeregaEngine::Pulls.collect`, one per source of the parent group
|
|
34
|
+
attr_reader :pulls
|
|
35
|
+
|
|
36
|
+
# Each source is wrapped in the serializer's presenter, so value reading
|
|
37
|
+
# and batch loaders alike see presenters.
|
|
38
|
+
def initialize(run, plan, sources, pulls)
|
|
39
|
+
@run = run
|
|
40
|
+
@plan = plan
|
|
41
|
+
@context = run.context
|
|
42
|
+
presenter = plan.presenter
|
|
43
|
+
@sources = presenter ? sources.map { |source| presenter.new(source, @context) } : sources
|
|
44
|
+
@pulls = pulls
|
|
45
|
+
@child_groups = nil
|
|
46
|
+
@serialized = nil
|
|
47
|
+
@loaded_batches = nil
|
|
48
|
+
@attribute_values = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Runs the preloads, and makes one child group per relation point, in the
|
|
52
|
+
# order of the relation points.
|
|
53
|
+
def discover
|
|
54
|
+
run_preloads
|
|
55
|
+
|
|
56
|
+
relation_points = plan.relation_points
|
|
57
|
+
return FROZEN_EMPTY_ARRAY if relation_points.empty?
|
|
58
|
+
|
|
59
|
+
@child_groups = relation_points.map { |point| child_group(point) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Builds the serialized objects.
|
|
63
|
+
def build
|
|
64
|
+
batches = plan.points.map { |point| batches_for(point) } if plan.batch_points?
|
|
65
|
+
@serialized = plan.result_builder(run.mode).call(@sources, @context, batches, relations)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Relation values of each relation point, taken from its built child
|
|
71
|
+
# group, or nil without relation points
|
|
72
|
+
def relations
|
|
73
|
+
child_groups = @child_groups
|
|
74
|
+
return unless child_groups
|
|
75
|
+
|
|
76
|
+
relation_points = plan.relation_points
|
|
77
|
+
relations = {}
|
|
78
|
+
index = 0
|
|
79
|
+
|
|
80
|
+
while index < relation_points.size
|
|
81
|
+
child_group = child_groups[index]
|
|
82
|
+
relations[relation_points[index]] = SeregaEngine::Pulls.take!(child_group.serialized, child_group.pulls)
|
|
83
|
+
index += 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
relations
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Reads the relation sources of the point, and makes their child group
|
|
90
|
+
def child_group(point)
|
|
91
|
+
batches = batches_for(point)
|
|
92
|
+
child_sources = []
|
|
93
|
+
pulls =
|
|
94
|
+
if point.conditional?
|
|
95
|
+
relation_sources = read_conditional_relation_sources(point, batches)
|
|
96
|
+
SeregaEngine::Pulls.collect_conditional(child_sources, relation_sources, point.many, point.collection_classes)
|
|
97
|
+
else
|
|
98
|
+
relation_sources = read_relation_sources(point, batches)
|
|
99
|
+
SeregaEngine::Pulls.collect(child_sources, relation_sources, point.many, point.collection_classes)
|
|
100
|
+
end
|
|
101
|
+
run.new_source_group(point.child_plan, child_sources, pulls)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Runs the preloads of all points with the serializer's preload handler.
|
|
105
|
+
# Preload handlers get the sources without presenters.
|
|
106
|
+
def run_preloads
|
|
107
|
+
points = plan.preload_points
|
|
108
|
+
return if points.empty?
|
|
109
|
+
|
|
110
|
+
serializer_class = plan.serializer_class
|
|
111
|
+
handler = serializer_class.preload_with
|
|
112
|
+
sources = plan.presenter ? @sources.map(&:__getobj__) : @sources
|
|
113
|
+
|
|
114
|
+
points.each do |point|
|
|
115
|
+
unless handler
|
|
116
|
+
raise SeregaError, "The :preload option requires a preload handler. Register one with `preload_with` (the :activerecord_preloads plugin does this for you)."
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
handler.call(sources, point.preloads)
|
|
120
|
+
rescue => error
|
|
121
|
+
SeregaUtils::SerializedAttributeError.call(error, point)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Loads the batch loaders of the point, each once for all sources of
|
|
126
|
+
# the group. An attribute with an inline batch loader uses it, other
|
|
127
|
+
# attributes use the named batch loaders of the serializer. Returns nil
|
|
128
|
+
# when the point has no batch loaders.
|
|
129
|
+
def batches_for(point)
|
|
130
|
+
names = point.batch_loaders
|
|
131
|
+
return if names.empty?
|
|
132
|
+
|
|
133
|
+
inline_loader = point.attribute.inline_batch_loader
|
|
134
|
+
loaders = plan.serializer_class.batch_loaders
|
|
135
|
+
loaded_batches = (@loaded_batches ||= {}.compare_by_identity)
|
|
136
|
+
names.to_h do |name|
|
|
137
|
+
loader = inline_loader || loaders[name]
|
|
138
|
+
[name, loaded_batches[loader] ||= loader.load(@sources, @context)]
|
|
139
|
+
end
|
|
140
|
+
rescue => error
|
|
141
|
+
SeregaUtils::SerializedAttributeError.call(error, point)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# SeregaAttributeValues of the serializer: the methods that read
|
|
145
|
+
# attribute values with plain Ruby code
|
|
146
|
+
def attribute_values
|
|
147
|
+
@attribute_values ||= plan.serializer_class::SeregaAttributeValues.new
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Reads the relation source of every source: what the relation
|
|
151
|
+
# attribute returns, for example `user.posts`.
|
|
152
|
+
def read_relation_sources(point, batches)
|
|
153
|
+
attribute = point.attribute
|
|
154
|
+
value_method = attribute.value_method
|
|
155
|
+
context = @context
|
|
156
|
+
|
|
157
|
+
if value_method
|
|
158
|
+
values = attribute_values
|
|
159
|
+
@sources.map { |source| values.__send__(value_method, source) }
|
|
160
|
+
else
|
|
161
|
+
@sources.map { |source| attribute.value(source, context, batches: batches) }
|
|
162
|
+
end
|
|
163
|
+
rescue => error
|
|
164
|
+
SeregaUtils::SerializedAttributeError.call(error, point)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Reads the relation source of every source of a conditional relation.
|
|
168
|
+
# A source failing the :if or :unless condition gets SKIP.
|
|
169
|
+
def read_conditional_relation_sources(point, batches)
|
|
170
|
+
attribute = point.attribute
|
|
171
|
+
value_method = attribute.value_method
|
|
172
|
+
values = attribute_values if value_method
|
|
173
|
+
conditions = attribute.conditions
|
|
174
|
+
context = @context
|
|
175
|
+
|
|
176
|
+
@sources.map do |source|
|
|
177
|
+
next SeregaEngine::SKIP unless conditions.satisfy?(source, context)
|
|
178
|
+
|
|
179
|
+
begin
|
|
180
|
+
value_method ? values.__send__(value_method, source) : attribute.value(source, context, batches: batches)
|
|
181
|
+
rescue => error
|
|
182
|
+
SeregaUtils::SerializedAttributeError.call(error, point)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|