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
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,209 +85,227 @@ 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
94
|
require_relative "serega/presenter"
|
|
66
|
-
require_relative "serega/
|
|
95
|
+
require_relative "serega/source_group"
|
|
67
96
|
require_relative "serega/plan_point"
|
|
68
97
|
require_relative "serega/plan"
|
|
69
|
-
require_relative "serega/
|
|
98
|
+
require_relative "serega/result_code"
|
|
99
|
+
require_relative "serega/result_builder"
|
|
100
|
+
require_relative "serega/plan/cache"
|
|
70
101
|
require_relative "serega/plugins"
|
|
71
102
|
|
|
72
103
|
class Serega
|
|
73
|
-
@config = SeregaConfig.new
|
|
74
|
-
@presenter_class = nil
|
|
75
|
-
|
|
76
|
-
# Validates `Serializer.attribute` params
|
|
77
|
-
check_attribute_params_class = Class.new(SeregaValidations::CheckAttributeParams)
|
|
78
|
-
check_attribute_params_class.serializer_class = self
|
|
79
|
-
const_set(:CheckAttributeParams, check_attribute_params_class)
|
|
80
|
-
|
|
81
|
-
# Validates `Serializer#new` params
|
|
82
|
-
check_initiate_params_class = Class.new(SeregaValidations::CheckInitiateParams)
|
|
83
|
-
check_initiate_params_class.serializer_class = self
|
|
84
|
-
const_set(:CheckInitiateParams, check_initiate_params_class)
|
|
85
|
-
|
|
86
|
-
# Validates `serializer#call(obj, PARAMS)` params
|
|
87
|
-
check_serialize_params_class = Class.new(SeregaValidations::CheckSerializeParams)
|
|
88
|
-
check_serialize_params_class.serializer_class = self
|
|
89
|
-
const_set(:CheckSerializeParams, check_serialize_params_class)
|
|
90
|
-
|
|
91
|
-
# Validates `Serializer.batch` params
|
|
92
|
-
check_batch_loader_params_class = Class.new(SeregaValidations::CheckBatchLoaderParams)
|
|
93
|
-
check_batch_loader_params_class.serializer_class = self
|
|
94
|
-
const_set(:CheckBatchLoaderParams, check_batch_loader_params_class)
|
|
95
|
-
|
|
96
|
-
# Assigns `SeregaEngineLoader` constant to current class
|
|
97
|
-
engine_loader_class = Class.new(SeregaEngine::Loader)
|
|
98
|
-
engine_loader_class.serializer_class = self
|
|
99
|
-
const_set(:SeregaEngineLoader, engine_loader_class)
|
|
100
|
-
|
|
101
104
|
#
|
|
102
|
-
#
|
|
105
|
+
# Class methods of serializers: they define attributes and serialize
|
|
103
106
|
#
|
|
104
107
|
module ClassMethods
|
|
105
|
-
#
|
|
106
|
-
#
|
|
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).
|
|
107
115
|
attr_reader :config
|
|
116
|
+
|
|
108
117
|
#
|
|
109
|
-
#
|
|
118
|
+
# Turns on a built-in feature by name, with its options. Subclasses get it
|
|
119
|
+
# too.
|
|
110
120
|
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
121
|
+
# ```ruby
|
|
122
|
+
# plugin :root, root_one: :user, root_many: :users
|
|
123
|
+
# ```
|
|
113
124
|
#
|
|
114
|
-
#
|
|
125
|
+
# See [Root Key and Metadata](https://github.com/andreyruby/serega/blob/master/docs/root-and-metadata.md).
|
|
115
126
|
#
|
|
116
127
|
def plugin(name, **opts)
|
|
128
|
+
check_unlocked
|
|
117
129
|
raise SeregaError, "This plugin is already loaded" if plugin_used?(name)
|
|
118
130
|
|
|
119
|
-
plugin = SeregaPlugins.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
#
|
|
123
|
-
# - **before_load_plugin** usually used to check requirements and to load additional plugins
|
|
124
|
-
# - **load_plugin** usually used to include plugin modules
|
|
125
|
-
# - **after_load_plugin** usually used to add config options
|
|
126
|
-
plugin.before_load_plugin(self, **opts) if plugin.respond_to?(:before_load_plugin)
|
|
127
|
-
plugin.load_plugin(self, **opts) if plugin.respond_to?(:load_plugin)
|
|
128
|
-
plugin.after_load_plugin(self, **opts) if plugin.respond_to?(:after_load_plugin)
|
|
129
|
-
|
|
130
|
-
# Store attached plugins, so we can check it is loaded later
|
|
131
|
-
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
|
|
132
134
|
|
|
133
135
|
plugin
|
|
134
136
|
end
|
|
135
137
|
|
|
136
|
-
#
|
|
137
138
|
# Checks plugin is used
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
#
|
|
141
|
-
# @return [Boolean] Is plugin used
|
|
142
|
-
#
|
|
143
|
-
# @private
|
|
144
|
-
def plugin_used?(name)
|
|
145
|
-
plugin_name =
|
|
146
|
-
case name
|
|
147
|
-
when Module then name.respond_to?(:plugin_name) ? name.plugin_name : name
|
|
148
|
-
else name
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
config.plugins.include?(plugin_name)
|
|
139
|
+
def plugin_used?(name) # :nodoc:
|
|
140
|
+
config.plugins.include?(name)
|
|
152
141
|
end
|
|
153
142
|
|
|
154
143
|
#
|
|
155
|
-
#
|
|
144
|
+
# Attributes by name, hidden and inherited ones too
|
|
156
145
|
#
|
|
157
|
-
#
|
|
146
|
+
# ```ruby
|
|
147
|
+
# UserSerializer.attributes.keys # => [:first_name, :email]
|
|
148
|
+
# ```
|
|
158
149
|
#
|
|
159
|
-
# @private
|
|
160
150
|
def attributes
|
|
161
|
-
@attributes
|
|
151
|
+
@attributes
|
|
162
152
|
end
|
|
163
153
|
|
|
164
|
-
#
|
|
165
154
|
# Lists defined batch loaders
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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) }
|
|
172
184
|
end
|
|
173
185
|
|
|
174
|
-
#
|
|
175
186
|
# Lists blocks given to `presenter`, in definition order
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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)
|
|
182
205
|
end
|
|
183
206
|
|
|
184
207
|
#
|
|
185
|
-
# Adds attribute
|
|
208
|
+
# Adds an attribute. By default its value is `object.<name>`, and the
|
|
209
|
+
# options change that:
|
|
186
210
|
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
# is serialized with this nested serializer. The nested serializer
|
|
194
|
-
# inherits from the `:base_serializer` attribute option or
|
|
195
|
-
# `config.base_serializer` (one of them is required).
|
|
211
|
+
# ```ruby
|
|
212
|
+
# attribute :first_name
|
|
213
|
+
# attribute :name, method: :full_name
|
|
214
|
+
# attribute :email, hide: true
|
|
215
|
+
# attribute :posts, serializer: PostSerializer
|
|
216
|
+
# ```
|
|
196
217
|
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
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
|
+
# ```
|
|
202
227
|
#
|
|
203
|
-
#
|
|
228
|
+
# See [Attributes](https://github.com/andreyruby/serega/blob/master/docs/attributes.md#all-options)
|
|
229
|
+
# for all options.
|
|
204
230
|
#
|
|
205
231
|
def attribute(name, **opts, &block)
|
|
206
|
-
|
|
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}")
|
|
207
235
|
attributes[attribute.name] = attribute
|
|
208
236
|
end
|
|
209
237
|
|
|
210
238
|
#
|
|
211
|
-
# 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.
|
|
212
242
|
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
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]) }
|
|
215
246
|
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
# Tags.where(post: posts).group(:post_id).pluck(:post_id, Arel.sql('ARRAY_AGG(tags.tag ORDER BY tag)')).to_h
|
|
219
|
-
# end
|
|
247
|
+
# attribute :likes_count, batch: true
|
|
248
|
+
# ```
|
|
220
249
|
#
|
|
221
|
-
#
|
|
250
|
+
# See [Batch loading](https://github.com/andreyruby/serega/blob/master/docs/batch-loading.md).
|
|
222
251
|
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
|
|
226
260
|
#
|
|
227
|
-
#
|
|
228
|
-
#
|
|
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.
|
|
229
266
|
#
|
|
230
|
-
#
|
|
267
|
+
# ```ruby
|
|
268
|
+
# formatter :string, :to_s
|
|
269
|
+
# formatter :iso8601, [:iso8601, 3]
|
|
270
|
+
# formatter(:money) { |cents, ctx| "#{cents / 100.0} #{ctx[:currency]}" }
|
|
231
271
|
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
# @param block [Proc] Batch loader
|
|
272
|
+
# attribute :created_at, format: :iso8601
|
|
273
|
+
# ```
|
|
235
274
|
#
|
|
236
|
-
#
|
|
275
|
+
# See [Formatters](https://github.com/andreyruby/serega/blob/master/docs/formatters.md).
|
|
237
276
|
#
|
|
238
|
-
def
|
|
239
|
-
|
|
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
|
|
240
282
|
|
|
241
|
-
|
|
242
|
-
|
|
283
|
+
value ||= block
|
|
284
|
+
SeregaValidations::CheckFormatter.call(name, value)
|
|
285
|
+
formatters[name] = SeregaFormatter.new({use: value, allow_nil: allow_nil})
|
|
243
286
|
end
|
|
244
287
|
|
|
245
288
|
#
|
|
246
|
-
# Defines presenter methods
|
|
247
|
-
# presenter class
|
|
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.
|
|
248
292
|
#
|
|
249
|
-
#
|
|
250
|
-
#
|
|
251
|
-
# [first_name, last_name].compact.join(" ")
|
|
252
|
-
# end
|
|
253
|
-
# end
|
|
293
|
+
# ```ruby
|
|
294
|
+
# attribute :name
|
|
254
295
|
#
|
|
255
|
-
#
|
|
296
|
+
# presenter do
|
|
297
|
+
# def name
|
|
298
|
+
# [first_name, last_name].compact.join(" ")
|
|
299
|
+
# end
|
|
300
|
+
# end
|
|
301
|
+
# ```
|
|
256
302
|
#
|
|
257
|
-
#
|
|
258
|
-
# nil when the serializer has none
|
|
303
|
+
# See [Presenter](https://github.com/andreyruby/serega/blob/master/docs/presenter.md).
|
|
259
304
|
#
|
|
260
305
|
def presenter(&block)
|
|
261
306
|
return @presenter_class unless block
|
|
262
307
|
|
|
308
|
+
check_unlocked
|
|
263
309
|
@presenter_class ||= Class.new(SeregaPresenter)
|
|
264
310
|
@presenter_class.class_exec(&block)
|
|
265
311
|
presenter_blocks << block
|
|
@@ -267,31 +313,23 @@ class Serega
|
|
|
267
313
|
end
|
|
268
314
|
|
|
269
315
|
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
# objects and that attribute's preloads. ORM plugins register a handler
|
|
275
|
-
# that performs the actual eager loading.
|
|
276
|
-
#
|
|
277
|
-
# @example with a block
|
|
278
|
-
# preload_with { |objects, preloads| MyORM.eager_load(objects, preloads) }
|
|
279
|
-
#
|
|
280
|
-
# @example with a callable value
|
|
281
|
-
# preload_with MyPreloader
|
|
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.
|
|
282
320
|
#
|
|
283
|
-
#
|
|
284
|
-
#
|
|
321
|
+
# ```ruby
|
|
322
|
+
# preload_with { |objects, preloads| RecordPreloader.call(objects, preloads) }
|
|
323
|
+
# preload_with AppPreloader
|
|
324
|
+
# ```
|
|
285
325
|
#
|
|
286
|
-
#
|
|
326
|
+
# See [Preloads](https://github.com/andreyruby/serega/blob/master/docs/preloads.md).
|
|
287
327
|
#
|
|
288
328
|
def preload_with(value = nil, &block)
|
|
289
329
|
return @preload_with if value.nil? && block.nil?
|
|
290
|
-
raise SeregaError, "preload_with accepts a single callable or a block, not both" if value && block
|
|
291
|
-
|
|
292
|
-
handler = value || block
|
|
293
|
-
raise SeregaError, "preload_with value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
|
|
294
330
|
|
|
331
|
+
check_unlocked
|
|
332
|
+
handler = checked_handler(:preload_with, value, block)
|
|
295
333
|
signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2)
|
|
296
334
|
raise SeregaError, "preload_with handler must accept two positional arguments: (objects, preloads)" unless signature == "2"
|
|
297
335
|
|
|
@@ -299,45 +337,25 @@ class Serega
|
|
|
299
337
|
end
|
|
300
338
|
|
|
301
339
|
#
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
# serialized instead of the provided objects, which allows to accept ids or
|
|
308
|
-
# other references and load the records in one place.
|
|
309
|
-
#
|
|
310
|
-
# The handler runs before the `:many` option is detected, so it may turn a
|
|
311
|
-
# single object into a collection and back. A provided `:many` serialization
|
|
312
|
-
# option is still used as is.
|
|
313
|
-
#
|
|
314
|
-
# The handler runs only for the serialized objects, and not for objects of
|
|
315
|
-
# nested serializers.
|
|
316
|
-
#
|
|
317
|
-
# @example with a block
|
|
318
|
-
# prepare_initial_objects { |user_ids| User.where(id: user_ids) }
|
|
319
|
-
#
|
|
320
|
-
# @example with a context
|
|
321
|
-
# prepare_initial_objects { |user_ids, ctx| User.where(id: user_ids, account: ctx[:account]) }
|
|
322
|
-
#
|
|
323
|
-
# @example with a keyword context
|
|
324
|
-
# prepare_initial_objects { |user_ids, ctx:| User.where(id: user_ids, account: ctx[:account]) }
|
|
325
|
-
#
|
|
326
|
-
# @example with a callable value
|
|
327
|
-
# prepare_initial_objects UsersLoader
|
|
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.
|
|
328
345
|
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
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
|
+
# ```
|
|
331
351
|
#
|
|
332
|
-
#
|
|
352
|
+
# See [Prepare initial objects](https://github.com/andreyruby/serega/blob/master/docs/prepare-initial-objects.md).
|
|
333
353
|
#
|
|
334
354
|
def prepare_initial_objects(value = nil, &block)
|
|
335
355
|
return @prepare_initial_objects if value.nil? && block.nil?
|
|
336
|
-
raise SeregaError, "prepare_initial_objects accepts a single callable or a block, not both" if value && block
|
|
337
|
-
|
|
338
|
-
handler = value || block
|
|
339
|
-
raise SeregaError, "prepare_initial_objects value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
|
|
340
356
|
|
|
357
|
+
check_unlocked
|
|
358
|
+
handler = checked_handler(:prepare_initial_objects, value, block)
|
|
341
359
|
signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2, keyword_args: [:ctx])
|
|
342
360
|
raise SeregaError, prepare_initial_objects_signature_error unless %w[1 2 1_ctx].include?(signature)
|
|
343
361
|
|
|
@@ -345,133 +363,176 @@ class Serega
|
|
|
345
363
|
@prepare_initial_objects = handler
|
|
346
364
|
end
|
|
347
365
|
|
|
348
|
-
#
|
|
349
366
|
# Signature of the registered prepare_initial_objects handler
|
|
350
|
-
#
|
|
351
|
-
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
def prepare_initial_objects_signature
|
|
355
|
-
@prepare_initial_objects_signature
|
|
356
|
-
end
|
|
367
|
+
attr_reader :prepare_initial_objects_signature # :nodoc:
|
|
368
|
+
|
|
369
|
+
# Patched in:
|
|
370
|
+
# - plugin :root (takes the options of plugins)
|
|
357
371
|
|
|
358
372
|
#
|
|
359
|
-
# Serializes
|
|
373
|
+
# Serializes an object or a list to a Hash or an Array of Hashes. `nil`
|
|
374
|
+
# serializes to `nil`.
|
|
375
|
+
#
|
|
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.
|
|
360
383
|
#
|
|
361
|
-
#
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
#
|
|
365
|
-
#
|
|
366
|
-
# @option opts [Boolean] :validate Validates provided modifiers (Default is true)
|
|
367
|
-
# @option opts [Hash] :context Serialization context
|
|
368
|
-
# @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
|
+
# ```
|
|
369
389
|
#
|
|
370
|
-
#
|
|
390
|
+
# See [Serializing](https://github.com/andreyruby/serega/blob/master/docs/serializing.md).
|
|
371
391
|
#
|
|
372
|
-
def call(object,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
serialize_opts = init_serialize_opts(opts)
|
|
376
|
-
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)
|
|
377
395
|
end
|
|
378
396
|
|
|
397
|
+
# Patched in:
|
|
398
|
+
# - plugin :root (takes the options of plugins)
|
|
399
|
+
|
|
379
400
|
#
|
|
380
|
-
# Serializes
|
|
381
|
-
#
|
|
382
|
-
# @param object [Object] Serialized object
|
|
383
|
-
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
|
|
384
|
-
# @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
|
|
385
|
-
# @option opts [Array, Hash, String, Symbol] :except Attributes to hide
|
|
386
|
-
# @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
|
|
387
|
-
# @option opts [Boolean] :validate Validates provided modifiers (Default is true)
|
|
388
|
-
# @option opts [Hash] :context Serialization context
|
|
389
|
-
# @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
|
|
390
402
|
#
|
|
391
|
-
#
|
|
403
|
+
# ```ruby
|
|
404
|
+
# UserSerializer.to_data(user) # => #<data first_name="Bruce">
|
|
405
|
+
# ```
|
|
392
406
|
#
|
|
393
|
-
def to_data(object,
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
serialize_opts = init_serialize_opts(opts)
|
|
397
|
-
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)
|
|
398
410
|
end
|
|
399
411
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
private
|
|
403
|
-
|
|
404
|
-
def init_modifier_opts(opts)
|
|
405
|
-
(!opts || opts.empty?) ? FROZEN_EMPTY_HASH : opts.slice(*config.initiate_keys)
|
|
406
|
-
end
|
|
412
|
+
# Patched in:
|
|
413
|
+
# - plugin :root (takes the options of plugins)
|
|
407
414
|
|
|
408
|
-
|
|
409
|
-
|
|
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)
|
|
410
425
|
end
|
|
411
426
|
|
|
412
427
|
# Patched in:
|
|
413
|
-
# - plugin :
|
|
414
|
-
def inherited(subclass)
|
|
415
|
-
config_class = Class.new(self::SeregaConfig)
|
|
416
|
-
config_class.serializer_class = subclass
|
|
417
|
-
subclass.const_set(:SeregaConfig, config_class)
|
|
418
|
-
subclass.instance_variable_set(:@config, subclass::SeregaConfig.new(config.opts))
|
|
419
|
-
|
|
420
|
-
attribute_class = Class.new(self::SeregaAttribute)
|
|
421
|
-
attribute_class.serializer_class = subclass
|
|
422
|
-
subclass.const_set(:SeregaAttribute, attribute_class)
|
|
428
|
+
# - plugin :root (takes the options of plugins)
|
|
423
429
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
427
442
|
|
|
428
|
-
|
|
429
|
-
data_builder_class.serializer_class = subclass
|
|
430
|
-
subclass.const_set(:SeregaDataBuilder, data_builder_class)
|
|
443
|
+
alias_method :to_h, :call
|
|
431
444
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
435
449
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
subclass.const_set(:SeregaPlanPoint, plan_point_class)
|
|
450
|
+
LOCK_MUTEX.synchronize do
|
|
451
|
+
next if @locked
|
|
439
452
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
453
|
+
SeregaUtils::EnumDeepFreeze.call(config.opts)
|
|
454
|
+
define_presenter_delegators if @presenter_class
|
|
455
|
+
@locked = true
|
|
456
|
+
end
|
|
457
|
+
end
|
|
443
458
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
447
497
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
498
|
+
# Shows whether `make_shareable!` was called
|
|
499
|
+
def shareable? # :nodoc:
|
|
500
|
+
@shareable
|
|
501
|
+
end
|
|
451
502
|
|
|
452
|
-
|
|
453
|
-
check_initiate_params_class.serializer_class = subclass
|
|
454
|
-
subclass.const_set(:CheckInitiateParams, check_initiate_params_class)
|
|
503
|
+
private
|
|
455
504
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
459
512
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
463
517
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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)
|
|
469
524
|
|
|
470
|
-
# Assign same batch loaders
|
|
525
|
+
# Assign same batch loaders before the attributes that use them
|
|
471
526
|
batch_loaders.each_value do |loader|
|
|
472
527
|
subclass.batch(loader.name, loader.block)
|
|
473
528
|
end
|
|
474
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
|
+
|
|
475
536
|
# Assign same preload handler
|
|
476
537
|
subclass.preload_with(preload_with) if preload_with
|
|
477
538
|
|
|
@@ -486,6 +547,38 @@ class Serega
|
|
|
486
547
|
super
|
|
487
548
|
end
|
|
488
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
|
+
|
|
489
582
|
def prepare_initial_objects_signature_error
|
|
490
583
|
<<~ERR.strip
|
|
491
584
|
prepare_initial_objects handler arguments should have one of this signatures:
|
|
@@ -497,118 +590,103 @@ class Serega
|
|
|
497
590
|
end
|
|
498
591
|
|
|
499
592
|
#
|
|
500
|
-
#
|
|
593
|
+
# Instance methods of serializers. An instance keeps its field selection
|
|
594
|
+
# and serializes many times.
|
|
501
595
|
#
|
|
502
596
|
module InstanceMethods
|
|
503
597
|
#
|
|
504
|
-
#
|
|
505
|
-
#
|
|
506
|
-
#
|
|
507
|
-
#
|
|
508
|
-
#
|
|
509
|
-
#
|
|
510
|
-
#
|
|
511
|
-
#
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
parse_modifiers(opts)
|
|
519
|
-
end
|
|
520
|
-
|
|
521
|
-
self.class::CheckInitiateParams.new(@opts).validate if opts&.fetch(:check_initiate_params) { config.check_initiate_params }
|
|
522
|
-
|
|
523
|
-
@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
|
|
524
612
|
end
|
|
525
613
|
|
|
526
|
-
#
|
|
527
614
|
# Plan for serialization.
|
|
528
615
|
# This plan can be traversed to find serialized attributes and nested attributes.
|
|
529
|
-
#
|
|
530
|
-
|
|
531
|
-
#
|
|
532
|
-
|
|
616
|
+
attr_reader :plan # :nodoc:
|
|
617
|
+
|
|
618
|
+
# Patched in:
|
|
619
|
+
# - plugin :root (takes the options of plugins)
|
|
533
620
|
|
|
534
621
|
#
|
|
535
|
-
# Serializes
|
|
536
|
-
#
|
|
537
|
-
# @param object [Object] Serialized object
|
|
538
|
-
# @param opts [Hash, nil] Serializing options
|
|
539
|
-
# @option opts [Hash] :context Serialization context
|
|
540
|
-
# @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
|
|
541
624
|
#
|
|
542
|
-
#
|
|
625
|
+
# ```ruby
|
|
626
|
+
# UserSerializer.new.to_h(user, context: {admin: true})
|
|
627
|
+
# ```
|
|
543
628
|
#
|
|
544
|
-
def call(object,
|
|
545
|
-
|
|
546
|
-
object
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
|
550
635
|
|
|
551
|
-
|
|
552
|
-
def to_h(object, opts = nil)
|
|
553
|
-
call(object, opts)
|
|
636
|
+
serialize_to(:hash, object, context, many, FROZEN_EMPTY_HASH)
|
|
554
637
|
end
|
|
555
638
|
|
|
556
|
-
|
|
557
|
-
|
|
639
|
+
alias_method :to_h, :call
|
|
640
|
+
|
|
558
641
|
# Patched in:
|
|
559
|
-
# - plugin :root (
|
|
560
|
-
|
|
561
|
-
# @param object [Object] Serialized object
|
|
562
|
-
# @param opts [Hash, nil] Serializing options
|
|
563
|
-
# @option opts [Hash] :context Serialization context
|
|
564
|
-
# @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
|
+
|
|
565
644
|
#
|
|
566
|
-
#
|
|
645
|
+
# Serializes like #to_h, to `Data` objects
|
|
567
646
|
#
|
|
568
|
-
def to_data(object,
|
|
569
|
-
|
|
570
|
-
object = prepare_objects(object, opts[:context])
|
|
571
|
-
opts = prepare_initial_serialization_opts(object, opts)
|
|
572
|
-
serialized_data = serialize(object, opts)
|
|
573
|
-
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)
|
|
574
649
|
end
|
|
575
650
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
attr_reader :opts
|
|
651
|
+
# Patched in:
|
|
652
|
+
# - plugin :root (takes the options of plugins)
|
|
579
653
|
|
|
580
|
-
|
|
581
|
-
|
|
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)
|
|
582
659
|
end
|
|
583
660
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
opts.each do |key, value|
|
|
588
|
-
value = parse_modifier(value) if (key == :only) || (key == :except) || (key == :with)
|
|
589
|
-
result[key] = value
|
|
590
|
-
end
|
|
661
|
+
# Patched in:
|
|
662
|
+
# - plugin :root (takes the options of plugins)
|
|
591
663
|
|
|
592
|
-
|
|
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))
|
|
593
670
|
end
|
|
594
671
|
|
|
595
|
-
|
|
596
|
-
# - plugin :string_modifiers (parses string modifiers differently)
|
|
597
|
-
def parse_modifier(value)
|
|
598
|
-
SeregaUtils::ToHash.call(value)
|
|
599
|
-
end
|
|
672
|
+
private
|
|
600
673
|
|
|
601
|
-
def
|
|
602
|
-
|
|
603
|
-
|
|
674
|
+
def config
|
|
675
|
+
self.class.config
|
|
676
|
+
end
|
|
604
677
|
|
|
605
|
-
|
|
606
|
-
|
|
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)
|
|
607
686
|
end
|
|
608
687
|
|
|
609
688
|
def prepare_objects(objects, context)
|
|
610
689
|
handler = self.class.prepare_initial_objects
|
|
611
|
-
return objects unless handler
|
|
612
690
|
|
|
613
691
|
case self.class.prepare_initial_objects_signature
|
|
614
692
|
when "1" then handler.call(objects)
|
|
@@ -617,24 +695,17 @@ class Serega
|
|
|
617
695
|
end
|
|
618
696
|
end
|
|
619
697
|
|
|
620
|
-
def prepare_initial_serialization_opts(object, opts)
|
|
621
|
-
opts[:level_queue] = SeregaEngine::LevelQueue.new
|
|
622
|
-
opts[:many] = SeregaUtils::CollectionDetector.call(object) unless opts.key?(:many)
|
|
623
|
-
opts[:plan] = plan
|
|
624
|
-
opts
|
|
625
|
-
end
|
|
626
|
-
|
|
627
698
|
# Patched in:
|
|
628
699
|
# - plugin :root (wraps result `{ root => result }`)
|
|
629
700
|
# - plugin :context_metadata (adds context metadata to final result)
|
|
630
701
|
# - plugin :metadata (adds metadata to final result)
|
|
631
|
-
def serialize(object,
|
|
632
|
-
|
|
633
|
-
opts[:level_queue].run
|
|
634
|
-
result
|
|
702
|
+
def serialize(object, _opts, context, many, mode)
|
|
703
|
+
SeregaEngine::Run.call(@plan, object, many, mode, context)
|
|
635
704
|
end
|
|
636
705
|
end
|
|
637
706
|
|
|
638
707
|
extend ClassMethods
|
|
639
708
|
include InstanceMethods
|
|
709
|
+
|
|
710
|
+
initialize_definition(SeregaConfig.new, {})
|
|
640
711
|
end
|