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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -4
  3. data/lib/panko/array_serializer.rb +55 -16
  4. data/lib/panko/code_gen/active_record/access_classifier.rb +65 -0
  5. data/lib/panko/code_gen/active_record/define_attribute_methods.rb +13 -0
  6. data/lib/panko/code_gen/code_builder.rb +32 -0
  7. data/lib/panko/code_gen/compile_cache.rb +28 -0
  8. data/lib/panko/code_gen/compiler.rb +56 -0
  9. data/lib/panko/code_gen/config.rb +79 -0
  10. data/lib/panko/code_gen/datetime_format.rb +38 -0
  11. data/lib/panko/code_gen/descriptor.rb +136 -0
  12. data/lib/panko/code_gen/descriptor_builder.rb +166 -0
  13. data/lib/panko/code_gen/dump.rb +52 -0
  14. data/lib/panko/code_gen/errors.rb +21 -0
  15. data/lib/panko/code_gen/filter.rb +29 -0
  16. data/lib/panko/code_gen/filter_adapter.rb +44 -0
  17. data/lib/panko/code_gen/filters/indexed.rb +62 -0
  18. data/lib/panko/code_gen/filters/none.rb +19 -0
  19. data/lib/panko/code_gen/generator.rb +23 -0
  20. data/lib/panko/code_gen/generators/banner.rb +27 -0
  21. data/lib/panko/code_gen/generators/class_emitter.rb +120 -0
  22. data/lib/panko/code_gen/generators/cycle_membership.rb +53 -0
  23. data/lib/panko/code_gen/generators/descriptor_walk.rb +22 -0
  24. data/lib/panko/code_gen/generators/fanout.rb +60 -0
  25. data/lib/panko/code_gen/generators/field_index.rb +31 -0
  26. data/lib/panko/code_gen/generators/field_walk.rb +38 -0
  27. data/lib/panko/code_gen/generators/generated_names.rb +73 -0
  28. data/lib/panko/code_gen/generators/hash_sink.rb +217 -0
  29. data/lib/panko/code_gen/generators/json_sink.rb +290 -0
  30. data/lib/panko/code_gen/generators/record_access/generic.rb +92 -0
  31. data/lib/panko/code_gen/generators/record_access/specialized.rb +90 -0
  32. data/lib/panko/code_gen/generators/release.rb +60 -0
  33. data/lib/panko/code_gen/generators/sink.rb +80 -0
  34. data/lib/panko/code_gen/instance_pool.rb +27 -0
  35. data/lib/panko/code_gen/runtime.rb +27 -0
  36. data/lib/panko/code_gen/serializer_cache.rb +242 -0
  37. data/lib/panko/code_gen/validators/callable_arity.rb +42 -0
  38. data/lib/panko/code_gen/validators/name_uniqueness.rb +44 -0
  39. data/lib/panko/code_gen/validators/source_resolution.rb +48 -0
  40. data/lib/panko/code_gen/validators/validator.rb +19 -0
  41. data/lib/panko/code_gen/writers_pool.rb +51 -0
  42. data/lib/panko/code_gen.rb +82 -0
  43. data/lib/panko/compile_all.rb +80 -0
  44. data/lib/panko/compile_all_result.rb +21 -0
  45. data/lib/panko/config.rb +71 -0
  46. data/lib/panko/descriptor.rb +93 -0
  47. data/lib/panko/response.rb +40 -8
  48. data/lib/panko/serializer.rb +175 -91
  49. data/lib/panko/serializer_resolver.rb +2 -8
  50. data/lib/panko/version.rb +1 -1
  51. data/lib/panko_serializer.rb +3 -5
  52. metadata +49 -35
  53. data/ext/panko_serializer/attributes_writer/active_record.c +0 -181
  54. data/ext/panko_serializer/attributes_writer/active_record.h +0 -17
  55. data/ext/panko_serializer/attributes_writer/attributes_writer.c +0 -55
  56. data/ext/panko_serializer/attributes_writer/attributes_writer.h +0 -35
  57. data/ext/panko_serializer/attributes_writer/common.c +0 -10
  58. data/ext/panko_serializer/attributes_writer/common.h +0 -9
  59. data/ext/panko_serializer/attributes_writer/hash.c +0 -13
  60. data/ext/panko_serializer/attributes_writer/hash.h +0 -7
  61. data/ext/panko_serializer/attributes_writer/plain.c +0 -13
  62. data/ext/panko_serializer/attributes_writer/plain.h +0 -7
  63. data/ext/panko_serializer/attributes_writer/type_cast/time_conversion.c +0 -156
  64. data/ext/panko_serializer/attributes_writer/type_cast/time_conversion.h +0 -10
  65. data/ext/panko_serializer/attributes_writer/type_cast/type_cast.c +0 -409
  66. data/ext/panko_serializer/attributes_writer/type_cast/type_cast.h +0 -81
  67. data/ext/panko_serializer/common.h +0 -9
  68. data/ext/panko_serializer/extconf.rb +0 -28
  69. data/ext/panko_serializer/panko_serializer.c +0 -181
  70. data/ext/panko_serializer/panko_serializer.h +0 -12
  71. data/ext/panko_serializer/serialization_descriptor/association.c +0 -90
  72. data/ext/panko_serializer/serialization_descriptor/association.h +0 -20
  73. data/ext/panko_serializer/serialization_descriptor/attribute.c +0 -96
  74. data/ext/panko_serializer/serialization_descriptor/attribute.h +0 -27
  75. data/ext/panko_serializer/serialization_descriptor/serialization_descriptor.c +0 -177
  76. data/ext/panko_serializer/serialization_descriptor/serialization_descriptor.h +0 -30
  77. data/lib/panko/association.rb +0 -17
  78. data/lib/panko/attribute.rb +0 -29
  79. data/lib/panko/object_writer.rb +0 -68
  80. data/lib/panko/serialization_descriptor.rb +0 -180
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panko::CodeGen
4
+ module Generators
5
+ # Mutual-recursion peers +require_relative+ each other. Ruby allows this because
6
+ # a peer's constant is read only when a method runs, after both files have loaded.
7
+ module Fanout
8
+ module_function
9
+
10
+ def emit_files(descriptor, output:, config:)
11
+ cyclic_ids = CycleMembership.cyclic_descriptor_ids(descriptor)
12
+ DescriptorWalk.in_emit_order(descriptor).map do |desc|
13
+ {
14
+ descriptor: desc,
15
+ basename: basename_for(desc, output),
16
+ source: build_file(desc, output, config, cyclic_ids)
17
+ }
18
+ end
19
+ end
20
+
21
+ def basename_for(descriptor, output)
22
+ "#{snake_case(descriptor.name)}_#{output}.rb"
23
+ end
24
+
25
+ def snake_case(camel)
26
+ camel.gsub(/(?<=.)([A-Z])/, '_\1').downcase
27
+ end
28
+ private_class_method :snake_case
29
+
30
+ def build_file(descriptor, output, config, cyclic_ids)
31
+ builder = CodeBuilder.new
32
+ builder.line "# frozen_string_literal: true"
33
+ builder.blank
34
+ Banner.emit(builder, descriptor, output: output, config: config)
35
+ deps = ordered_dependencies(descriptor)
36
+ deps.each do |dep|
37
+ builder.line %(require_relative "#{snake_case(dep.name)}_#{output}")
38
+ end
39
+ builder.blank if deps.any?
40
+ emitter = ClassEmitter.new(Panko::CodeGen::Generator.sink_for(output))
41
+ emitter.emit_class(descriptor, config, builder, cyclic_ids)
42
+ builder.to_s + "\n"
43
+ end
44
+ private_class_method :build_file
45
+
46
+ # Seeded with +descriptor+ itself: a self-loop needs no +require_relative+.
47
+ def ordered_dependencies(descriptor)
48
+ seen = {descriptor.__id__ => true}
49
+ deps = []
50
+ descriptor.associations.flat_map(&:descriptors).each do |target|
51
+ next if seen[target.__id__]
52
+ seen[target.__id__] = true
53
+ deps << target
54
+ end
55
+ deps
56
+ end
57
+ private_class_method :ordered_dependencies
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panko::CodeGen
4
+ module Generators
5
+ # Indexes are written into each Field's +filters.drops?(N)+ check, so
6
+ # filtering a Field costs one Array lookup instead of a Hash lookup.
7
+ module FieldIndex
8
+ module_function
9
+
10
+ # Look indexes up by +GeneratedNames.filter_key+, never by position:
11
+ # {FieldWalk} emits Associations before Method Attributes.
12
+ def build(descriptor)
13
+ index = {}
14
+ i = 0
15
+ fields = descriptor.attributes + descriptor.method_attributes + descriptor.associations
16
+ fields.each do |field|
17
+ index[GeneratedNames.filter_key(field)] = i
18
+ i += 1
19
+ end
20
+ index
21
+ end
22
+
23
+ # The +name: N+ shorthand parses only for identifier-style names.
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(", ")}}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panko::CodeGen
4
+ module Generators
5
+ # Both Record-access strategies and both output modes share this walk,
6
+ # so field order and filter indexes cannot differ between them.
7
+ module FieldWalk
8
+ module_function
9
+
10
+ # +attribute_emit+ lets the Specialized strategy pick each Attribute's
11
+ # read from the Model's column metadata instead of one +read_expr+ for all.
12
+ def emit_fields(descriptor, config, field_index, builder, sink, read_expr:, attribute_emit: nil)
13
+ sink.open_record(builder)
14
+ descriptor.attributes.each do |attribute|
15
+ index = field_index.fetch(GeneratedNames.filter_key(attribute))
16
+ if attribute_emit
17
+ attribute_emit.call(attribute, index)
18
+ else
19
+ sink.attribute(attribute, read_expr.call(attribute.source), config, index, builder)
20
+ end
21
+ end
22
+ descriptor.associations.each do |association|
23
+ sink.association(
24
+ association, read_expr.call(association.source), config,
25
+ field_index.fetch(GeneratedNames.filter_key(association)), builder
26
+ )
27
+ end
28
+ descriptor.method_attributes.each do |method_attribute|
29
+ sink.method_attribute(
30
+ method_attribute, config,
31
+ field_index.fetch(GeneratedNames.filter_key(method_attribute)), builder
32
+ )
33
+ end
34
+ sink.close_record(builder)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panko::CodeGen
4
+ module Generators
5
+ # Names that generated source and the emitters must agree on, kept here
6
+ # so a rename edits one method.
7
+ module GeneratedNames
8
+ module_function
9
+
10
+ def class_name(descriptor, suffix)
11
+ "#{descriptor.name}_#{suffix}"
12
+ end
13
+
14
+ def serializer_ivar(association)
15
+ "@#{association.name}_serializer"
16
+ end
17
+
18
+ def variant_serializer_ivar(association, index)
19
+ "#{serializer_ivar(association)}_#{index}"
20
+ end
21
+
22
+ def callable_ivar(method_attribute)
23
+ "@cb_#{method_attribute.name}"
24
+ end
25
+
26
+ def if_guard_ivar(association)
27
+ "@cb_if_#{association.name}"
28
+ end
29
+
30
+ def write_one
31
+ "_write_one"
32
+ end
33
+
34
+ def to_hash
35
+ "_to_hash"
36
+ end
37
+
38
+ def generic_write_one
39
+ "_generic_write_one"
40
+ end
41
+
42
+ def generic_to_hash
43
+ "_generic_to_hash"
44
+ end
45
+
46
+ def write_one_hash
47
+ "_write_one_hash"
48
+ end
49
+
50
+ def write_one_object
51
+ "_write_one_object"
52
+ end
53
+
54
+ def to_hash_hash
55
+ "_to_hash_hash"
56
+ end
57
+
58
+ def to_hash_object
59
+ "_to_hash_object"
60
+ end
61
+
62
+ def field_index_const
63
+ "FIELD_INDEX"
64
+ end
65
+
66
+ # Associations are keyed by Source, not output name, because nested
67
+ # filters are looked up by Source (+Filter::Indexed#child+).
68
+ def filter_key(field)
69
+ field.is_a?(Association) ? field.source : field.name
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panko::CodeGen
4
+ module Generators
5
+ # Values go through +Panko::CodeGen.cast_datetime+ so Hash mode matches what
6
+ # Oj's +:rails+ mode does to them in JSON mode.
7
+ class HashSink < Sink
8
+ def output
9
+ :hash
10
+ end
11
+
12
+ def suffix
13
+ "Hash"
14
+ end
15
+
16
+ def entry_name
17
+ GeneratedNames.to_hash
18
+ end
19
+
20
+ def generic_entry_name
21
+ GeneratedNames.generic_to_hash
22
+ end
23
+
24
+ def split_hash_helper
25
+ GeneratedNames.to_hash_hash
26
+ end
27
+
28
+ def split_object_helper
29
+ GeneratedNames.to_hash_object
30
+ end
31
+
32
+ def entry_params
33
+ "record, context, scope, filters"
34
+ end
35
+
36
+ # Hash mode needs no constants beyond +FIELD_INDEX+.
37
+ def emit_class_constants(descriptor, config, builder)
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
+ builder.line "result = #{GeneratedNames.to_hash}(record, context, scope, filters)"
50
+ builder.line "root_key ? {root_key => result} : result"
51
+ else
52
+ builder.line "#{GeneratedNames.to_hash}(record, context, scope, filters)"
53
+ end
54
+ end
55
+ builder.line "end"
56
+ end
57
+
58
+ def emit_serialize_many(config, builder)
59
+ signature = config.supports_root_key ?
60
+ "def serialize_many(records, context: nil, scope: nil, filters: nil, root_key: nil)" :
61
+ "def serialize_many(records, context: nil, scope: nil, filters: nil)"
62
+ builder.line signature
63
+ builder.indent do
64
+ builder.line "filters = Panko::CodeGen::Filter.wrap(filters, #{GeneratedNames.field_index_const})"
65
+ if config.supports_root_key
66
+ builder.line "validate_root_key!(root_key)"
67
+ builder.line "result = records.map { |r| #{GeneratedNames.to_hash}(r, context, scope, filters) }"
68
+ builder.line "root_key ? {root_key => result} : result"
69
+ else
70
+ builder.line "records.map { |r| #{GeneratedNames.to_hash}(r, context, scope, filters) }"
71
+ end
72
+ end
73
+ builder.line "end"
74
+ end
75
+
76
+ def open_record(builder)
77
+ builder.line "result = {}"
78
+ end
79
+
80
+ def close_record(builder)
81
+ builder.line "result"
82
+ end
83
+
84
+ # +cast: false+ skips +cast_datetime+ when the column type proves the cast would
85
+ # return the value unchanged; the wrapper otherwise runs on every write.
86
+ def attribute(attribute, read_expr, config, index, builder, cast: true)
87
+ value_expr = cast ? "Panko::CodeGen.cast_datetime(#{read_expr})" : read_expr
88
+ builder.line "unless filters.drops?(#{index})"
89
+ builder.indent do
90
+ builder.line "result[#{key_literal(attribute.name, config)}] = #{value_expr}"
91
+ end
92
+ builder.line "end"
93
+ end
94
+
95
+ def specialized_attribute(attribute, ar_model, config, index, builder)
96
+ if RecordAccess::Specialized.datetime_column_attribute?(attribute, ar_model)
97
+ datetime_column_attribute(attribute, config, index, builder)
98
+ else
99
+ attribute(
100
+ attribute,
101
+ RecordAccess::Specialized.attribute_read_expr(attribute, ar_model),
102
+ config,
103
+ index,
104
+ builder,
105
+ cast: !RecordAccess::Specialized.plain_column_attribute?(attribute, ar_model)
106
+ )
107
+ end
108
+ end
109
+
110
+ def method_attribute(method_attribute, config, index, builder)
111
+ builder.line "unless filters.drops?(#{index})"
112
+ builder.indent do
113
+ builder.line "value = #{method_attribute_call_expression(method_attribute)}"
114
+ builder.line "unless value.equal?(Panko::CodeGen::SKIP)"
115
+ builder.indent do
116
+ builder.line "result[#{key_literal(method_attribute.name, config)}] = Panko::CodeGen.cast_datetime(value)"
117
+ end
118
+ builder.line "end"
119
+ end
120
+ builder.line "end"
121
+ end
122
+
123
+ def association(association, source_read_expr, config, index, builder)
124
+ key_lit = key_literal(association.name, config)
125
+ builder.line "unless filters.drops?(#{index})"
126
+ builder.indent do
127
+ with_if_guard(association, builder) do
128
+ case association.kind
129
+ when :has_one
130
+ builder.line "value = #{source_read_expr}"
131
+ if config.null_for_missing_has_one
132
+ has_one_default(association, key_lit, builder)
133
+ else
134
+ has_one_omit(association, key_lit, builder)
135
+ end
136
+ when :has_many
137
+ has_many(association, source_read_expr, key_lit, builder)
138
+ end
139
+ end
140
+ end
141
+ builder.line "end"
142
+ end
143
+
144
+ private
145
+
146
+ def key_literal(name, config)
147
+ case config.hash_output_key_type
148
+ when :symbol then ":#{name}"
149
+ else %("#{name}")
150
+ end
151
+ end
152
+
153
+ def has_one_default(association, key_lit, builder)
154
+ builder.line "result[#{key_lit}] = if value.nil?"
155
+ builder.indent { builder.line "nil" }
156
+ builder.line "else"
157
+ builder.indent do
158
+ child_write(association, "value", builder) do |ivar|
159
+ "#{ivar}.#{GeneratedNames.to_hash}(value, context, scope, #{child_filter_expr(association)})"
160
+ end
161
+ end
162
+ builder.line "end"
163
+ end
164
+
165
+ def has_one_omit(association, key_lit, builder)
166
+ builder.line "unless value.nil?"
167
+ builder.indent do
168
+ if association.variants.empty?
169
+ builder.line "result[#{key_lit}] = #{GeneratedNames.serializer_ivar(association)}." \
170
+ "#{GeneratedNames.to_hash}(value, context, scope, #{child_filter_expr(association)})"
171
+ else
172
+ builder.line "result[#{key_lit}] = begin"
173
+ builder.indent do
174
+ child_write(association, "value", builder) do |ivar|
175
+ "#{ivar}.#{GeneratedNames.to_hash}(value, context, scope, #{child_filter_expr(association)})"
176
+ end
177
+ end
178
+ builder.line "end"
179
+ end
180
+ end
181
+ builder.line "end"
182
+ end
183
+
184
+ # +child_filter+ is looked up once per record, not once per element.
185
+ def has_many(association, source_read_expr, key_lit, builder)
186
+ builder.line "child_filter = #{child_filter_expr(association)}"
187
+ if association.variants.empty?
188
+ builder.line(
189
+ "result[#{key_lit}] = #{source_read_expr}.map { |element| " \
190
+ "#{GeneratedNames.serializer_ivar(association)}.#{GeneratedNames.to_hash}(element, context, scope, child_filter) }"
191
+ )
192
+ return
193
+ end
194
+ builder.line "result[#{key_lit}] = #{source_read_expr}.map do |element|"
195
+ builder.indent do
196
+ child_write(association, "element", builder) do |ivar|
197
+ "#{ivar}.#{GeneratedNames.to_hash}(element, context, scope, child_filter)"
198
+ end
199
+ end
200
+ builder.line "end"
201
+ end
202
+
203
+ # +format_raw+ returns nil for raw values it cannot format; the typed-read
204
+ # fallback then goes through +cast_datetime+ so a Time still becomes a String.
205
+ def datetime_column_attribute(attribute, config, index, builder)
206
+ source_lit = %("#{attribute.source}")
207
+ builder.line "unless filters.drops?(#{index})"
208
+ builder.indent do
209
+ builder.line "value = Panko::CodeGen::DateTimeFormat.format_raw(record.read_attribute_before_type_cast(#{source_lit}))"
210
+ builder.line "result[#{key_literal(attribute.name, config)}] = value || " \
211
+ "Panko::CodeGen.cast_datetime(record._read_attribute(#{source_lit}))"
212
+ end
213
+ builder.line "end"
214
+ end
215
+ end
216
+ end
217
+ end