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
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class Serega
|
|
4
|
-
#
|
|
5
|
-
# Low-level class used by the serializer to construct the serialized response.
|
|
6
|
-
#
|
|
7
|
-
# Serialization is level-by-level. `#serialize` builds the result container(s)
|
|
8
|
-
# and enqueues this level; the batch queue later calls `#process` for each level,
|
|
9
|
-
# which resolves every attribute for every object and enqueues child levels for
|
|
10
|
-
# relations.
|
|
11
|
-
#
|
|
12
|
-
# @private
|
|
13
|
-
class SeregaObjectSerializer
|
|
14
|
-
#
|
|
15
|
-
# SeregaObjectSerializer instance methods
|
|
16
|
-
#
|
|
17
|
-
# @private
|
|
18
|
-
module InstanceMethods
|
|
19
|
-
attr_reader :context, :plan, :many, :opts, :level_queue, :presenter
|
|
20
|
-
|
|
21
|
-
# @param plan [SeregaPlan] Serialization plan
|
|
22
|
-
# @param context [Hash] Serialization context
|
|
23
|
-
# @param many [Boolean] is object is enumerable
|
|
24
|
-
# @param opts [Hash] Any custom options
|
|
25
|
-
#
|
|
26
|
-
# @return [SeregaObjectSerializer] New SeregaObjectSerializer
|
|
27
|
-
def initialize(context:, plan:, many: nil, **opts)
|
|
28
|
-
@context = context
|
|
29
|
-
@plan = plan
|
|
30
|
-
@many = many
|
|
31
|
-
@opts = opts
|
|
32
|
-
@level_queue = opts[:level_queue]
|
|
33
|
-
# Looked up once here and reused for every enqueued chunk of the level.
|
|
34
|
-
@presenter = self.class.serializer_class.presenter
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Enqueues this level and returns its result container(s). The containers are
|
|
38
|
-
# returned immediately but filled in place once the queue is processed.
|
|
39
|
-
#
|
|
40
|
-
# @param object [Object] Serialized object(s)
|
|
41
|
-
#
|
|
42
|
-
# @return [Hash, Array<Hash>, nil] Serialized object(s)
|
|
43
|
-
def serialize(object)
|
|
44
|
-
return if object.nil?
|
|
45
|
-
|
|
46
|
-
case serialize_mode(object)
|
|
47
|
-
when :many then enqueue(object.to_a)
|
|
48
|
-
when :many_for_one then enqueue([object]) # `many` on, but a sole object was given — wrap it, don't raise
|
|
49
|
-
else enqueue([object])[0] # :one
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Resolves every attribute of one level onto its containers. For each point,
|
|
54
|
-
# preloads and batch loaders run once over all of the level's objects; the
|
|
55
|
-
# value is then resolved and assigned per object, in attribute order.
|
|
56
|
-
#
|
|
57
|
-
# @param level [SeregaEngine::Level] level to resolve
|
|
58
|
-
# @return [void]
|
|
59
|
-
def process(level)
|
|
60
|
-
objects = level.objects
|
|
61
|
-
|
|
62
|
-
plan.points.each do |point|
|
|
63
|
-
point.run_preloads(objects)
|
|
64
|
-
batches = point.load_batches(level)
|
|
65
|
-
# One child serializer per relation point per level (reused for every object),
|
|
66
|
-
# instead of one per object — child objects are grouped into one child level anyway.
|
|
67
|
-
child_serializer = point.child_serializer(context: context, **opts)
|
|
68
|
-
|
|
69
|
-
objects.each_with_index do |object, index|
|
|
70
|
-
resolve_point(object, point, level.containers[index], batches, child_serializer)
|
|
71
|
-
rescue => error
|
|
72
|
-
SeregaUtils::SerializedAttributeError.call(error, point)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
private
|
|
78
|
-
|
|
79
|
-
# Enqueues this chunk of objects onto the queue and returns their result
|
|
80
|
-
# containers. This is where objects enter their level, so every object a
|
|
81
|
-
# point resolves against and a batch loader receives has the same shape.
|
|
82
|
-
#
|
|
83
|
-
# Each object is wrapped in the serializer's presenter before it is enqueued,
|
|
84
|
-
# so the whole level — value resolution and batch loaders alike — sees
|
|
85
|
-
# presenters. Serializers without a presenter enqueue the objects as they
|
|
86
|
-
# are — wrapping would only add overhead and break class checks
|
|
87
|
-
# (object.is_a?, Hash === object) without changing anything.
|
|
88
|
-
def enqueue(objects)
|
|
89
|
-
objects = objects.map { |object| presenter.new(object, context) } if presenter
|
|
90
|
-
|
|
91
|
-
level_queue.enqueue(self, objects)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Patched in:
|
|
95
|
-
# - plugin :if (skips the point for objects failing an :if/:unless condition)
|
|
96
|
-
def resolve_point(object, point, container, batches, child_serializer)
|
|
97
|
-
value = point.value(object, context, batches: batches)
|
|
98
|
-
final_value = child_serializer ? child_serializer.serialize(value) : value
|
|
99
|
-
write_value(final_value, point, container)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Patched in:
|
|
103
|
-
# - plugin :if (skips assigning when an :if_value/:unless_value condition fails)
|
|
104
|
-
def write_value(final_value, point, container)
|
|
105
|
-
container[point.name] = final_value
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# How to serialize `object`, deciding whether the result is a collection or a
|
|
109
|
-
# single object and checking the object type only once:
|
|
110
|
-
# - :many — `many` is on and the object is a collection
|
|
111
|
-
# - :many_for_one — `many` is on but a sole object was given (wrap it, don't raise)
|
|
112
|
-
# - :one — serialize the object on its own
|
|
113
|
-
def serialize_mode(object)
|
|
114
|
-
case many
|
|
115
|
-
when NilClass then SeregaUtils::CollectionDetector.call(object) ? :many : :one
|
|
116
|
-
when TrueClass then SeregaUtils::CollectionDetector.call(object) ? :many : :many_for_one
|
|
117
|
-
else :one # many == false
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
extend Serega::SeregaHelpers::SerializerClassHelper
|
|
123
|
-
include InstanceMethods
|
|
124
|
-
end
|
|
125
|
-
end
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class Serega
|
|
4
|
-
module SeregaPlugins
|
|
5
|
-
#
|
|
6
|
-
# Plugin :camel_case
|
|
7
|
-
#
|
|
8
|
-
# CamelCases every attribute name automatically, replacing `_x` with `X`
|
|
9
|
-
# throughout the string. Runs once, when the attribute is defined, not on
|
|
10
|
-
# every serialization.
|
|
11
|
-
#
|
|
12
|
-
# Provide a custom transformation with
|
|
13
|
-
# `plugin :camel_case, transform: ->(name) { name.camelize }`.
|
|
14
|
-
#
|
|
15
|
-
# Skip camelCase for a single attribute with `camel_case: false`.
|
|
16
|
-
#
|
|
17
|
-
# @example Define plugin
|
|
18
|
-
# class AppSerializer < Serega
|
|
19
|
-
# plugin :camel_case
|
|
20
|
-
# end
|
|
21
|
-
#
|
|
22
|
-
# class UserSerializer < AppSerializer
|
|
23
|
-
# attribute :first_name
|
|
24
|
-
# attribute :last_name
|
|
25
|
-
# attribute :full_name, camel_case: false, value: proc { |user| [user.first_name, user.last_name].compact.join(" ") }
|
|
26
|
-
# end
|
|
27
|
-
#
|
|
28
|
-
# require "ostruct"
|
|
29
|
-
# user = OpenStruct.new(first_name: "Bruce", last_name: "Wayne")
|
|
30
|
-
# UserSerializer.to_h(user) # {firstName: "Bruce", lastName: "Wayne", full_name: "Bruce Wayne"}
|
|
31
|
-
#
|
|
32
|
-
module CamelCase
|
|
33
|
-
# Default camel-case transformation
|
|
34
|
-
TRANSFORM_DEFAULT = proc { |attribute_name|
|
|
35
|
-
attribute_name.gsub!(/_[a-z]/) { |m| m[-1].upcase! }
|
|
36
|
-
attribute_name
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
# @return [Symbol] Plugin name
|
|
40
|
-
# @private
|
|
41
|
-
def self.plugin_name
|
|
42
|
-
:camel_case
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Checks requirements to load plugin
|
|
46
|
-
#
|
|
47
|
-
# @param serializer_class [Class<Serega>] Current serializer class
|
|
48
|
-
# @param opts [Hash] plugin options
|
|
49
|
-
#
|
|
50
|
-
# @return [void]
|
|
51
|
-
#
|
|
52
|
-
# @private
|
|
53
|
-
def self.before_load_plugin(serializer_class, **opts)
|
|
54
|
-
allowed_keys = %i[transform]
|
|
55
|
-
opts.each_key do |key|
|
|
56
|
-
next if allowed_keys.include?(key)
|
|
57
|
-
|
|
58
|
-
raise SeregaError,
|
|
59
|
-
"Plugin #{plugin_name.inspect} does not accept the #{key.inspect} option. Allowed options:\n" \
|
|
60
|
-
" - :transform [#call] - Custom transformation"
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
# Applies plugin code to specific serializer
|
|
66
|
-
#
|
|
67
|
-
# @param serializer_class [Class<Serega>] Current serializer class
|
|
68
|
-
# @param _opts [Hash] Plugin options
|
|
69
|
-
#
|
|
70
|
-
# @return [void]
|
|
71
|
-
#
|
|
72
|
-
# @private
|
|
73
|
-
def self.load_plugin(serializer_class, **_opts)
|
|
74
|
-
serializer_class::SeregaConfig.include(ConfigInstanceMethods)
|
|
75
|
-
serializer_class::SeregaAttributeNormalizer.include(AttributeNormalizerInstanceMethods)
|
|
76
|
-
serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
#
|
|
80
|
-
# Adds config options and runs other callbacks after plugin was loaded
|
|
81
|
-
#
|
|
82
|
-
# @param serializer_class [Class<Serega>] Current serializer class
|
|
83
|
-
# @param opts [Hash] Plugin options
|
|
84
|
-
#
|
|
85
|
-
# @return [void]
|
|
86
|
-
#
|
|
87
|
-
# @private
|
|
88
|
-
def self.after_load_plugin(serializer_class, **opts)
|
|
89
|
-
config = serializer_class.config
|
|
90
|
-
config.opts[:camel_case] = {}
|
|
91
|
-
config.camel_case.transform = opts[:transform] || TRANSFORM_DEFAULT
|
|
92
|
-
|
|
93
|
-
config.attribute_keys << :camel_case
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
#
|
|
97
|
-
# Config class additional/patched instance methods
|
|
98
|
-
#
|
|
99
|
-
# @see Serega::SeregaConfig
|
|
100
|
-
#
|
|
101
|
-
module ConfigInstanceMethods
|
|
102
|
-
# @return [Serega::SeregaPlugins::CamelCase::CamelCaseConfig] `camel_case` plugin config
|
|
103
|
-
def camel_case
|
|
104
|
-
@camel_case ||= CamelCaseConfig.new(opts.fetch(:camel_case))
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
#
|
|
109
|
-
# Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
|
|
110
|
-
#
|
|
111
|
-
# @see Serega::SeregaValidations::CheckAttributeParams
|
|
112
|
-
#
|
|
113
|
-
# @private
|
|
114
|
-
module CheckAttributeParamsInstanceMethods
|
|
115
|
-
private
|
|
116
|
-
|
|
117
|
-
def check_opts
|
|
118
|
-
super
|
|
119
|
-
CheckOptCamelCase.call(opts)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
#
|
|
124
|
-
# Validator for attribute :camel_case option
|
|
125
|
-
#
|
|
126
|
-
# @private
|
|
127
|
-
class CheckOptCamelCase
|
|
128
|
-
class << self
|
|
129
|
-
#
|
|
130
|
-
# Checks attribute :camel_case option must be boolean
|
|
131
|
-
#
|
|
132
|
-
# @param opts [Hash] Attribute options
|
|
133
|
-
#
|
|
134
|
-
# @raise [SeregaError] Attribute validation error
|
|
135
|
-
#
|
|
136
|
-
# @return [void]
|
|
137
|
-
#
|
|
138
|
-
def call(opts)
|
|
139
|
-
camel_case_option_exists = opts.key?(:camel_case)
|
|
140
|
-
return unless camel_case_option_exists
|
|
141
|
-
|
|
142
|
-
value = opts[:camel_case]
|
|
143
|
-
return if value.equal?(true) || value.equal?(false)
|
|
144
|
-
|
|
145
|
-
raise SeregaError, "Attribute option :camel_case must have a boolean value, but #{value.class} was provided"
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# CamelCase config object
|
|
151
|
-
class CamelCaseConfig
|
|
152
|
-
attr_reader :opts
|
|
153
|
-
|
|
154
|
-
#
|
|
155
|
-
# Initializes CamelCaseConfig object
|
|
156
|
-
#
|
|
157
|
-
# @param opts [Hash] camel_case plugin options
|
|
158
|
-
# @option opts [#call] :transform Callable object that transforms original attribute name
|
|
159
|
-
#
|
|
160
|
-
# @return [Serega::SeregaPlugins::CamelCase::CamelCaseConfig] CamelCaseConfig object
|
|
161
|
-
#
|
|
162
|
-
def initialize(opts)
|
|
163
|
-
@opts = opts
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# @return [#call] defined object that transforms name
|
|
167
|
-
def transform
|
|
168
|
-
opts.fetch(:transform)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Sets transformation callable object
|
|
172
|
-
#
|
|
173
|
-
# @param value [#call] transformation
|
|
174
|
-
#
|
|
175
|
-
# @return [#call] camel_case plugin transformation callable object
|
|
176
|
-
def transform=(value)
|
|
177
|
-
raise SeregaError, "Transform value must respond to #call" unless value.respond_to?(:call)
|
|
178
|
-
|
|
179
|
-
params = value.is_a?(Proc) ? value.parameters : value.method(:call).parameters
|
|
180
|
-
if params.count != 1 || !params.all? { |param| (param[0] == :req) || (param[0] == :opt) }
|
|
181
|
-
raise SeregaError, "Transform value must respond to #call and accept 1 regular parameter"
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
opts[:transform] = value
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
#
|
|
189
|
-
# SeregaAttributeNormalizer additional/patched instance methods
|
|
190
|
-
#
|
|
191
|
-
# @see SeregaAttributeNormalizer::AttributeInstanceMethods
|
|
192
|
-
#
|
|
193
|
-
# @private
|
|
194
|
-
module AttributeNormalizerInstanceMethods
|
|
195
|
-
private
|
|
196
|
-
|
|
197
|
-
#
|
|
198
|
-
# Patch for original `prepare_name` method
|
|
199
|
-
#
|
|
200
|
-
# Makes camelCased name
|
|
201
|
-
#
|
|
202
|
-
def prepare_name
|
|
203
|
-
res = super
|
|
204
|
-
return res if init_opts[:camel_case] == false
|
|
205
|
-
|
|
206
|
-
self.class.serializer_class.config.camel_case.transform.call(+res.to_s).to_sym
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
register_plugin(CamelCase.plugin_name, CamelCase)
|
|
212
|
-
end
|
|
213
|
-
end
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class Serega
|
|
4
|
-
module SeregaPlugins
|
|
5
|
-
#
|
|
6
|
-
# Plugin :depth_limit
|
|
7
|
-
#
|
|
8
|
-
# Guards against malicious queries that serialize too much, or against
|
|
9
|
-
# accidentally serializing objects with cyclic relations.
|
|
10
|
-
#
|
|
11
|
-
# Depth limit is checked when a serialization plan is built, i.e. when
|
|
12
|
-
# `#new` is called (`SomeSerializer.new(with: params[:with])`) — including
|
|
13
|
-
# internally by every class-level serialization method. Instantiating the
|
|
14
|
-
# serializer early surfaces depth errors sooner.
|
|
15
|
-
#
|
|
16
|
-
# When the limit is exceeded, `Serega::DepthLimitError` is raised;
|
|
17
|
-
# details are available via `Serega::DepthLimitError#details`.
|
|
18
|
-
#
|
|
19
|
-
# Limit can be checked or changed with config options:
|
|
20
|
-
#
|
|
21
|
-
# - config.depth_limit.limit
|
|
22
|
-
# - config.depth_limit.limit=
|
|
23
|
-
#
|
|
24
|
-
# There is no default limit — it must be set when enabling the plugin.
|
|
25
|
-
#
|
|
26
|
-
# @example
|
|
27
|
-
#
|
|
28
|
-
# class AppSerializer < Serega
|
|
29
|
-
# plugin :depth_limit, limit: 10 # set limit for all child classes
|
|
30
|
-
# end
|
|
31
|
-
#
|
|
32
|
-
# class UserSerializer < AppSerializer
|
|
33
|
-
# config.depth_limit.limit = 5 # overrides limit for UserSerializer
|
|
34
|
-
# end
|
|
35
|
-
#
|
|
36
|
-
module DepthLimit
|
|
37
|
-
# @return [Symbol] Plugin name
|
|
38
|
-
# @private
|
|
39
|
-
def self.plugin_name
|
|
40
|
-
:depth_limit
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Checks requirements
|
|
44
|
-
#
|
|
45
|
-
# @param serializer_class [Class<Serega>] Current serializer class
|
|
46
|
-
# @param opts [Hash] Plugin options
|
|
47
|
-
#
|
|
48
|
-
# @return [void]
|
|
49
|
-
#
|
|
50
|
-
# @private
|
|
51
|
-
def self.before_load_plugin(serializer_class, **opts)
|
|
52
|
-
allowed_keys = %i[limit]
|
|
53
|
-
opts.each_key do |key|
|
|
54
|
-
next if allowed_keys.include?(key)
|
|
55
|
-
|
|
56
|
-
raise SeregaError,
|
|
57
|
-
"Plugin #{plugin_name.inspect} does not accept the #{key.inspect} option. Allowed options:\n" \
|
|
58
|
-
" - :limit [Integer] - Maximum serialization depth."
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
#
|
|
63
|
-
# Applies plugin code to specific serializer
|
|
64
|
-
#
|
|
65
|
-
# @param serializer_class [Class<Serega>] Current serializer class
|
|
66
|
-
# @param _opts [Hash] Plugin options
|
|
67
|
-
#
|
|
68
|
-
# @return [void]
|
|
69
|
-
#
|
|
70
|
-
# @private
|
|
71
|
-
def self.load_plugin(serializer_class, **_opts)
|
|
72
|
-
serializer_class::SeregaPlan.include(PlanInstanceMethods)
|
|
73
|
-
serializer_class::SeregaConfig.include(ConfigInstanceMethods)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
#
|
|
77
|
-
# Adds config options and runs other callbacks after plugin was loaded
|
|
78
|
-
#
|
|
79
|
-
# @param serializer_class [Class<Serega>] Current serializer class
|
|
80
|
-
# @param opts [Hash] Plugin options
|
|
81
|
-
#
|
|
82
|
-
# @return [void]
|
|
83
|
-
#
|
|
84
|
-
# @private
|
|
85
|
-
def self.after_load_plugin(serializer_class, **opts)
|
|
86
|
-
config = serializer_class.config
|
|
87
|
-
limit = opts.fetch(:limit) { raise SeregaError, "Please provide :limit option. Example: `plugin :depth_limit, limit: 10`" }
|
|
88
|
-
config.opts[:depth_limit] = {}
|
|
89
|
-
config.depth_limit.limit = limit
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# DepthLimit config object
|
|
93
|
-
class DepthLimitConfig
|
|
94
|
-
attr_reader :opts
|
|
95
|
-
|
|
96
|
-
#
|
|
97
|
-
# Initializes DepthLimitConfig object
|
|
98
|
-
#
|
|
99
|
-
# @param opts [Hash] depth_limit plugin options
|
|
100
|
-
#
|
|
101
|
-
# @return [SeregaPlugins::DepthLimit::DepthLimitConfig] DepthLimitConfig object
|
|
102
|
-
#
|
|
103
|
-
def initialize(opts)
|
|
104
|
-
@opts = opts
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# @return [Integer] defined depth limit
|
|
108
|
-
def limit
|
|
109
|
-
opts.fetch(:limit)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
#
|
|
113
|
-
# Set depth limit
|
|
114
|
-
#
|
|
115
|
-
# @param value [Integer] depth limit
|
|
116
|
-
#
|
|
117
|
-
# @return [Integer] depth limit
|
|
118
|
-
def limit=(value)
|
|
119
|
-
raise SeregaError, "Depth limit must be an Integer" unless value.is_a?(Integer)
|
|
120
|
-
|
|
121
|
-
opts[:limit] = value
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
#
|
|
126
|
-
# Serega::SeregaConfig additional/patched class methods
|
|
127
|
-
#
|
|
128
|
-
# @see Serega::SeregaConfig
|
|
129
|
-
#
|
|
130
|
-
module ConfigInstanceMethods
|
|
131
|
-
# @return [Serega::SeregaPlugins::DepthLimit::DepthLimitConfig] current depth_limit config
|
|
132
|
-
def depth_limit
|
|
133
|
-
@depth_limit ||= DepthLimitConfig.new(opts.fetch(:depth_limit))
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
#
|
|
138
|
-
# SeregaPlan additional/patched instance methods
|
|
139
|
-
#
|
|
140
|
-
# @see SeregaPlan
|
|
141
|
-
#
|
|
142
|
-
# @private
|
|
143
|
-
module PlanInstanceMethods
|
|
144
|
-
#
|
|
145
|
-
# Initializes serialization plan
|
|
146
|
-
# Overrides original method (adds depth_limit validation)
|
|
147
|
-
#
|
|
148
|
-
def initialize(parent_plan_point, *)
|
|
149
|
-
check_depth_limit_exceeded(parent_plan_point)
|
|
150
|
-
super
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
private
|
|
154
|
-
|
|
155
|
-
def check_depth_limit_exceeded(current_point)
|
|
156
|
-
plan = self
|
|
157
|
-
depth_level = 1
|
|
158
|
-
point = current_point
|
|
159
|
-
|
|
160
|
-
while point
|
|
161
|
-
depth_level += 1
|
|
162
|
-
plan = point.plan
|
|
163
|
-
point = plan.parent_plan_point
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
root_serializer = plan.class.serializer_class
|
|
167
|
-
root_depth_limit = root_serializer.config.depth_limit.limit
|
|
168
|
-
|
|
169
|
-
if depth_level > root_depth_limit
|
|
170
|
-
fields_chain = [current_point.name]
|
|
171
|
-
fields_chain << current_point.name while (current_point = current_point.plan.parent_plan_point)
|
|
172
|
-
details = "#{root_serializer} (depth limit: #{root_depth_limit}) -> #{fields_chain.reverse!.join(" -> ")}"
|
|
173
|
-
raise DepthLimitError.new("Depth limit was exceeded", details)
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
register_plugin(DepthLimit.plugin_name, DepthLimit)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
#
|
|
183
|
-
# Special error for depth_limit plugin
|
|
184
|
-
#
|
|
185
|
-
class DepthLimitError < SeregaError
|
|
186
|
-
#
|
|
187
|
-
# Details of why depth limit error happens.
|
|
188
|
-
#
|
|
189
|
-
# @return [String] error details
|
|
190
|
-
#
|
|
191
|
-
attr_reader :details
|
|
192
|
-
|
|
193
|
-
#
|
|
194
|
-
# Initializes new error
|
|
195
|
-
#
|
|
196
|
-
# @param message [String] Error message
|
|
197
|
-
# @param details [String] Error additional details
|
|
198
|
-
#
|
|
199
|
-
# @return [DepthLimitError] error instance
|
|
200
|
-
#
|
|
201
|
-
def initialize(message, details = nil)
|
|
202
|
-
super(message)
|
|
203
|
-
@details = details
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
end
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class Serega
|
|
4
|
-
module SeregaPlugins
|
|
5
|
-
#
|
|
6
|
-
# Plugin :explicit_many_option
|
|
7
|
-
#
|
|
8
|
-
# Requires the `:many` option on every relationship attribute (an
|
|
9
|
-
# attribute with the `:serializer` option or a block defining a nested
|
|
10
|
-
# serializer), so it's always explicit whether it returns one object or many.
|
|
11
|
-
#
|
|
12
|
-
# @example
|
|
13
|
-
# class BaseSerializer < Serega
|
|
14
|
-
# plugin :explicit_many_option
|
|
15
|
-
# config.base_serializer = self
|
|
16
|
-
# end
|
|
17
|
-
#
|
|
18
|
-
# class PostSerializer < BaseSerializer
|
|
19
|
-
# attribute :text
|
|
20
|
-
#
|
|
21
|
-
# attribute :user, many: false do
|
|
22
|
-
# attribute :name
|
|
23
|
-
# end
|
|
24
|
-
#
|
|
25
|
-
# attribute :comments, serializer: PostSerializer, many: true
|
|
26
|
-
# end
|
|
27
|
-
#
|
|
28
|
-
module ExplicitManyOption
|
|
29
|
-
# @return [Symbol] Plugin name
|
|
30
|
-
# @private
|
|
31
|
-
def self.plugin_name
|
|
32
|
-
:explicit_many_option
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
# Applies plugin code to specific serializer
|
|
37
|
-
#
|
|
38
|
-
# @param serializer_class [Class<Serega>] Current serializer class
|
|
39
|
-
# @param _opts [Hash] Plugin options
|
|
40
|
-
#
|
|
41
|
-
# @return [void]
|
|
42
|
-
#
|
|
43
|
-
# @private
|
|
44
|
-
def self.load_plugin(serializer_class, **_opts)
|
|
45
|
-
require_relative "validations/check_opt_many"
|
|
46
|
-
|
|
47
|
-
serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
# Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
|
|
52
|
-
#
|
|
53
|
-
# @see Serega::SeregaValidations::CheckAttributeParams
|
|
54
|
-
#
|
|
55
|
-
# @private
|
|
56
|
-
module CheckAttributeParamsInstanceMethods
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
def check_opts
|
|
60
|
-
super
|
|
61
|
-
|
|
62
|
-
CheckOptMany.call(opts, block)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
register_plugin(ExplicitManyOption.plugin_name, ExplicitManyOption)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class Serega
|
|
4
|
-
module SeregaPlugins
|
|
5
|
-
module ExplicitManyOption
|
|
6
|
-
#
|
|
7
|
-
# Validator for attribute :many option
|
|
8
|
-
#
|
|
9
|
-
# @private
|
|
10
|
-
class CheckOptMany
|
|
11
|
-
class << self
|
|
12
|
-
#
|
|
13
|
-
# Checks attribute :many option must be provided with relations
|
|
14
|
-
# (attributes with the :serializer option or a block defining
|
|
15
|
-
# a nested serializer)
|
|
16
|
-
#
|
|
17
|
-
# @param opts [Hash] Attribute options
|
|
18
|
-
# @param block [nil, Proc] Attribute block
|
|
19
|
-
#
|
|
20
|
-
# @raise [SeregaError] Attribute validation error
|
|
21
|
-
#
|
|
22
|
-
# @return [void]
|
|
23
|
-
#
|
|
24
|
-
def call(opts, block = nil)
|
|
25
|
-
return if !opts[:serializer] && !block
|
|
26
|
-
|
|
27
|
-
many_option_exists = opts.key?(:many)
|
|
28
|
-
return if many_option_exists
|
|
29
|
-
|
|
30
|
-
raise SeregaError,
|
|
31
|
-
"Attribute option :many [Boolean] must be provided" \
|
|
32
|
-
" for attributes with :serializer option or a block"
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|