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
@@ -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
@@ -1,26 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Serega
4
- # @private
5
- module SeregaUtils
4
+ module SeregaUtils # :nodoc:
6
5
  #
7
6
  # Utility to check if an object should be serialized as a collection.
8
7
  #
9
8
  # Hashes and Structs are Enumerable, but enumerate their own member
10
9
  # values, so they are treated as single objects.
11
10
  #
12
- # @private
13
- class CollectionDetector
11
+ class CollectionDetector # :nodoc:
12
+ # Number of classes a Hash of known classes keeps
13
+ MAX_KNOWN_CLASSES = 8
14
+
14
15
  class << self
15
- #
16
- # Checks if provided object is a collection of objects
17
- #
18
- # @param object [Object] Serialized object
19
- #
20
- # @return [Boolean] whether object should be serialized as a collection
21
- #
22
- def call(object)
23
- object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)
16
+ # Checks if provided object is a collection of objects. Remembers the
17
+ # answer for the class of the object in `known_classes`, up to
18
+ # MAX_KNOWN_CLASSES classes: `is_a?(Enumerable)` checks all
19
+ # ancestors of a class that does not include Enumerable.
20
+ def call(object, known_classes)
21
+ return true if object.instance_of?(Array)
22
+
23
+ object_class = object.class
24
+ collection = known_classes[object_class]
25
+ return collection unless collection.nil?
26
+
27
+ collection = object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)
28
+ known_classes[object_class] = collection if known_classes.size < MAX_KNOWN_CLASSES
29
+ collection
24
30
  end
25
31
  end
26
32
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaUtils # :nodoc:
5
+ #
6
+ # Merges nested hashes
7
+ #
8
+ class DeepMerge # :nodoc:
9
+ class << self
10
+ # Merges `other` into `hash` in place. Nested hashes merge into new
11
+ # hashes, thus `other` stays unchanged. Other values of `other` win.
12
+ def call(hash, other)
13
+ hash.merge!(other) { |_key, this_val, other_val| merge_values(this_val, other_val) }
14
+ end
15
+
16
+ private
17
+
18
+ def merge_values(this_val, other_val)
19
+ return other_val unless this_val.is_a?(Hash) && other_val.is_a?(Hash)
20
+
21
+ this_val.merge(other_val) { |_key, this_nested, other_nested| merge_values(this_nested, other_nested) }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -4,23 +4,15 @@ class Serega
4
4
  #
5
5
  # Utilities
6
6
  #
7
- # @private
8
- module SeregaUtils
7
+ module SeregaUtils # :nodoc:
9
8
  #
10
- # Duplicates nested hashes and arrays
11
- # It does not duplicate any non-Array and non-Hash values
9
+ # Duplicates nested hashes, arrays and strings
10
+ # It does not duplicate any other values
12
11
  #
13
- # @private
14
- class EnumDeepDup
12
+ class EnumDeepDup # :nodoc:
15
13
  class << self
16
- #
17
- # Deeply duplicate provided Array or Hash data
18
- # It does not duplicate any non-Array and non-Hash values
19
- #
20
- # @param data [Hash, Array] Data to duplicate
21
- #
22
- # @return [Hash, Array] Duplicated data
23
- #
14
+ # Deeply duplicate provided Array, Hash or String data
15
+ # It does not duplicate any other values
24
16
  def call(data)
25
17
  case data
26
18
  when Hash
@@ -30,9 +22,11 @@ class Serega
30
22
  when Array
31
23
  data = data.dup
32
24
  dup_array_values(data)
25
+ when String
26
+ data.dup
27
+ else
28
+ data
33
29
  end
34
-
35
- data
36
30
  end
37
31
 
38
32
  private
@@ -4,21 +4,13 @@ class Serega
4
4
  #
5
5
  # Utilities
6
6
  #
7
- # @private
8
- module SeregaUtils
7
+ module SeregaUtils # :nodoc:
9
8
  #
10
- # Utility to freeze nested hashes and arrays
9
+ # Utility to freeze nested hashes, arrays and strings
11
10
  #
12
- # @private
13
- class EnumDeepFreeze
11
+ class EnumDeepFreeze # :nodoc:
14
12
  class << self
15
- #
16
- # Freezes nested hashes and arrays
17
- #
18
- # @param data[Hash, Array] data to freeze
19
- #
20
- # @return [Hash, Array] same deeply frozen data
21
- #
13
+ # Freezes nested hashes and arrays, replaces strings with frozen copies
22
14
  def call(data)
23
15
  case data
24
16
  when Hash
@@ -27,9 +19,11 @@ class Serega
27
19
  when Array
28
20
  data.map! { |value| call(value) }
29
21
  data.freeze
22
+ when String
23
+ -data
24
+ else
25
+ data
30
26
  end
31
-
32
- data
33
27
  end
34
28
  end
35
29
  end
@@ -4,31 +4,22 @@ class Serega
4
4
  #
5
5
  # Utilities
6
6
  #
7
- # @private
8
- module SeregaUtils
7
+ module SeregaUtils # :nodoc:
9
8
  #
10
9
  # Utility to make method arguments signature
11
10
  #
12
- # @private
13
- class MethodSignature
11
+ class MethodSignature # :nodoc:
14
12
  SYMBOL_TO_PROC_SIGNATURE_RUBY2 = [[:rest]]
15
13
  SYMBOL_TO_PROC_SIGNATURE_RUBY3 = [[:req], [:rest]]
16
14
  private_constant :SYMBOL_TO_PROC_SIGNATURE_RUBY2
17
15
  private_constant :SYMBOL_TO_PROC_SIGNATURE_RUBY3
18
16
 
19
17
  class << self
20
- #
21
- # Generates method arguments signature
22
- #
23
- # @param callable [#call] callable object
24
- # @param pos_limit [Integer] Max count of positional parameters
25
- # @param keyword_args [Array<Symbol>] List of accepted keyword argument names
26
- #
27
- # @return [String] Method signature which consist of number of positional parameters and
28
- # keyword parameter names joined with undescrore.
29
- #
18
+ # Generates method arguments signature: the count of positional
19
+ # parameters and the keyword parameter names, joined with underscores,
20
+ # like "1_ctx"
30
21
  def call(callable, pos_limit:, keyword_args: [])
31
- params = callable.is_a?(Proc) ? callable.parameters : callable.method(:call).parameters
22
+ params = (callable.is_a?(Proc) || callable.is_a?(Method)) ? callable.parameters : callable.method(:call).parameters
32
23
 
33
24
  # Procs (but not lambdas) can accept all provided parameters
34
25
  return full_signature(pos_limit, keyword_args) if params.empty? && callable.is_a?(Proc) && !callable.lambda?
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaUtils # :nodoc:
5
+ #
6
+ # Parses fields listed in one String, with nested fields in parentheses
7
+ #
8
+ class ParseStringModifiers # :nodoc:
9
+ SEPARATOR = /[,()]/
10
+ COMMA = ",".ord
11
+ LPAREN = "(".ord
12
+ private_constant :SEPARATOR, :COMMA, :LPAREN
13
+
14
+ class << self
15
+ # Parses string modifiers
16
+ #
17
+ # parse("user") => { user: {} }
18
+ # parse("user(id)") => { user: { id: {} } }
19
+ # parse("user(id,name)") => { user: { id: {}, name: {} } }
20
+ # parse("user,comments") => { user: {}, comments: {} }
21
+ # parse("user(comments(text))") => { user: { comments: { text: {} } } }
22
+ def parse(fields)
23
+ result = {}
24
+ storage = result
25
+ parent_storages = nil
26
+ start = 0
27
+
28
+ while (index = fields.byteindex(SEPARATOR, start))
29
+ name = extract_name(fields, start, index)
30
+ separator = fields.getbyte(index)
31
+
32
+ if separator == COMMA
33
+ storage[name] = FROZEN_EMPTY_HASH if name
34
+ elsif separator == LPAREN
35
+ if name
36
+ (parent_storages ||= []) << storage
37
+ storage = storage[name] = {}
38
+ end
39
+ else
40
+ storage[name] = FROZEN_EMPTY_HASH if name
41
+ storage = parent_storages&.pop || result
42
+ end
43
+
44
+ start = index + 1
45
+ end
46
+
47
+ name = extract_name(fields, start, fields.bytesize)
48
+ storage[name] = FROZEN_EMPTY_HASH if name
49
+
50
+ result
51
+ end
52
+
53
+ private
54
+
55
+ def extract_name(fields, start, finish)
56
+ name = fields.byteslice(start, finish - start)
57
+ name.strip!
58
+ name.empty? ? nil : name.to_sym
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ #
5
+ # Utilities
6
+ #
7
+ module SeregaUtils # :nodoc:
8
+ #
9
+ # Values stored in the current Ractor by key
10
+ #
11
+ class RactorLocal # :nodoc:
12
+ class << self
13
+ # Returns the key of the owner value with the name
14
+ def key(owner, name)
15
+ :"serega_#{name}_#{owner.object_id}"
16
+ end
17
+
18
+ # :nocov:
19
+ if defined?(Ractor.[])
20
+ # Returns the value of the key in the current Ractor, stores the
21
+ # block result when there is none
22
+ def fetch(key)
23
+ Ractor[key] ||= yield
24
+ end
25
+ else
26
+ # Returns the value of the key in the current Ractor, stores the
27
+ # block result when there is none
28
+ def fetch(key)
29
+ Ractor.current[key] ||= yield
30
+ end
31
+ end
32
+ # :nocov:
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,29 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Serega
4
- # @private
5
- module SeregaUtils
4
+ module SeregaUtils # :nodoc:
6
5
  #
7
6
  # Reraises an error, adding which attribute and serializer were being
8
- # serialized when it happened. Shared by the synchronous serialization walk
9
- # and the batch attach phase so the message stays identical for every
10
- # attribute, whether its value is resolved inline or during batch loading.
7
+ # serialized when it happened, and where the attribute is defined. Value
8
+ # reads, preloads, batch loaders and attribute conditions use it.
11
9
  #
12
- # @private
13
- module SerializedAttributeError
14
- module_function
15
-
16
- #
17
- # @param error [Exception] Original error
18
- # @param point [SeregaPlanPoint] Plan point being serialized
19
- #
20
- # @return [void]
21
- #
22
- def call(error, point)
23
- raise error.exception(<<~MESSAGE.strip)
24
- #{error.message}
25
- (when serializing '#{point.name}' attribute in #{point.class.serializer_class})
26
- MESSAGE
10
+ module SerializedAttributeError # :nodoc:
11
+ # File name of the generated code in backtraces
12
+ GENERATED_CODE_FILE = "(serega generated code)"
13
+
14
+ class << self
15
+ # Reraises an error raised while a value is read, preloaded or batch
16
+ # loaded. An error raised in the generated code gets a backtrace line at
17
+ # the attribute, above the line of the generated code:
18
+ #
19
+ # app/serializers/user_serializer.rb:3:in 'attribute :full_name of UserSerializer'
20
+ # (serega generated code):20:in 'call'
21
+ def call(error, point)
22
+ location = point.attribute.location
23
+ serializer_class = point.plan.serializer_class
24
+ defined_at = location ? ", #{location}" : ""
25
+
26
+ exception = error.exception(<<~MESSAGE.strip)
27
+ #{error.message}
28
+ (when serializing the '#{point.name}' attribute in #{serializer_class}#{defined_at})
29
+ MESSAGE
30
+ add_attribute_line(exception, "#{location}:in 'attribute :#{point.name} of #{serializer_class}'") if location
31
+ raise exception
32
+ end
33
+
34
+ # Reraises an error raised by an :if, :unless, :if_value or
35
+ # :unless_value condition
36
+ def condition(error, attribute, condition_name)
37
+ location = attribute.location
38
+ defined_at = location ? ", #{location}" : ""
39
+
40
+ raise error.exception(<<~MESSAGE.strip)
41
+ #{error.message}
42
+ (when checking the :#{condition_name} condition of the '#{attribute.name}' attribute in #{attribute.serializer_class}#{defined_at})
43
+ MESSAGE
44
+ end
45
+
46
+ private
47
+
48
+ # Puts the line above the line of the generated code that called this
49
+ # module. The backtrace and the call stack share the outer lines, up
50
+ # to that line: the backtrace has the line of the read there, the
51
+ # call stack the line of the rescue.
52
+ def add_attribute_line(exception, line)
53
+ backtrace = exception.backtrace
54
+ return unless backtrace
55
+
56
+ index = backtrace.size - shared_outer_lines_count(backtrace, caller(2)) - 1
57
+ return if index.negative? || !backtrace[index].start_with?(GENERATED_CODE_FILE)
58
+
59
+ exception.set_backtrace(backtrace.dup.insert(index, line))
60
+ end
61
+
62
+ def shared_outer_lines_count(backtrace, stack)
63
+ count = 0
64
+ count += 1 while count < backtrace.size && count < stack.size && backtrace[-1 - count] == stack[-1 - count]
65
+ count
66
+ end
27
67
  end
28
68
  end
29
69
  end
@@ -1,20 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Serega
4
- # @private
5
- module SeregaUtils
4
+ module SeregaUtils # :nodoc:
6
5
  #
7
6
  # Utility to transform almost anything to Hash
8
7
  #
9
- # @private
10
- class ToHash
8
+ class ToHash # :nodoc:
11
9
  class << self
12
- #
13
10
  # Constructs deep hashes from provided data
14
11
  #
15
- # @param value [Array, Hash, String, Symbol, NilClass, FalseClass, TrueClass] Value to transform
16
- #
17
- # @example
18
12
  # Serega::SeregaUtils::ToHash.(nil) # => {}
19
13
  # Serega::SeregaUtils::ToHash.(true) # => {}
20
14
  # Serega::SeregaUtils::ToHash.(false) # => {}
@@ -24,16 +18,13 @@ class Serega
24
18
  # Serega::SeregaUtils::ToHash.({ foo: nil, bar: true }) # => {:foo=>{}, :bar=>{}}
25
19
  # Serega::SeregaUtils::ToHash.({ foo: :bar }) # => {:foo=>{:bar=>{}}}
26
20
  # Serega::SeregaUtils::ToHash.({ foo: [:bar] }) # => {:foo=>{:bar=>{}}}
27
- #
28
- # @return [Hash] Transformed data
29
- #
30
21
  def call(value)
31
22
  case value
32
23
  when Array then array_to_hash(value)
33
24
  when Hash then hash_to_hash(value)
34
- when NilClass, FalseClass, TrueClass then nil_to_hash(value)
35
- when String then string_to_hash(value)
36
- when Symbol then symbol_to_hash(value)
25
+ when NilClass, FalseClass, TrueClass then FROZEN_EMPTY_HASH
26
+ when String then {value.to_sym => FROZEN_EMPTY_HASH}
27
+ when Symbol then {value => FROZEN_EMPTY_HASH}
37
28
  else raise SeregaError, "Can't convert #{value.class} class object to hash"
38
29
  end
39
30
  end
@@ -63,18 +54,6 @@ class Serega
63
54
  end
64
55
  res
65
56
  end
66
-
67
- def nil_to_hash(_value)
68
- FROZEN_EMPTY_HASH
69
- end
70
-
71
- def string_to_hash(value)
72
- {value.to_sym => FROZEN_EMPTY_HASH}
73
- end
74
-
75
- def symbol_to_hash(value)
76
- {value => FROZEN_EMPTY_HASH}
77
- end
78
57
  end
79
58
  end
80
59
  end
@@ -1,18 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Serega
4
- # @private
5
- module SeregaValidations
4
+ module SeregaValidations # :nodoc:
6
5
  #
7
6
  # Attribute parameters validators
8
7
  #
9
- # @private
10
- module Attribute
8
+ module Attribute # :nodoc:
11
9
  #
12
10
  # Attribute `block` parameter validator
13
11
  #
14
- # @private
15
- class CheckBlock
12
+ class CheckBlock # :nodoc:
16
13
  # Explains the changed attribute block behavior. Shown when the block
17
14
  # looks like an old-style value block — it accepts parameters or
18
15
  # defines no attributes.
@@ -23,25 +20,16 @@ class Serega
23
20
  " use the `value: <callable>` option instead."
24
21
 
25
22
  class << self
26
- #
27
23
  # Checks block parameter provided with attribute.
28
24
  #
29
25
  # The block defines attributes of a nested anonymous serializer, so
30
26
  # it is executed in the context of that serializer and must accept
31
27
  # no parameters.
32
28
  #
33
- # @example
34
29
  # attribute :statistics, method: :itself do
35
30
  # attribute :likes_count
36
31
  # attribute :comments_count
37
32
  # end
38
- #
39
- # @param block [Proc] Block that defines nested serializer attributes
40
- #
41
- # @raise [SeregaError] SeregaError that block has invalid arguments
42
- #
43
- # @return [void]
44
- #
45
33
  def call(block)
46
34
  return unless block
47
35