serega 0.42.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +124 -1207
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +186 -131
- data/lib/serega/attribute_normalizer.rb +164 -345
- data/lib/serega/attribute_normalizers/batch.rb +45 -0
- data/lib/serega/attribute_normalizers/block.rb +81 -0
- data/lib/serega/attribute_normalizers/delegate.rb +34 -0
- data/lib/serega/attribute_normalizers/hash_access.rb +37 -0
- data/lib/serega/attribute_normalizers/preloads.rb +46 -0
- data/lib/serega/attribute_value_resolvers/batch.rb +5 -54
- data/lib/serega/attribute_value_resolvers/const.rb +2 -24
- data/lib/serega/attribute_value_resolvers/delegate.rb +17 -28
- data/lib/serega/attribute_value_resolvers/hash_access.rb +34 -52
- data/lib/serega/attribute_value_resolvers/keyword.rb +10 -25
- data/lib/serega/attribute_values.rb +46 -0
- data/lib/serega/batch_loader.rb +37 -0
- data/lib/serega/conditions.rb +56 -0
- data/lib/serega/config.rb +105 -144
- data/lib/serega/engine/pulls.rb +64 -0
- data/lib/serega/engine/run.rb +106 -0
- data/lib/serega/errors.rb +36 -8
- data/lib/serega/formatter.rb +98 -0
- data/lib/serega/plan/cache.rb +96 -0
- data/lib/serega/plan/per_ractor_cache.rb +23 -0
- data/lib/serega/plan.rb +101 -128
- data/lib/serega/plan_point.rb +44 -151
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +13 -80
- data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +8 -75
- data/lib/serega/plugins/context_metadata/context_metadata.rb +40 -123
- data/lib/serega/plugins/metadata/check_params.rb +80 -0
- data/lib/serega/plugins/metadata/meta_attribute.rb +40 -79
- data/lib/serega/plugins/metadata/metadata.rb +40 -168
- data/lib/serega/plugins/root/root.rb +91 -235
- data/lib/serega/plugins.rb +16 -49
- data/lib/serega/presenter.rb +61 -19
- data/lib/serega/result_builder.rb +68 -0
- data/lib/serega/result_code.rb +203 -0
- data/lib/serega/shareable.rb +52 -0
- data/lib/serega/source_group.rb +187 -0
- data/lib/serega/utils/collection_detector.rb +19 -13
- data/lib/serega/utils/deep_merge.rb +26 -0
- data/lib/serega/utils/enum_deep_dup.rb +10 -16
- data/lib/serega/utils/enum_deep_freeze.rb +8 -14
- data/lib/serega/utils/method_signature.rb +6 -15
- data/lib/serega/utils/parse_string_modifiers.rb +63 -0
- data/lib/serega/utils/ractor_local.rb +36 -0
- data/lib/serega/utils/serialized_attribute_error.rb +60 -20
- data/lib/serega/utils/to_hash.rb +5 -26
- data/lib/serega/validations/attribute/check_block.rb +3 -15
- data/lib/serega/validations/attribute/check_name.rb +4 -15
- data/lib/serega/validations/attribute/check_opt_base_serializer.rb +3 -15
- data/lib/serega/validations/attribute/check_opt_batch.rb +26 -56
- data/lib/serega/validations/attribute/check_opt_condition.rb +55 -0
- data/lib/serega/validations/attribute/check_opt_delegate.rb +10 -63
- data/lib/serega/validations/attribute/check_opt_format.rb +36 -0
- data/lib/serega/validations/attribute/check_opt_hash_access.rb +11 -59
- data/lib/serega/validations/attribute/check_opt_hide.rb +3 -14
- data/lib/serega/validations/attribute/check_opt_many.rb +18 -20
- data/lib/serega/validations/attribute/check_opt_method.rb +3 -25
- data/lib/serega/validations/attribute/check_opt_preload.rb +3 -26
- data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -18
- data/lib/serega/validations/attribute/check_opt_value.rb +3 -21
- data/lib/serega/validations/check_attribute_params.rb +92 -71
- data/lib/serega/validations/check_batch_loader_params.rb +43 -62
- data/lib/serega/validations/check_formatter.rb +80 -0
- data/lib/serega/validations/check_serialize_keywords.rb +23 -0
- data/lib/serega/validations/initiate/check_modifiers.rb +14 -20
- data/lib/serega/validations/utils/check_allowed_keys.rb +3 -13
- data/lib/serega/validations/utils/check_hash_access_value.rb +44 -0
- data/lib/serega/validations/utils/check_opt_is_bool.rb +3 -13
- data/lib/serega/validations/utils/check_opt_is_hash.rb +3 -13
- data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +3 -13
- data/lib/serega/version.rb +1 -4
- data/lib/serega.rb +459 -388
- data/sig/serega/plugins.rbs +49 -0
- data/sig/serega.rbs +142 -0
- metadata +42 -38
- data/lib/serega/data_builder.rb +0 -54
- data/lib/serega/engine/level.rb +0 -59
- data/lib/serega/engine/level_queue.rb +0 -56
- data/lib/serega/engine/loader.rb +0 -70
- data/lib/serega/helpers/serializer_class_helper.rb +0 -19
- data/lib/serega/object_serializer.rb +0 -125
- data/lib/serega/plugins/camel_case/camel_case.rb +0 -213
- data/lib/serega/plugins/depth_limit/depth_limit.rb +0 -206
- data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +0 -69
- data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +0 -38
- data/lib/serega/plugins/formatters/formatters.rb +0 -322
- data/lib/serega/plugins/if/if.rb +0 -287
- data/lib/serega/plugins/if/validations/check_opt_if.rb +0 -72
- data/lib/serega/plugins/if/validations/check_opt_if_value.rb +0 -77
- data/lib/serega/plugins/if/validations/check_opt_unless.rb +0 -72
- data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +0 -77
- data/lib/serega/plugins/metadata/validations/check_block.rb +0 -47
- data/lib/serega/plugins/metadata/validations/check_opt_const.rb +0 -39
- data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +0 -35
- data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +0 -35
- data/lib/serega/plugins/metadata/validations/check_opt_value.rb +0 -61
- data/lib/serega/plugins/metadata/validations/check_opts.rb +0 -59
- data/lib/serega/plugins/metadata/validations/check_path.rb +0 -50
- data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +0 -111
- data/lib/serega/plugins/string_modifiers/string_modifiers.rb +0 -63
- data/lib/serega/utils/symbol_name.rb +0 -39
- data/lib/serega/validations/attribute/check_opt_const.rb +0 -40
- data/lib/serega/validations/check_initiate_params.rb +0 -57
- data/lib/serega/validations/check_serialize_params.rb +0 -54
|
@@ -0,0 +1,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
|
-
#
|
|
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
|
-
#
|
|
13
|
-
|
|
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
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
#
|
|
8
|
-
module SeregaUtils
|
|
7
|
+
module SeregaUtils # :nodoc:
|
|
9
8
|
#
|
|
10
|
-
# Duplicates nested hashes and
|
|
11
|
-
# It does not duplicate any
|
|
9
|
+
# Duplicates nested hashes, arrays and strings
|
|
10
|
+
# It does not duplicate any other values
|
|
12
11
|
#
|
|
13
|
-
#
|
|
14
|
-
class EnumDeepDup
|
|
12
|
+
class EnumDeepDup # :nodoc:
|
|
15
13
|
class << self
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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
|
-
#
|
|
8
|
-
module SeregaUtils
|
|
7
|
+
module SeregaUtils # :nodoc:
|
|
9
8
|
#
|
|
10
|
-
# Utility to freeze nested hashes and
|
|
9
|
+
# Utility to freeze nested hashes, arrays and strings
|
|
11
10
|
#
|
|
12
|
-
#
|
|
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
|
-
#
|
|
8
|
-
module SeregaUtils
|
|
7
|
+
module SeregaUtils # :nodoc:
|
|
9
8
|
#
|
|
10
9
|
# Utility to make method arguments signature
|
|
11
10
|
#
|
|
12
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
9
|
-
#
|
|
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
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
data/lib/serega/utils/to_hash.rb
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Serega
|
|
4
|
-
#
|
|
5
|
-
module SeregaUtils
|
|
4
|
+
module SeregaUtils # :nodoc:
|
|
6
5
|
#
|
|
7
6
|
# Utility to transform almost anything to Hash
|
|
8
7
|
#
|
|
9
|
-
#
|
|
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
|
|
35
|
-
when String then
|
|
36
|
-
when Symbol then
|
|
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
|
-
#
|
|
5
|
-
module SeregaValidations
|
|
4
|
+
module SeregaValidations # :nodoc:
|
|
6
5
|
#
|
|
7
6
|
# Attribute parameters validators
|
|
8
7
|
#
|
|
9
|
-
#
|
|
10
|
-
module Attribute
|
|
8
|
+
module Attribute # :nodoc:
|
|
11
9
|
#
|
|
12
10
|
# Attribute `block` parameter validator
|
|
13
11
|
#
|
|
14
|
-
#
|
|
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
|
|