serega 0.41.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 +123 -1205
- 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 +106 -145
- 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 -145
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +14 -100
- 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 +108 -0
- 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 +480 -372
- data/sig/serega/plugins.rbs +49 -0
- data/sig/serega.rbs +142 -0
- metadata +43 -39
- 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 -118
- 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/presenter/presenter.rb +0 -242
- 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
data/lib/serega.rb
CHANGED
|
@@ -1,53 +1,81 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
3
4
|
require_relative "serega/version"
|
|
4
5
|
|
|
5
|
-
# Parent class
|
|
6
|
+
# Parent class of serializers. A serializer lists attributes and serializes
|
|
7
|
+
# objects to Hashes, JSON, Data or Struct objects:
|
|
8
|
+
#
|
|
9
|
+
# ```ruby
|
|
10
|
+
# class UserSerializer < Serega
|
|
11
|
+
# attribute :first_name
|
|
12
|
+
# attribute :email, hide: true
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# UserSerializer.to_h(user) # => {first_name: "Bruce"}
|
|
16
|
+
# UserSerializer.to_h(user, with: :email) # => {first_name: "Bruce", email: "bruce@wayne.com"}
|
|
17
|
+
# ```
|
|
18
|
+
#
|
|
19
|
+
# See the [guides](https://github.com/andreyruby/serega/blob/master/docs/README.md).
|
|
6
20
|
class Serega
|
|
7
21
|
# Frozen hash
|
|
8
|
-
|
|
9
|
-
# @private
|
|
10
|
-
FROZEN_EMPTY_HASH = {}.freeze
|
|
22
|
+
FROZEN_EMPTY_HASH = {}.freeze # :nodoc:
|
|
11
23
|
|
|
12
24
|
# Frozen array
|
|
13
|
-
|
|
14
|
-
# @private
|
|
15
|
-
FROZEN_EMPTY_ARRAY = [].freeze
|
|
25
|
+
FROZEN_EMPTY_ARRAY = [].freeze # :nodoc:
|
|
16
26
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
# Locks serializers one at a time
|
|
28
|
+
LOCK_MUTEX = Mutex.new # :nodoc:
|
|
29
|
+
|
|
30
|
+
# Errors raised when another Ractor reads a value that is not shareable,
|
|
31
|
+
# empty on a Ruby without Ractors
|
|
32
|
+
# :nocov:
|
|
33
|
+
RACTOR_ISOLATION_ERRORS = (defined?(Ractor) ? [Ractor::IsolationError] : []).freeze # :nodoc:
|
|
34
|
+
# :nocov:
|
|
35
|
+
|
|
36
|
+
# Path prefix of the Serega source files
|
|
37
|
+
LIB_PATH = File.expand_path("serega", __dir__).freeze # :nodoc:
|
|
20
38
|
end
|
|
21
39
|
|
|
22
40
|
require_relative "serega/errors"
|
|
23
|
-
require_relative "serega/helpers/serializer_class_helper"
|
|
24
41
|
require_relative "serega/utils/collection_detector"
|
|
42
|
+
require_relative "serega/utils/deep_merge"
|
|
25
43
|
require_relative "serega/utils/enum_deep_dup"
|
|
26
44
|
require_relative "serega/utils/enum_deep_freeze"
|
|
27
45
|
require_relative "serega/utils/method_signature"
|
|
46
|
+
require_relative "serega/utils/parse_string_modifiers"
|
|
47
|
+
require_relative "serega/utils/ractor_local"
|
|
28
48
|
require_relative "serega/utils/serialized_attribute_error"
|
|
29
|
-
require_relative "serega/utils/symbol_name"
|
|
30
49
|
require_relative "serega/utils/to_hash"
|
|
31
50
|
require_relative "serega/attribute_value_resolvers/batch"
|
|
32
51
|
require_relative "serega/attribute_value_resolvers/const"
|
|
33
52
|
require_relative "serega/attribute_value_resolvers/delegate"
|
|
34
53
|
require_relative "serega/attribute_value_resolvers/hash_access"
|
|
35
54
|
require_relative "serega/attribute_value_resolvers/keyword"
|
|
55
|
+
require_relative "serega/attribute_values"
|
|
56
|
+
require_relative "serega/conditions"
|
|
57
|
+
require_relative "serega/formatter"
|
|
36
58
|
require_relative "serega/attribute"
|
|
59
|
+
require_relative "serega/attribute_normalizers/batch"
|
|
60
|
+
require_relative "serega/attribute_normalizers/block"
|
|
61
|
+
require_relative "serega/attribute_normalizers/delegate"
|
|
62
|
+
require_relative "serega/attribute_normalizers/hash_access"
|
|
63
|
+
require_relative "serega/attribute_normalizers/preloads"
|
|
37
64
|
require_relative "serega/attribute_normalizer"
|
|
38
|
-
require_relative "serega/engine/
|
|
39
|
-
require_relative "serega/engine/
|
|
40
|
-
require_relative "serega/engine/loader"
|
|
65
|
+
require_relative "serega/engine/run"
|
|
66
|
+
require_relative "serega/engine/pulls"
|
|
41
67
|
require_relative "serega/validations/utils/check_allowed_keys"
|
|
68
|
+
require_relative "serega/validations/utils/check_hash_access_value"
|
|
42
69
|
require_relative "serega/validations/utils/check_opt_is_bool"
|
|
43
70
|
require_relative "serega/validations/utils/check_opt_is_hash"
|
|
44
71
|
require_relative "serega/validations/utils/check_opt_is_string_or_symbol"
|
|
45
72
|
require_relative "serega/validations/attribute/check_block"
|
|
46
73
|
require_relative "serega/validations/attribute/check_name"
|
|
47
74
|
require_relative "serega/validations/attribute/check_opt_base_serializer"
|
|
48
|
-
require_relative "serega/validations/attribute/
|
|
75
|
+
require_relative "serega/validations/attribute/check_opt_condition"
|
|
49
76
|
require_relative "serega/validations/attribute/check_opt_hide"
|
|
50
77
|
require_relative "serega/validations/attribute/check_opt_delegate"
|
|
78
|
+
require_relative "serega/validations/attribute/check_opt_format"
|
|
51
79
|
require_relative "serega/validations/attribute/check_opt_batch"
|
|
52
80
|
require_relative "serega/validations/attribute/check_opt_hash_access"
|
|
53
81
|
require_relative "serega/validations/attribute/check_opt_many"
|
|
@@ -57,208 +85,251 @@ require_relative "serega/validations/attribute/check_opt_serializer"
|
|
|
57
85
|
require_relative "serega/validations/attribute/check_opt_value"
|
|
58
86
|
require_relative "serega/validations/initiate/check_modifiers"
|
|
59
87
|
require_relative "serega/validations/check_attribute_params"
|
|
60
|
-
require_relative "serega/validations/check_initiate_params"
|
|
61
88
|
require_relative "serega/validations/check_batch_loader_params"
|
|
62
|
-
require_relative "serega/validations/
|
|
89
|
+
require_relative "serega/validations/check_formatter"
|
|
90
|
+
require_relative "serega/validations/check_serialize_keywords"
|
|
63
91
|
|
|
92
|
+
require_relative "serega/batch_loader"
|
|
64
93
|
require_relative "serega/config"
|
|
65
|
-
require_relative "serega/
|
|
94
|
+
require_relative "serega/presenter"
|
|
95
|
+
require_relative "serega/source_group"
|
|
66
96
|
require_relative "serega/plan_point"
|
|
67
97
|
require_relative "serega/plan"
|
|
68
|
-
require_relative "serega/
|
|
98
|
+
require_relative "serega/result_code"
|
|
99
|
+
require_relative "serega/result_builder"
|
|
100
|
+
require_relative "serega/plan/cache"
|
|
69
101
|
require_relative "serega/plugins"
|
|
70
102
|
|
|
71
103
|
class Serega
|
|
72
|
-
@config = SeregaConfig.new
|
|
73
|
-
|
|
74
|
-
# Validates `Serializer.attribute` params
|
|
75
|
-
check_attribute_params_class = Class.new(SeregaValidations::CheckAttributeParams)
|
|
76
|
-
check_attribute_params_class.serializer_class = self
|
|
77
|
-
const_set(:CheckAttributeParams, check_attribute_params_class)
|
|
78
|
-
|
|
79
|
-
# Validates `Serializer#new` params
|
|
80
|
-
check_initiate_params_class = Class.new(SeregaValidations::CheckInitiateParams)
|
|
81
|
-
check_initiate_params_class.serializer_class = self
|
|
82
|
-
const_set(:CheckInitiateParams, check_initiate_params_class)
|
|
83
|
-
|
|
84
|
-
# Validates `serializer#call(obj, PARAMS)` params
|
|
85
|
-
check_serialize_params_class = Class.new(SeregaValidations::CheckSerializeParams)
|
|
86
|
-
check_serialize_params_class.serializer_class = self
|
|
87
|
-
const_set(:CheckSerializeParams, check_serialize_params_class)
|
|
88
|
-
|
|
89
|
-
# Validates `Serializer.batch` params
|
|
90
|
-
check_batch_loader_params_class = Class.new(SeregaValidations::CheckBatchLoaderParams)
|
|
91
|
-
check_batch_loader_params_class.serializer_class = self
|
|
92
|
-
const_set(:CheckBatchLoaderParams, check_batch_loader_params_class)
|
|
93
|
-
|
|
94
|
-
# Assigns `SeregaEngineLoader` constant to current class
|
|
95
|
-
engine_loader_class = Class.new(SeregaEngine::Loader)
|
|
96
|
-
engine_loader_class.serializer_class = self
|
|
97
|
-
const_set(:SeregaEngineLoader, engine_loader_class)
|
|
98
|
-
|
|
99
104
|
#
|
|
100
|
-
#
|
|
105
|
+
# Class methods of serializers: they define attributes and serialize
|
|
101
106
|
#
|
|
102
107
|
module ClassMethods
|
|
103
|
-
#
|
|
104
|
-
#
|
|
108
|
+
# Config of the serializer. Subclasses copy it.
|
|
109
|
+
#
|
|
110
|
+
# ```ruby
|
|
111
|
+
# config.hide_by_default = true
|
|
112
|
+
# ```
|
|
113
|
+
#
|
|
114
|
+
# See [Configuration](https://github.com/andreyruby/serega/blob/master/docs/configuration.md).
|
|
105
115
|
attr_reader :config
|
|
116
|
+
|
|
106
117
|
#
|
|
107
|
-
#
|
|
118
|
+
# Turns on a built-in feature by name, with its options. Subclasses get it
|
|
119
|
+
# too.
|
|
108
120
|
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
121
|
+
# ```ruby
|
|
122
|
+
# plugin :root, root_one: :user, root_many: :users
|
|
123
|
+
# ```
|
|
111
124
|
#
|
|
112
|
-
#
|
|
125
|
+
# See [Root Key and Metadata](https://github.com/andreyruby/serega/blob/master/docs/root-and-metadata.md).
|
|
113
126
|
#
|
|
114
127
|
def plugin(name, **opts)
|
|
128
|
+
check_unlocked
|
|
115
129
|
raise SeregaError, "This plugin is already loaded" if plugin_used?(name)
|
|
116
130
|
|
|
117
|
-
plugin = SeregaPlugins.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
# - **before_load_plugin** usually used to check requirements and to load additional plugins
|
|
122
|
-
# - **load_plugin** usually used to include plugin modules
|
|
123
|
-
# - **after_load_plugin** usually used to add config options
|
|
124
|
-
plugin.before_load_plugin(self, **opts) if plugin.respond_to?(:before_load_plugin)
|
|
125
|
-
plugin.load_plugin(self, **opts) if plugin.respond_to?(:load_plugin)
|
|
126
|
-
plugin.after_load_plugin(self, **opts) if plugin.respond_to?(:after_load_plugin)
|
|
127
|
-
|
|
128
|
-
# Store attached plugins, so we can check it is loaded later
|
|
129
|
-
config.plugins << (plugin.respond_to?(:plugin_name) ? plugin.plugin_name : plugin)
|
|
131
|
+
plugin = SeregaPlugins.find(name)
|
|
132
|
+
plugin.apply(self, **opts)
|
|
133
|
+
config.plugins << name
|
|
130
134
|
|
|
131
135
|
plugin
|
|
132
136
|
end
|
|
133
137
|
|
|
134
|
-
#
|
|
135
138
|
# Checks plugin is used
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
#
|
|
139
|
-
# @return [Boolean] Is plugin used
|
|
140
|
-
#
|
|
141
|
-
# @private
|
|
142
|
-
def plugin_used?(name)
|
|
143
|
-
plugin_name =
|
|
144
|
-
case name
|
|
145
|
-
when Module then name.respond_to?(:plugin_name) ? name.plugin_name : name
|
|
146
|
-
else name
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
config.plugins.include?(plugin_name)
|
|
139
|
+
def plugin_used?(name) # :nodoc:
|
|
140
|
+
config.plugins.include?(name)
|
|
150
141
|
end
|
|
151
142
|
|
|
152
143
|
#
|
|
153
|
-
#
|
|
144
|
+
# Attributes by name, hidden and inherited ones too
|
|
154
145
|
#
|
|
155
|
-
#
|
|
146
|
+
# ```ruby
|
|
147
|
+
# UserSerializer.attributes.keys # => [:first_name, :email]
|
|
148
|
+
# ```
|
|
156
149
|
#
|
|
157
|
-
# @private
|
|
158
150
|
def attributes
|
|
159
|
-
@attributes
|
|
151
|
+
@attributes
|
|
160
152
|
end
|
|
161
153
|
|
|
162
|
-
#
|
|
163
154
|
# Lists defined batch loaders
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
155
|
+
def batch_loaders # :nodoc:
|
|
156
|
+
@batch_loaders
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Lists defined formatters
|
|
160
|
+
def formatters # :nodoc:
|
|
161
|
+
@formatters
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Cached serialization plans
|
|
165
|
+
def plan_cache # :nodoc:
|
|
166
|
+
@plan_cache
|
|
167
|
+
# :nocov:
|
|
168
|
+
rescue *RACTOR_ISOLATION_ERRORS
|
|
169
|
+
raise SeregaError, @unshareable_reason if @unshareable_reason
|
|
170
|
+
|
|
171
|
+
raise SeregaError, "#{self} can not serialize in other Ractors. Call `make_shareable!` at the end of its definition"
|
|
172
|
+
# :nocov:
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Returns the presenter class that wraps objects in the current Ractor,
|
|
176
|
+
# `nil` without presenter methods. A shareable serializer gives each
|
|
177
|
+
# Ractor its own subclass of the presenter, which defines delegators
|
|
178
|
+
# in its own `Delegators` module.
|
|
179
|
+
def local_presenter # :nodoc:
|
|
180
|
+
key = @presenter_key
|
|
181
|
+
return @presenter_class unless key
|
|
182
|
+
|
|
183
|
+
SeregaUtils::RactorLocal.fetch(key) { Class.new(@presenter_class) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Lists blocks given to `presenter`, in definition order
|
|
187
|
+
def presenter_blocks # :nodoc:
|
|
188
|
+
@presenter_blocks
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Returns (and caches) the Data class of the serialized objects with the
|
|
192
|
+
# given members
|
|
193
|
+
def data_class_for(point_names) # :nodoc:
|
|
194
|
+
classes = @data_classes
|
|
195
|
+
classes = SeregaUtils::RactorLocal.fetch(SeregaUtils::RactorLocal.key(self, :data_classes)) { {} } if classes.frozen?
|
|
196
|
+
classes[point_names] ||= Data.define(*point_names)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Returns (and caches) the Struct class of the serialized objects with
|
|
200
|
+
# the given members
|
|
201
|
+
def struct_class_for(point_names) # :nodoc:
|
|
202
|
+
classes = @struct_classes
|
|
203
|
+
classes = SeregaUtils::RactorLocal.fetch(SeregaUtils::RactorLocal.key(self, :struct_classes)) { {} } if classes.frozen?
|
|
204
|
+
classes[point_names] ||= Struct.new(*point_names)
|
|
170
205
|
end
|
|
171
206
|
|
|
172
207
|
#
|
|
173
|
-
# Adds attribute
|
|
208
|
+
# Adds an attribute. By default its value is `object.<name>`, and the
|
|
209
|
+
# options change that:
|
|
174
210
|
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
# attribute :comments_count
|
|
192
|
-
# end
|
|
211
|
+
# ```ruby
|
|
212
|
+
# attribute :first_name
|
|
213
|
+
# attribute :name, method: :full_name
|
|
214
|
+
# attribute :email, hide: true
|
|
215
|
+
# attribute :posts, serializer: PostSerializer
|
|
216
|
+
# ```
|
|
217
|
+
#
|
|
218
|
+
# A block defines the attributes of a nested serializer. It inherits from
|
|
219
|
+
# the `:base_serializer` option or `config.base_serializer`:
|
|
220
|
+
#
|
|
221
|
+
# ```ruby
|
|
222
|
+
# attribute :statistics, method: :itself do
|
|
223
|
+
# attribute :likes_count
|
|
224
|
+
# attribute :comments_count
|
|
225
|
+
# end
|
|
226
|
+
# ```
|
|
193
227
|
#
|
|
194
|
-
#
|
|
228
|
+
# See [Attributes](https://github.com/andreyruby/serega/blob/master/docs/attributes.md#all-options)
|
|
229
|
+
# for all options.
|
|
195
230
|
#
|
|
196
231
|
def attribute(name, **opts, &block)
|
|
197
|
-
|
|
232
|
+
check_unlocked
|
|
233
|
+
location = caller_locations.find { |caller_location| !caller_location.path.start_with?(LIB_PATH) }
|
|
234
|
+
attribute = SeregaAttribute.new(self, name: name, opts: opts, block: block, location: location && "#{location.path}:#{location.lineno}")
|
|
198
235
|
attributes[attribute.name] = attribute
|
|
199
236
|
end
|
|
200
237
|
|
|
201
238
|
#
|
|
202
|
-
# Defines a batch loader
|
|
239
|
+
# Defines a named batch loader: a callable or a block that receives the
|
|
240
|
+
# objects, and the context as a second parameter or as the `:ctx`
|
|
241
|
+
# keyword. It returns the values by object id. Subclasses inherit it.
|
|
203
242
|
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
243
|
+
# ```ruby
|
|
244
|
+
# batch :likes_count, ->(users) { Like.where(user: users).group(:user_id).count }
|
|
245
|
+
# batch(:rank) { |users, ctx:| Rank.for(users, ctx[:season]) }
|
|
206
246
|
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
# Tags.where(post: posts).group(:post_id).pluck(:post_id, Arel.sql('ARRAY_AGG(tags.tag ORDER BY tag)')).to_h
|
|
210
|
-
# end
|
|
247
|
+
# attribute :likes_count, batch: true
|
|
248
|
+
# ```
|
|
211
249
|
#
|
|
212
|
-
#
|
|
250
|
+
# See [Batch loading](https://github.com/andreyruby/serega/blob/master/docs/batch-loading.md).
|
|
213
251
|
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
252
|
+
def batch(name, value = nil, &block)
|
|
253
|
+
check_unlocked
|
|
254
|
+
raise SeregaError, "Batch loader must be defined with a callable value or block" if !value == !block
|
|
255
|
+
|
|
256
|
+
batch_loader = SeregaBatchLoader.new(name: name, block: value || block)
|
|
257
|
+
batch_loaders[batch_loader.name] = batch_loader
|
|
258
|
+
end
|
|
259
|
+
|
|
217
260
|
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
261
|
+
# Defines a named formatter for the `:format` attribute option. A formatter
|
|
262
|
+
# is a method name called on the value, an Array of a method name and its
|
|
263
|
+
# arguments, or a callable or a block that receives the value, and the
|
|
264
|
+
# context as a second parameter or as the `:ctx` keyword. With
|
|
265
|
+
# `allow_nil: true` a `nil` value stays `nil`. Subclasses inherit it.
|
|
220
266
|
#
|
|
221
|
-
#
|
|
267
|
+
# ```ruby
|
|
268
|
+
# formatter :string, :to_s
|
|
269
|
+
# formatter :iso8601, [:iso8601, 3]
|
|
270
|
+
# formatter(:money) { |cents, ctx| "#{cents / 100.0} #{ctx[:currency]}" }
|
|
222
271
|
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
# @param block [Proc] Batch loader
|
|
272
|
+
# attribute :created_at, format: :iso8601
|
|
273
|
+
# ```
|
|
226
274
|
#
|
|
227
|
-
#
|
|
275
|
+
# See [Formatters](https://github.com/andreyruby/serega/blob/master/docs/formatters.md).
|
|
228
276
|
#
|
|
229
|
-
def
|
|
230
|
-
|
|
277
|
+
def formatter(name, value = nil, allow_nil: false, &block)
|
|
278
|
+
check_unlocked
|
|
279
|
+
raise SeregaError, "Formatter name must be a Symbol" unless name.is_a?(Symbol)
|
|
280
|
+
raise SeregaError, "Formatter must be defined with a value or a block" if !value == !block
|
|
281
|
+
raise SeregaError, "Invalid option :allow_nil => #{allow_nil.inspect}. Must have a boolean value" unless allow_nil == true || allow_nil == false
|
|
231
282
|
|
|
232
|
-
|
|
233
|
-
|
|
283
|
+
value ||= block
|
|
284
|
+
SeregaValidations::CheckFormatter.call(name, value)
|
|
285
|
+
formatters[name] = SeregaFormatter.new({use: value, allow_nil: allow_nil})
|
|
234
286
|
end
|
|
235
287
|
|
|
236
288
|
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
289
|
+
# Defines presenter methods: attributes read their values from them. The
|
|
290
|
+
# block runs in the presenter class of the serializer, and each call adds
|
|
291
|
+
# methods. Returns the presenter class, `nil` without one.
|
|
292
|
+
#
|
|
293
|
+
# ```ruby
|
|
294
|
+
# attribute :name
|
|
239
295
|
#
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
#
|
|
296
|
+
# presenter do
|
|
297
|
+
# def name
|
|
298
|
+
# [first_name, last_name].compact.join(" ")
|
|
299
|
+
# end
|
|
300
|
+
# end
|
|
301
|
+
# ```
|
|
302
|
+
#
|
|
303
|
+
# See [Presenter](https://github.com/andreyruby/serega/blob/master/docs/presenter.md).
|
|
243
304
|
#
|
|
244
|
-
|
|
245
|
-
|
|
305
|
+
def presenter(&block)
|
|
306
|
+
return @presenter_class unless block
|
|
307
|
+
|
|
308
|
+
check_unlocked
|
|
309
|
+
@presenter_class ||= Class.new(SeregaPresenter)
|
|
310
|
+
@presenter_class.class_exec(&block)
|
|
311
|
+
presenter_blocks << block
|
|
312
|
+
@presenter_class
|
|
313
|
+
end
|
|
314
|
+
|
|
246
315
|
#
|
|
247
|
-
#
|
|
248
|
-
#
|
|
316
|
+
# Sets the handler that preloads the `:preload` value of an attribute. It
|
|
317
|
+
# runs once per attribute with a preload, with the objects to serialize
|
|
318
|
+
# and the preloads. Without arguments, returns the handler. Subclasses
|
|
319
|
+
# inherit it.
|
|
249
320
|
#
|
|
250
|
-
#
|
|
251
|
-
#
|
|
321
|
+
# ```ruby
|
|
322
|
+
# preload_with { |objects, preloads| RecordPreloader.call(objects, preloads) }
|
|
323
|
+
# preload_with AppPreloader
|
|
324
|
+
# ```
|
|
252
325
|
#
|
|
253
|
-
#
|
|
326
|
+
# See [Preloads](https://github.com/andreyruby/serega/blob/master/docs/preloads.md).
|
|
254
327
|
#
|
|
255
328
|
def preload_with(value = nil, &block)
|
|
256
329
|
return @preload_with if value.nil? && block.nil?
|
|
257
|
-
raise SeregaError, "preload_with accepts a single callable or a block, not both" if value && block
|
|
258
|
-
|
|
259
|
-
handler = value || block
|
|
260
|
-
raise SeregaError, "preload_with value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
|
|
261
330
|
|
|
331
|
+
check_unlocked
|
|
332
|
+
handler = checked_handler(:preload_with, value, block)
|
|
262
333
|
signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2)
|
|
263
334
|
raise SeregaError, "preload_with handler must accept two positional arguments: (objects, preloads)" unless signature == "2"
|
|
264
335
|
|
|
@@ -266,45 +337,25 @@ class Serega
|
|
|
266
337
|
end
|
|
267
338
|
|
|
268
339
|
#
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
# serialized instead of the provided objects, which allows to accept ids or
|
|
275
|
-
# other references and load the records in one place.
|
|
276
|
-
#
|
|
277
|
-
# The handler runs before the `:many` option is detected, so it may turn a
|
|
278
|
-
# single object into a collection and back. A provided `:many` serialization
|
|
279
|
-
# option is still used as is.
|
|
280
|
-
#
|
|
281
|
-
# The handler runs only for the serialized objects, and not for objects of
|
|
282
|
-
# nested serializers.
|
|
340
|
+
# Sets the handler that replaces the objects given to the serializer, for
|
|
341
|
+
# example ids with records. It runs once per serialization, before Serega
|
|
342
|
+
# detects a list, and receives the objects, and the context as a second
|
|
343
|
+
# parameter or as the `:ctx` keyword. Objects of relations skip it.
|
|
344
|
+
# Without arguments, returns the handler.
|
|
283
345
|
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
346
|
+
# ```ruby
|
|
347
|
+
# prepare_initial_objects { |user_ids| User.where(id: user_ids) }
|
|
348
|
+
# prepare_initial_objects { |ids, ctx:| User.where(id: ids, account: ctx[:account]) }
|
|
349
|
+
# prepare_initial_objects UsersLoader
|
|
350
|
+
# ```
|
|
286
351
|
#
|
|
287
|
-
#
|
|
288
|
-
# prepare_initial_objects { |user_ids, ctx| User.where(id: user_ids, account: ctx[:account]) }
|
|
289
|
-
#
|
|
290
|
-
# @example with a keyword context
|
|
291
|
-
# prepare_initial_objects { |user_ids, ctx:| User.where(id: user_ids, account: ctx[:account]) }
|
|
292
|
-
#
|
|
293
|
-
# @example with a callable value
|
|
294
|
-
# prepare_initial_objects UsersLoader
|
|
295
|
-
#
|
|
296
|
-
# @param value [#call, nil] Handler accepting (objects), (objects, context) or (objects, ctx:)
|
|
297
|
-
# @param block [Proc] Handler accepting (objects), (objects, context) or (objects, ctx:)
|
|
298
|
-
#
|
|
299
|
-
# @return [#call, nil] The registered handler
|
|
352
|
+
# See [Prepare initial objects](https://github.com/andreyruby/serega/blob/master/docs/prepare-initial-objects.md).
|
|
300
353
|
#
|
|
301
354
|
def prepare_initial_objects(value = nil, &block)
|
|
302
355
|
return @prepare_initial_objects if value.nil? && block.nil?
|
|
303
|
-
raise SeregaError, "prepare_initial_objects accepts a single callable or a block, not both" if value && block
|
|
304
|
-
|
|
305
|
-
handler = value || block
|
|
306
|
-
raise SeregaError, "prepare_initial_objects value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
|
|
307
356
|
|
|
357
|
+
check_unlocked
|
|
358
|
+
handler = checked_handler(:prepare_initial_objects, value, block)
|
|
308
359
|
signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2, keyword_args: [:ctx])
|
|
309
360
|
raise SeregaError, prepare_initial_objects_signature_error unless %w[1 2 1_ctx].include?(signature)
|
|
310
361
|
|
|
@@ -312,143 +363,222 @@ class Serega
|
|
|
312
363
|
@prepare_initial_objects = handler
|
|
313
364
|
end
|
|
314
365
|
|
|
315
|
-
#
|
|
316
366
|
# Signature of the registered prepare_initial_objects handler
|
|
317
|
-
#
|
|
318
|
-
# @return [String, nil] Handler signature
|
|
319
|
-
#
|
|
320
|
-
# @private
|
|
321
|
-
def prepare_initial_objects_signature
|
|
322
|
-
@prepare_initial_objects_signature
|
|
323
|
-
end
|
|
367
|
+
attr_reader :prepare_initial_objects_signature # :nodoc:
|
|
324
368
|
|
|
369
|
+
# Patched in:
|
|
370
|
+
# - plugin :root (takes the options of plugins)
|
|
371
|
+
|
|
372
|
+
#
|
|
373
|
+
# Serializes an object or a list to a Hash or an Array of Hashes. `nil`
|
|
374
|
+
# serializes to `nil`.
|
|
325
375
|
#
|
|
326
|
-
#
|
|
376
|
+
# - `only`, `except`, `with`: fields to serialize, to skip and to add,
|
|
377
|
+
# see [Selecting fields](https://github.com/andreyruby/serega/blob/master/docs/selecting-fields.md);
|
|
378
|
+
# - `check_initiate_params`: `false` skips unknown fields, by default
|
|
379
|
+
# `config.check_initiate_params`;
|
|
380
|
+
# - `context`: data for attribute values and conditions;
|
|
381
|
+
# - `many`: `true` serializes a list, `false` one object. By default an
|
|
382
|
+
# `Enumerable` that is not a `Hash` or `Struct` is a list.
|
|
327
383
|
#
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
# @option opts [Boolean] :validate Validates provided modifiers (Default is true)
|
|
334
|
-
# @option opts [Hash] :context Serialization context
|
|
335
|
-
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
|
|
384
|
+
# ```ruby
|
|
385
|
+
# UserSerializer.to_h(user) # => {first_name: "Bruce"}
|
|
386
|
+
# UserSerializer.to_h([user], with: :email) # => [{first_name: "Bruce", email: "bruce@wayne.com"}]
|
|
387
|
+
# UserSerializer.to_h(user, context: {admin: true})
|
|
388
|
+
# ```
|
|
336
389
|
#
|
|
337
|
-
#
|
|
390
|
+
# See [Serializing](https://github.com/andreyruby/serega/blob/master/docs/serializing.md).
|
|
338
391
|
#
|
|
339
|
-
def call(object,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
serialize_opts = init_serialize_opts(opts)
|
|
343
|
-
new(modifiers_opts).to_h(object, serialize_opts)
|
|
392
|
+
def call(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil)
|
|
393
|
+
new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
|
|
394
|
+
.to_h(object, context: context, many: many)
|
|
344
395
|
end
|
|
345
396
|
|
|
397
|
+
# Patched in:
|
|
398
|
+
# - plugin :root (takes the options of plugins)
|
|
399
|
+
|
|
346
400
|
#
|
|
347
|
-
# Serializes
|
|
348
|
-
#
|
|
349
|
-
# @param object [Object] Serialized object
|
|
350
|
-
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
|
|
351
|
-
# @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
|
|
352
|
-
# @option opts [Array, Hash, String, Symbol] :except Attributes to hide
|
|
353
|
-
# @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
|
|
354
|
-
# @option opts [Boolean] :validate Validates provided modifiers (Default is true)
|
|
355
|
-
# @option opts [Hash] :context Serialization context
|
|
356
|
-
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
|
|
401
|
+
# Serializes like #to_h, to `Data` objects
|
|
357
402
|
#
|
|
358
|
-
#
|
|
403
|
+
# ```ruby
|
|
404
|
+
# UserSerializer.to_data(user) # => #<data first_name="Bruce">
|
|
405
|
+
# ```
|
|
359
406
|
#
|
|
360
|
-
def to_data(object,
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
serialize_opts = init_serialize_opts(opts)
|
|
364
|
-
new(modifiers_opts).to_data(object, serialize_opts)
|
|
407
|
+
def to_data(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil)
|
|
408
|
+
new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
|
|
409
|
+
.to_data(object, context: context, many: many)
|
|
365
410
|
end
|
|
366
411
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
private
|
|
370
|
-
|
|
371
|
-
def init_modifier_opts(opts)
|
|
372
|
-
(!opts || opts.empty?) ? FROZEN_EMPTY_HASH : opts.slice(*config.initiate_keys)
|
|
373
|
-
end
|
|
412
|
+
# Patched in:
|
|
413
|
+
# - plugin :root (takes the options of plugins)
|
|
374
414
|
|
|
375
|
-
|
|
376
|
-
|
|
415
|
+
#
|
|
416
|
+
# Serializes like #to_h, to `Struct` objects
|
|
417
|
+
#
|
|
418
|
+
# ```ruby
|
|
419
|
+
# UserSerializer.to_struct(user) # => #<struct first_name="Bruce">
|
|
420
|
+
# ```
|
|
421
|
+
#
|
|
422
|
+
def to_struct(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil)
|
|
423
|
+
new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
|
|
424
|
+
.to_struct(object, context: context, many: many)
|
|
377
425
|
end
|
|
378
426
|
|
|
379
427
|
# Patched in:
|
|
380
|
-
# - plugin :
|
|
381
|
-
# - plugin :presenter (defines Presenter)
|
|
382
|
-
def inherited(subclass)
|
|
383
|
-
config_class = Class.new(self::SeregaConfig)
|
|
384
|
-
config_class.serializer_class = subclass
|
|
385
|
-
subclass.const_set(:SeregaConfig, config_class)
|
|
386
|
-
subclass.instance_variable_set(:@config, subclass::SeregaConfig.new(config.opts))
|
|
387
|
-
|
|
388
|
-
attribute_class = Class.new(self::SeregaAttribute)
|
|
389
|
-
attribute_class.serializer_class = subclass
|
|
390
|
-
subclass.const_set(:SeregaAttribute, attribute_class)
|
|
428
|
+
# - plugin :root (takes the options of plugins)
|
|
391
429
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
430
|
+
#
|
|
431
|
+
# Serializes like #to_h, to a JSON String: `JSON.generate` of the #to_h
|
|
432
|
+
# result
|
|
433
|
+
#
|
|
434
|
+
# ```ruby
|
|
435
|
+
# UserSerializer.to_json(user) # => "{\"first_name\":\"Bruce\"}"
|
|
436
|
+
# ```
|
|
437
|
+
#
|
|
438
|
+
def to_json(object, only: nil, with: nil, except: nil, check_initiate_params: nil, context: nil, many: nil)
|
|
439
|
+
new(only: only, with: with, except: except, check_initiate_params: check_initiate_params)
|
|
440
|
+
.to_json(object, context: context, many: many)
|
|
441
|
+
end
|
|
395
442
|
|
|
396
|
-
|
|
397
|
-
data_builder_class.serializer_class = subclass
|
|
398
|
-
subclass.const_set(:SeregaDataBuilder, data_builder_class)
|
|
443
|
+
alias_method :to_h, :call
|
|
399
444
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
445
|
+
# Locks serializer definitions and config from changes.
|
|
446
|
+
# Called when a serialization plan is built for the serializer.
|
|
447
|
+
def lock # :nodoc:
|
|
448
|
+
return if @locked
|
|
403
449
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
subclass.const_set(:SeregaPlanPoint, plan_point_class)
|
|
450
|
+
LOCK_MUTEX.synchronize do
|
|
451
|
+
next if @locked
|
|
407
452
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
453
|
+
SeregaUtils::EnumDeepFreeze.call(config.opts)
|
|
454
|
+
define_presenter_delegators if @presenter_class
|
|
455
|
+
@locked = true
|
|
456
|
+
end
|
|
457
|
+
end
|
|
411
458
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
459
|
+
#
|
|
460
|
+
# Makes the serializer shareable between Ractors. Call it at the end of a
|
|
461
|
+
# definition to serialize in other Ractors. It locks the serializer and
|
|
462
|
+
# its nested serializers defined with blocks. A serializer with a
|
|
463
|
+
# definition that can not be shared serializes in the main Ractor only.
|
|
464
|
+
#
|
|
465
|
+
# ```ruby
|
|
466
|
+
# class UserSerializer < Serega
|
|
467
|
+
# attribute :first_name
|
|
468
|
+
#
|
|
469
|
+
# make_shareable!
|
|
470
|
+
# end
|
|
471
|
+
# ```
|
|
472
|
+
#
|
|
473
|
+
# See [Ractors](https://github.com/andreyruby/serega/blob/master/docs/ractors.md).
|
|
474
|
+
#
|
|
475
|
+
def make_shareable!
|
|
476
|
+
return self if @shareable
|
|
477
|
+
|
|
478
|
+
lock
|
|
479
|
+
attributes.each_value { |attribute| attribute.serializer.make_shareable! if attribute.initials[:block] }
|
|
480
|
+
@shareable = true
|
|
481
|
+
# :nocov:
|
|
482
|
+
if defined?(Ractor)
|
|
483
|
+
require_relative "serega/shareable"
|
|
484
|
+
require_relative "serega/plan/per_ractor_cache"
|
|
485
|
+
# Other Ractors can not define methods in the presenter, thus each
|
|
486
|
+
# Ractor wraps objects in its own subclass of it
|
|
487
|
+
if @presenter_class
|
|
488
|
+
@presenter_class::Delegators.freeze
|
|
489
|
+
@presenter_key = SeregaUtils::RactorLocal.key(self, :presenter)
|
|
490
|
+
end
|
|
491
|
+
@unshareable_reason = SeregaShareable.call(self)
|
|
492
|
+
@plan_cache = SeregaPlan::PerRactorCache.new(self) unless @unshareable_reason
|
|
493
|
+
end
|
|
494
|
+
# :nocov:
|
|
495
|
+
self
|
|
496
|
+
end
|
|
415
497
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
498
|
+
# Shows whether `make_shareable!` was called
|
|
499
|
+
def shareable? # :nodoc:
|
|
500
|
+
@shareable
|
|
501
|
+
end
|
|
419
502
|
|
|
420
|
-
|
|
421
|
-
check_initiate_params_class.serializer_class = subclass
|
|
422
|
-
subclass.const_set(:CheckInitiateParams, check_initiate_params_class)
|
|
503
|
+
private
|
|
423
504
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
505
|
+
# Defines the presenter delegators of the attributes that read a method of
|
|
506
|
+
# the object. Later reads skip method_missing, also in other Ractors,
|
|
507
|
+
# which can not define methods in classes of the main Ractor.
|
|
508
|
+
def define_presenter_delegators
|
|
509
|
+
object_methods = attributes.each_value.filter_map(&:object_method)
|
|
510
|
+
@presenter_class.define_delegators(object_methods)
|
|
511
|
+
end
|
|
427
512
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
513
|
+
def check_unlocked
|
|
514
|
+
raise SeregaError, "#{self} can not be changed after it was made shareable" if @shareable
|
|
515
|
+
raise SeregaError, "#{self} can not be changed after it was used for serialization" if @locked
|
|
516
|
+
end
|
|
431
517
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
518
|
+
# Patched in:
|
|
519
|
+
# - plugin :metadata (copies meta_attributes to subclasses)
|
|
520
|
+
def inherited(subclass)
|
|
521
|
+
subclass.const_set(:SeregaConfig, Class.new(self::SeregaConfig))
|
|
522
|
+
subclass.const_set(:SeregaAttributeValues, Class.new(self::SeregaAttributeValues))
|
|
523
|
+
subclass.send(:initialize_definition, subclass::SeregaConfig.new(config.opts), formatters.dup)
|
|
437
524
|
|
|
438
|
-
# Assign same batch loaders
|
|
525
|
+
# Assign same batch loaders before the attributes that use them
|
|
439
526
|
batch_loaders.each_value do |loader|
|
|
440
527
|
subclass.batch(loader.name, loader.block)
|
|
441
528
|
end
|
|
442
529
|
|
|
530
|
+
# Assign same attributes
|
|
531
|
+
attributes.each_value do |attr|
|
|
532
|
+
subclass_attribute = SeregaAttribute.new(subclass, **attr.initials)
|
|
533
|
+
subclass.attributes[subclass_attribute.name] = subclass_attribute
|
|
534
|
+
end
|
|
535
|
+
|
|
443
536
|
# Assign same preload handler
|
|
444
537
|
subclass.preload_with(preload_with) if preload_with
|
|
445
538
|
|
|
446
539
|
# Assign same initial objects handler
|
|
447
540
|
subclass.prepare_initial_objects(prepare_initial_objects) if prepare_initial_objects
|
|
448
541
|
|
|
542
|
+
# Assign same presenter blocks
|
|
543
|
+
presenter_blocks.each do |presenter_block|
|
|
544
|
+
subclass.presenter(&presenter_block)
|
|
545
|
+
end
|
|
546
|
+
|
|
449
547
|
super
|
|
450
548
|
end
|
|
451
549
|
|
|
550
|
+
# Starts the definition of a serializer class with its config and the
|
|
551
|
+
# formatters it inherits. Serega and each subclass set the same instance
|
|
552
|
+
# variables in the same order.
|
|
553
|
+
def initialize_definition(config, formatters)
|
|
554
|
+
@config = config
|
|
555
|
+
@attributes = {}
|
|
556
|
+
@batch_loaders = {}
|
|
557
|
+
@formatters = formatters
|
|
558
|
+
@presenter_class = nil
|
|
559
|
+
@presenter_blocks = []
|
|
560
|
+
@presenter_key = nil
|
|
561
|
+
@data_classes = {}
|
|
562
|
+
@struct_classes = {}
|
|
563
|
+
@preload_with = nil
|
|
564
|
+
@prepare_initial_objects = nil
|
|
565
|
+
@prepare_initial_objects_signature = nil
|
|
566
|
+
@locked = false
|
|
567
|
+
@shareable = false
|
|
568
|
+
@unshareable_reason = nil
|
|
569
|
+
@plan_cache = SeregaPlan::Cache.new(self)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# The handler given to `method_name` as a value or a block
|
|
573
|
+
def checked_handler(method_name, value, block)
|
|
574
|
+
raise SeregaError, "#{method_name} accepts a single callable or a block, not both" if value && block
|
|
575
|
+
|
|
576
|
+
handler = value || block
|
|
577
|
+
raise SeregaError, "#{method_name} value must be a Proc or respond to #call" unless handler.respond_to?(:call)
|
|
578
|
+
|
|
579
|
+
handler
|
|
580
|
+
end
|
|
581
|
+
|
|
452
582
|
def prepare_initial_objects_signature_error
|
|
453
583
|
<<~ERR.strip
|
|
454
584
|
prepare_initial_objects handler arguments should have one of this signatures:
|
|
@@ -460,118 +590,103 @@ class Serega
|
|
|
460
590
|
end
|
|
461
591
|
|
|
462
592
|
#
|
|
463
|
-
#
|
|
593
|
+
# Instance methods of serializers. An instance keeps its field selection
|
|
594
|
+
# and serializes many times.
|
|
464
595
|
#
|
|
465
596
|
module InstanceMethods
|
|
466
597
|
#
|
|
467
|
-
#
|
|
468
|
-
#
|
|
469
|
-
#
|
|
470
|
-
#
|
|
471
|
-
#
|
|
472
|
-
#
|
|
473
|
-
#
|
|
474
|
-
#
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
parse_modifiers(opts)
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
self.class::CheckInitiateParams.new(@opts).validate if opts&.fetch(:check_initiate_params) { config.check_initiate_params }
|
|
485
|
-
|
|
486
|
-
@plan = self.class::SeregaPlan.call(@opts)
|
|
598
|
+
# Prepares a serializer with a field selection. The options are the field
|
|
599
|
+
# options of Serega::ClassMethods#to_h.
|
|
600
|
+
#
|
|
601
|
+
# ```ruby
|
|
602
|
+
# serializer = UserSerializer.new(only: [:first_name])
|
|
603
|
+
# serializer.to_h(user) # => {first_name: "Bruce"}
|
|
604
|
+
# serializer.to_h(users) # => [{first_name: "Bruce"}, {first_name: "Alfred"}]
|
|
605
|
+
# ```
|
|
606
|
+
#
|
|
607
|
+
def initialize(only: nil, with: nil, except: nil, check_initiate_params: nil)
|
|
608
|
+
modifiers = !(only.nil? && with.nil? && except.nil?)
|
|
609
|
+
check_initiate_params = config.check_initiate_params if modifiers && check_initiate_params.nil?
|
|
610
|
+
@plan = self.class.plan_cache.fetch(only, with, except, check_initiate_params: modifiers && check_initiate_params)
|
|
611
|
+
@direct_hash_builder = @plan.direct_hash_builder
|
|
487
612
|
end
|
|
488
613
|
|
|
489
|
-
#
|
|
490
614
|
# Plan for serialization.
|
|
491
615
|
# This plan can be traversed to find serialized attributes and nested attributes.
|
|
492
|
-
#
|
|
493
|
-
|
|
494
|
-
#
|
|
495
|
-
|
|
616
|
+
attr_reader :plan # :nodoc:
|
|
617
|
+
|
|
618
|
+
# Patched in:
|
|
619
|
+
# - plugin :root (takes the options of plugins)
|
|
496
620
|
|
|
497
621
|
#
|
|
498
|
-
# Serializes
|
|
499
|
-
#
|
|
500
|
-
# @param object [Object] Serialized object
|
|
501
|
-
# @param opts [Hash, nil] Serializing options
|
|
502
|
-
# @option opts [Hash] :context Serialization context
|
|
503
|
-
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
|
|
622
|
+
# Serializes an object or a list to a Hash or an Array of Hashes, with the
|
|
623
|
+
# serialization options of Serega::ClassMethods#to_h
|
|
504
624
|
#
|
|
505
|
-
#
|
|
625
|
+
# ```ruby
|
|
626
|
+
# UserSerializer.new.to_h(user, context: {admin: true})
|
|
627
|
+
# ```
|
|
506
628
|
#
|
|
507
|
-
def call(object,
|
|
508
|
-
|
|
509
|
-
object
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
629
|
+
def call(object, context: nil, many: nil)
|
|
630
|
+
builder = @direct_hash_builder
|
|
631
|
+
if builder && object && !many && (context.nil? || context.is_a?(Hash))
|
|
632
|
+
many = SeregaUtils::CollectionDetector.call(object, @plan.collection_classes) if many.nil?
|
|
633
|
+
return builder.call_one(object, context.nil? ? FROZEN_EMPTY_HASH : context) unless many
|
|
634
|
+
end
|
|
513
635
|
|
|
514
|
-
|
|
515
|
-
def to_h(object, opts = nil)
|
|
516
|
-
call(object, opts)
|
|
636
|
+
serialize_to(:hash, object, context, many, FROZEN_EMPTY_HASH)
|
|
517
637
|
end
|
|
518
638
|
|
|
519
|
-
|
|
520
|
-
|
|
639
|
+
alias_method :to_h, :call
|
|
640
|
+
|
|
521
641
|
# Patched in:
|
|
522
|
-
# - plugin :root (
|
|
523
|
-
|
|
524
|
-
# @param object [Object] Serialized object
|
|
525
|
-
# @param opts [Hash, nil] Serializing options
|
|
526
|
-
# @option opts [Hash] :context Serialization context
|
|
527
|
-
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
|
|
642
|
+
# - plugin :root (takes the options of plugins)
|
|
643
|
+
|
|
528
644
|
#
|
|
529
|
-
#
|
|
645
|
+
# Serializes like #to_h, to `Data` objects
|
|
530
646
|
#
|
|
531
|
-
def to_data(object,
|
|
532
|
-
|
|
533
|
-
object = prepare_objects(object, opts[:context])
|
|
534
|
-
opts = prepare_initial_serialization_opts(object, opts)
|
|
535
|
-
serialized_data = serialize(object, opts)
|
|
536
|
-
self.class::SeregaDataBuilder.call(self, serialized_data)
|
|
647
|
+
def to_data(object, context: nil, many: nil)
|
|
648
|
+
serialize_to(:data, object, context, many, FROZEN_EMPTY_HASH)
|
|
537
649
|
end
|
|
538
650
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
attr_reader :opts
|
|
651
|
+
# Patched in:
|
|
652
|
+
# - plugin :root (takes the options of plugins)
|
|
542
653
|
|
|
543
|
-
|
|
544
|
-
|
|
654
|
+
#
|
|
655
|
+
# Serializes like #to_h, to `Struct` objects
|
|
656
|
+
#
|
|
657
|
+
def to_struct(object, context: nil, many: nil)
|
|
658
|
+
serialize_to(:struct, object, context, many, FROZEN_EMPTY_HASH)
|
|
545
659
|
end
|
|
546
660
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
opts.each do |key, value|
|
|
551
|
-
value = parse_modifier(value) if (key == :only) || (key == :except) || (key == :with)
|
|
552
|
-
result[key] = value
|
|
553
|
-
end
|
|
661
|
+
# Patched in:
|
|
662
|
+
# - plugin :root (takes the options of plugins)
|
|
554
663
|
|
|
555
|
-
|
|
664
|
+
#
|
|
665
|
+
# Serializes like #to_h, to a JSON String: `JSON.generate` of the #to_h
|
|
666
|
+
# result
|
|
667
|
+
#
|
|
668
|
+
def to_json(object, context: nil, many: nil)
|
|
669
|
+
JSON.generate(call(object, context: context, many: many))
|
|
556
670
|
end
|
|
557
671
|
|
|
558
|
-
|
|
559
|
-
# - plugin :string_modifiers (parses string modifiers differently)
|
|
560
|
-
def parse_modifier(value)
|
|
561
|
-
SeregaUtils::ToHash.call(value)
|
|
562
|
-
end
|
|
672
|
+
private
|
|
563
673
|
|
|
564
|
-
def
|
|
565
|
-
|
|
566
|
-
|
|
674
|
+
def config
|
|
675
|
+
self.class.config
|
|
676
|
+
end
|
|
567
677
|
|
|
568
|
-
|
|
569
|
-
|
|
678
|
+
# Serializes the object with the serialization options. `opts` holds the
|
|
679
|
+
# options other than :context and :many.
|
|
680
|
+
def serialize_to(mode, object, context, many, opts)
|
|
681
|
+
SeregaValidations::CheckSerializeKeywords.call(context, many)
|
|
682
|
+
context ||= FROZEN_EMPTY_HASH
|
|
683
|
+
object = prepare_objects(object, context) if self.class.prepare_initial_objects_signature
|
|
684
|
+
many = SeregaUtils::CollectionDetector.call(object, @plan.collection_classes) if many.nil?
|
|
685
|
+
serialize(object, opts, context, many, mode)
|
|
570
686
|
end
|
|
571
687
|
|
|
572
688
|
def prepare_objects(objects, context)
|
|
573
689
|
handler = self.class.prepare_initial_objects
|
|
574
|
-
return objects unless handler
|
|
575
690
|
|
|
576
691
|
case self.class.prepare_initial_objects_signature
|
|
577
692
|
when "1" then handler.call(objects)
|
|
@@ -580,24 +695,17 @@ class Serega
|
|
|
580
695
|
end
|
|
581
696
|
end
|
|
582
697
|
|
|
583
|
-
def prepare_initial_serialization_opts(object, opts)
|
|
584
|
-
opts[:level_queue] = SeregaEngine::LevelQueue.new
|
|
585
|
-
opts[:many] = SeregaUtils::CollectionDetector.call(object) unless opts.key?(:many)
|
|
586
|
-
opts[:plan] = plan
|
|
587
|
-
opts
|
|
588
|
-
end
|
|
589
|
-
|
|
590
698
|
# Patched in:
|
|
591
699
|
# - plugin :root (wraps result `{ root => result }`)
|
|
592
700
|
# - plugin :context_metadata (adds context metadata to final result)
|
|
593
701
|
# - plugin :metadata (adds metadata to final result)
|
|
594
|
-
def serialize(object,
|
|
595
|
-
|
|
596
|
-
opts[:level_queue].run
|
|
597
|
-
result
|
|
702
|
+
def serialize(object, _opts, context, many, mode)
|
|
703
|
+
SeregaEngine::Run.call(@plan, object, many, mode, context)
|
|
598
704
|
end
|
|
599
705
|
end
|
|
600
706
|
|
|
601
707
|
extend ClassMethods
|
|
602
708
|
include InstanceMethods
|
|
709
|
+
|
|
710
|
+
initialize_definition(SeregaConfig.new, {})
|
|
603
711
|
end
|