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,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panko
4
+ # Process-global settings, read at serialize time. Set them in an
5
+ # initializer, before serializers compile:
6
+ #
7
+ # Panko.configure do |config|
8
+ # config.auto_specialization.capacity = 32
9
+ # end
10
+ class Config
11
+ # The engine compiles a specialized Generated Class for each named
12
+ # ActiveRecord class a serializer sees, up to +capacity+.
13
+ class AutoSpecialization
14
+ # @return [Boolean] +false+ routes every record class to the generic path
15
+ attr_reader :enabled
16
+
17
+ # @return [Integer] max specialized variants per serializer class and output
18
+ # mode; past it, record classes use the generic path, with one warning
19
+ # per serializer class
20
+ attr_reader :capacity
21
+
22
+ def initialize
23
+ @enabled = true
24
+ @capacity = 16
25
+ end
26
+
27
+ def enabled=(value)
28
+ unless value.equal?(true) || value.equal?(false)
29
+ raise ArgumentError,
30
+ "auto_specialization.enabled: must be true or false; got #{value.inspect}:#{value.class}"
31
+ end
32
+ @enabled = value
33
+ end
34
+
35
+ def capacity=(value)
36
+ unless value.is_a?(Integer) && value.positive?
37
+ raise ArgumentError,
38
+ "auto_specialization.capacity: must be a positive Integer; got #{value.inspect}:#{value.class}"
39
+ end
40
+ @capacity = value
41
+ end
42
+ end
43
+
44
+ @auto_specialization = AutoSpecialization.new
45
+ @writer_pool_max_bytes = 1_048_576
46
+
47
+ class << self
48
+ attr_reader :auto_specialization
49
+
50
+ # Largest +serialize_to_json+ output, in bytes, whose writer goes back to
51
+ # the pool. +Oj::StringWriter#reset+ keeps the grown buffer, so a larger
52
+ # writer is dropped instead of holding that memory. Read on every checkin.
53
+ #
54
+ # @return [Integer] default 1_048_576 (1 MB)
55
+ attr_reader :writer_pool_max_bytes
56
+
57
+ def writer_pool_max_bytes=(value)
58
+ unless value.is_a?(Integer) && value.positive?
59
+ raise ArgumentError,
60
+ "writer_pool_max_bytes: must be a positive Integer; got #{value.inspect}:#{value.class}"
61
+ end
62
+ @writer_pool_max_bytes = value
63
+ end
64
+ end
65
+ end
66
+
67
+ # Yields the {Config} class for block-style configuration.
68
+ def self.configure
69
+ yield Config
70
+ end
71
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "code_gen/serializer_cache"
4
+
5
+ module Panko
6
+ # Public, read-only view of a serializer's shape, for tools like association
7
+ # preloaders. Separate from Panko::CodeGen::Descriptor so the engine can change.
8
+ class Descriptor
9
+ Attribute = Data.define(:name, :source)
10
+ MethodAttribute = Data.define(:name, :source)
11
+ Association = Data.define(:name, :source, :kind, :descriptor)
12
+
13
+ attr_reader :serializer, :attributes, :method_attributes, :associations
14
+
15
+ # Cached on the serializer class. A concurrent first build is harmless:
16
+ # both threads build frozen views with the same content.
17
+ def self.for(serializer_class)
18
+ serializer_class._cg_public_descriptor ||= wrap(
19
+ CodeGen::SerializerCache.descriptor_for(serializer_class)
20
+ )
21
+ end
22
+
23
+ def self.wrap(internal)
24
+ new(
25
+ serializer: internal.parent_class,
26
+ attributes: internal.attributes.map { |a| Attribute.new(name: a.name, source: a.source) }.freeze,
27
+ method_attributes: internal.method_attributes.map { |m| wrap_method_attribute(m) }.freeze,
28
+ associations: internal.associations.map do |as|
29
+ Association.new(name: as.name, source: as.source, kind: as.kind, descriptor: wrap(as.descriptor))
30
+ end.freeze
31
+ ).freeze
32
+ end
33
+ private_class_method :wrap
34
+
35
+ # A Callable body has no method name, so +source+ is nil. The DSL only
36
+ # makes Symbol bodies.
37
+ def self.wrap_method_attribute(method_attribute)
38
+ body = method_attribute.body
39
+ MethodAttribute.new(name: method_attribute.name, source: body.is_a?(Symbol) ? body : nil)
40
+ end
41
+ private_class_method :wrap_method_attribute
42
+
43
+ def initialize(serializer:, attributes:, method_attributes:, associations:)
44
+ @serializer = serializer
45
+ @attributes = attributes
46
+ @method_attributes = method_attributes
47
+ @associations = associations
48
+ end
49
+
50
+ # Resolves each level on first read and memoizes it, so only the levels a
51
+ # caller visits cost anything. +filters+ has the shape
52
+ # Panko::CodeGen::Runtime.runtime_filters returns.
53
+ class Filtered < Descriptor
54
+ def initialize(skeleton, filters)
55
+ @skeleton = skeleton
56
+ @filters = filters
57
+ end
58
+
59
+ def serializer
60
+ @skeleton.serializer
61
+ end
62
+
63
+ def attributes
64
+ @attributes ||= @skeleton.attributes.select { |a| keep?(a.name) }.freeze
65
+ end
66
+
67
+ def method_attributes
68
+ @method_attributes ||= @skeleton.method_attributes.select { |m| keep?(m.name) }.freeze
69
+ end
70
+
71
+ # Associations are filtered by +source+, the declared relation name, as
72
+ # the serialize path does.
73
+ def associations
74
+ @associations ||= @skeleton.associations.filter_map do |as|
75
+ next unless keep?(as.source)
76
+ sub = @filters[as.source]
77
+ (sub.is_a?(Hash) && !sub.empty?) ? as.with(descriptor: Filtered.new(as.descriptor, sub)) : as
78
+ end.freeze
79
+ end
80
+
81
+ private
82
+
83
+ # FilterAdapter never emits both :only and :except at one level.
84
+ def keep?(name)
85
+ only = @filters[:only]
86
+ return only.include?(name) unless only.nil?
87
+ except = @filters[:except]
88
+ return !except.include?(name) unless except.nil?
89
+ true
90
+ end
91
+ end
92
+ end
93
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "oj"
4
+ require_relative "code_gen/writers_pool"
4
5
 
5
6
  module Panko
6
7
  JsonValue = Struct.new(:value) do
@@ -13,6 +14,17 @@ module Panko
13
14
  end
14
15
  end
15
16
 
17
+ # A serializer and the object it serializes, written straight into the Response writer.
18
+ SerializerValue = Struct.new(:serializer, :object) do
19
+ def to_json
20
+ serializer.serialize_to_json(object)
21
+ end
22
+
23
+ def write_json(writer, key = nil)
24
+ serializer.write_json(object, writer, key)
25
+ end
26
+ end
27
+
16
28
  class ResponseCreator
17
29
  def self.value(value)
18
30
  Panko::Response.new(value)
@@ -28,19 +40,39 @@ module Panko
28
40
  end
29
41
 
30
42
  def self.serializer(data, serializer, options = {})
31
- json serializer.new(options).serialize_to_json(data)
43
+ Panko::SerializerValue.new(serializer.new(options), data)
32
44
  end
33
45
  end
34
46
 
35
47
  class Response
48
+ # Picked on first use, like the generated classes' pools, so an app that loads
49
+ # ActiveSupport after Panko still shares one writer stack with them.
50
+ def self.writers
51
+ @writers ||= defined?(ActiveSupport::IsolatedExecutionState) ?
52
+ Panko::CodeGen::WritersPool::IsolatedExecutionState.new :
53
+ Panko::CodeGen::WritersPool::ThreadLocal.new
54
+ end
55
+
36
56
  def initialize(data)
37
57
  @data = data
38
58
  end
39
59
 
40
60
  def to_json(_options = nil)
41
- writer = Oj::StringWriter.new(mode: :rails)
42
- write(writer, @data)
43
- writer.to_s
61
+ writers = Response.writers
62
+ writer = writers.checkout
63
+ result = nil
64
+ begin
65
+ write(writer, @data)
66
+ result = writer.to_s
67
+ ensure
68
+ writers.checkin(writer, result)
69
+ end
70
+ end
71
+
72
+ # Writes this Response into +writer+ as the value of +key+, or as the next element when
73
+ # +key+ is nil.
74
+ def write_json(writer, key = nil)
75
+ write(writer, @data, key)
44
76
  end
45
77
 
46
78
  def self.create
@@ -74,10 +106,10 @@ module Panko
74
106
  end
75
107
 
76
108
  def write_value(writer, value, key = nil)
77
- if value.is_a?(Panko::ArraySerializer) ||
78
- value.is_a?(Panko::Serializer) ||
79
- value.is_a?(Panko::Response) ||
80
- value.is_a?(Panko::JsonValue)
109
+ case value
110
+ when Panko::ArraySerializer, Panko::Response, Panko::SerializerValue
111
+ value.write_json(writer, key)
112
+ when Panko::Serializer, Panko::JsonValue
81
113
  writer.push_json(value.to_json, key)
82
114
  else
83
115
  writer.push_value(value, key)
@@ -1,146 +1,230 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "serialization_descriptor"
3
+ require_relative "code_gen"
4
+ require_relative "code_gen/descriptor_builder"
5
+ require_relative "code_gen/runtime"
4
6
  require "oj"
5
7
 
6
- class SerializationContext
7
- attr_accessor :context, :scope
8
+ module Panko
9
+ class Serializer
10
+ # A method field that returns SKIP leaves its key out of the output.
11
+ SKIP = Panko::CodeGen::SKIP
8
12
 
9
- def initialize(context, scope)
10
- @context = context
11
- @scope = scope
12
- end
13
+ EMPTY_MODELS = [].freeze
13
14
 
14
- def self.create(options)
15
- if options.key?(:context) || options.key?(:scope)
16
- SerializationContext.new(options[:context], options[:scope])
17
- else
18
- EmptySerializerContext.new
19
- end
20
- end
21
- end
15
+ class << self
16
+ attr_accessor :_cg_attributes, :_cg_method_attributes, :_cg_associations
22
17
 
23
- class EmptySerializerContext
24
- def scope
25
- nil
26
- end
18
+ # Not copied on inheritance: each class compiles its own.
19
+ attr_accessor :_cg_state
27
20
 
28
- def context
29
- nil
30
- end
31
- end
21
+ # One-entry inline caches: frozen [model, pool] pairs.
22
+ attr_accessor :_cg_last_json, :_cg_last_hash
32
23
 
33
- module Panko
34
- class Serializer
35
- SKIP = Object.new.freeze
24
+ attr_accessor :_cg_public_descriptor
36
25
 
37
- class << self
38
- def inherited(base)
39
- if _descriptor.nil?
40
- base._descriptor = Panko::SerializationDescriptor.new
26
+ # Lets the unfiltered hot path skip filter resolution. A stale +true+ only
27
+ # costs a +respond_to?+ inside +runtime_filters+.
28
+ attr_accessor :_cg_has_filters_for
41
29
 
42
- base._descriptor.attributes = []
43
- base._descriptor.aliases = {}
30
+ attr_accessor :_cg_models
44
31
 
45
- base._descriptor.method_fields = []
32
+ def inherited(base)
33
+ base._cg_attributes = (_cg_attributes || []).dup
34
+ base._cg_method_attributes = (_cg_method_attributes || []).dup
35
+ base._cg_associations = (_cg_associations || []).dup
36
+ base._cg_has_filters_for = base.respond_to?(:filters_for)
37
+ base._cg_models = _cg_models || EMPTY_MODELS
38
+ end
46
39
 
47
- base._descriptor.has_many_associations = []
48
- base._descriptor.has_one_associations = []
49
- else
50
- base._descriptor = Panko::SerializationDescriptor.duplicate(_descriptor)
51
- end
52
- base._descriptor.type = base
40
+ # Declares the record classes this serializer expects, so Panko.compile_all
41
+ # can compile a specialized variant for each at boot. A hint only: records
42
+ # of other classes still serialize. Subclasses inherit the list; a second
43
+ # call replaces it.
44
+ #
45
+ # @param models [Array<Class>]
46
+ # @raise [ArgumentError] when an argument is not a Class
47
+ def models(*models)
48
+ invalid = models.reject { |model| model.is_a?(Class) }
49
+ raise ArgumentError, "models: expected classes, got #{invalid.inspect}" unless invalid.empty?
50
+ self._cg_models = models.freeze
53
51
  end
54
52
 
55
- attr_accessor :_descriptor
53
+ # A +filters_for+ defined or stubbed after the first serialize must still apply.
54
+ def singleton_method_added(method)
55
+ super
56
+ @_cg_has_filters_for = true if method == :filters_for
57
+ end
56
58
 
57
59
  def attributes(*attrs)
58
- @_descriptor.attributes.push(*attrs.map { |attr| Attribute.create(attr) }).uniq!
60
+ attrs.each { |attr| add_attribute(attr.to_sym, attr.to_sym) }
59
61
  end
60
62
 
61
63
  def aliases(aliases = {})
62
- aliases.each do |attr, alias_name|
63
- @_descriptor.attributes << Attribute.create(attr, alias_name: alias_name)
64
- end
64
+ aliases.each { |source, output| add_attribute(source.to_sym, output.to_sym) }
65
65
  end
66
66
 
67
+ # A method named like a declared attribute turns that attribute into a
68
+ # method field with the same output key.
67
69
  def method_added(method)
68
70
  super
71
+ return if _cg_attributes.nil?
72
+ index = _cg_attributes.index { |attribute| attribute.source == method }
73
+ return if index.nil?
74
+ attribute = _cg_attributes.delete_at(index)
75
+ _cg_method_attributes << Panko::CodeGen::MethodAttribute.new(name: attribute.name, body: method)
76
+ end
69
77
 
70
- return if @_descriptor.nil?
71
-
72
- deleted_attr = @_descriptor.attributes.delete(method)
73
- @_descriptor.method_fields << Attribute.create(deleted_attr.name, alias_name: deleted_attr.alias_name) unless deleted_attr.nil?
78
+ # The static public view of this serializer's declared shape.
79
+ def descriptor
80
+ Panko::Descriptor.for(self)
74
81
  end
75
82
 
76
83
  def has_one(name, options = {})
77
- serializer_const = options[:serializer]
78
- if serializer_const.is_a?(String)
79
- serializer_const = Panko::SerializerResolver.resolve(serializer_const, self)
80
- end
81
- serializer_const ||= Panko::SerializerResolver.resolve(name.to_s, self)
84
+ add_association(:has_one, name, options)
85
+ end
86
+
87
+ def has_many(name, options = {})
88
+ add_association(:has_many, name, options)
89
+ end
90
+
91
+ private
82
92
 
83
- raise "Can't find serializer for #{self.name}.#{name} has_one relationship." if serializer_const.nil?
93
+ def add_attribute(source, output)
94
+ return if _cg_attributes.any? { |attribute| attribute.source == source }
95
+ _cg_attributes << Panko::CodeGen::Attribute.new(name: output, source: source)
96
+ end
84
97
 
85
- @_descriptor.has_one_associations << Panko::Association.new(
86
- name,
87
- options.fetch(:name, name).to_s,
88
- Panko::SerializationDescriptor.build(serializer_const, options)
98
+ def add_association(kind, name, options)
99
+ serializer = resolve_association_serializer(name, options, kind)
100
+ descriptor = Panko::CodeGen::DescriptorBuilder.build(serializer)
101
+ only, except = association_filters(serializer, options)
102
+ descriptor = Panko::CodeGen::DescriptorBuilder.narrow(descriptor, only, except)
103
+ _cg_associations << Panko::CodeGen::Association.new(
104
+ name: options.fetch(:name, name).to_s.to_sym,
105
+ kind: kind,
106
+ descriptor: descriptor,
107
+ source: name.to_sym
89
108
  )
90
109
  end
91
110
 
92
- def has_many(name, options = {})
93
- serializer_const = options[:serializer] || options[:each_serializer]
94
- if serializer_const.is_a?(String)
95
- serializer_const = Panko::SerializerResolver.resolve(serializer_const, self)
111
+ # The nested serializer's +filters_for+ runs once, at declaration, with nil
112
+ # context and scope. Its keys win over the declared +only:+/+except:+.
113
+ def association_filters(serializer, options)
114
+ only = options[:only]
115
+ except = options[:except]
116
+
117
+ if serializer.respond_to?(:filters_for)
118
+ filters = serializer.filters_for(nil, nil)
119
+ only = filters[:only] if filters.key?(:only)
120
+ except = filters[:except] if filters.key?(:except)
96
121
  end
97
- serializer_const ||= Panko::SerializerResolver.resolve(name.to_s, self)
98
122
 
99
- raise "Can't find serializer for #{self.name}.#{name} has_many relationship." if serializer_const.nil?
123
+ [only, except]
124
+ end
100
125
 
101
- @_descriptor.has_many_associations << Panko::Association.new(
102
- name,
103
- options.fetch(:name, name).to_s,
104
- Panko::SerializationDescriptor.build(serializer_const, options)
105
- )
126
+ def resolve_association_serializer(name, options, kind)
127
+ serializer = options[:serializer] || options[:each_serializer]
128
+ serializer = Panko::SerializerResolver.resolve(serializer, self) if serializer.is_a?(String)
129
+ serializer ||= Panko::SerializerResolver.resolve(name.to_s, self)
130
+ raise "Can't find serializer for #{self.name}.#{name} #{kind} relationship." if serializer.nil?
131
+ serializer
106
132
  end
107
133
  end
108
134
 
109
- def initialize(options = {})
110
- # this "_skip_init" trick is so I can create serializers from serialization descriptor
111
- return if options[:_skip_init]
135
+ # A literal +{}+ default would allocate a Hash on every +.new+.
136
+ EMPTY_OPTIONS = {}.freeze
112
137
 
113
- @serialization_context = SerializationContext.create(options)
114
- @descriptor = Panko::SerializationDescriptor.build(self.class, options, @serialization_context)
115
- @used = false
116
- end
138
+ def initialize(options = EMPTY_OPTIONS)
139
+ # Unset ivars read as nil, so +.new+ skips four Hash lookups and four writes.
140
+ return if options.equal?(EMPTY_OPTIONS)
117
141
 
118
- def context
119
- @serialization_context.context
142
+ @context = options[:context]
143
+ @scope = options[:scope]
144
+ @only = options[:only]
145
+ @except = options[:except]
120
146
  end
121
147
 
122
- def scope
123
- @serialization_context.scope
148
+ # Generated code sets these per record on the instance a method field runs on.
149
+ attr_reader :object, :context, :scope
150
+
151
+ # The class-level view, narrowed by this instance's +only+/+except+ and +filters_for+.
152
+ def descriptor
153
+ klass = self.class
154
+ filters = if @only || @except || klass._cg_has_filters_for
155
+ Panko::CodeGen::Runtime.runtime_filters(klass, @context, @scope, @only, @except)
156
+ end
157
+ base = Panko::Descriptor.for(klass)
158
+ filters ? Panko::Descriptor::Filtered.new(base, filters) : base
124
159
  end
125
160
 
126
- attr_writer :serialization_context
127
- attr_reader :object
161
+ # Both methods inline the pool checkout instead of calling a shared Runtime
162
+ # method, which would go polymorphic once an app has more than one serializer.
128
163
 
129
164
  def serialize(object)
130
- serialize_with_writer(object, Panko::ObjectWriter.new).output
165
+ klass = self.class
166
+ cached = klass._cg_last_hash
167
+ pool = if cached && object.instance_of?(cached[0])
168
+ cached[1]
169
+ else
170
+ Panko::CodeGen::SerializerCache.variant_pool(klass, :hash, object.class)
171
+ end
172
+ filters = if @only || @except || klass._cg_has_filters_for
173
+ Panko::CodeGen::Runtime.runtime_filters(klass, @context, @scope, @only, @except)
174
+ end
175
+ stack = pool.stack
176
+ instance = stack.pop || pool.build
177
+ begin
178
+ instance.serialize_one(object, context: @context, scope: @scope, filters: filters)
179
+ ensure
180
+ # A pooled instance must not hold the last record or request context.
181
+ instance._release
182
+ stack.push(instance)
183
+ end
131
184
  end
132
185
 
133
186
  def serialize_to_json(object)
134
- serialize_with_writer(object, Oj::StringWriter.new(mode: :rails)).to_s
187
+ klass = self.class
188
+ cached = klass._cg_last_json
189
+ pool = if cached && object.instance_of?(cached[0])
190
+ cached[1]
191
+ else
192
+ Panko::CodeGen::SerializerCache.variant_pool(klass, :json, object.class)
193
+ end
194
+ filters = if @only || @except || klass._cg_has_filters_for
195
+ Panko::CodeGen::Runtime.runtime_filters(klass, @context, @scope, @only, @except)
196
+ end
197
+ stack = pool.stack
198
+ instance = stack.pop || pool.build
199
+ begin
200
+ instance.serialize_one(object, context: @context, scope: @scope, filters: filters)
201
+ ensure
202
+ instance._release
203
+ stack.push(instance)
204
+ end
135
205
  end
136
206
 
137
- private
138
-
139
- def serialize_with_writer(object, writer)
140
- raise ArgumentError.new("Panko::Serializer instances are single-use") if @used
141
- Panko.serialize_object(object, writer, @descriptor)
142
- @used = true
143
- writer
207
+ # Writes +object+ into +writer+ (an +Oj::StringWriter+) as the value of +key+, or as the
208
+ # next element when +key+ is nil. The bytes match what +serialize_to_json+ returns.
209
+ def write_json(object, writer, key = nil)
210
+ klass = self.class
211
+ cached = klass._cg_last_json
212
+ pool = if cached && object.instance_of?(cached[0])
213
+ cached[1]
214
+ else
215
+ Panko::CodeGen::SerializerCache.variant_pool(klass, :json, object.class)
216
+ end
217
+ filters = if @only || @except || klass._cg_has_filters_for
218
+ Panko::CodeGen::Runtime.runtime_filters(klass, @context, @scope, @only, @except)
219
+ end
220
+ stack = pool.stack
221
+ instance = stack.pop || pool.build
222
+ begin
223
+ instance.serialize_one_to(writer, object, key, context: @context, scope: @scope, filters: filters)
224
+ ensure
225
+ instance._release
226
+ stack.push(instance)
227
+ end
144
228
  end
145
229
  end
146
230
  end
@@ -21,14 +21,8 @@ class Panko::SerializerResolver
21
21
 
22
22
  private
23
23
 
24
- if Module.method_defined?(:module_parent_name)
25
- def namespace_for(from)
26
- from.module_parent_name
27
- end
28
- else
29
- def namespace_for(from)
30
- from.parent_name
31
- end
24
+ def namespace_for(from)
25
+ from.module_parent_name
32
26
  end
33
27
 
34
28
  def safe_serializer_get(name)
data/lib/panko/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Panko
4
- VERSION = "0.8.5"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "panko/version"
4
- require "panko/attribute"
5
- require "panko/association"
4
+ require "panko/config"
6
5
  require "panko/serializer"
6
+ require "panko/compile_all"
7
+ require "panko/descriptor"
7
8
  require "panko/array_serializer"
8
9
  require "panko/response"
9
10
  require "panko/serializer_resolver"
10
- require "panko/object_writer"
11
11
 
12
- # C Extension
13
12
  require "oj"
14
- require "panko/panko_serializer"