lutaml-model 0.8.128 → 0.8.130

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: d822754e3a7baeb6edb87dcfa7fb61c90c5da8b44920b2ad612905cb8ee9a278
4
- data.tar.gz: ef8bcf9d531357fcd8d3fd07fc5053015c1b0e6202530866adf01f7e932123de
3
+ metadata.gz: 600c15c36628ae5051c4c447ebd9f6b8fe7e2b5b5c70dc894e2de663f2725a32
4
+ data.tar.gz: a5bcfd67b0416116fdfe806123ce7c679866fc292d19eb6eb1c6ea8849ac2e72
5
5
  SHA512:
6
- metadata.gz: 9c4c5d63a666d0b8b99279de9dcf7fbee3031b694abba3729636249f701a4f67388507c13c778318f61d22ac1ce9a21940bf35960299e2a569fb024e302e3560
7
- data.tar.gz: 7eb201762de275df248d27f7fc7c5e1019cd65e63822059b7075a69aae661d8a18f8123522797155665d8688d8147a6e06d46fdffde8fbc35e9f4347bd8424b9
6
+ metadata.gz: 46d83e87714efcd4d5c60f552eaf94aac748b5c121f458008a07eeece815e1121a00ad95623dc85bad3cab77204de675c0cd876ca0745fe9dfcb63655304ae67
7
+ data.tar.gz: be9904e6941df96b90c3feed829f399475a247ecc1900d86787d8dba224d1ef0a7ad8d68838ad13d5c4136df54d034dccb10bb72988424a95cf6541205243739
@@ -57,9 +57,8 @@ module Lutaml
57
57
  end
58
58
 
59
59
  def hash_mappings
60
- return @root_mappings if @root_mappings
61
-
62
- @child_mappings
60
+ @hash_mappings = @root_mappings || @child_mappings if @hash_mappings.nil?
61
+ @hash_mappings
63
62
  end
64
63
 
65
64
  def deep_dup
@@ -81,7 +80,8 @@ module Lutaml
81
80
  end
82
81
 
83
82
  def root_mapping?
84
- name == "root_mapping"
83
+ @root_mapping = name == "root_mapping" if @root_mapping.nil?
84
+ @root_mapping
85
85
  end
86
86
 
87
87
  # The writer symbol for this rule's target, interned once per
@@ -28,7 +28,9 @@ module Lutaml
28
28
  end
29
29
 
30
30
  if model_class.include?(Lutaml::Model::Serialize)
31
- instance = model_class.new(lutaml_register: child_register)
31
+ # #1003: constructor-equivalent state via the compiled walk
32
+ # seed — no per-attribute determine_value walk per instance.
33
+ instance = model_class.allocate_for_walk(child_register)
32
34
  else
33
35
  instance = model_class.new
34
36
  register_accessor_methods_for(instance, child_register)
@@ -430,7 +432,7 @@ child_hash, format, { mappings: type.child_rules })
430
432
  children << child
431
433
  end
432
434
  end
433
- instance = model_class.new(lutaml_register: register)
435
+ instance = model_class.allocate_for_walk(register)
434
436
  setters.each { |name, value| instance.public_send(name, value) }
435
437
  absent&.each do |rule|
436
438
  process_mapping_rule(doc, instance, format, rule, options, nil)
@@ -477,7 +479,7 @@ options)
477
479
  to_method = rule.custom_methods[:to]
478
480
  # lutaml-model#550: custom methods may declare a third context
479
481
  # parameter to receive the options passed to `to_*`.
480
- if instance.method(to_method).parameters.size >= 3
482
+ if Lutaml::Model::MappingRule.custom_method_arity(instance.class, to_method).to_i >= 3
481
483
  return instance.public_send(to_method, instance, hash,
482
484
  options[:context])
483
485
  end
@@ -353,7 +353,7 @@ register = self.register)
353
353
  doc.value = {}
354
354
  # lutaml-model#550: custom methods may declare a third context
355
355
  # parameter to receive the options passed to `to_*`.
356
- if model_instance.method(to_method).parameters.size >= 3
356
+ if Lutaml::Model::MappingRule.custom_method_arity(model_instance.class, to_method).to_i >= 3
357
357
  model_instance.public_send(to_method, model_instance,
358
358
  doc, options[:context])
359
359
  else
@@ -624,6 +624,13 @@ instance_object = nil)
624
624
  value.frozen?
625
625
  end
626
626
 
627
+ # Static classification of this attribute's default form (:none,
628
+ # :static, :proc, :delegate) — the constructor's determine_value
629
+ # and the compiled walk-seed share it.
630
+ def default_kind
631
+ @default_kind ||= _default_kind
632
+ end
633
+
627
634
  def default_value(register, instance_object = nil)
628
635
  # The kind is fixed at definition; the common case (no default
629
636
  # at all) paid three option checks per absent attribute per
@@ -1368,7 +1375,9 @@ instance_object = nil)
1368
1375
  def set_default_for_collection
1369
1376
  validate_collection_range
1370
1377
  attr = self
1371
- @options[:default] ||= -> { attr.build_collection } if initialize_empty?
1378
+ if initialize_empty?
1379
+ @options[:default] ||= -> { attr.build_collection }
1380
+ end
1372
1381
  end
1373
1382
 
1374
1383
  def validate_options!(options)
@@ -300,12 +300,31 @@ module Lutaml
300
300
  end
301
301
  end
302
302
 
303
+ # Arity of a custom (from:/to:) method, memoized per class+method:
304
+ # the reflection (method()/parameters) allocated per call and ran
305
+ # per value in the serialize/deserialize hot loops (42% of a
306
+ # custom-heavy to_json profile).
307
+ def self.custom_method_arity(klass, method_name)
308
+ per_klass = (@custom_method_arities ||= {}.compare_by_identity)
309
+ cache = (per_klass[klass] ||= {}.compare_by_identity)
310
+ cached = cache[method_name]
311
+ return cached if cached
312
+
313
+ begin
314
+ arity = klass.instance_method(method_name).parameters.size
315
+ rescue NameError
316
+ arity = nil
317
+ end
318
+ cache[method_name] = arity
319
+ arity
320
+ end
321
+
303
322
  def serialize(model, parent = nil, doc = nil, context = nil)
304
323
  if custom_methods[:to]
305
324
  to_method = custom_methods[:to]
306
325
  # lutaml-model#550: custom methods may declare a fourth
307
326
  # context parameter to receive the options passed to `to_*`.
308
- if model.method(to_method).parameters.size >= 4
327
+ if self.class.custom_method_arity(model.class, to_method).to_i >= 4
309
328
  model.public_send(to_method, model, parent, doc, context)
310
329
  else
311
330
  model.public_send(to_method, model, parent, doc)
@@ -555,7 +574,7 @@ context = nil, pre_cast: false)
555
574
  target = mapper_class.new
556
575
  # lutaml-model#550: custom methods may declare a third context
557
576
  # parameter to receive the options passed to `from_*`.
558
- if target.method(custom).parameters.size >= 3
577
+ if self.class.custom_method_arity(target.class, custom).to_i >= 3
559
578
  target.public_send(custom, model, value, context)
560
579
  else
561
580
  target.public_send(custom, model, value)
@@ -71,9 +71,10 @@ module Lutaml
71
71
  end
72
72
 
73
73
  def as_models(schema, options: {})
74
- unless Config.xml_adapter.name.end_with?("NokogiriAdapter")
75
- raise Error, XML_ADAPTER_NOT_SET_MESSAGE
76
- end
74
+ # The schema builder resolves its own DOM builder from the
75
+ # configured adapter type (falling back to nokogiri); a stale
76
+ # adapter-class name check here rejected every caller after
77
+ # Config.xml_adapter became symbol-based (#251).
77
78
 
78
79
  parsed_schema = Lutaml::Xml::Schema::Xsd.parse(
79
80
  schema, location: options[:location]
@@ -120,6 +120,84 @@ module Lutaml
120
120
  RUBY
121
121
  end
122
122
 
123
+ # The walk-side seed (#1003): constructor-equivalent state in one
124
+ # compiled pass. Unlike the deserialization seed (sentinels, lazy
125
+ # collections — readers answer uninitialized), the programmatic
126
+ # constructor resolves defaults and leaves non-defaulted
127
+ # attributes nil with explicit using-default marks; the KV walk's
128
+ # absent-attribute rendering contract depends on exactly that.
129
+ def walk_state_defaults_name!(register_id)
130
+ @walk_state_defaults_names ||= {}
131
+ name = @walk_state_defaults_names[register_id]
132
+ return name if name
133
+
134
+ method_name = :"__init_walk_state_defaults_#{register_id}"
135
+ compile_walk_state_defaults!(method_name, register_id)
136
+ @walk_state_defaults_names[register_id] = method_name
137
+ method_name
138
+ end
139
+
140
+ def compile_walk_state_defaults!(method_name, register_id = nil)
141
+ attrs = attributes(register_id)
142
+ marks = {}
143
+ simple = []
144
+ complex = []
145
+ attrs.each do |name, attr|
146
+ next if attr.derived?
147
+
148
+ # Inline only the provably-simple shape: no default, no
149
+ # value set (enum attributes materialize through their
150
+ # writer), plain ivar-safe name. The constructor's end
151
+ # state for that shape is exactly `nil` + a false mark.
152
+ # Everything else — defaults, enums, delegated and exotic
153
+ # attributes — routes through the constructor's own
154
+ # per-attribute code, so its state is identical by
155
+ # construction.
156
+ marks[name] = false
157
+ if attr.default_kind == :none && attr.options[:values].nil? &&
158
+ name.to_s.match?(PLAIN_NAME)
159
+ simple << name
160
+ else
161
+ complex << name
162
+ end
163
+ end
164
+
165
+ marks_line = if marks.empty?
166
+ "@using_default = {}"
167
+ else
168
+ pairs = marks.map { |k, v| "#{k.inspect} => #{v}" }
169
+ "@using_default = { #{pairs.join(', ')} }"
170
+ end
171
+ simple_lines = simple.map { |n| "@#{n} = nil" }
172
+ complex_lines = complex.map do |n|
173
+ "initialize_single_attribute({}, #{n.inspect}, " \
174
+ "self.class.attributes(@lutaml_register)[#{n.inspect}], " \
175
+ "Serialize::DEFAULT_VALUE_MAP)"
176
+ end
177
+
178
+ unless AttributeDefinition.source_compilation?
179
+ define_method(method_name) do
180
+ @using_default = marks
181
+ simple.each { |n| IVAR_SET.bind_call(self, :"@#{n}", nil) }
182
+ complex.each do |n|
183
+ initialize_single_attribute(
184
+ {}, n, self.class.attributes(@lutaml_register)[n],
185
+ Serialize::DEFAULT_VALUE_MAP
186
+ )
187
+ end
188
+ end
189
+ return
190
+ end
191
+
192
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
193
+ def #{method_name}
194
+ #{marks_line}
195
+ #{simple_lines.join("\n")}
196
+ #{complex_lines.join("\n")}
197
+ end
198
+ RUBY
199
+ end
200
+
123
201
  # Block-form raw-state reader (no String eval), defined on self.
124
202
  def define_raw_state_reader(name)
125
203
  ivar = :"@#{name}"
@@ -363,6 +363,18 @@ module Lutaml
363
363
  #
364
364
  # @param register [Symbol, nil] The register context
365
365
  # @return [Object] The allocated instance
366
+ # Walk-side allocation (#1003): the KV walk's instantiation —
367
+ # constructor-equivalent state via the compiled walk seed
368
+ # (defaults resolved, non-defaulted attributes nil) without the
369
+ # per-attribute determine_value walk.
370
+ def allocate_for_walk(register = nil)
371
+ register_id = extract_register_id(register)
372
+ ensure_imports!(register_id) if finalized? && deferred_imports_pending?(register_id)
373
+ instance = allocate
374
+ instance.init_walk_state(register_id)
375
+ instance
376
+ end
377
+
366
378
  def allocate_for_deserialization(register = nil)
367
379
  register_id = extract_register_id(register)
368
380
  # Deferred imports (symbol-form import_model_attributes) must
@@ -190,6 +190,18 @@ module Lutaml
190
190
  register_in_reference_store
191
191
  end
192
192
 
193
+ # Walk-side construction (#1003): constructor-equivalent state
194
+ # (defaults resolved, non-defaulted attributes nil, explicit
195
+ # using-default marks) seeded in one compiled pass, replacing the
196
+ # per-attribute determine_value walk on every parsed instance.
197
+ def init_walk_state(register)
198
+ register_id = self.class.extract_register_id(register)
199
+ @lutaml_register = register_id
200
+ public_send(self.class.walk_state_defaults_name!(register_id))
201
+ define_singleton_attribute_methods
202
+ register_in_reference_store
203
+ end
204
+
193
205
  def extract_register_id(attrs, options)
194
206
  register = attrs&.dig(:lutaml_register) || options&.dig(:register)
195
207
  self.class.extract_register_id(register)
@@ -378,19 +390,27 @@ module Lutaml
378
390
  self.class.attributes(lutaml_register).each do |name, attr|
379
391
  next if attr.derived?
380
392
 
381
- value = determine_value(attrs, name, attr)
382
- default = using_default?(name)
383
- value = self.class.apply_value_map(value, vmap, attr)
384
- # Performance: Only call ensure_utf8 for string values
385
- value = self.class.ensure_utf8(value) if value.is_a?(::String)
386
- # Interpolating the writer symbol allocated a String per
387
- # attribute per instance; the per-register writer table is
388
- # interned once per class.
389
- public_send(self.class.instantiate_writers(lutaml_register)[name], value)
390
- using_default_for(name) if default
393
+ initialize_single_attribute(attrs, name, attr, vmap)
391
394
  end
392
395
  end
393
396
 
397
+ # The constructor's per-attribute body, callable one attribute at
398
+ # a time — the compiled walk seed (#1003) routes every attribute
399
+ # it does not inline through this exact code, so its state is
400
+ # identical by construction.
401
+ def initialize_single_attribute(attrs, name, attr, vmap)
402
+ value = determine_value(attrs, name, attr)
403
+ default = using_default?(name)
404
+ value = self.class.apply_value_map(value, vmap, attr)
405
+ # Performance: Only call ensure_utf8 for string values
406
+ value = self.class.ensure_utf8(value) if value.is_a?(::String)
407
+ # Interpolating the writer symbol allocated a String per
408
+ # attribute per instance; the per-register writer table is
409
+ # interned once per class.
410
+ public_send(self.class.instantiate_writers(lutaml_register)[name], value)
411
+ using_default_for(name) if default
412
+ end
413
+
394
414
  def determine_value(attrs, name, attr)
395
415
  # One hash pass: a sentinel fetch distinguishes absent keys
396
416
  # from present-nil values, replacing two key probes plus the
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.128"
5
+ VERSION = "0.8.130"
6
6
  end
7
7
  end
@@ -18,6 +18,7 @@ module Lutaml
18
18
  map_attribute :id, to: :id
19
19
  map_attribute :itemType, to: :item_type
20
20
  map_element :annotation, to: :annotation
21
+ map_element :simpleType, to: :simple_type
21
22
  end
22
23
 
23
24
  Lutaml::Xml::Schema::Xsd.register_model(self, :list)
@@ -104,6 +104,14 @@ RSpec.describe "KV group-then-bulk hydration" do
104
104
  @built += 1
105
105
  super
106
106
  end
107
+
108
+ # The walk and group lanes construct through the compiled walk
109
+ # seed (#1003) rather than new; count that entry too so the
110
+ # wiring proof tracks the real construction path.
111
+ def allocate_for_walk(register = nil)
112
+ @built += 1
113
+ super
114
+ end
107
115
  end
108
116
  end
109
117
  holder = Class.new(Lutaml::Model::Serializable) do
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/schema"
5
+ require "tmpdir"
6
+
7
+ # Runtime XSD-to-classes compilation (#251): Schema.from_xml compiles a
8
+ # schema into generated Serializable classes, and with load_classes: the
9
+ # classes are available in-process for direct parsing and serialization —
10
+ # no generated files on disk.
11
+ RSpec.describe "runtime XSD class loading" do
12
+ let(:xsd) do
13
+ <<~XSD
14
+ <?xml version="1.0" encoding="UTF-8"?>
15
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
16
+ <xs:complexType name="EntryType">
17
+ <xs:sequence>
18
+ <xs:element name="name" type="xs:string"/>
19
+ <xs:element name="year" type="xs:integer"/>
20
+ </xs:sequence>
21
+ <xs:attribute name="id" type="xs:string" use="required"/>
22
+ </xs:complexType>
23
+ <xs:element name="entry" type="EntryType"/>
24
+ </xs:schema>
25
+ XSD
26
+ end
27
+
28
+ let(:instance_xml) do
29
+ <<~XML
30
+ <entry id="e1"><name>Alpha</name><year>2024</year></entry>
31
+ XML
32
+ end
33
+
34
+ it "compiles a schema, loads the classes, and round-trips an instance" do
35
+ Dir.mktmpdir do |dir|
36
+ Lutaml::Model::Schema.from_xml(
37
+ xsd, output_dir: File.join(dir, "runtime_pilot_models"),
38
+ load_classes: true
39
+ )
40
+
41
+ doc = RuntimePilotModels::EntryType.from_xml(instance_xml)
42
+
43
+ expect(doc.name).to eq("Alpha")
44
+ expect(doc.year).to eq(2024)
45
+ expect(doc.id).to eq("e1")
46
+
47
+ expect(doc.to_xml).to include("<name>Alpha</name>")
48
+ expect(doc.to_xml).to include("<year>2024</year>")
49
+ expect(doc.to_xml).to include('id="e1"')
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_helper"
4
+
5
+ RSpec.describe "xs:list nested simpleType mapping" do
6
+ it "maps the inline simpleType of a list from the parsed XSD" do
7
+ xsd = <<~XSD
8
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
9
+ <xs:element name="r">
10
+ <xs:simpleType>
11
+ <xs:list>
12
+ <xs:simpleType>
13
+ <xs:restriction base="xs:string">
14
+ <xs:enumeration value="a"/>
15
+ </xs:restriction>
16
+ </xs:simpleType>
17
+ </xs:list>
18
+ </xs:simpleType>
19
+ </xs:element>
20
+ </xs:schema>
21
+ XSD
22
+
23
+ schema = Lutaml::Xml::Schema::Xsd.parse(xsd)
24
+ list = schema.element.first.simple_type.list
25
+
26
+ expect(list.simple_type).to be_a(Lutaml::Xml::Schema::Xsd::SimpleType)
27
+ values = Array(list.simple_type.restriction.enumeration).map(&:value)
28
+ expect(values).to eq(["a"])
29
+ end
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.128
4
+ version: 0.8.130
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-24 00:00:00.000000000 Z
11
+ date: 2026-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -1837,6 +1837,7 @@ files:
1837
1837
  - spec/lutaml/model/schema/renderers/namespace_spec.rb
1838
1838
  - spec/lutaml/model/schema/renderers/restricted_type_spec.rb
1839
1839
  - spec/lutaml/model/schema/renderers/union_spec.rb
1840
+ - spec/lutaml/model/schema/xml_compiler_runtime_spec.rb
1840
1841
  - spec/lutaml/model/schema/xsd_schema_spec.rb
1841
1842
  - spec/lutaml/model/schema/yaml_schema_spec.rb
1842
1843
  - spec/lutaml/model/sequence_choice_compositor_spec.rb
@@ -2054,6 +2055,7 @@ files:
2054
2055
  - spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb
2055
2056
  - spec/lutaml/xml/schema/xsd/liquid_methods/schema_spec.rb
2056
2057
  - spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb
2058
+ - spec/lutaml/xml/schema/xsd/list_simple_type_spec.rb
2057
2059
  - spec/lutaml/xml/schema/xsd/namespace_uri_remapping_spec.rb
2058
2060
  - spec/lutaml/xml/schema/xsd/schema_helper_methods_spec.rb
2059
2061
  - spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb