serega 0.35.0 → 0.37.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 +74 -79
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +0 -5
- data/lib/serega/attribute_normalizer.rb +28 -70
- data/lib/serega/batch/attribute_loader.rb +35 -20
- data/lib/serega/batch/attribute_loaders.rb +65 -16
- data/lib/serega/batch/level.rb +48 -0
- data/lib/serega/config.rb +8 -10
- data/lib/serega/data_builder.rb +52 -0
- data/lib/serega/object_serializer.rb +7 -1
- data/lib/serega/plan.rb +33 -10
- data/lib/serega/plan_point.rb +0 -19
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +9 -54
- data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +1 -1
- data/lib/serega/plugins/if/if.rb +23 -0
- data/lib/serega/plugins/if/validations/check_opt_if.rb +7 -4
- data/lib/serega/plugins/if/validations/check_opt_if_value.rb +7 -4
- data/lib/serega/plugins/if/validations/check_opt_unless.rb +7 -4
- data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +7 -4
- data/lib/serega/plugins/root/root.rb +80 -0
- data/lib/serega/validations/attribute/check_opt_preload.rb +11 -0
- data/lib/serega/validations/check_attribute_params.rb +0 -1
- data/lib/serega.rb +108 -28
- metadata +5 -8
- data/lib/serega/plugins/activerecord_preloads/lib/active_record_objects.rb +0 -31
- data/lib/serega/utils/format_user_preloads.rb +0 -58
- data/lib/serega/utils/preload_paths.rb +0 -53
- data/lib/serega/utils/preloads_constructor.rb +0 -77
- data/lib/serega/validations/attribute/check_opt_preload_path.rb +0 -89
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Batch feature main module
|
|
6
|
+
#
|
|
7
|
+
module SeregaBatch
|
|
8
|
+
#
|
|
9
|
+
# Objects serialized at one plan level, together with their loaded batch results.
|
|
10
|
+
#
|
|
11
|
+
# Every attribute serialized at the same level shares one Level, so a named batch
|
|
12
|
+
# loader reused by several of them runs once for the whole set of objects. The same
|
|
13
|
+
# loader over a different level (deeper nesting) uses a different Level and loads
|
|
14
|
+
# again.
|
|
15
|
+
#
|
|
16
|
+
class Level
|
|
17
|
+
# @return [Array] Objects gathered for this level
|
|
18
|
+
attr_reader :objects
|
|
19
|
+
|
|
20
|
+
# @param context [Hash] Serialization context
|
|
21
|
+
def initialize(context)
|
|
22
|
+
@context = context
|
|
23
|
+
@objects = []
|
|
24
|
+
@results = {}.compare_by_identity
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Adds a chunk of serialized objects to this level.
|
|
28
|
+
# @param objects [Array] Objects being serialized at this level
|
|
29
|
+
# @return [void]
|
|
30
|
+
def add_objects(objects)
|
|
31
|
+
@objects.concat(objects)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Loads and returns values for the given batch loader, once per loader.
|
|
35
|
+
#
|
|
36
|
+
# Freezes the gathered objects on the first load: all of a level's objects
|
|
37
|
+
# are added before any of its batches load, so sealing the set here guards
|
|
38
|
+
# against a later stray `add_objects` and against a loader mutating it.
|
|
39
|
+
#
|
|
40
|
+
# @param loader [SeregaBatch::Loader] Named batch loader
|
|
41
|
+
# @return [Object] Loaded values
|
|
42
|
+
def load(loader)
|
|
43
|
+
@objects.freeze
|
|
44
|
+
@results[loader] ||= loader.load(@objects, @context)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/serega/config.rb
CHANGED
|
@@ -23,7 +23,6 @@ class Serega
|
|
|
23
23
|
delegate
|
|
24
24
|
default
|
|
25
25
|
preload
|
|
26
|
-
preload_path
|
|
27
26
|
batch
|
|
28
27
|
].freeze,
|
|
29
28
|
serialize_keys: %i[context many].freeze,
|
|
@@ -116,27 +115,26 @@ class Serega
|
|
|
116
115
|
end
|
|
117
116
|
|
|
118
117
|
# Returns :hide_by_default config option
|
|
119
|
-
# @return [Boolean,
|
|
118
|
+
# @return [Boolean, Symbol] Current :hide_by_default config option
|
|
120
119
|
def hide_by_default
|
|
121
120
|
opts.fetch(:hide_by_default)
|
|
122
121
|
end
|
|
123
122
|
|
|
124
123
|
# Sets :hide_by_default config option
|
|
125
124
|
#
|
|
126
|
-
# @param value [Boolean,
|
|
125
|
+
# @param value [Boolean, Symbol] Accepted values:
|
|
126
|
+
# - false (default) — nothing is hidden by default
|
|
127
|
+
# - true — all attributes are hidden by default
|
|
128
|
+
# - :auto — hides attributes that declare :preload or :batch
|
|
127
129
|
#
|
|
128
|
-
# @return [Boolean,
|
|
130
|
+
# @return [Boolean, Symbol] New :hide_by_default config option
|
|
129
131
|
def hide_by_default=(value)
|
|
130
132
|
opts[:hide_by_default] =
|
|
131
133
|
case value
|
|
132
|
-
when true, false
|
|
133
|
-
value
|
|
134
|
-
when Array
|
|
135
|
-
invalid = value - %i[preload batch]
|
|
136
|
-
raise SeregaError, "Must have true, false, or an Array of [:preload, :batch], #{value.inspect} provided" if invalid.any?
|
|
134
|
+
when true, false, :auto
|
|
137
135
|
value
|
|
138
136
|
else
|
|
139
|
-
raise SeregaError, "Must have true, false, or
|
|
137
|
+
raise SeregaError, "Must have true, false, or :auto, #{value.inspect} provided"
|
|
140
138
|
end
|
|
141
139
|
end
|
|
142
140
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Builds Data objects from serialized hashes.
|
|
6
|
+
# Used by `#to_data` / `.to_data`.
|
|
7
|
+
#
|
|
8
|
+
class SeregaDataBuilder
|
|
9
|
+
# SeregaDataBuilder class methods
|
|
10
|
+
module SeregaDataBuilderClassMethods
|
|
11
|
+
#
|
|
12
|
+
# Converts serialized Hash/Array result into a tree of Ruby Data objects
|
|
13
|
+
# following the serializer's plan structure.
|
|
14
|
+
#
|
|
15
|
+
# @param serializer [Serega] Serializer instance carrying the plan
|
|
16
|
+
# @param serialized [Hash, Array, nil] Serialized output from `#to_h`
|
|
17
|
+
#
|
|
18
|
+
# @return [Data, Array<Data>, nil] Serialization result as Data object(s)
|
|
19
|
+
#
|
|
20
|
+
def call(serializer, serialized)
|
|
21
|
+
build(serialized, serializer.plan)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def build(serialized, plan)
|
|
27
|
+
case serialized
|
|
28
|
+
when Array then serialized.map { |item| hash_to_data(item, plan) }
|
|
29
|
+
when Hash then hash_to_data(serialized, plan)
|
|
30
|
+
else serialized
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def hash_to_data(hash, plan)
|
|
35
|
+
hash_data = hash.to_h do |key, value|
|
|
36
|
+
child_plan = plan.points_hash.fetch(key).child_plan
|
|
37
|
+
value = build(value, child_plan) if child_plan
|
|
38
|
+
[key, value]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
build_data_object(plan, hash_data)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_data_object(plan, hash_data)
|
|
45
|
+
plan.data_class.new(**hash_data)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
extend SeregaHelpers::SerializerClassHelper
|
|
50
|
+
extend SeregaDataBuilderClassMethods
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -34,7 +34,13 @@ class Serega
|
|
|
34
34
|
def serialize(object)
|
|
35
35
|
return if object.nil?
|
|
36
36
|
|
|
37
|
-
array?(object, many)
|
|
37
|
+
if array?(object, many)
|
|
38
|
+
batch_loaders.add_objects(plan, object.to_a) if plan.batch?
|
|
39
|
+
serialize_array(object)
|
|
40
|
+
else
|
|
41
|
+
batch_loaders.add_objects(plan, [object]) if plan.batch?
|
|
42
|
+
serialize_object(object)
|
|
43
|
+
end
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
private
|
data/lib/serega/plan.rb
CHANGED
|
@@ -27,6 +27,18 @@ class Serega
|
|
|
27
27
|
cached_plan_for(opts, max_cache_size)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
#
|
|
31
|
+
# Returns (and caches) the Data class for the given set of field names.
|
|
32
|
+
# Uses the Array as cache key so the same Data class is reused across
|
|
33
|
+
# all plan instances with identical fields.
|
|
34
|
+
#
|
|
35
|
+
# @param point_names [Array<Symbol>] Attribute names for the Data members
|
|
36
|
+
# @return [Class] Subclass of Data
|
|
37
|
+
#
|
|
38
|
+
def data_class_for(point_names)
|
|
39
|
+
(@data_classes ||= {})[point_names] ||= Data.define(*point_names)
|
|
40
|
+
end
|
|
41
|
+
|
|
30
42
|
private
|
|
31
43
|
|
|
32
44
|
def cached_plan_for(opts, max_cache_size)
|
|
@@ -65,9 +77,9 @@ class Serega
|
|
|
65
77
|
# @return [Array<SeregaPlanPoint>] points to serialize
|
|
66
78
|
attr_reader :points
|
|
67
79
|
|
|
68
|
-
#
|
|
69
|
-
# @return [
|
|
70
|
-
attr_reader :
|
|
80
|
+
# Named serialization points
|
|
81
|
+
# @return [Hash] Named points
|
|
82
|
+
attr_reader :points_hash
|
|
71
83
|
|
|
72
84
|
#
|
|
73
85
|
# Instantiate new serialization plan
|
|
@@ -84,9 +96,9 @@ class Serega
|
|
|
84
96
|
# @return [SeregaPlan] Serialization plan
|
|
85
97
|
#
|
|
86
98
|
def initialize(parent_plan_point, modifiers)
|
|
87
|
-
@has_batch_points = false # should be before assigning points, generated points can change this attribute
|
|
88
99
|
@parent_plan_point = parent_plan_point
|
|
89
100
|
@points = attributes_points(modifiers)
|
|
101
|
+
@points_hash = points.to_h { |point| [point.name, point] }
|
|
90
102
|
end
|
|
91
103
|
|
|
92
104
|
#
|
|
@@ -97,14 +109,21 @@ class Serega
|
|
|
97
109
|
end
|
|
98
110
|
|
|
99
111
|
#
|
|
100
|
-
#
|
|
101
|
-
# It is needed to initialize Batch Attribute Loaders when serialization starts
|
|
112
|
+
# @return [Boolean] whether any point of this plan is batch loaded
|
|
102
113
|
#
|
|
103
|
-
def
|
|
104
|
-
return if
|
|
114
|
+
def batch?
|
|
115
|
+
return @batch if defined?(@batch)
|
|
105
116
|
|
|
106
|
-
@
|
|
107
|
-
|
|
117
|
+
@batch = points.any?(&:batch?)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns the Data class whose members match this plan's serialized fields.
|
|
121
|
+
# Delegates to the class-level cache so identical field sets share one Data class.
|
|
122
|
+
#
|
|
123
|
+
# @return [Class] Subclass of Data
|
|
124
|
+
#
|
|
125
|
+
def data_class
|
|
126
|
+
@data_class ||= self.class.data_class_for(point_names)
|
|
108
127
|
end
|
|
109
128
|
|
|
110
129
|
private
|
|
@@ -130,6 +149,10 @@ class Serega
|
|
|
130
149
|
|
|
131
150
|
points.freeze
|
|
132
151
|
end
|
|
152
|
+
|
|
153
|
+
def point_names
|
|
154
|
+
@point_names ||= points.map(&:name)
|
|
155
|
+
end
|
|
133
156
|
end
|
|
134
157
|
|
|
135
158
|
extend ClassMethods
|
data/lib/serega/plan_point.rb
CHANGED
|
@@ -25,14 +25,6 @@ class Serega
|
|
|
25
25
|
# @return [Hash] Attributes to serialize
|
|
26
26
|
attr_reader :modifiers
|
|
27
27
|
|
|
28
|
-
# Shows preloads for nested attributes
|
|
29
|
-
# @return [Hash] preloads for nested attributes
|
|
30
|
-
attr_reader :preloads
|
|
31
|
-
|
|
32
|
-
# Shows preloads_path for current attribute
|
|
33
|
-
# @return [Array<Symbol>, nil] preloads path for current attribute
|
|
34
|
-
attr_reader :preloads_path
|
|
35
|
-
|
|
36
28
|
#
|
|
37
29
|
# Initializes plan point
|
|
38
30
|
#
|
|
@@ -97,9 +89,6 @@ class Serega
|
|
|
97
89
|
|
|
98
90
|
def set_normalized_vars
|
|
99
91
|
@child_plan = prepare_child_plan
|
|
100
|
-
@preloads = prepare_preloads
|
|
101
|
-
@preloads_path = prepare_preloads_path
|
|
102
|
-
plan.mark_as_has_batch_points if batch?
|
|
103
92
|
end
|
|
104
93
|
|
|
105
94
|
def prepare_child_plan
|
|
@@ -109,14 +98,6 @@ class Serega
|
|
|
109
98
|
|
|
110
99
|
serializer::SeregaPlan.new(self, fields)
|
|
111
100
|
end
|
|
112
|
-
|
|
113
|
-
def prepare_preloads
|
|
114
|
-
SeregaUtils::PreloadsConstructor.call(child_plan)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def prepare_preloads_path
|
|
118
|
-
attribute.preloads_path
|
|
119
|
-
end
|
|
120
101
|
end
|
|
121
102
|
|
|
122
103
|
extend SeregaHelpers::SerializerClassHelper
|
|
@@ -7,9 +7,8 @@ class Serega
|
|
|
7
7
|
#
|
|
8
8
|
# Automatically preloads associations to serialized objects
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# to preload all associations.
|
|
10
|
+
# Every association declared with `:preload` is loaded once during serialization using
|
|
11
|
+
# ActiveRecord::Associations::Preloader, so there are no N+1 queries.
|
|
13
12
|
#
|
|
14
13
|
# @example
|
|
15
14
|
# class AppSerializer < Serega
|
|
@@ -39,7 +38,7 @@ class Serega
|
|
|
39
38
|
# attribute :downloads_count, preload: :downloads, value: proc { |album| album.downloads.count }
|
|
40
39
|
# end
|
|
41
40
|
#
|
|
42
|
-
# UserSerializer.to_h(user)
|
|
41
|
+
# UserSerializer.to_h(user)
|
|
43
42
|
#
|
|
44
43
|
module ActiverecordPreloads
|
|
45
44
|
#
|
|
@@ -72,62 +71,18 @@ class Serega
|
|
|
72
71
|
#
|
|
73
72
|
def self.load_plugin(serializer_class, **_opts)
|
|
74
73
|
require_relative "lib/preloader"
|
|
75
|
-
require_relative "lib/active_record_objects"
|
|
76
|
-
|
|
77
|
-
serializer_class.include(InstanceMethods)
|
|
78
|
-
serializer_class::SeregaBatchAttributeLoader.include(BatchAttributeLoaderInstanceMethods)
|
|
79
74
|
end
|
|
80
75
|
|
|
81
76
|
#
|
|
82
|
-
#
|
|
77
|
+
# Registers the ActiveRecord preload handler
|
|
83
78
|
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# Preloads associations to batch-generated data
|
|
88
|
-
def load_one(serializer_class, batch_loader_name, context)
|
|
89
|
-
batch_loaded_data = super
|
|
90
|
-
|
|
91
|
-
preloads = point.preloads
|
|
92
|
-
return batch_loaded_data if preloads.empty?
|
|
93
|
-
|
|
94
|
-
ar_objects = ActiveRecordObjects.call(batch_loaded_data)
|
|
95
|
-
Preloader.preload(ar_objects, preloads)
|
|
96
|
-
|
|
97
|
-
batch_loaded_data
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
79
|
+
# @param serializer_class [Class<Serega>] Current serializer class
|
|
80
|
+
# @param _opts [Hash] Plugin options
|
|
101
81
|
#
|
|
102
|
-
#
|
|
82
|
+
# @return [void]
|
|
103
83
|
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# Preloads associations to object
|
|
107
|
-
#
|
|
108
|
-
# @param object [Object] Any object
|
|
109
|
-
# @return provided object
|
|
110
|
-
#
|
|
111
|
-
def preload_associations_to(object)
|
|
112
|
-
return object if object.nil? || (object.is_a?(Array) && object.empty?)
|
|
113
|
-
|
|
114
|
-
preloads = preloads() # `preloads()` method comes from :preloads plugin
|
|
115
|
-
return object if preloads.empty?
|
|
116
|
-
|
|
117
|
-
Preloader.preload(object, preloads)
|
|
118
|
-
object
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
private
|
|
122
|
-
|
|
123
|
-
#
|
|
124
|
-
# Override original #serialize method
|
|
125
|
-
# Preloads associations to object before serialization
|
|
126
|
-
#
|
|
127
|
-
def serialize(object, _opts)
|
|
128
|
-
preload_associations_to(object)
|
|
129
|
-
super
|
|
130
|
-
end
|
|
84
|
+
def self.after_load_plugin(serializer_class, **_opts)
|
|
85
|
+
serializer_class.preload_with { |objects, preloads| Preloader.preload(objects, preloads) }
|
|
131
86
|
end
|
|
132
87
|
end
|
|
133
88
|
|
|
@@ -14,7 +14,7 @@ class Serega
|
|
|
14
14
|
# @return [Object] provided object with preloaded associations
|
|
15
15
|
#
|
|
16
16
|
def preload(object, preloads)
|
|
17
|
-
return object if object.nil? || (object.is_a?(Array) && object.empty?)
|
|
17
|
+
return object if object.nil? || (object.is_a?(Array) && object.empty?)
|
|
18
18
|
|
|
19
19
|
preload_handler = handlers.find { |handler| handler.fit?(object) }
|
|
20
20
|
raise SeregaError, "Can't preload #{preloads.inspect} to #{object.inspect}" unless preload_handler
|
data/lib/serega/plugins/if/if.rb
CHANGED
|
@@ -69,6 +69,7 @@ class Serega
|
|
|
69
69
|
serializer_class::SeregaPlanPoint.include(PlanPointInstanceMethods)
|
|
70
70
|
serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
|
|
71
71
|
serializer_class::SeregaObjectSerializer.include(ObjectSerializerInstanceMethods)
|
|
72
|
+
serializer_class::SeregaDataBuilder.extend(DataBuilderClassMethods)
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
#
|
|
@@ -215,6 +216,7 @@ class Serega
|
|
|
215
216
|
when "1" then condition.call(object)
|
|
216
217
|
when "2" then condition.call(object, context)
|
|
217
218
|
when "1_ctx" then condition.call(object, ctx: context)
|
|
219
|
+
when "2_ctx" then condition.call(object, context, ctx: context)
|
|
218
220
|
else # "0"
|
|
219
221
|
condition.call
|
|
220
222
|
end
|
|
@@ -239,6 +241,27 @@ class Serega
|
|
|
239
241
|
end
|
|
240
242
|
end
|
|
241
243
|
|
|
244
|
+
#
|
|
245
|
+
# SeregaDataBuilder additional/patched class methods
|
|
246
|
+
#
|
|
247
|
+
# Overrides `build_data_object` to handle plans where some keys were skipped
|
|
248
|
+
# by `:if`/`:unless`/`:if_value`/`:unless_value` conditions, so the Data class
|
|
249
|
+
# is built from the actually-present keys rather than the full plan.
|
|
250
|
+
#
|
|
251
|
+
# @see Serega::SeregaDataBuilder
|
|
252
|
+
#
|
|
253
|
+
module DataBuilderClassMethods
|
|
254
|
+
private
|
|
255
|
+
|
|
256
|
+
def build_data_object(plan, hash_data)
|
|
257
|
+
if hash_data.size < plan.points.size
|
|
258
|
+
plan.class.data_class_for(hash_data.keys).new(**hash_data)
|
|
259
|
+
else
|
|
260
|
+
super
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
242
265
|
#
|
|
243
266
|
# SeregaObjectSerializer additional/patched class methods
|
|
244
267
|
#
|
|
@@ -47,6 +47,8 @@ class Serega
|
|
|
47
47
|
true
|
|
48
48
|
when "1_ctx" # (object, :ctx)
|
|
49
49
|
true
|
|
50
|
+
when "2_ctx" # (object, context, :ctx)
|
|
51
|
+
true
|
|
50
52
|
else
|
|
51
53
|
false
|
|
52
54
|
end
|
|
@@ -55,10 +57,11 @@ class Serega
|
|
|
55
57
|
def signature_error
|
|
56
58
|
<<~ERROR.strip
|
|
57
59
|
Invalid attribute option :if parameters, valid parameters signatures:
|
|
58
|
-
- ()
|
|
59
|
-
- (object)
|
|
60
|
-
- (object, context)
|
|
61
|
-
- (object, :ctx)
|
|
60
|
+
- () # no parameters
|
|
61
|
+
- (object) # one positional parameter
|
|
62
|
+
- (object, context) # two positional parameters
|
|
63
|
+
- (object, :ctx) # one positional parameter and :ctx keyword
|
|
64
|
+
- (object, context, :ctx) # two positional parameters and :ctx keyword
|
|
62
65
|
ERROR
|
|
63
66
|
end
|
|
64
67
|
end
|
|
@@ -52,6 +52,8 @@ class Serega
|
|
|
52
52
|
true
|
|
53
53
|
when "1_ctx" # (value, :ctx)
|
|
54
54
|
true
|
|
55
|
+
when "2_ctx" # (value, context, :ctx)
|
|
56
|
+
true
|
|
55
57
|
else
|
|
56
58
|
false
|
|
57
59
|
end
|
|
@@ -60,10 +62,11 @@ class Serega
|
|
|
60
62
|
def signature_error
|
|
61
63
|
<<~ERROR.strip
|
|
62
64
|
Invalid attribute option :if_value parameters, valid parameters signatures:
|
|
63
|
-
- ()
|
|
64
|
-
- (value)
|
|
65
|
-
- (value, context)
|
|
66
|
-
- (value, :ctx)
|
|
65
|
+
- () # no parameters
|
|
66
|
+
- (value) # one positional parameter
|
|
67
|
+
- (value, context) # two positional parameters
|
|
68
|
+
- (value, :ctx) # one positional parameter and :ctx keyword
|
|
69
|
+
- (value, context, :ctx) # two positional parameters and :ctx keyword
|
|
67
70
|
ERROR
|
|
68
71
|
end
|
|
69
72
|
end
|
|
@@ -47,6 +47,8 @@ class Serega
|
|
|
47
47
|
true
|
|
48
48
|
when "1_ctx" # (object, :ctx)
|
|
49
49
|
true
|
|
50
|
+
when "2_ctx" # (object, context, :ctx)
|
|
51
|
+
true
|
|
50
52
|
else
|
|
51
53
|
false
|
|
52
54
|
end
|
|
@@ -55,10 +57,11 @@ class Serega
|
|
|
55
57
|
def signature_error
|
|
56
58
|
<<~ERROR.strip
|
|
57
59
|
Invalid attribute option :unless parameters, valid parameters signatures:
|
|
58
|
-
- ()
|
|
59
|
-
- (object)
|
|
60
|
-
- (object, context)
|
|
61
|
-
- (object, :ctx)
|
|
60
|
+
- () # no parameters
|
|
61
|
+
- (object) # one positional parameter
|
|
62
|
+
- (object, context) # two positional parameters
|
|
63
|
+
- (object, :ctx) # one positional parameter and :ctx keyword
|
|
64
|
+
- (object, context, :ctx) # two positional parameters and :ctx keyword
|
|
62
65
|
ERROR
|
|
63
66
|
end
|
|
64
67
|
end
|
|
@@ -52,6 +52,8 @@ class Serega
|
|
|
52
52
|
true
|
|
53
53
|
when "1_ctx" # (value, :ctx)
|
|
54
54
|
true
|
|
55
|
+
when "2_ctx" # (value, context, :ctx)
|
|
56
|
+
true
|
|
55
57
|
else
|
|
56
58
|
false
|
|
57
59
|
end
|
|
@@ -60,10 +62,11 @@ class Serega
|
|
|
60
62
|
def signature_error
|
|
61
63
|
<<~ERROR.strip
|
|
62
64
|
Invalid attribute option :unless_value parameters, valid parameters signatures:
|
|
63
|
-
- ()
|
|
64
|
-
- (value)
|
|
65
|
-
- (value, context)
|
|
66
|
-
- (value, :ctx)
|
|
65
|
+
- () # no parameters
|
|
66
|
+
- (value) # one positional parameter
|
|
67
|
+
- (value, context) # two positional parameters
|
|
68
|
+
- (value, :ctx) # one positional parameter and :ctx keyword
|
|
69
|
+
- (value, context, :ctx) # two positional parameters and :ctx keyword
|
|
67
70
|
ERROR
|
|
68
71
|
end
|
|
69
72
|
end
|
|
@@ -92,6 +92,7 @@ class Serega
|
|
|
92
92
|
serializer_class.extend(ClassMethods)
|
|
93
93
|
serializer_class.include(InstanceMethods)
|
|
94
94
|
serializer_class::SeregaConfig.include(ConfigInstanceMethods)
|
|
95
|
+
serializer_class::SeregaDataBuilder.extend(DataBuilderClassMethods)
|
|
95
96
|
end
|
|
96
97
|
|
|
97
98
|
#
|
|
@@ -214,6 +215,22 @@ class Serega
|
|
|
214
215
|
# @see Serega
|
|
215
216
|
#
|
|
216
217
|
module InstanceMethods
|
|
218
|
+
#
|
|
219
|
+
# Serializes provided object to a tree of Ruby Data objects.
|
|
220
|
+
# When a root key is configured, the result is wrapped in an outer Data object
|
|
221
|
+
# whose members include the root key plus any metadata keys.
|
|
222
|
+
#
|
|
223
|
+
# @param object [Object] Serialized object
|
|
224
|
+
# @param opts [Hash, nil] Serializing options (`:root`, `:many`, `:context`, etc.)
|
|
225
|
+
#
|
|
226
|
+
# @return [Data, Array<Data>, nil] Serialization result as Data object(s)
|
|
227
|
+
#
|
|
228
|
+
def to_data(object, opts = nil)
|
|
229
|
+
opts = prepare_initial_serialization_opts(object, opts)
|
|
230
|
+
serialized_data = serialize(object, opts)
|
|
231
|
+
self.class::SeregaDataBuilder.call(self, serialized_data, opts)
|
|
232
|
+
end
|
|
233
|
+
|
|
217
234
|
private
|
|
218
235
|
|
|
219
236
|
def serialize(object, opts)
|
|
@@ -230,6 +247,69 @@ class Serega
|
|
|
230
247
|
(opts.fetch(:many) { object.is_a?(Enumerable) }) ? root.many : root.one
|
|
231
248
|
end
|
|
232
249
|
end
|
|
250
|
+
|
|
251
|
+
#
|
|
252
|
+
# SeregaDataBuilder additional/patched class methods
|
|
253
|
+
#
|
|
254
|
+
# Overrides `call` to handle the root key: the serialized result is
|
|
255
|
+
# unwrapped from the root key, converted to a Data tree by the base
|
|
256
|
+
# implementation, and then re-wrapped together with any metadata keys
|
|
257
|
+
# in an outer Data object. Metadata values (arbitrary hashes/arrays)
|
|
258
|
+
# are recursively converted to Data objects via `deep_hash_to_data`.
|
|
259
|
+
#
|
|
260
|
+
# When the root key is `nil` (disabled), delegates directly to the base.
|
|
261
|
+
#
|
|
262
|
+
# @see Serega::SeregaDataBuilder
|
|
263
|
+
#
|
|
264
|
+
module DataBuilderClassMethods
|
|
265
|
+
#
|
|
266
|
+
# @param serializer [Serega] Serializer instance carrying the plan
|
|
267
|
+
# @param serialized_result [Hash, Array, nil] Full serialized output (possibly wrapped under root key)
|
|
268
|
+
# @param serialization_opts [Hash] Serialization options used to determine the root key
|
|
269
|
+
#
|
|
270
|
+
# @return [Data, Array<Data>, nil] Serialization result as Data object(s)
|
|
271
|
+
#
|
|
272
|
+
def call(serializer, serialized_result, serialization_opts)
|
|
273
|
+
root_key = resolve_root_key(serializer, serialization_opts)
|
|
274
|
+
return super(serializer, serialized_result) unless root_key
|
|
275
|
+
|
|
276
|
+
root_data = super(serializer, serialized_result.fetch(root_key))
|
|
277
|
+
build_data_objects(serialized_result, root_key, root_data)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
private
|
|
281
|
+
|
|
282
|
+
def resolve_root_key(serializer, serialization_opts)
|
|
283
|
+
if serialization_opts.key?(:root)
|
|
284
|
+
serialization_opts[:root]
|
|
285
|
+
else
|
|
286
|
+
config_root = serializer.class.config.root
|
|
287
|
+
serialization_opts[:many] ? config_root.many : config_root.one
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def build_data_objects(serialized_result, root_key, root_data)
|
|
292
|
+
converted_hash =
|
|
293
|
+
serialized_result.to_h do |key, val|
|
|
294
|
+
value = (key == root_key) ? root_data : deep_hash_to_data(val)
|
|
295
|
+
[key, value]
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
Data.define(*converted_hash.keys).new(**converted_hash)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def deep_hash_to_data(value)
|
|
302
|
+
case value
|
|
303
|
+
when Hash
|
|
304
|
+
converted = value.transform_values { |nested| deep_hash_to_data(nested) }
|
|
305
|
+
Data.define(*value.keys).new(**converted)
|
|
306
|
+
when Array
|
|
307
|
+
value.map { |item| deep_hash_to_data(item) }
|
|
308
|
+
else
|
|
309
|
+
value
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
233
313
|
end
|
|
234
314
|
|
|
235
315
|
register_plugin(Root.plugin_name, Root)
|
|
@@ -19,6 +19,17 @@ class Serega
|
|
|
19
19
|
def call(opts)
|
|
20
20
|
return unless opts.key?(:preload)
|
|
21
21
|
|
|
22
|
+
check_opt_preload(opts)
|
|
23
|
+
check_usage_with_other_params(opts)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def check_opt_preload(opts)
|
|
29
|
+
raise SeregaError, "Option :preload value can not be `true`" if opts[:preload] == true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check_usage_with_other_params(opts)
|
|
22
33
|
raise SeregaError, "Option :preload can not be used together with option :const" if opts.key?(:const)
|
|
23
34
|
end
|
|
24
35
|
end
|
|
@@ -65,7 +65,6 @@ class Serega
|
|
|
65
65
|
Attribute::CheckOptMethod.call(opts, block)
|
|
66
66
|
Attribute::CheckOptMany.call(opts)
|
|
67
67
|
Attribute::CheckOptPreload.call(opts)
|
|
68
|
-
Attribute::CheckOptPreloadPath.call(opts)
|
|
69
68
|
Attribute::CheckOptSerializer.call(opts)
|
|
70
69
|
Attribute::CheckOptValue.call(opts, block)
|
|
71
70
|
Attribute::CheckOptBatch.call(self.class.serializer_class, opts, block)
|