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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -1207
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +186 -131
  5. data/lib/serega/attribute_normalizer.rb +164 -345
  6. data/lib/serega/attribute_normalizers/batch.rb +45 -0
  7. data/lib/serega/attribute_normalizers/block.rb +81 -0
  8. data/lib/serega/attribute_normalizers/delegate.rb +34 -0
  9. data/lib/serega/attribute_normalizers/hash_access.rb +37 -0
  10. data/lib/serega/attribute_normalizers/preloads.rb +46 -0
  11. data/lib/serega/attribute_value_resolvers/batch.rb +5 -54
  12. data/lib/serega/attribute_value_resolvers/const.rb +2 -24
  13. data/lib/serega/attribute_value_resolvers/delegate.rb +17 -28
  14. data/lib/serega/attribute_value_resolvers/hash_access.rb +34 -52
  15. data/lib/serega/attribute_value_resolvers/keyword.rb +10 -25
  16. data/lib/serega/attribute_values.rb +46 -0
  17. data/lib/serega/batch_loader.rb +37 -0
  18. data/lib/serega/conditions.rb +56 -0
  19. data/lib/serega/config.rb +105 -144
  20. data/lib/serega/engine/pulls.rb +64 -0
  21. data/lib/serega/engine/run.rb +106 -0
  22. data/lib/serega/errors.rb +36 -8
  23. data/lib/serega/formatter.rb +98 -0
  24. data/lib/serega/plan/cache.rb +96 -0
  25. data/lib/serega/plan/per_ractor_cache.rb +23 -0
  26. data/lib/serega/plan.rb +101 -128
  27. data/lib/serega/plan_point.rb +44 -151
  28. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +13 -80
  29. data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +8 -75
  30. data/lib/serega/plugins/context_metadata/context_metadata.rb +40 -123
  31. data/lib/serega/plugins/metadata/check_params.rb +80 -0
  32. data/lib/serega/plugins/metadata/meta_attribute.rb +40 -79
  33. data/lib/serega/plugins/metadata/metadata.rb +40 -168
  34. data/lib/serega/plugins/root/root.rb +91 -235
  35. data/lib/serega/plugins.rb +16 -49
  36. data/lib/serega/presenter.rb +61 -19
  37. data/lib/serega/result_builder.rb +68 -0
  38. data/lib/serega/result_code.rb +203 -0
  39. data/lib/serega/shareable.rb +52 -0
  40. data/lib/serega/source_group.rb +187 -0
  41. data/lib/serega/utils/collection_detector.rb +19 -13
  42. data/lib/serega/utils/deep_merge.rb +26 -0
  43. data/lib/serega/utils/enum_deep_dup.rb +10 -16
  44. data/lib/serega/utils/enum_deep_freeze.rb +8 -14
  45. data/lib/serega/utils/method_signature.rb +6 -15
  46. data/lib/serega/utils/parse_string_modifiers.rb +63 -0
  47. data/lib/serega/utils/ractor_local.rb +36 -0
  48. data/lib/serega/utils/serialized_attribute_error.rb +60 -20
  49. data/lib/serega/utils/to_hash.rb +5 -26
  50. data/lib/serega/validations/attribute/check_block.rb +3 -15
  51. data/lib/serega/validations/attribute/check_name.rb +4 -15
  52. data/lib/serega/validations/attribute/check_opt_base_serializer.rb +3 -15
  53. data/lib/serega/validations/attribute/check_opt_batch.rb +26 -56
  54. data/lib/serega/validations/attribute/check_opt_condition.rb +55 -0
  55. data/lib/serega/validations/attribute/check_opt_delegate.rb +10 -63
  56. data/lib/serega/validations/attribute/check_opt_format.rb +36 -0
  57. data/lib/serega/validations/attribute/check_opt_hash_access.rb +11 -59
  58. data/lib/serega/validations/attribute/check_opt_hide.rb +3 -14
  59. data/lib/serega/validations/attribute/check_opt_many.rb +18 -20
  60. data/lib/serega/validations/attribute/check_opt_method.rb +3 -25
  61. data/lib/serega/validations/attribute/check_opt_preload.rb +3 -26
  62. data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -18
  63. data/lib/serega/validations/attribute/check_opt_value.rb +3 -21
  64. data/lib/serega/validations/check_attribute_params.rb +92 -71
  65. data/lib/serega/validations/check_batch_loader_params.rb +43 -62
  66. data/lib/serega/validations/check_formatter.rb +80 -0
  67. data/lib/serega/validations/check_serialize_keywords.rb +23 -0
  68. data/lib/serega/validations/initiate/check_modifiers.rb +14 -20
  69. data/lib/serega/validations/utils/check_allowed_keys.rb +3 -13
  70. data/lib/serega/validations/utils/check_hash_access_value.rb +44 -0
  71. data/lib/serega/validations/utils/check_opt_is_bool.rb +3 -13
  72. data/lib/serega/validations/utils/check_opt_is_hash.rb +3 -13
  73. data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +3 -13
  74. data/lib/serega/version.rb +1 -4
  75. data/lib/serega.rb +459 -388
  76. data/sig/serega/plugins.rbs +49 -0
  77. data/sig/serega.rbs +142 -0
  78. metadata +42 -38
  79. data/lib/serega/data_builder.rb +0 -54
  80. data/lib/serega/engine/level.rb +0 -59
  81. data/lib/serega/engine/level_queue.rb +0 -56
  82. data/lib/serega/engine/loader.rb +0 -70
  83. data/lib/serega/helpers/serializer_class_helper.rb +0 -19
  84. data/lib/serega/object_serializer.rb +0 -125
  85. data/lib/serega/plugins/camel_case/camel_case.rb +0 -213
  86. data/lib/serega/plugins/depth_limit/depth_limit.rb +0 -206
  87. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +0 -69
  88. data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +0 -38
  89. data/lib/serega/plugins/formatters/formatters.rb +0 -322
  90. data/lib/serega/plugins/if/if.rb +0 -287
  91. data/lib/serega/plugins/if/validations/check_opt_if.rb +0 -72
  92. data/lib/serega/plugins/if/validations/check_opt_if_value.rb +0 -77
  93. data/lib/serega/plugins/if/validations/check_opt_unless.rb +0 -72
  94. data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +0 -77
  95. data/lib/serega/plugins/metadata/validations/check_block.rb +0 -47
  96. data/lib/serega/plugins/metadata/validations/check_opt_const.rb +0 -39
  97. data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +0 -35
  98. data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +0 -35
  99. data/lib/serega/plugins/metadata/validations/check_opt_value.rb +0 -61
  100. data/lib/serega/plugins/metadata/validations/check_opts.rb +0 -59
  101. data/lib/serega/plugins/metadata/validations/check_path.rb +0 -50
  102. data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +0 -111
  103. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +0 -63
  104. data/lib/serega/utils/symbol_name.rb +0 -39
  105. data/lib/serega/validations/attribute/check_opt_const.rb +0 -40
  106. data/lib/serega/validations/check_initiate_params.rb +0 -57
  107. data/lib/serega/validations/check_serialize_params.rb +0 -54
@@ -1,58 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Serega
4
- # Plugins are stored here
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
- @plugins = {}
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
- # Registers given plugin to be able to load it using symbol name.
11
- #
12
- # @param name [Symbol] Plugin name
13
- # @param mod [Module] Plugin module
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
@@ -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 serialized object are available directly inside presenter methods.
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
- # @private
38
- class SeregaPresenter < SimpleDelegator
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
- # @param object [Object] Serialized object to wrap
41
- # @param ctx [Hash, nil] Serialization context
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 serialized object.
97
+ # Delegates all missing methods to the wrapped object.
54
98
  #
55
- # Creates delegator method after first #method_missing hit to improve
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, *_args, &_block) # rubocop:disable Style/MissingRespondToMissing -- base SimpleDelegator class has this method
59
- super.tap do
60
- self.class.def_delegator :__getobj__, name
61
- end
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