panko_serializer 1.0.0 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 933667f4549f885acffbbd822422f568a0b5ba737b05bc2f333299e9d2ba3085
4
- data.tar.gz: 20f6302ef51561dce29e0608ad2a22334c0d8625f62cf815d7d31fb9a30e49f6
3
+ metadata.gz: d979b96c2fb100900b8ee11656f8a9d8d78eecb28b58d3d6c26e1db63c34c829
4
+ data.tar.gz: 43f9c3badbeff01f514923de689c6fb300b72db18e3d0a751684009afcf559db
5
5
  SHA512:
6
- metadata.gz: a520312057f8f41687f6051faff37eb2061ca2b4d4939d54631b52250e120277c920b455480ca0977d4112a7f80200b960948c85895377574f0b324c1be976ac
7
- data.tar.gz: '0779d69d34164e6d6fd361909bc1a4395c047f80190f61753dbcff817bef8ddd415278dfccd863bfff53ff0d5558dce6b6137d8c47e3cd8a989ec7cac9860761'
6
+ metadata.gz: 6ec1e557bc67c0bcb2743582fd93a18410b05161c3ea760943cc33e65f1c571f7a3d0d99bdb25870a8a87177e19bdc652cea15a4d5da56e6b087df56bf80640f
7
+ data.tar.gz: 792a57264e47d36207515fe6c4d2bdd9d4fab7ba0e34fe8b6b558e997ae691897b78e36a07306075438fe680f667b6d67c71f3d701301c7c5abc353893388be4
@@ -4,7 +4,7 @@ module Panko::CodeGen
4
4
  module ActiveRecord
5
5
  # Columns read through AR's own generated reader classify as +:column+.
6
6
  # A user override of a column reader classifies as +:method+, so the
7
- # Specialized path never bypasses it. Call +DefineAttributeMethods.ensure!+
7
+ # Specialized path never bypasses it. Call +define_attribute_methods+
8
8
  # first, or non-column readers (+attribute+, +alias_attribute+) raise +UnknownSourceError+.
9
9
  module AccessClassifier
10
10
  def self.classify(klass, source)
@@ -5,28 +5,34 @@ module Panko::CodeGen
5
5
  # A table, not +to_s.upcase+: the +:hash+ suffix is +Hash+, not +HASH+.
6
6
  OUTPUT_SUFFIXES = {json: "JSON", hash: "Hash"}.freeze
7
7
 
8
- def initialize(descriptor, output:, config:, validator: Validators::Validator.new,
9
- generator: Generator.new)
8
+ def initialize(descriptor, output:, config:)
10
9
  @descriptor = descriptor
11
10
  @output = output
12
11
  @config = config
13
- @validator = validator
14
- @generator = generator
15
- @cache = CompileCache.new
12
+ # Identity, not equality: equal but distinct Descriptors compile to separate classes.
13
+ @compiled = {}.compare_by_identity
16
14
  end
17
15
 
18
16
  def compile
19
- @validator.validate(@descriptor, output: @output, config: @config)
20
- source = @generator.emit(@descriptor, output: @output, config: @config)
17
+ Validators::Validator.validate(@descriptor, output: @output, config: @config)
18
+ source = Generator.emit(@descriptor, output: @output, config: @config)
21
19
  namespace = Class.new
22
20
  bind_anonymous_parents(namespace)
23
- namespace.module_eval(source, synthetic_path, 1)
24
- cache_descendants(@descriptor, namespace)
25
- @cache.get(@descriptor)
21
+ evaluate(namespace, source)
22
+ collect_classes(@descriptor, namespace)
23
+ @compiled.fetch(@descriptor)
26
24
  end
27
25
 
28
26
  private
29
27
 
28
+ # SyntaxError is a ScriptError, which a bare +rescue+ (Panko.compile_all, the
29
+ # auto-specialization fallback) does not catch.
30
+ def evaluate(namespace, source)
31
+ namespace.module_eval(source, synthetic_path, 1)
32
+ rescue SyntaxError => error
33
+ raise CompileError, "#{@descriptor.name}: generated #{@output} source does not parse: #{error.message}"
34
+ end
35
+
30
36
  # A Hash, not one constant per class: +const_set+ gives an anonymous class a name,
31
37
  # which would change +parent_class.name+ for the next compile.
32
38
  def bind_anonymous_parents(namespace)
@@ -43,13 +49,12 @@ module Panko::CodeGen
43
49
  "(#{GENERATOR_NAME}: #{@descriptor.name}/#{@output})"
44
50
  end
45
51
 
46
- def cache_descendants(descriptor, namespace)
47
- @cache.lookup_or_compile(descriptor) do
48
- klass = namespace.const_get(:"#{descriptor.name}_#{OUTPUT_SUFFIXES.fetch(@output)}")
49
- @cache.set(descriptor, klass)
50
- descriptor.associations.each do |assoc|
51
- assoc.descriptors.each { |child| cache_descendants(child, namespace) }
52
- end
52
+ # Records the class before descending, so a cycle back to this Descriptor stops here.
53
+ def collect_classes(descriptor, namespace)
54
+ return if @compiled.key?(descriptor)
55
+ @compiled[descriptor] = namespace.const_get(:"#{descriptor.name}_#{OUTPUT_SUFFIXES.fetch(@output)}")
56
+ descriptor.associations.each do |assoc|
57
+ assoc.descriptors.each { |child| collect_classes(child, namespace) }
53
58
  end
54
59
  end
55
60
  end
@@ -8,7 +8,6 @@ module Panko::CodeGen
8
8
  # - +supports_root_key+: when +false+, passing +root_key:+ raises +ArgumentError+.
9
9
  # - +json_column_emit+ (JSON mode, Specialized path only): +:wire_format+ writes the
10
10
  # stored bytes after a well-formedness check, +:html_safe+ the typed value, escaped.
11
- # - +pool_writer+: no effect in Hash output mode.
12
11
  # - +guarded_model+: a record of another class goes to the generic body.
13
12
  # Needs a named +descriptor.model+.
14
13
  Config = Data.define(
@@ -17,7 +16,6 @@ module Panko::CodeGen
17
16
  :hash_record_key_type,
18
17
  :hash_output_key_type,
19
18
  :json_column_emit,
20
- :pool_writer,
21
19
  :guarded_model
22
20
  )
23
21
 
@@ -27,7 +25,6 @@ module Panko::CodeGen
27
25
  hash_record_key_type: :string,
28
26
  hash_output_key_type: :string,
29
27
  json_column_emit: :wire_format,
30
- pool_writer: true,
31
28
  guarded_model: false
32
29
  }.freeze
33
30
 
@@ -50,7 +47,6 @@ module Panko::CodeGen
50
47
  validate_enum!(:hash_record_key_type, values[:hash_record_key_type])
51
48
  validate_enum!(:hash_output_key_type, values[:hash_output_key_type])
52
49
  validate_json_column_emit!(values[:json_column_emit])
53
- validate_boolean!(:pool_writer, values[:pool_writer])
54
50
  validate_boolean!(:guarded_model, values[:guarded_model])
55
51
  end
56
52
 
@@ -94,8 +94,10 @@ module Panko::CodeGen
94
94
  StructuralValidation.validate_kind!("Association#kind", kind)
95
95
  StructuralValidation.validate_descriptor!("Association#descriptor", descriptor)
96
96
  StructuralValidation.validate_symbol!("Association#source", source)
97
- # +if+ must be a Callable: a Symbol passes this check but fails at compile
98
- # (Symbol has no +arity+).
97
+ # A method name is not supported here (unlike a MethodAttribute body), so a Symbol is rejected up front.
98
+ if if_callable.is_a?(Symbol)
99
+ raise DescriptorError, "Association#if: must be a Callable, not a Symbol; got #{if_callable.inspect}"
100
+ end
99
101
  StructuralValidation.validate_callable!("Association#if", if_callable) unless if_callable.nil?
100
102
  StructuralValidation.validate_array_of!("Association#variants", variants, Descriptor, "Descriptor")
101
103
  validate_variants!(variants)
@@ -87,7 +87,7 @@ module Panko
87
87
  # A declared model missing one of the sources would stop the whole parent from compiling,
88
88
  # so it is left out and its records get the unspecialized child.
89
89
  def serves?(klass, descriptor)
90
- ActiveRecord::DefineAttributeMethods.ensure!(klass)
90
+ klass.define_attribute_methods
91
91
  sources = descriptor.attributes.map(&:source) + descriptor.associations.map(&:source)
92
92
  sources.all? do |source|
93
93
  ActiveRecord::AccessClassifier.classify(klass, source)
@@ -2,19 +2,16 @@
2
2
 
3
3
  module Panko::CodeGen
4
4
  class Dump
5
- def initialize(descriptor, output:, config:, path:,
6
- validator: Validators::Validator.new, generator: Generator.new)
5
+ def initialize(descriptor, output:, config:, path:)
7
6
  @descriptor = descriptor
8
7
  @output = output
9
8
  @config = config
10
9
  @path = path
11
- @validator = validator
12
- @generator = generator
13
10
  end
14
11
 
15
12
  def dump
16
13
  validate_path!
17
- @validator.validate(@descriptor, output: @output, config: @config)
14
+ Validators::Validator.validate(@descriptor, output: @output, config: @config)
18
15
  if @descriptor.associations.empty?
19
16
  write_flat
20
17
  else
@@ -34,7 +31,7 @@ module Panko::CodeGen
34
31
  end
35
32
 
36
33
  def write_flat
37
- source = @generator.emit(@descriptor, output: @output, config: @config)
34
+ source = Generator.emit(@descriptor, output: @output, config: @config)
38
35
  File.write(@path, source)
39
36
  end
40
37
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Panko::CodeGen
4
- class Generator
4
+ module Generator
5
5
  OUTPUT_MODES = %i[json hash].freeze
6
6
 
7
7
  # Sinks are stateless, so one frozen instance per mode serves every emit.
@@ -16,8 +16,8 @@ module Panko::CodeGen
16
16
  end
17
17
  end
18
18
 
19
- def emit(descriptor, output:, config:)
20
- Generators::ClassEmitter.new(self.class.sink_for(output)).emit(descriptor, config)
19
+ def self.emit(descriptor, output:, config:)
20
+ Generators::ClassEmitter.new(sink_for(output)).emit(descriptor, config)
21
21
  end
22
22
  end
23
23
  end
@@ -76,16 +76,16 @@ module Panko::CodeGen
76
76
  builder.line "_construct_cache[descriptor.__id__] = self" if cyclic_self
77
77
  descriptor.method_attributes.each_with_index do |method_attribute, index|
78
78
  next if method_attribute.body.is_a?(Symbol)
79
- ivar = GeneratedNames.callable_ivar(method_attribute)
79
+ ivar = GeneratedNames.callable_ivar(index)
80
80
  builder.line "#{ivar} = descriptor.method_attributes[#{index}].body"
81
81
  end
82
82
  descriptor.associations.each_with_index do |assoc, i|
83
83
  if assoc.if
84
- builder.line "#{GeneratedNames.if_guard_ivar(assoc)} = descriptor.associations[#{i}].if"
84
+ builder.line "#{GeneratedNames.if_guard_ivar(i)} = descriptor.associations[#{i}].if"
85
85
  end
86
86
  builder.line emit_serializer_assignment(descriptor, assoc, i, cyclic_ids)
87
87
  assoc.variants.each_with_index do |variant, v|
88
- builder.line "#{GeneratedNames.variant_serializer_ivar(assoc, v)} = " \
88
+ builder.line "#{GeneratedNames.variant_serializer_ivar(i, v)} = " \
89
89
  "#{GeneratedNames.class_name(variant, @sink.suffix)}.new(descriptor: descriptor.associations[#{i}].variants[#{v}])"
90
90
  end
91
91
  end
@@ -96,7 +96,7 @@ module Panko::CodeGen
96
96
  # The cache is passed only when both ends are cyclic: a child outside
97
97
  # the cycle has a constructor without the +_construct_cache:+ kwarg.
98
98
  def emit_serializer_assignment(descriptor, assoc, i, cyclic_ids)
99
- ivar = GeneratedNames.serializer_ivar(assoc)
99
+ ivar = GeneratedNames.serializer_ivar(i)
100
100
  if assoc.descriptor.equal?(descriptor)
101
101
  "#{ivar} = self"
102
102
  elsif cyclic_ids[descriptor.__id__] && cyclic_ids[assoc.descriptor.__id__]
@@ -20,11 +20,9 @@ module Panko::CodeGen
20
20
  index
21
21
  end
22
22
 
23
- # The +name: N+ shorthand parses only for identifier-style names.
23
+ # Ruby 3.4+ +Hash#inspect+ quotes a Symbol key that is not an identifier (+"2fa": 0+).
24
24
  def to_hash_literal(field_index)
25
- return "{}" if field_index.empty?
26
- pairs = field_index.map { |name, idx| "#{name}: #{idx}" }
27
- "{#{pairs.join(", ")}}"
25
+ field_index.inspect
28
26
  end
29
27
  end
30
28
  end
@@ -19,15 +19,15 @@ module Panko::CodeGen
19
19
  sink.attribute(attribute, read_expr.call(attribute.source), config, index, builder)
20
20
  end
21
21
  end
22
- descriptor.associations.each do |association|
22
+ descriptor.associations.each_with_index do |association, position|
23
23
  sink.association(
24
- association, read_expr.call(association.source), config,
24
+ association, position, read_expr.call(association.source), config,
25
25
  field_index.fetch(GeneratedNames.filter_key(association)), builder
26
26
  )
27
27
  end
28
- descriptor.method_attributes.each do |method_attribute|
28
+ descriptor.method_attributes.each_with_index do |method_attribute, position|
29
29
  sink.method_attribute(
30
- method_attribute, config,
30
+ method_attribute, position, config,
31
31
  field_index.fetch(GeneratedNames.filter_key(method_attribute)), builder
32
32
  )
33
33
  end
@@ -11,20 +11,40 @@ module Panko::CodeGen
11
11
  "#{descriptor.name}_#{suffix}"
12
12
  end
13
13
 
14
- def serializer_ivar(association)
15
- "@#{association.name}_serializer"
14
+ # Field names can hold any character, so instance variables are named by
15
+ # the Field's position in its Descriptor list, never by its name.
16
+ def serializer_ivar(position)
17
+ "@child_#{position}"
16
18
  end
17
19
 
18
- def variant_serializer_ivar(association, index)
19
- "#{serializer_ivar(association)}_#{index}"
20
+ def variant_serializer_ivar(position, index)
21
+ "#{serializer_ivar(position)}_#{index}"
20
22
  end
21
23
 
22
- def callable_ivar(method_attribute)
23
- "@cb_#{method_attribute.name}"
24
+ def callable_ivar(position)
25
+ "@cb_#{position}"
24
26
  end
25
27
 
26
- def if_guard_ivar(association)
27
- "@cb_if_#{association.name}"
28
+ def if_guard_ivar(position)
29
+ "@cb_if_#{position}"
30
+ end
31
+
32
+ # Names reach generated source only as escaped literals, so a quote or an
33
+ # interpolation in a name cannot change the code.
34
+ def string_literal(name)
35
+ name.to_s.inspect
36
+ end
37
+
38
+ def symbol_literal(name)
39
+ name.to_sym.inspect
40
+ end
41
+
42
+ PLAIN_METHOD_NAME = /\A[A-Za-z_][A-Za-z0-9_]*[?!]?\z/
43
+
44
+ # A name dot syntax cannot parse (+2fa+, +a-b+) is called through +__send__+.
45
+ def method_call(receiver, name)
46
+ return "#{receiver}.#{name}" if PLAIN_METHOD_NAME.match?(name.to_s)
47
+ "#{receiver}.__send__(#{symbol_literal(name)})"
28
48
  end
29
49
 
30
50
  def write_one
@@ -107,10 +107,10 @@ module Panko::CodeGen
107
107
  end
108
108
  end
109
109
 
110
- def method_attribute(method_attribute, config, index, builder)
110
+ def method_attribute(method_attribute, position, config, index, builder)
111
111
  builder.line "unless filters.drops?(#{index})"
112
112
  builder.indent do
113
- builder.line "value = #{method_attribute_call_expression(method_attribute)}"
113
+ builder.line "value = #{method_attribute_call_expression(method_attribute, position)}"
114
114
  builder.line "unless value.equal?(Panko::CodeGen::SKIP)"
115
115
  builder.indent do
116
116
  builder.line "result[#{key_literal(method_attribute.name, config)}] = Panko::CodeGen.cast_datetime(value)"
@@ -120,21 +120,21 @@ module Panko::CodeGen
120
120
  builder.line "end"
121
121
  end
122
122
 
123
- def association(association, source_read_expr, config, index, builder)
123
+ def association(association, position, source_read_expr, config, index, builder)
124
124
  key_lit = key_literal(association.name, config)
125
125
  builder.line "unless filters.drops?(#{index})"
126
126
  builder.indent do
127
- with_if_guard(association, builder) do
127
+ with_if_guard(association, position, builder) do
128
128
  case association.kind
129
129
  when :has_one
130
130
  builder.line "value = #{source_read_expr}"
131
131
  if config.null_for_missing_has_one
132
- has_one_default(association, key_lit, builder)
132
+ has_one_default(association, position, key_lit, builder)
133
133
  else
134
- has_one_omit(association, key_lit, builder)
134
+ has_one_omit(association, position, key_lit, builder)
135
135
  end
136
136
  when :has_many
137
- has_many(association, source_read_expr, key_lit, builder)
137
+ has_many(association, position, source_read_expr, key_lit, builder)
138
138
  end
139
139
  end
140
140
  end
@@ -145,33 +145,33 @@ module Panko::CodeGen
145
145
 
146
146
  def key_literal(name, config)
147
147
  case config.hash_output_key_type
148
- when :symbol then ":#{name}"
149
- else %("#{name}")
148
+ when :symbol then GeneratedNames.symbol_literal(name)
149
+ else GeneratedNames.string_literal(name)
150
150
  end
151
151
  end
152
152
 
153
- def has_one_default(association, key_lit, builder)
153
+ def has_one_default(association, position, key_lit, builder)
154
154
  builder.line "result[#{key_lit}] = if value.nil?"
155
155
  builder.indent { builder.line "nil" }
156
156
  builder.line "else"
157
157
  builder.indent do
158
- child_write(association, "value", builder) do |ivar|
158
+ child_write(association, position, "value", builder) do |ivar|
159
159
  "#{ivar}.#{GeneratedNames.to_hash}(value, context, scope, #{child_filter_expr(association)})"
160
160
  end
161
161
  end
162
162
  builder.line "end"
163
163
  end
164
164
 
165
- def has_one_omit(association, key_lit, builder)
165
+ def has_one_omit(association, position, key_lit, builder)
166
166
  builder.line "unless value.nil?"
167
167
  builder.indent do
168
168
  if association.variants.empty?
169
- builder.line "result[#{key_lit}] = #{GeneratedNames.serializer_ivar(association)}." \
169
+ builder.line "result[#{key_lit}] = #{GeneratedNames.serializer_ivar(position)}." \
170
170
  "#{GeneratedNames.to_hash}(value, context, scope, #{child_filter_expr(association)})"
171
171
  else
172
172
  builder.line "result[#{key_lit}] = begin"
173
173
  builder.indent do
174
- child_write(association, "value", builder) do |ivar|
174
+ child_write(association, position, "value", builder) do |ivar|
175
175
  "#{ivar}.#{GeneratedNames.to_hash}(value, context, scope, #{child_filter_expr(association)})"
176
176
  end
177
177
  end
@@ -182,18 +182,18 @@ module Panko::CodeGen
182
182
  end
183
183
 
184
184
  # +child_filter+ is looked up once per record, not once per element.
185
- def has_many(association, source_read_expr, key_lit, builder)
185
+ def has_many(association, position, source_read_expr, key_lit, builder)
186
186
  builder.line "child_filter = #{child_filter_expr(association)}"
187
187
  if association.variants.empty?
188
188
  builder.line(
189
189
  "result[#{key_lit}] = #{source_read_expr}.map { |element| " \
190
- "#{GeneratedNames.serializer_ivar(association)}.#{GeneratedNames.to_hash}(element, context, scope, child_filter) }"
190
+ "#{GeneratedNames.serializer_ivar(position)}.#{GeneratedNames.to_hash}(element, context, scope, child_filter) }"
191
191
  )
192
192
  return
193
193
  end
194
194
  builder.line "result[#{key_lit}] = #{source_read_expr}.map do |element|"
195
195
  builder.indent do
196
- child_write(association, "element", builder) do |ivar|
196
+ child_write(association, position, "element", builder) do |ivar|
197
197
  "#{ivar}.#{GeneratedNames.to_hash}(element, context, scope, child_filter)"
198
198
  end
199
199
  end
@@ -203,7 +203,7 @@ module Panko::CodeGen
203
203
  # +format_raw+ returns nil for raw values it cannot format; the typed-read
204
204
  # fallback then goes through +cast_datetime+ so a Time still becomes a String.
205
205
  def datetime_column_attribute(attribute, config, index, builder)
206
- source_lit = %("#{attribute.source}")
206
+ source_lit = GeneratedNames.string_literal(attribute.source)
207
207
  builder.line "unless filters.drops?(#{index})"
208
208
  builder.indent do
209
209
  builder.line "value = Panko::CodeGen::DateTimeFormat.format_raw(record.read_attribute_before_type_cast(#{source_lit}))"
@@ -32,7 +32,6 @@ module Panko::CodeGen
32
32
  end
33
33
 
34
34
  def emit_class_constants(descriptor, config, builder)
35
- return unless config.pool_writer
36
35
  subclass = defined?(ActiveSupport::IsolatedExecutionState) ? "IsolatedExecutionState" : "ThreadLocal"
37
36
  builder.line "POOL = Panko::CodeGen::WritersPool::#{subclass}.new"
38
37
  end
@@ -47,7 +46,7 @@ module Panko::CodeGen
47
46
  if config.supports_root_key
48
47
  builder.line "validate_root_key!(root_key)"
49
48
  end
50
- with_writer(config, builder) { emit_serialize_one_body(config, builder) }
49
+ with_writer(builder) { emit_serialize_one_body(config, builder) }
51
50
  end
52
51
  builder.line "end"
53
52
  end
@@ -62,7 +61,7 @@ module Panko::CodeGen
62
61
  if config.supports_root_key
63
62
  builder.line "validate_root_key!(root_key)"
64
63
  end
65
- with_writer(config, builder) { emit_serialize_many_body(config, builder) }
64
+ with_writer(builder) { emit_serialize_many_body(config, builder) }
66
65
  end
67
66
  builder.line "end"
68
67
  end
@@ -103,7 +102,7 @@ module Panko::CodeGen
103
102
  def attribute(attribute, read_expr, config, index, builder, cast: true)
104
103
  builder.line "unless filters.drops?(#{index})"
105
104
  builder.indent do
106
- builder.line %(writer.push_value(#{read_expr}, "#{attribute.name}"))
105
+ builder.line "writer.push_value(#{read_expr}, #{GeneratedNames.string_literal(attribute.name)})"
107
106
  end
108
107
  builder.line "end"
109
108
  end
@@ -119,33 +118,34 @@ module Panko::CodeGen
119
118
  end
120
119
 
121
120
  # +equal?+, not +==+, so an object that overrides +==+ cannot match +SKIP+.
122
- def method_attribute(method_attribute, config, index, builder)
121
+ def method_attribute(method_attribute, position, config, index, builder)
123
122
  builder.line "unless filters.drops?(#{index})"
124
123
  builder.indent do
125
- builder.line "value = #{method_attribute_call_expression(method_attribute)}"
124
+ builder.line "value = #{method_attribute_call_expression(method_attribute, position)}"
126
125
  builder.line "unless value.equal?(Panko::CodeGen::SKIP)"
127
126
  builder.indent do
128
- builder.line %(writer.push_value(value, "#{method_attribute.name}"))
127
+ builder.line "writer.push_value(value, #{GeneratedNames.string_literal(method_attribute.name)})"
129
128
  end
130
129
  builder.line "end"
131
130
  end
132
131
  builder.line "end"
133
132
  end
134
133
 
135
- def association(association, source_read_expr, config, index, builder)
134
+ def association(association, position, source_read_expr, config, index, builder)
135
+ key_lit = GeneratedNames.string_literal(association.name)
136
136
  builder.line "unless filters.drops?(#{index})"
137
137
  builder.indent do
138
- with_if_guard(association, builder) do
138
+ with_if_guard(association, position, builder) do
139
139
  case association.kind
140
140
  when :has_one
141
141
  builder.line "value = #{source_read_expr}"
142
142
  if config.null_for_missing_has_one
143
- has_one_default(association, builder)
143
+ has_one_default(association, position, key_lit, builder)
144
144
  else
145
- has_one_omit(association, builder)
145
+ has_one_omit(association, position, key_lit, builder)
146
146
  end
147
147
  when :has_many
148
- has_many(association, source_read_expr, builder)
148
+ has_many(association, position, source_read_expr, key_lit, builder)
149
149
  end
150
150
  end
151
151
  end
@@ -155,21 +155,16 @@ module Panko::CodeGen
155
155
  private
156
156
 
157
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
158
+ def with_writer(builder, &block)
159
+ builder.line "writer = POOL.checkout"
160
+ builder.line "result = nil"
161
+ builder.line "begin"
162
+ builder.indent(&block)
163
+ builder.line "ensure"
164
+ builder.indent do
165
+ builder.line "POOL.checkin(writer, result)"
172
166
  end
167
+ builder.line "end"
173
168
  end
174
169
 
175
170
  def emit_serialize_one_body(config, builder)
@@ -205,36 +200,36 @@ module Panko::CodeGen
205
200
 
206
201
  # A non-nil +has_one+ pushes its key separately instead of +push_object("name")+: the
207
202
  # child's +_write_one+ opens its own +push_object+ frame.
208
- def has_one_default(association, builder)
203
+ def has_one_default(association, position, key_lit, builder)
209
204
  builder.line "if value.nil?"
210
- builder.indent { builder.line %(writer.push_value(nil, "#{association.name}")) }
205
+ builder.indent { builder.line "writer.push_value(nil, #{key_lit})" }
211
206
  builder.line "else"
212
207
  builder.indent do
213
- builder.line %(writer.push_key("#{association.name}"))
214
- child_write(association, "value", builder) do |ivar|
208
+ builder.line "writer.push_key(#{key_lit})"
209
+ child_write(association, position, "value", builder) do |ivar|
215
210
  "#{ivar}.#{GeneratedNames.write_one}(value, writer, context, scope, #{child_filter_expr(association)})"
216
211
  end
217
212
  end
218
213
  builder.line "end"
219
214
  end
220
215
 
221
- def has_one_omit(association, builder)
216
+ def has_one_omit(association, position, key_lit, builder)
222
217
  builder.line "unless value.nil?"
223
218
  builder.indent do
224
- builder.line %(writer.push_key("#{association.name}"))
225
- child_write(association, "value", builder) do |ivar|
219
+ builder.line "writer.push_key(#{key_lit})"
220
+ child_write(association, position, "value", builder) do |ivar|
226
221
  "#{ivar}.#{GeneratedNames.write_one}(value, writer, context, scope, #{child_filter_expr(association)})"
227
222
  end
228
223
  end
229
224
  builder.line "end"
230
225
  end
231
226
 
232
- def has_many(association, source_read_expr, builder)
227
+ def has_many(association, position, source_read_expr, key_lit, builder)
233
228
  builder.line "child_filter = #{child_filter_expr(association)}"
234
- builder.line %(writer.push_array("#{association.name}"))
229
+ builder.line "writer.push_array(#{key_lit})"
235
230
  builder.line "#{source_read_expr}.each do |element|"
236
231
  builder.indent do
237
- child_write(association, "element", builder) do |ivar|
232
+ child_write(association, position, "element", builder) do |ivar|
238
233
  "#{ivar}.#{GeneratedNames.write_one}(element, writer, context, scope, child_filter)"
239
234
  end
240
235
  end
@@ -246,11 +241,11 @@ module Panko::CodeGen
246
241
  # (non-String, empty, malformed) falls back to the typed read.
247
242
  def json_column_attribute(attribute, config, index, builder)
248
243
  if config.json_column_emit == :html_safe
249
- attribute(attribute, %(record._read_attribute("#{attribute.source}")), config, index, builder)
244
+ attribute(attribute, "record._read_attribute(#{GeneratedNames.string_literal(attribute.source)})", config, index, builder)
250
245
  return
251
246
  end
252
- source_lit = %("#{attribute.source}")
253
- key_lit = %("#{attribute.name}")
247
+ source_lit = GeneratedNames.string_literal(attribute.source)
248
+ key_lit = GeneratedNames.string_literal(attribute.name)
254
249
  builder.line "unless filters.drops?(#{index})"
255
250
  builder.indent do
256
251
  builder.line %(raw = record.read_attribute_before_type_cast(#{source_lit}))
@@ -277,11 +272,12 @@ module Panko::CodeGen
277
272
  end
278
273
 
279
274
  def datetime_column_attribute(attribute, index, builder)
280
- source_lit = %("#{attribute.source}")
275
+ source_lit = GeneratedNames.string_literal(attribute.source)
281
276
  builder.line "unless filters.drops?(#{index})"
282
277
  builder.indent do
283
278
  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}"))
279
+ builder.line "writer.push_value(value || record._read_attribute(#{source_lit}), " \
280
+ "#{GeneratedNames.string_literal(attribute.name)})"
285
281
  end
286
282
  builder.line "end"
287
283
  end
@@ -27,7 +27,7 @@ module Panko::CodeGen
27
27
  builder.indent do
28
28
  FieldWalk.emit_fields(
29
29
  descriptor, config, field_index, builder, sink,
30
- read_expr: ->(source) { "record.#{source}" }
30
+ read_expr: ->(source) { GeneratedNames.method_call("record", source) }
31
31
  )
32
32
  end
33
33
  builder.line "end"
@@ -60,7 +60,7 @@ module Panko::CodeGen
60
60
  builder.indent do
61
61
  FieldWalk.emit_fields(
62
62
  descriptor, config, field_index, builder, sink,
63
- read_expr: ->(source) { "record.#{source}" }
63
+ read_expr: ->(source) { GeneratedNames.method_call("record", source) }
64
64
  )
65
65
  end
66
66
  builder.line "end"
@@ -82,8 +82,8 @@ module Panko::CodeGen
82
82
 
83
83
  def self.hash_read_expr(source_name, config)
84
84
  case config.hash_record_key_type
85
- when :symbol then "record[:#{source_name}]"
86
- else %(record["#{source_name}"])
85
+ when :symbol then "record[#{GeneratedNames.symbol_literal(source_name)}]"
86
+ else "record[#{GeneratedNames.string_literal(source_name)}]"
87
87
  end
88
88
  end
89
89
  end
@@ -13,7 +13,7 @@ module Panko::CodeGen
13
13
  Generic.emit_parent_class_ivar_writes(descriptor, builder)
14
14
  FieldWalk.emit_fields(
15
15
  descriptor, config, field_index, builder, sink,
16
- read_expr: ->(source) { "record.#{source}" },
16
+ read_expr: ->(source) { GeneratedNames.method_call("record", source) },
17
17
  attribute_emit: ->(attribute, index) { sink.specialized_attribute(attribute, ar_model, config, index, builder) }
18
18
  )
19
19
  end
@@ -45,21 +45,23 @@ module Panko::CodeGen
45
45
  # SELECT alias named like a method (+thing_ids+ next to +has_many :things+) returns the selected value.
46
46
  def self.attribute_read_expr(attribute, ar_model)
47
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}))
48
+ method_read = GeneratedNames.method_call("record", source)
49
+ return method_read if ar_model.nil?
50
+ source_lit = GeneratedNames.string_literal(source)
51
+ return "record._read_attribute(#{source_lit})" if ActiveRecord::AccessClassifier.classify(ar_model, source) == :column
52
+ return method_read if declared_attribute?(ar_model, source)
53
+ "(record._has_attribute?(#{source_lit}) ? record._read_attribute(#{source_lit}) : #{method_read})"
52
54
  end
53
55
 
54
56
  def self.declared_attribute?(ar_model, source)
55
57
  ar_model.attribute_types.key?(source.to_s) || ar_model.attribute_aliases.key?(source.to_s)
56
58
  end
57
59
 
58
- # Called here too so the Generator works without the validators; repeating +ensure!+ is safe.
60
+ # Called here too so the Generator works without the validators.
59
61
  def self.ar_model(descriptor)
60
62
  model = descriptor.model
61
63
  return nil unless model.respond_to?(:columns_hash) && model.respond_to?(:attribute_methods_generated?)
62
- ActiveRecord::DefineAttributeMethods.ensure!(model)
64
+ model.define_attribute_methods
63
65
  model
64
66
  end
65
67
 
@@ -25,11 +25,11 @@ module Panko::CodeGen
25
25
  end
26
26
  end
27
27
 
28
- def child_ivars(assoc)
28
+ def child_ivars(assoc, position)
29
29
  variants = assoc.variants.each_with_index.map do |variant, index|
30
- [GeneratedNames.variant_serializer_ivar(assoc, index), variant]
30
+ [GeneratedNames.variant_serializer_ivar(position, index), variant]
31
31
  end
32
- [[GeneratedNames.serializer_ivar(assoc), assoc.descriptor], *variants]
32
+ [[GeneratedNames.serializer_ivar(position), assoc.descriptor], *variants]
33
33
  end
34
34
 
35
35
  # Every class defines +_release+, even an empty one: the serializers call it
@@ -42,8 +42,8 @@ module Panko::CodeGen
42
42
  builder.line "@context = nil"
43
43
  builder.line "@scope = nil"
44
44
  end
45
- descriptor.associations.each do |assoc|
46
- child_ivars(assoc).each do |ivar, child|
45
+ descriptor.associations.each_with_index do |assoc, position|
46
+ child_ivars(assoc, position).each do |ivar, child|
47
47
  next if child.equal?(descriptor)
48
48
  # Ends the chain without per-call visited state; such serializers keep their last record until the next call.
49
49
  next if cyclic_ids[descriptor.__id__] && cyclic_ids[child.__id__]
@@ -6,14 +6,14 @@ module Panko::CodeGen
6
6
  # The child's +FIELD_INDEX+ is read through its class name when the method runs, so
7
7
  # self-recursive Descriptors resolve it.
8
8
  def child_filter_expr(association)
9
- "filters.child(:#{GeneratedNames.filter_key(association)}, " \
9
+ "filters.child(#{GeneratedNames.symbol_literal(GeneratedNames.filter_key(association))}, " \
10
10
  "#{GeneratedNames.class_name(association.descriptor, suffix)}::#{GeneratedNames.field_index_const})"
11
11
  end
12
12
 
13
13
  # +case+/+when+, not an +if+/+elsif+ chain of +instance_of?+: under YJIT the chain gets
14
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))
15
+ def child_write(association, position, record_expr, builder, &call_for)
16
+ base_call = call_for.call(GeneratedNames.serializer_ivar(position))
17
17
  if association.variants.empty?
18
18
  builder.line base_call
19
19
  return
@@ -24,7 +24,7 @@ module Panko::CodeGen
24
24
  builder.line "when #{model}"
25
25
  builder.indent do
26
26
  builder.line "if #{record_expr}.instance_of?(#{model})"
27
- builder.indent { builder.line call_for.call(GeneratedNames.variant_serializer_ivar(association, index)) }
27
+ builder.indent { builder.line call_for.call(GeneratedNames.variant_serializer_ivar(position, index)) }
28
28
  builder.line "else"
29
29
  builder.indent { builder.line base_call }
30
30
  builder.line "end"
@@ -42,9 +42,9 @@ module Panko::CodeGen
42
42
  end
43
43
 
44
44
  # The guard wraps the Source read too, so a falsy +if:+ skips the read.
45
- def with_if_guard(association, builder)
45
+ def with_if_guard(association, position, builder)
46
46
  if association.if
47
- builder.line "if #{if_guard_call_expression(GeneratedNames.if_guard_ivar(association), association.if.arity)}"
47
+ builder.line "if #{if_guard_call_expression(GeneratedNames.if_guard_ivar(position), association.if.arity)}"
48
48
  builder.indent { yield }
49
49
  builder.line "end"
50
50
  else
@@ -64,10 +64,10 @@ module Panko::CodeGen
64
64
 
65
65
  # Symbol bodies need the explicit +self.+: a bare name such as +record+ or +value+ would
66
66
  # resolve to a local of the generated method.
67
- def method_attribute_call_expression(method_attribute)
67
+ def method_attribute_call_expression(method_attribute, position)
68
68
  body = method_attribute.body
69
- return "self.#{body}" if body.is_a?(Symbol)
70
- ivar = GeneratedNames.callable_ivar(method_attribute)
69
+ return GeneratedNames.method_call("self", body) if body.is_a?(Symbol)
70
+ ivar = GeneratedNames.callable_ivar(position)
71
71
  case body.arity
72
72
  when 0 then "#{ivar}.call"
73
73
  when 1 then "#{ivar}.call(record)"
@@ -23,7 +23,7 @@ module Panko::CodeGen
23
23
  def classify_attributes!(descriptor)
24
24
  model = descriptor.model
25
25
  return unless ar_class?(model)
26
- ActiveRecord::DefineAttributeMethods.ensure!(model)
26
+ model.define_attribute_methods
27
27
  descriptor.attributes.each do |attribute|
28
28
  classify_or_raise!(model, attribute, descriptor.name)
29
29
  end
@@ -2,16 +2,12 @@
2
2
 
3
3
  module Panko::CodeGen
4
4
  module Validators
5
- class Validator
5
+ module Validator
6
6
  # Order matters: an arity error is reported before a source or name error.
7
- DEFAULT_RULES = [CallableArity, SourceResolution, NameUniqueness].freeze
7
+ RULES = [CallableArity, SourceResolution, NameUniqueness].freeze
8
8
 
9
- def initialize(rules: DEFAULT_RULES)
10
- @rules = rules
11
- end
12
-
13
- def validate(descriptor, output:, config:)
14
- @rules.each { |rule| rule.validate(descriptor, output: output, config: config) }
9
+ def self.validate(descriptor, output:, config:)
10
+ RULES.each { |rule| rule.validate(descriptor, output: output, config: config) }
15
11
  nil
16
12
  end
17
13
  end
@@ -16,7 +16,6 @@ require_relative "code_gen/code_builder"
16
16
  require_relative "code_gen/filter"
17
17
  require_relative "code_gen/datetime_format"
18
18
  require_relative "code_gen/active_record/access_classifier"
19
- require_relative "code_gen/active_record/define_attribute_methods"
20
19
  require_relative "code_gen/validators/callable_arity"
21
20
  require_relative "code_gen/validators/source_resolution"
22
21
  require_relative "code_gen/validators/name_uniqueness"
@@ -36,7 +35,6 @@ require_relative "code_gen/generators/banner"
36
35
  require_relative "code_gen/generators/class_emitter"
37
36
  require_relative "code_gen/generators/fanout"
38
37
  require_relative "code_gen/generator"
39
- require_relative "code_gen/compile_cache"
40
38
  require_relative "code_gen/compiler"
41
39
  require_relative "code_gen/dump"
42
40
  require_relative "code_gen/writers_pool"
@@ -104,7 +104,8 @@ module Panko
104
104
  name: options.fetch(:name, name).to_s.to_sym,
105
105
  kind: kind,
106
106
  descriptor: descriptor,
107
- source: name.to_sym
107
+ source: name.to_sym,
108
+ if: options[:if]
108
109
  )
109
110
  end
110
111
 
data/lib/panko/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Panko
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panko_serializer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yosi Attias
@@ -68,9 +68,7 @@ files:
68
68
  - lib/panko/array_serializer.rb
69
69
  - lib/panko/code_gen.rb
70
70
  - lib/panko/code_gen/active_record/access_classifier.rb
71
- - lib/panko/code_gen/active_record/define_attribute_methods.rb
72
71
  - lib/panko/code_gen/code_builder.rb
73
- - lib/panko/code_gen/compile_cache.rb
74
72
  - lib/panko/code_gen/compiler.rb
75
73
  - lib/panko/code_gen/config.rb
76
74
  - lib/panko/code_gen/datetime_format.rb
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Panko::CodeGen
4
- module ActiveRecord
5
- module DefineAttributeMethods
6
- def self.ensure!(klass)
7
- return if klass.attribute_methods_generated?
8
- klass.define_attribute_methods
9
- nil
10
- end
11
- end
12
- end
13
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Panko::CodeGen
4
- # Descriptor -> Generated Class, one entry per unique Descriptor in a tree.
5
- class CompileCache
6
- # Keyed by +__id__+, so equal but distinct Descriptors get separate entries.
7
- def initialize
8
- @store = {}
9
- end
10
-
11
- def get(descriptor)
12
- @store[descriptor.__id__]
13
- end
14
-
15
- def set(descriptor, generated_class)
16
- @store[descriptor.__id__] = generated_class
17
- end
18
-
19
- # The block must call +#set+ before it descends into children, so a recursive
20
- # call on the same Descriptor hits the entry and the cycle ends.
21
- def lookup_or_compile(descriptor)
22
- cached = @store[descriptor.__id__]
23
- return cached if cached
24
- yield
25
- @store[descriptor.__id__]
26
- end
27
- end
28
- end