lutaml-model 0.8.68 → 0.8.70

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: '08259545535ddbd9c57aad9adfcb1fa66ebf9179fe00cbfa6be408285d1cdca5'
4
- data.tar.gz: 84c75404c371bb433b18d9cc87fab24b252b15fe8c2555544fc7b12478ff37c6
3
+ metadata.gz: 2e3d309f15ab3866bd793f01385378fc7294127de341e8914a27838bfd6b956c
4
+ data.tar.gz: ac90f885dd393c008adb09187586ec2698d9e1f40a8f3fd9bb579ad12e776fe3
5
5
  SHA512:
6
- metadata.gz: 2b730dcc7e7f555270929f4a42eb9b7a2f7e2d26a4a73fb1c75cbaa6415d2d54c0c5674ba2fad1bdba1cda106c3f625f19eed4db5d58d59334c79311c5dc46ad
7
- data.tar.gz: 4a8fa22b33898a088d8ba3e9a2b00c8bed316f73a0d0e2dcca776aa290f484bf9519952c7e2076161779faaff4aa5d16fb3be52b10e963366369bf663d75d6f4
6
+ metadata.gz: 5911aff11ba087c00b6854ee18106df409516688b093e28149f6dbff8c31ea00d15cc82ff67cfe85fbb0c61d34d0d14849d43d1c4e2a114be41e244d293f5b1c
7
+ data.tar.gz: 91b861ce8a517ee382284684f2516493d96f5aef6a1588e9a01eefb1108e9fb86a37a91dbbda39cf02a0b38f32ce8f9603dc5498f2e7b4fec647d21bfaca6587
@@ -177,18 +177,21 @@ module Lutaml
177
177
  .substitution_for(type).any?
178
178
 
179
179
  if type.is_a?(Class) && type.include?(Lutaml::Model::Serialize)
180
- child_rows = kv_group_plan(type, format,
181
- Lutaml::Model::Register
182
- .resolve_for_child(type, register))
180
+ child_register = Lutaml::Model::Register
181
+ .resolve_for_child(type, register)
182
+ child_rows = kv_group_plan(type, format, child_register)
183
183
  return false unless child_rows
184
184
 
185
185
  rows ||= []
186
- rows << [rule, attr, :model, type, child_rows]
186
+ # child_register rides the row: the plan was BUILT under it,
187
+ # and rebuilding the resolution per model row per document
188
+ # cost a Concurrent::Map hop each time (TODO.perf 14).
189
+ rows << [rule, attr, :model, type, child_rows, child_register]
187
190
  elsif type.is_a?(Class) && type < Lutaml::Model::Type::Value &&
188
191
  !attr.value_policy.whole_value?(type) &&
189
192
  !Lutaml::Model::Attribute.custom_from_probe?(type)
190
193
  rows ||= []
191
- rows << [rule, attr, :scalar, nil, nil]
194
+ rows << [rule, attr, :scalar, nil, nil, nil]
192
195
  else
193
196
  return false
194
197
  end
@@ -208,7 +211,7 @@ module Lutaml
208
211
  setters = []
209
212
  children = []
210
213
  absent = []
211
- rows.each do |rule, attr, kind, type, child_rows|
214
+ rows.each do |rule, attr, kind, type, child_rows, child_register|
212
215
  # One hash pass per row: fetch with a sentinel distinguishes
213
216
  # absent keys from present-nil values (both read as nil), so
214
217
  # the separate string_or_symbol_key? probe goes away.
@@ -223,8 +226,6 @@ module Lutaml
223
226
  next
224
227
  end
225
228
 
226
- child_register = Lutaml::Model::Register.resolve_for_child(type,
227
- register)
228
229
  if attr.collection?
229
230
  # A present-but-nil collection reaches the per-rule walk,
230
231
  # which owns the sentinel interplay for that edge
@@ -151,8 +151,19 @@ model_class: nil)
151
151
  return value.public_send(:"to_#{format}")
152
152
  end
153
153
 
154
- # Wrap value in type and call to_#{format}
155
154
  if rule.attribute_type.is_a?(Class) && rule.attribute_type < Lutaml::Model::Type::Value
155
+ plan = serialize_plan(rule)
156
+ # Identity only under identity_fast_type's exact condition
157
+ # (no custom to AND no custom from): a custom from means the
158
+ # wrapper constructor normalizes the value, so the raw value
159
+ # must still pass through it.
160
+ if plan[:identity_type] && value.instance_of?(plan[:identity_type])
161
+ return value
162
+ end
163
+ if (to_fn = plan[:to_fn])
164
+ return to_fn.call(rule.attribute_type.new(value))
165
+ end
166
+
156
167
  wrapped_value = rule.attribute_type.new(value)
157
168
  wrapped_value.public_send(:"to_#{format}")
158
169
  else
@@ -187,9 +198,23 @@ model_class: nil)
187
198
  reference: reference_type?(rule),
188
199
  union: ::Lutaml::Model::Type::Union.rule?(rule),
189
200
  nested: nested_model?(rule),
201
+ # The custom to_<format> serializer, resolved once per rule:
202
+ # the default path is pure identity (to_<format> returns the
203
+ # wrapped .value), so values without a custom serializer skip
204
+ # the wrap-allocate-cast ceremony entirely, and custom ones
205
+ # skip the per-value registry lookup.
206
+ to_fn: custom_to_fn(rule),
190
207
  }
191
208
  end
192
209
 
210
+ def custom_to_fn(rule)
211
+ type = rule.attribute_type
212
+ return nil unless type.is_a?(::Class) && type < ::Lutaml::Model::Type::Value
213
+
214
+ ::Lutaml::Model::Type::Value
215
+ .format_type_serializer_for(@format, type)&.fetch(:to, nil)
216
+ end
217
+
193
218
  # Builtin scalars whose type registers no custom to_<format>
194
219
  # serializer serialize as the value itself — Type::Value#to_<format>'s
195
220
  # default — without the wrap-allocate-cast ceremony per field.
@@ -12,6 +12,14 @@ module Lutaml
12
12
  # options are normalized into value_map entries at MappingRule construction
13
13
  # time, so downstream code never needs to re-interpret them.
14
14
  module RenderPolicy
15
+ # Isolated holder: host transformations freeze after compile; an
16
+ # ivar cache on them raises FrozenError (learned the hard way).
17
+ SKIP_PLANS = ::Class.new do
18
+ def self.cache
19
+ @cache ||= {}.compare_by_identity
20
+ end
21
+ end.cache
22
+
15
23
  def self.derived_attribute_for?(context_obj, attr_name)
16
24
  return false unless context_obj.is_a?(Lutaml::Model::Serialize) &&
17
25
  context_obj.class.is_a?(Class) &&
@@ -52,16 +60,26 @@ module Lutaml
52
60
  # @param context_obj [Object] The context object (model_instance or delegate_obj)
53
61
  # @return [Boolean] true if should skip
54
62
  def check_skip_logic?(value, rule, context_obj)
55
- attr_name = extract_attribute_name(rule)
56
-
57
- to_map = to_value_map(rule)
58
-
59
- case value
60
- when nil
63
+ # Rule-constant parts (attribute name, :to value map) are frozen
64
+ # per rule; recomputing them per value showed in the to_yaml
65
+ # wall profile. The host transformation freezes itself after
66
+ # compile, so the cache lives in an isolated holder (TypeProbeCache
67
+ # precedent): host object -> identity-keyed rule plans.
68
+ per_host = (SKIP_PLANS[self] ||= {}.compare_by_identity)
69
+ skip_plan = (per_host[rule] ||= [extract_attribute_name(rule), to_value_map(rule)])
70
+ attr_name, to_map = skip_plan
71
+
72
+ # Plain conditionals, not `when ->(v)` literals: each lambda
73
+ # literal in a case/when allocates on every call — two per
74
+ # value here, 16% of to_yaml's allocations in the object-mode
75
+ # profile.
76
+ if value.nil?
61
77
  return to_map[:nil] == :omitted
62
- when ->(v) { Lutaml::Model::Utils.empty?(v) }
78
+ end
79
+ if Lutaml::Model::Utils.empty?(value)
63
80
  return to_map[:empty] == :omitted
64
- when ->(v) { Lutaml::Model::Utils.uninitialized?(v) }
81
+ end
82
+ if Lutaml::Model::Utils.uninitialized?(value)
65
83
  return to_map[:omitted] == :omitted || to_map[:omitted].nil?
66
84
  end
67
85
 
@@ -197,8 +197,11 @@ module Lutaml
197
197
  end
198
198
 
199
199
  def attr_value(attrs, name, attribute)
200
- value = Utils.fetch_str_or_sym(attrs, name,
201
- attribute.default(lutaml_register, self))
200
+ # The default is computed only on absence: it used to be
201
+ # evaluated eagerly as the fetch fallback on EVERY value,
202
+ # paying proc/static default dispatch per present attribute.
203
+ value = Utils.fetch_str_or_sym(attrs, name, Utils::MISSING)
204
+ value = attribute.default(lutaml_register, self) if value.equal?(Utils::MISSING)
202
205
  attribute.cast_value(value, lutaml_register)
203
206
  end
204
207
 
@@ -368,15 +371,20 @@ module Lutaml
368
371
  value = self.class.apply_value_map(value, vmap, attr)
369
372
  # Performance: Only call ensure_utf8 for string values
370
373
  value = self.class.ensure_utf8(value) if value.is_a?(::String)
371
- public_send(:"#{name}=", value)
374
+ # Interpolating the writer symbol allocated a String per
375
+ # attribute per instance; the per-register writer table is
376
+ # interned once per class.
377
+ public_send(self.class.instantiate_writers(lutaml_register)[name], value)
372
378
  using_default_for(name) if default
373
379
  end
374
380
  end
375
381
 
376
382
  def determine_value(attrs, name, attr)
377
- if attrs.key?(name) || attrs.key?(name.to_s)
378
- return attr_value(attrs, name, attr)
379
- end
383
+ # One hash pass: a sentinel fetch distinguishes absent keys
384
+ # from present-nil values, replacing two key probes plus the
385
+ # re-fetch inside attr_value (four hash ops per attribute).
386
+ value = Utils.fetch_str_or_sym(attrs, name, Utils::MISSING)
387
+ return attr.cast_value(value, lutaml_register) unless value.equal?(Utils::MISSING)
380
388
 
381
389
  resolved = attr.default_value(lutaml_register, self)
382
390
  return Lutaml::Model::UninitializedClass.instance if Lutaml::Model::Utils.uninitialized?(resolved)
@@ -67,12 +67,14 @@ from: nil)
67
67
  # @param type_class [Class] the type class to look up
68
68
  # @return [Hash, nil] { to: Proc, from: Proc } or nil
69
69
  def format_type_serializer_for(format, type_class)
70
- # Hot path: memoize per [format, resolved class] — the hierarchy
71
- # walk below only runs on a miss. Format serializers register at
72
- # load time, so a hit is stable.
73
- cache = SERIALIZER_LOOKUP_CACHE
74
- key = [format, type_class]
75
- return cache[key] if cache.key?(key)
70
+ # Hot path: memoized per format -> class with NO key
71
+ # allocation — the previous [format, class] array key
72
+ # allocated on every lookup (2k arrays per 200 serializes in
73
+ # the object-mode profile). Identity-keyed nested maps; the
74
+ # hierarchy walk below only runs on a miss, and format
75
+ # serializers register at load time, so a hit is stable.
76
+ per_format = (SERIALIZER_LOOKUP_CACHE[format] ||= {}.compare_by_identity)
77
+ return per_format[type_class] if per_format.key?(type_class)
76
78
 
77
79
  klass = type_class
78
80
  found = nil
@@ -85,7 +87,7 @@ from: nil)
85
87
 
86
88
  klass = klass.superclass
87
89
  end
88
- cache[key] = found
90
+ per_format[type_class] = found
89
91
  found
90
92
  end
91
93
 
@@ -3,6 +3,10 @@
3
3
  module Lutaml
4
4
  module Model
5
5
  module Utils
6
+ # Fetch sentinel distinguishing absent keys from present-nil
7
+ # values; shared by the deserialization fast paths.
8
+ MISSING = Object.new.freeze
9
+
6
10
  class << self
7
11
  UNINITIALIZED = Lutaml::Model::UninitializedClass.instance
8
12
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.68"
5
+ VERSION = "0.8.70"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.68
4
+ version: 0.8.70
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.