panko_serializer 0.8.5 → 1.0.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 +6 -4
- data/lib/panko/array_serializer.rb +55 -16
- data/lib/panko/code_gen/active_record/access_classifier.rb +65 -0
- data/lib/panko/code_gen/active_record/define_attribute_methods.rb +13 -0
- data/lib/panko/code_gen/code_builder.rb +32 -0
- data/lib/panko/code_gen/compile_cache.rb +28 -0
- data/lib/panko/code_gen/compiler.rb +56 -0
- data/lib/panko/code_gen/config.rb +79 -0
- data/lib/panko/code_gen/datetime_format.rb +38 -0
- data/lib/panko/code_gen/descriptor.rb +136 -0
- data/lib/panko/code_gen/descriptor_builder.rb +166 -0
- data/lib/panko/code_gen/dump.rb +52 -0
- data/lib/panko/code_gen/errors.rb +21 -0
- data/lib/panko/code_gen/filter.rb +29 -0
- data/lib/panko/code_gen/filter_adapter.rb +44 -0
- data/lib/panko/code_gen/filters/indexed.rb +62 -0
- data/lib/panko/code_gen/filters/none.rb +19 -0
- data/lib/panko/code_gen/generator.rb +23 -0
- data/lib/panko/code_gen/generators/banner.rb +27 -0
- data/lib/panko/code_gen/generators/class_emitter.rb +120 -0
- data/lib/panko/code_gen/generators/cycle_membership.rb +53 -0
- data/lib/panko/code_gen/generators/descriptor_walk.rb +22 -0
- data/lib/panko/code_gen/generators/fanout.rb +60 -0
- data/lib/panko/code_gen/generators/field_index.rb +31 -0
- data/lib/panko/code_gen/generators/field_walk.rb +38 -0
- data/lib/panko/code_gen/generators/generated_names.rb +73 -0
- data/lib/panko/code_gen/generators/hash_sink.rb +217 -0
- data/lib/panko/code_gen/generators/json_sink.rb +290 -0
- data/lib/panko/code_gen/generators/record_access/generic.rb +92 -0
- data/lib/panko/code_gen/generators/record_access/specialized.rb +90 -0
- data/lib/panko/code_gen/generators/release.rb +60 -0
- data/lib/panko/code_gen/generators/sink.rb +80 -0
- data/lib/panko/code_gen/instance_pool.rb +27 -0
- data/lib/panko/code_gen/runtime.rb +27 -0
- data/lib/panko/code_gen/serializer_cache.rb +242 -0
- data/lib/panko/code_gen/validators/callable_arity.rb +42 -0
- data/lib/panko/code_gen/validators/name_uniqueness.rb +44 -0
- data/lib/panko/code_gen/validators/source_resolution.rb +48 -0
- data/lib/panko/code_gen/validators/validator.rb +19 -0
- data/lib/panko/code_gen/writers_pool.rb +51 -0
- data/lib/panko/code_gen.rb +82 -0
- data/lib/panko/compile_all.rb +80 -0
- data/lib/panko/compile_all_result.rb +21 -0
- data/lib/panko/config.rb +71 -0
- data/lib/panko/descriptor.rb +93 -0
- data/lib/panko/response.rb +40 -8
- data/lib/panko/serializer.rb +175 -91
- data/lib/panko/serializer_resolver.rb +2 -8
- data/lib/panko/version.rb +1 -1
- data/lib/panko_serializer.rb +3 -5
- metadata +49 -35
- data/ext/panko_serializer/attributes_writer/active_record.c +0 -181
- data/ext/panko_serializer/attributes_writer/active_record.h +0 -17
- data/ext/panko_serializer/attributes_writer/attributes_writer.c +0 -55
- data/ext/panko_serializer/attributes_writer/attributes_writer.h +0 -35
- data/ext/panko_serializer/attributes_writer/common.c +0 -10
- data/ext/panko_serializer/attributes_writer/common.h +0 -9
- data/ext/panko_serializer/attributes_writer/hash.c +0 -13
- data/ext/panko_serializer/attributes_writer/hash.h +0 -7
- data/ext/panko_serializer/attributes_writer/plain.c +0 -13
- data/ext/panko_serializer/attributes_writer/plain.h +0 -7
- data/ext/panko_serializer/attributes_writer/type_cast/time_conversion.c +0 -156
- data/ext/panko_serializer/attributes_writer/type_cast/time_conversion.h +0 -10
- data/ext/panko_serializer/attributes_writer/type_cast/type_cast.c +0 -409
- data/ext/panko_serializer/attributes_writer/type_cast/type_cast.h +0 -81
- data/ext/panko_serializer/common.h +0 -9
- data/ext/panko_serializer/extconf.rb +0 -28
- data/ext/panko_serializer/panko_serializer.c +0 -181
- data/ext/panko_serializer/panko_serializer.h +0 -12
- data/ext/panko_serializer/serialization_descriptor/association.c +0 -90
- data/ext/panko_serializer/serialization_descriptor/association.h +0 -20
- data/ext/panko_serializer/serialization_descriptor/attribute.c +0 -96
- data/ext/panko_serializer/serialization_descriptor/attribute.h +0 -27
- data/ext/panko_serializer/serialization_descriptor/serialization_descriptor.c +0 -177
- data/ext/panko_serializer/serialization_descriptor/serialization_descriptor.h +0 -30
- data/lib/panko/association.rb +0 -17
- data/lib/panko/attribute.rb +0 -29
- data/lib/panko/object_writer.rb +0 -68
- data/lib/panko/serialization_descriptor.rb +0 -180
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panko::CodeGen
|
|
4
|
+
module Generators
|
|
5
|
+
class JsonSink < Sink
|
|
6
|
+
def output
|
|
7
|
+
:json
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def suffix
|
|
11
|
+
"JSON"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def entry_name
|
|
15
|
+
GeneratedNames.write_one
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generic_entry_name
|
|
19
|
+
GeneratedNames.generic_write_one
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def split_hash_helper
|
|
23
|
+
GeneratedNames.write_one_hash
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def split_object_helper
|
|
27
|
+
GeneratedNames.write_one_object
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def entry_params
|
|
31
|
+
"record, writer, context, scope, filters"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def emit_class_constants(descriptor, config, builder)
|
|
35
|
+
return unless config.pool_writer
|
|
36
|
+
subclass = defined?(ActiveSupport::IsolatedExecutionState) ? "IsolatedExecutionState" : "ThreadLocal"
|
|
37
|
+
builder.line "POOL = Panko::CodeGen::WritersPool::#{subclass}.new"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def emit_serialize_one(config, builder)
|
|
41
|
+
signature = config.supports_root_key ?
|
|
42
|
+
"def serialize_one(record, context: nil, scope: nil, filters: nil, root_key: nil)" :
|
|
43
|
+
"def serialize_one(record, context: nil, scope: nil, filters: nil)"
|
|
44
|
+
builder.line signature
|
|
45
|
+
builder.indent do
|
|
46
|
+
builder.line "filters = Panko::CodeGen::Filter.wrap(filters, #{GeneratedNames.field_index_const})"
|
|
47
|
+
if config.supports_root_key
|
|
48
|
+
builder.line "validate_root_key!(root_key)"
|
|
49
|
+
end
|
|
50
|
+
with_writer(config, builder) { emit_serialize_one_body(config, builder) }
|
|
51
|
+
end
|
|
52
|
+
builder.line "end"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def emit_serialize_many(config, builder)
|
|
56
|
+
signature = config.supports_root_key ?
|
|
57
|
+
"def serialize_many(records, context: nil, scope: nil, filters: nil, root_key: nil)" :
|
|
58
|
+
"def serialize_many(records, context: nil, scope: nil, filters: nil)"
|
|
59
|
+
builder.line signature
|
|
60
|
+
builder.indent do
|
|
61
|
+
builder.line "filters = Panko::CodeGen::Filter.wrap(filters, #{GeneratedNames.field_index_const})"
|
|
62
|
+
if config.supports_root_key
|
|
63
|
+
builder.line "validate_root_key!(root_key)"
|
|
64
|
+
end
|
|
65
|
+
with_writer(config, builder) { emit_serialize_many_body(config, builder) }
|
|
66
|
+
end
|
|
67
|
+
builder.line "end"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Write into a writer the caller owns, so a JSON document that holds this output
|
|
71
|
+
# (Panko::Response) gets it without a String copy. No pooled writer, no root key.
|
|
72
|
+
def emit_serialize_to(config, builder)
|
|
73
|
+
builder.line "def serialize_one_to(writer, record, key = nil, context: nil, scope: nil, filters: nil)"
|
|
74
|
+
builder.indent do
|
|
75
|
+
builder.line "filters = Panko::CodeGen::Filter.wrap(filters, #{GeneratedNames.field_index_const})"
|
|
76
|
+
builder.line "writer.push_key(key) if key"
|
|
77
|
+
builder.line "#{GeneratedNames.write_one}(record, writer, context, scope, filters)"
|
|
78
|
+
builder.line "nil"
|
|
79
|
+
end
|
|
80
|
+
builder.line "end"
|
|
81
|
+
builder.blank
|
|
82
|
+
builder.line "def serialize_many_to(writer, records, key = nil, context: nil, scope: nil, filters: nil)"
|
|
83
|
+
builder.indent do
|
|
84
|
+
builder.line "filters = Panko::CodeGen::Filter.wrap(filters, #{GeneratedNames.field_index_const})"
|
|
85
|
+
builder.line "writer.push_array(key)"
|
|
86
|
+
builder.line "records.each { |r| #{GeneratedNames.write_one}(r, writer, context, scope, filters) }"
|
|
87
|
+
builder.line "writer.pop"
|
|
88
|
+
builder.line "nil"
|
|
89
|
+
end
|
|
90
|
+
builder.line "end"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def open_record(builder)
|
|
94
|
+
builder.line "writer.push_object"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def close_record(builder)
|
|
98
|
+
builder.line "writer.pop"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# +cast:+ exists to match {HashSink#attribute} and is ignored: Oj's +:rails+ mode formats
|
|
102
|
+
# JSON values.
|
|
103
|
+
def attribute(attribute, read_expr, config, index, builder, cast: true)
|
|
104
|
+
builder.line "unless filters.drops?(#{index})"
|
|
105
|
+
builder.indent do
|
|
106
|
+
builder.line %(writer.push_value(#{read_expr}, "#{attribute.name}"))
|
|
107
|
+
end
|
|
108
|
+
builder.line "end"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def specialized_attribute(attribute, ar_model, config, index, builder)
|
|
112
|
+
if RecordAccess::Specialized.json_column_attribute?(attribute, ar_model)
|
|
113
|
+
json_column_attribute(attribute, config, index, builder)
|
|
114
|
+
elsif RecordAccess::Specialized.datetime_column_attribute?(attribute, ar_model)
|
|
115
|
+
datetime_column_attribute(attribute, index, builder)
|
|
116
|
+
else
|
|
117
|
+
attribute(attribute, RecordAccess::Specialized.attribute_read_expr(attribute, ar_model), config, index, builder)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# +equal?+, not +==+, so an object that overrides +==+ cannot match +SKIP+.
|
|
122
|
+
def method_attribute(method_attribute, config, index, builder)
|
|
123
|
+
builder.line "unless filters.drops?(#{index})"
|
|
124
|
+
builder.indent do
|
|
125
|
+
builder.line "value = #{method_attribute_call_expression(method_attribute)}"
|
|
126
|
+
builder.line "unless value.equal?(Panko::CodeGen::SKIP)"
|
|
127
|
+
builder.indent do
|
|
128
|
+
builder.line %(writer.push_value(value, "#{method_attribute.name}"))
|
|
129
|
+
end
|
|
130
|
+
builder.line "end"
|
|
131
|
+
end
|
|
132
|
+
builder.line "end"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def association(association, source_read_expr, config, index, builder)
|
|
136
|
+
builder.line "unless filters.drops?(#{index})"
|
|
137
|
+
builder.indent do
|
|
138
|
+
with_if_guard(association, builder) do
|
|
139
|
+
case association.kind
|
|
140
|
+
when :has_one
|
|
141
|
+
builder.line "value = #{source_read_expr}"
|
|
142
|
+
if config.null_for_missing_has_one
|
|
143
|
+
has_one_default(association, builder)
|
|
144
|
+
else
|
|
145
|
+
has_one_omit(association, builder)
|
|
146
|
+
end
|
|
147
|
+
when :has_many
|
|
148
|
+
has_many(association, source_read_expr, builder)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
builder.line "end"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
# +result+ starts nil so a body that raises hands checkin nil, and the writer is dropped.
|
|
158
|
+
def with_writer(config, builder, &block)
|
|
159
|
+
if config.pool_writer
|
|
160
|
+
builder.line "writer = POOL.checkout"
|
|
161
|
+
builder.line "result = nil"
|
|
162
|
+
builder.line "begin"
|
|
163
|
+
builder.indent(&block)
|
|
164
|
+
builder.line "ensure"
|
|
165
|
+
builder.indent do
|
|
166
|
+
builder.line "POOL.checkin(writer, result)"
|
|
167
|
+
end
|
|
168
|
+
builder.line "end"
|
|
169
|
+
else
|
|
170
|
+
builder.line "writer = Oj::StringWriter.new(mode: :rails)"
|
|
171
|
+
yield
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def emit_serialize_one_body(config, builder)
|
|
176
|
+
if config.supports_root_key
|
|
177
|
+
builder.line "if root_key"
|
|
178
|
+
builder.indent do
|
|
179
|
+
builder.line "writer.push_object"
|
|
180
|
+
builder.line "writer.push_key(root_key)"
|
|
181
|
+
end
|
|
182
|
+
builder.line "end"
|
|
183
|
+
end
|
|
184
|
+
builder.line "#{GeneratedNames.write_one}(record, writer, context, scope, filters)"
|
|
185
|
+
builder.line "writer.pop if root_key" if config.supports_root_key
|
|
186
|
+
builder.line "result = writer.to_s"
|
|
187
|
+
builder.line "result.chomp!"
|
|
188
|
+
builder.line "result"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def emit_serialize_many_body(config, builder)
|
|
192
|
+
if config.supports_root_key
|
|
193
|
+
builder.line "writer.push_object if root_key"
|
|
194
|
+
builder.line "writer.push_array(root_key)"
|
|
195
|
+
else
|
|
196
|
+
builder.line "writer.push_array"
|
|
197
|
+
end
|
|
198
|
+
builder.line "records.each { |r| #{GeneratedNames.write_one}(r, writer, context, scope, filters) }"
|
|
199
|
+
builder.line "writer.pop"
|
|
200
|
+
builder.line "writer.pop if root_key" if config.supports_root_key
|
|
201
|
+
builder.line "result = writer.to_s"
|
|
202
|
+
builder.line "result.chomp!"
|
|
203
|
+
builder.line "result"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# A non-nil +has_one+ pushes its key separately instead of +push_object("name")+: the
|
|
207
|
+
# child's +_write_one+ opens its own +push_object+ frame.
|
|
208
|
+
def has_one_default(association, builder)
|
|
209
|
+
builder.line "if value.nil?"
|
|
210
|
+
builder.indent { builder.line %(writer.push_value(nil, "#{association.name}")) }
|
|
211
|
+
builder.line "else"
|
|
212
|
+
builder.indent do
|
|
213
|
+
builder.line %(writer.push_key("#{association.name}"))
|
|
214
|
+
child_write(association, "value", builder) do |ivar|
|
|
215
|
+
"#{ivar}.#{GeneratedNames.write_one}(value, writer, context, scope, #{child_filter_expr(association)})"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
builder.line "end"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def has_one_omit(association, builder)
|
|
222
|
+
builder.line "unless value.nil?"
|
|
223
|
+
builder.indent do
|
|
224
|
+
builder.line %(writer.push_key("#{association.name}"))
|
|
225
|
+
child_write(association, "value", builder) do |ivar|
|
|
226
|
+
"#{ivar}.#{GeneratedNames.write_one}(value, writer, context, scope, #{child_filter_expr(association)})"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
builder.line "end"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def has_many(association, source_read_expr, builder)
|
|
233
|
+
builder.line "child_filter = #{child_filter_expr(association)}"
|
|
234
|
+
builder.line %(writer.push_array("#{association.name}"))
|
|
235
|
+
builder.line "#{source_read_expr}.each do |element|"
|
|
236
|
+
builder.indent do
|
|
237
|
+
child_write(association, "element", builder) do |ivar|
|
|
238
|
+
"#{ivar}.#{GeneratedNames.write_one}(element, writer, context, scope, child_filter)"
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
builder.line "end"
|
|
242
|
+
builder.line "writer.pop"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Raw column bytes are pushed only after a strict parse accepts them; anything else
|
|
246
|
+
# (non-String, empty, malformed) falls back to the typed read.
|
|
247
|
+
def json_column_attribute(attribute, config, index, builder)
|
|
248
|
+
if config.json_column_emit == :html_safe
|
|
249
|
+
attribute(attribute, %(record._read_attribute("#{attribute.source}")), config, index, builder)
|
|
250
|
+
return
|
|
251
|
+
end
|
|
252
|
+
source_lit = %("#{attribute.source}")
|
|
253
|
+
key_lit = %("#{attribute.name}")
|
|
254
|
+
builder.line "unless filters.drops?(#{index})"
|
|
255
|
+
builder.indent do
|
|
256
|
+
builder.line %(raw = record.read_attribute_before_type_cast(#{source_lit}))
|
|
257
|
+
builder.line "if raw.is_a?(String) && !raw.empty? && (begin"
|
|
258
|
+
builder.indent do
|
|
259
|
+
builder.line "Oj.sc_parse(Panko::CodeGen::JSON_NOOP_PARSER, raw, Panko::CodeGen::JSON_STRICT_PARSE_OPTS)"
|
|
260
|
+
builder.line "true"
|
|
261
|
+
end
|
|
262
|
+
builder.line "rescue Oj::ParseError, EncodingError"
|
|
263
|
+
builder.indent do
|
|
264
|
+
builder.line "false"
|
|
265
|
+
end
|
|
266
|
+
builder.line "end)"
|
|
267
|
+
builder.indent do
|
|
268
|
+
builder.line "writer.push_json(raw, #{key_lit})"
|
|
269
|
+
end
|
|
270
|
+
builder.line "else"
|
|
271
|
+
builder.indent do
|
|
272
|
+
builder.line "writer.push_value(record._read_attribute(#{source_lit}), #{key_lit})"
|
|
273
|
+
end
|
|
274
|
+
builder.line "end"
|
|
275
|
+
end
|
|
276
|
+
builder.line "end"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def datetime_column_attribute(attribute, index, builder)
|
|
280
|
+
source_lit = %("#{attribute.source}")
|
|
281
|
+
builder.line "unless filters.drops?(#{index})"
|
|
282
|
+
builder.indent do
|
|
283
|
+
builder.line "value = Panko::CodeGen::DateTimeFormat.format_raw(record.read_attribute_before_type_cast(#{source_lit}))"
|
|
284
|
+
builder.line %(writer.push_value(value || record._read_attribute(#{source_lit}), "#{attribute.name}"))
|
|
285
|
+
end
|
|
286
|
+
builder.line "end"
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panko::CodeGen
|
|
4
|
+
module Generators
|
|
5
|
+
module RecordAccess
|
|
6
|
+
# Both field bodies are inlined under one +is_a?(Hash)+ branch: this saves a call per record,
|
|
7
|
+
# and each arm owns its call sites, so a Hash and an object never share one inline cache.
|
|
8
|
+
module Generic
|
|
9
|
+
# Above this field count the two bodies go into separate helper methods, which halves each
|
|
10
|
+
# method's size for JITs that compile whole methods (ZJIT).
|
|
11
|
+
FUSED_DISPATCH_MAX_FIELDS = 64
|
|
12
|
+
|
|
13
|
+
def self.emit(descriptor, config, field_index, builder, sink, method_name: sink.entry_name)
|
|
14
|
+
return emit_split(descriptor, config, field_index, builder, sink, method_name: method_name) if split_dispatch?(descriptor)
|
|
15
|
+
|
|
16
|
+
builder.line "def #{method_name}(#{sink.entry_params})"
|
|
17
|
+
builder.indent do
|
|
18
|
+
emit_parent_class_ivar_writes(descriptor, builder)
|
|
19
|
+
builder.line "if record.is_a?(Hash)"
|
|
20
|
+
builder.indent do
|
|
21
|
+
FieldWalk.emit_fields(
|
|
22
|
+
descriptor, config, field_index, builder, sink,
|
|
23
|
+
read_expr: ->(source) { hash_read_expr(source, config) }
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
builder.line "else"
|
|
27
|
+
builder.indent do
|
|
28
|
+
FieldWalk.emit_fields(
|
|
29
|
+
descriptor, config, field_index, builder, sink,
|
|
30
|
+
read_expr: ->(source) { "record.#{source}" }
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
builder.line "end"
|
|
34
|
+
end
|
|
35
|
+
builder.line "end"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.emit_split(descriptor, config, field_index, builder, sink, method_name: sink.entry_name)
|
|
39
|
+
builder.line "def #{method_name}(#{sink.entry_params})"
|
|
40
|
+
builder.indent do
|
|
41
|
+
emit_parent_class_ivar_writes(descriptor, builder)
|
|
42
|
+
builder.line "if record.is_a?(Hash)"
|
|
43
|
+
builder.indent { builder.line "#{sink.split_hash_helper}(#{sink.entry_params})" }
|
|
44
|
+
builder.line "else"
|
|
45
|
+
builder.indent { builder.line "#{sink.split_object_helper}(#{sink.entry_params})" }
|
|
46
|
+
builder.line "end"
|
|
47
|
+
end
|
|
48
|
+
builder.line "end"
|
|
49
|
+
builder.blank
|
|
50
|
+
builder.line "def #{sink.split_hash_helper}(#{sink.entry_params})"
|
|
51
|
+
builder.indent do
|
|
52
|
+
FieldWalk.emit_fields(
|
|
53
|
+
descriptor, config, field_index, builder, sink,
|
|
54
|
+
read_expr: ->(source) { hash_read_expr(source, config) }
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
builder.line "end"
|
|
58
|
+
builder.blank
|
|
59
|
+
builder.line "def #{sink.split_object_helper}(#{sink.entry_params})"
|
|
60
|
+
builder.indent do
|
|
61
|
+
FieldWalk.emit_fields(
|
|
62
|
+
descriptor, config, field_index, builder, sink,
|
|
63
|
+
read_expr: ->(source) { "record.#{source}" }
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
builder.line "end"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.split_dispatch?(descriptor)
|
|
70
|
+
descriptor.attributes.size + descriptor.method_attributes.size + descriptor.associations.size >
|
|
71
|
+
FUSED_DISPATCH_MAX_FIELDS
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Only a Symbol-body Method Attribute reads +@object+ / +@context+ / +@scope+ on +self+;
|
|
75
|
+
# Callable bodies get them as arguments, so without one the writes are wasted work per record.
|
|
76
|
+
def self.emit_parent_class_ivar_writes(descriptor, builder)
|
|
77
|
+
return if descriptor.method_attributes.none? { |method_attribute| method_attribute.body.is_a?(Symbol) }
|
|
78
|
+
builder.line "@object = record"
|
|
79
|
+
builder.line "@context = context"
|
|
80
|
+
builder.line "@scope = scope"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.hash_read_expr(source_name, config)
|
|
84
|
+
case config.hash_record_key_type
|
|
85
|
+
when :symbol then "record[:#{source_name}]"
|
|
86
|
+
else %(record["#{source_name}"])
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panko::CodeGen
|
|
4
|
+
module Generators
|
|
5
|
+
module RecordAccess
|
|
6
|
+
# Records are assumed to be instances of the Descriptor's Model, so there is no Hash branch.
|
|
7
|
+
module Specialized
|
|
8
|
+
def self.emit(descriptor, config, field_index, builder, sink)
|
|
9
|
+
ar_model = ar_model(descriptor)
|
|
10
|
+
builder.line "def #{sink.entry_name}(#{sink.entry_params})"
|
|
11
|
+
builder.indent do
|
|
12
|
+
emit_model_guard(descriptor, config, "#{sink.generic_entry_name}(#{sink.entry_params})", builder)
|
|
13
|
+
Generic.emit_parent_class_ivar_writes(descriptor, builder)
|
|
14
|
+
FieldWalk.emit_fields(
|
|
15
|
+
descriptor, config, field_index, builder, sink,
|
|
16
|
+
read_expr: ->(source) { "record.#{source}" },
|
|
17
|
+
attribute_emit: ->(attribute, index) { sink.specialized_attribute(attribute, ar_model, config, index, builder) }
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
builder.line "end"
|
|
21
|
+
emit_generic_twin(config, builder) do
|
|
22
|
+
Generic.emit(descriptor, config, field_index, builder, sink, method_name: sink.generic_entry_name)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Under auto-specialization nothing promises every record has the Model's class, so a record of
|
|
27
|
+
# another class (Hash, STI sibling, mixed array) goes to the generic twin instead of wrong output.
|
|
28
|
+
def self.emit_model_guard(descriptor, config, fallback_call, builder)
|
|
29
|
+
return unless config.guarded_model
|
|
30
|
+
model_name = descriptor.model.name
|
|
31
|
+
if model_name.nil?
|
|
32
|
+
raise CompileError,
|
|
33
|
+
"#{descriptor.name}: guarded_model requires a named Model class; got anonymous #{descriptor.model.inspect}"
|
|
34
|
+
end
|
|
35
|
+
builder.line "return #{fallback_call} unless record.instance_of?(::#{model_name})"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.emit_generic_twin(config, builder)
|
|
39
|
+
return unless config.guarded_model
|
|
40
|
+
builder.blank
|
|
41
|
+
yield
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A method the Model does not declare as an attribute reads the loaded attribute first, so a
|
|
45
|
+
# SELECT alias named like a method (+thing_ids+ next to +has_many :things+) returns the selected value.
|
|
46
|
+
def self.attribute_read_expr(attribute, ar_model)
|
|
47
|
+
source = attribute.source
|
|
48
|
+
return "record.#{source}" if ar_model.nil?
|
|
49
|
+
return %(record._read_attribute("#{source}")) if ActiveRecord::AccessClassifier.classify(ar_model, source) == :column
|
|
50
|
+
return "record.#{source}" if declared_attribute?(ar_model, source)
|
|
51
|
+
%((record._has_attribute?("#{source}") ? record._read_attribute("#{source}") : record.#{source}))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.declared_attribute?(ar_model, source)
|
|
55
|
+
ar_model.attribute_types.key?(source.to_s) || ar_model.attribute_aliases.key?(source.to_s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Called here too so the Generator works without the validators; repeating +ensure!+ is safe.
|
|
59
|
+
def self.ar_model(descriptor)
|
|
60
|
+
model = descriptor.model
|
|
61
|
+
return nil unless model.respond_to?(:columns_hash) && model.respond_to?(:attribute_methods_generated?)
|
|
62
|
+
ActiveRecord::DefineAttributeMethods.ensure!(model)
|
|
63
|
+
model
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Requires a +:column+ result because the column fast paths read past a user-overridden reader.
|
|
67
|
+
def self.json_column_attribute?(attribute, ar_model)
|
|
68
|
+
return false if ar_model.nil?
|
|
69
|
+
return false unless ActiveRecord::AccessClassifier.json_typed?(ar_model, attribute.source)
|
|
70
|
+
ActiveRecord::AccessClassifier.classify(ar_model, attribute.source) == :column
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Only under +:utc+ is the zoneless raw DB string really UTC, so the fast path's trailing "Z" is true.
|
|
74
|
+
# The +::+ matters: bare +ActiveRecord+ here resolves to +Panko::CodeGen::ActiveRecord+.
|
|
75
|
+
def self.datetime_column_attribute?(attribute, ar_model)
|
|
76
|
+
return false if ar_model.nil?
|
|
77
|
+
return false unless ::ActiveRecord.default_timezone == :utc
|
|
78
|
+
return false unless ActiveRecord::AccessClassifier.datetime_typed?(ar_model, attribute.source)
|
|
79
|
+
ActiveRecord::AccessClassifier.classify(ar_model, attribute.source) == :column
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.plain_column_attribute?(attribute, ar_model)
|
|
83
|
+
return false if ar_model.nil?
|
|
84
|
+
return false unless ActiveRecord::AccessClassifier.plain_typed?(ar_model, attribute.source)
|
|
85
|
+
ActiveRecord::AccessClassifier.classify(ar_model, attribute.source) == :column
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panko::CodeGen
|
|
4
|
+
module Generators
|
|
5
|
+
# A pooled instance that kept +@object+ / +@context+ / +@scope+ after
|
|
6
|
+
# checkin would keep the last record graph alive in fiber-local storage.
|
|
7
|
+
module Release
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Must match the gate in +RecordAccess::Generic.emit_parent_class_ivar_writes+.
|
|
11
|
+
def ivar_writes?(descriptor)
|
|
12
|
+
descriptor.method_attributes.any? { |method_attribute| method_attribute.body.is_a?(Symbol) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Must follow the same edges as {emit} so the answer matches the
|
|
16
|
+
# emitted code.
|
|
17
|
+
def subtree_releases?(descriptor, cyclic_ids, seen = {})
|
|
18
|
+
return false if seen[descriptor.__id__]
|
|
19
|
+
seen[descriptor.__id__] = true
|
|
20
|
+
return true if ivar_writes?(descriptor)
|
|
21
|
+
descriptor.associations.flat_map(&:descriptors).any? do |child|
|
|
22
|
+
next false if child.equal?(descriptor)
|
|
23
|
+
next false if cyclic_ids[descriptor.__id__] && cyclic_ids[child.__id__]
|
|
24
|
+
subtree_releases?(child, cyclic_ids, seen)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def child_ivars(assoc)
|
|
29
|
+
variants = assoc.variants.each_with_index.map do |variant, index|
|
|
30
|
+
[GeneratedNames.variant_serializer_ivar(assoc, index), variant]
|
|
31
|
+
end
|
|
32
|
+
[[GeneratedNames.serializer_ivar(assoc), assoc.descriptor], *variants]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Every class defines +_release+, even an empty one: the serializers call it
|
|
36
|
+
# on every checkin without checking.
|
|
37
|
+
def emit(descriptor, builder, cyclic_ids)
|
|
38
|
+
builder.line "def _release"
|
|
39
|
+
builder.indent do
|
|
40
|
+
if ivar_writes?(descriptor)
|
|
41
|
+
builder.line "@object = nil"
|
|
42
|
+
builder.line "@context = nil"
|
|
43
|
+
builder.line "@scope = nil"
|
|
44
|
+
end
|
|
45
|
+
descriptor.associations.each do |assoc|
|
|
46
|
+
child_ivars(assoc).each do |ivar, child|
|
|
47
|
+
next if child.equal?(descriptor)
|
|
48
|
+
# Ends the chain without per-call visited state; such serializers keep their last record until the next call.
|
|
49
|
+
next if cyclic_ids[descriptor.__id__] && cyclic_ids[child.__id__]
|
|
50
|
+
next unless subtree_releases?(child, cyclic_ids)
|
|
51
|
+
builder.line "#{ivar}._release"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
builder.line "nil"
|
|
55
|
+
end
|
|
56
|
+
builder.line "end"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panko::CodeGen
|
|
4
|
+
module Generators
|
|
5
|
+
class Sink
|
|
6
|
+
# The child's +FIELD_INDEX+ is read through its class name when the method runs, so
|
|
7
|
+
# self-recursive Descriptors resolve it.
|
|
8
|
+
def child_filter_expr(association)
|
|
9
|
+
"filters.child(:#{GeneratedNames.filter_key(association)}, " \
|
|
10
|
+
"#{GeneratedNames.class_name(association.descriptor, suffix)}::#{GeneratedNames.field_index_const})"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# +case+/+when+, not an +if+/+elsif+ chain of +instance_of?+: under YJIT the chain gets
|
|
14
|
+
# slower once the call site has warmed on one class. HashSink uses the +case+ as an expression.
|
|
15
|
+
def child_write(association, record_expr, builder, &call_for)
|
|
16
|
+
base_call = call_for.call(GeneratedNames.serializer_ivar(association))
|
|
17
|
+
if association.variants.empty?
|
|
18
|
+
builder.line base_call
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
builder.line "case #{record_expr}"
|
|
22
|
+
subclass_first(association.variants).each do |variant, index|
|
|
23
|
+
model = "::#{variant.model.name}"
|
|
24
|
+
builder.line "when #{model}"
|
|
25
|
+
builder.indent do
|
|
26
|
+
builder.line "if #{record_expr}.instance_of?(#{model})"
|
|
27
|
+
builder.indent { builder.line call_for.call(GeneratedNames.variant_serializer_ivar(association, index)) }
|
|
28
|
+
builder.line "else"
|
|
29
|
+
builder.indent { builder.line base_call }
|
|
30
|
+
builder.line "end"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
builder.line "else"
|
|
34
|
+
builder.indent { builder.line base_call }
|
|
35
|
+
builder.line "end"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Each subclass before its ancestors, so a parent's +when+ arm does not take a declared
|
|
39
|
+
# subclass.
|
|
40
|
+
def subclass_first(variants)
|
|
41
|
+
variants.each_with_index.sort_by { |variant, index| [-variant.model.ancestors.size, index] }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The guard wraps the Source read too, so a falsy +if:+ skips the read.
|
|
45
|
+
def with_if_guard(association, builder)
|
|
46
|
+
if association.if
|
|
47
|
+
builder.line "if #{if_guard_call_expression(GeneratedNames.if_guard_ivar(association), association.if.arity)}"
|
|
48
|
+
builder.indent { yield }
|
|
49
|
+
builder.line "end"
|
|
50
|
+
else
|
|
51
|
+
yield
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The +callable_arity+ rule limits arity to +0..3+, so +else+ is arity 3.
|
|
56
|
+
def if_guard_call_expression(ivar, arity)
|
|
57
|
+
case arity
|
|
58
|
+
when 0 then "#{ivar}.call"
|
|
59
|
+
when 1 then "#{ivar}.call(record)"
|
|
60
|
+
when 2 then "#{ivar}.call(record, context)"
|
|
61
|
+
else "#{ivar}.call(record, context, scope)"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Symbol bodies need the explicit +self.+: a bare name such as +record+ or +value+ would
|
|
66
|
+
# resolve to a local of the generated method.
|
|
67
|
+
def method_attribute_call_expression(method_attribute)
|
|
68
|
+
body = method_attribute.body
|
|
69
|
+
return "self.#{body}" if body.is_a?(Symbol)
|
|
70
|
+
ivar = GeneratedNames.callable_ivar(method_attribute)
|
|
71
|
+
case body.arity
|
|
72
|
+
when 0 then "#{ivar}.call"
|
|
73
|
+
when 1 then "#{ivar}.call(record)"
|
|
74
|
+
when 2 then "#{ivar}.call(record, context)"
|
|
75
|
+
else "#{ivar}.call(record, context, scope)"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panko::CodeGen
|
|
4
|
+
# Fiber-local stack of Generated Class instances. An instance cannot be shared: writing a record
|
|
5
|
+
# can set +@object+/+@context+/+@scope+, so a nested serialize of the same class needs its own.
|
|
6
|
+
#
|
|
7
|
+
# After a code reload or SerializerCache.reset!, each fiber keeps its old stack (and the old
|
|
8
|
+
# class) until it ends. Accepted: reloads are a development feature and reset! is for tests.
|
|
9
|
+
class InstancePool
|
|
10
|
+
def initialize(key, compiled, descriptor)
|
|
11
|
+
# The pool's own object_id keeps a pool rebuilt after SerializerCache.reset! off the old
|
|
12
|
+
# pool's stacks, which hold instances of the old compiled class.
|
|
13
|
+
@key = :"#{key}_#{object_id}"
|
|
14
|
+
@compiled = compiled
|
|
15
|
+
@descriptor = descriptor
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns the stack itself so a checkout and checkin cost one +Thread.current+ lookup.
|
|
19
|
+
def stack
|
|
20
|
+
Thread.current[@key] ||= []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build
|
|
24
|
+
@compiled.new(descriptor: @descriptor)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../code_gen"
|
|
4
|
+
require_relative "serializer_cache"
|
|
5
|
+
require_relative "filter_adapter"
|
|
6
|
+
|
|
7
|
+
module Panko
|
|
8
|
+
module CodeGen
|
|
9
|
+
module Runtime
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def runtime_filters(serializer_class, context, scope, only, except)
|
|
13
|
+
if serializer_class.respond_to?(:filters_for)
|
|
14
|
+
overrides = serializer_class.filters_for(context, scope) || {}
|
|
15
|
+
only = overrides.fetch(:only, only)
|
|
16
|
+
except = overrides.fetch(:except, except)
|
|
17
|
+
end
|
|
18
|
+
return nil if blank?(only) && blank?(except)
|
|
19
|
+
FilterAdapter.to_engine_filters(blank?(only) ? nil : only, blank?(except) ? nil : except)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def blank?(filter)
|
|
23
|
+
filter.nil? || (filter.respond_to?(:empty?) && filter.empty?)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|