lutaml-model 0.8.136 → 0.8.138

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: 78585d088d67236f3378f1860c5916e37a1bad8bde58bb48954fe8057ada72b9
4
- data.tar.gz: afbf8b41d1b733685ad4d066c07e5217eff0674a1eb66bbbc75e6d9cdd2bf31e
3
+ metadata.gz: 9b04af29b0ab95619a185f91ca138ff213b771112707395df0aea9d093978aa5
4
+ data.tar.gz: c3830c610420cceb888b370417cf2f83a044aa00e10cb3bca7c71c3833587dc4
5
5
  SHA512:
6
- metadata.gz: c2b9f8b00bfcc541d7ccebc179c9d0ce13fc530c594d605d7c4aeafd5dc1eb386c253eebdcc597a3257be3978e1fe74b8ba19b9bc05053d45a4662bd0bae7c63
7
- data.tar.gz: 5e739f42a165ca2280473a03addd0418061f584e28d1f0d8fc7a015e0855a30e56dab47b47024bbc3fdc4ded50927654e2659a5d00ea5bd68b6c997cfca54224
6
+ metadata.gz: 205e61d1960bfff5771363582c8db6600f6f703a255b69e7684d40abc04434dc9ef786ec260518930afb4584db219acade52068975fea7ac39604cab82891d28
7
+ data.tar.gz: 1a19fc45f5880487573f2849203247015edd843f9c7000a6de992c70aaa439dbcc36e4741f1027b0ca441ee8358ec3ce1559e3f877f91476b80a3b661a3fecfd
@@ -25,7 +25,12 @@ jobs:
25
25
  rake:
26
26
  uses: metanorma/ci/.github/workflows/dependent-rake.yml@main
27
27
  with:
28
- command: bundle exec rspec
28
+ # #1020: the spec-file count distinguishes a truncated checkout from
29
+ # a full one, and the explicit rspec-exit line separates an
30
+ # in-process exit (green summary, nonzero code) from test failures.
31
+ # set +e keeps the capture alive under the runner's errexit; the
32
+ # final exit preserves rspec's own status — nothing is masked.
33
+ command: set +e; echo "spec-files=$(find spec -name '*_spec.rb' 2>/dev/null | wc -l)"; bundle exec rspec > rspec-run.log 2>&1; code=$?; echo "rspec-exit=$code"; tail -25 rspec-run.log; exit $code
29
34
  repos-json-file: .github/workflows/dependent-repos.json
30
35
  after-setup-ruby: |
31
36
  if [ -f frontend/package.json ]; then cd frontend && npm install && npm run build; fi
@@ -39,7 +44,7 @@ jobs:
39
44
  rake-with-tools:
40
45
  uses: metanorma/ci/.github/workflows/dependent-rake.yml@main
41
46
  with:
42
- command: bundle exec rspec
47
+ command: set +e; echo "spec-files=$(find spec -name '*_spec.rb' 2>/dev/null | wc -l)"; bundle exec rspec; code=$?; echo "rspec-exit=$code"; exit $code
43
48
  repos-json-file: .github/workflows/dependent-repos-tools.json
44
49
  setup-tools: 'inkscape,ghostscript,exiftool,ffmpeg,imagemagick,yq'
45
50
  after-setup-ruby: |
@@ -370,11 +370,33 @@ module Lutaml
370
370
  def allocate_for_walk(register = nil)
371
371
  register_id = extract_register_id(register)
372
372
  ensure_imports!(register_id) if finalized? && deferred_imports_pending?(register_id)
373
+ # Classes with their own initialize set up internal state the
374
+ # compiled seed cannot know about (SchemaRepository's
375
+ # @parsed_schemas/@namespace_registry/@type_index): construct
376
+ # through the real constructor for them, exactly as the walk
377
+ # did before the compiled seed existed. The seed stays the
378
+ # fast path for classes using the standard initializer.
379
+ return new(lutaml_register: register) if constructs_through_initialize?
380
+ # did before the compiled seed existed, register included. The
381
+ # seed stays the fast path for classes using the standard
382
+ # initializer.
383
+ return new(lutaml_register: register) if constructs_through_initialize?
384
+
373
385
  instance = allocate
374
386
  instance.init_walk_state(register_id)
375
387
  instance
376
388
  end
377
389
 
390
+ # True when the class does not use Serializable's standard
391
+ # initializer — its initialize is constructor contract, not
392
+ # optional setup, and the compiled walk seed would skip it.
393
+ # (The standard initializer's owner is Serialize::Builder; a
394
+ # class- or parent-defined initialize makes the owner the class.)
395
+ def constructs_through_initialize?
396
+ @constructs_through_initialize ||=
397
+ instance_method(:initialize).owner != Lutaml::Model::Serialize::Builder
398
+ end
399
+
378
400
  def allocate_for_deserialization(register = nil)
379
401
  register_id = extract_register_id(register)
380
402
  # Deferred imports (symbol-form import_model_attributes) must
@@ -389,6 +411,14 @@ module Lutaml
389
411
  # stack limit — models without pending deferred imports skip
390
412
  # it entirely.
391
413
  ensure_imports!(register_id) if finalized? && deferred_imports_pending?(register_id)
414
+ # Custom-initialize classes (#1032's twin): their initialize is
415
+ # constructor contract — internal state the compiled seeding
416
+ # cannot know about — so they construct through the real
417
+ # constructor, then carry the parse's register context.
418
+ if constructs_through_initialize?
419
+ instance = new(lutaml_register: register_id)
420
+ return instance
421
+ end
392
422
  instance = allocate
393
423
  instance.finalize_deserialization(register_id)
394
424
  instance
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.136"
5
+ VERSION = "0.8.138"
6
6
  end
7
7
  end
@@ -122,6 +122,8 @@ module Lutaml
122
122
  # them (compiling here silently dropped pi_settings).
123
123
  return opt_out!(:processing_instruction) unless
124
124
  mapping.processing_instruction_mappings.empty?
125
+ # Wildcard attribute families ride the interpretive path too
126
+ return opt_out!(:all_attributes) if mapping.all_attributes_rule
125
127
 
126
128
  rows = []
127
129
  attr_rows = [] # [[rule, attr, row_name]] hydration metadata
@@ -38,7 +38,7 @@ module Lutaml
38
38
  @xml_instance
39
39
  end
40
40
 
41
- attr_reader :root_element, :namespace_uri, :namespace_prefix, :element_sequence, :mappings_imported, :namespace_class, :namespace_param, :element_name, :documentation_text, :type_name_value, :namespace_scope, :mapper_class, :xml_space, :consolidation_maps, :processing_instruction_mappings, :any_element_rule
41
+ attr_reader :root_element, :namespace_uri, :namespace_prefix, :element_sequence, :mappings_imported, :namespace_class, :namespace_param, :element_name, :documentation_text, :type_name_value, :namespace_scope, :mapper_class, :xml_space, :consolidation_maps, :processing_instruction_mappings, :any_element_rule, :all_attributes_rule
42
42
 
43
43
  def initialize
44
44
  super
@@ -49,6 +49,7 @@ module Lutaml
49
49
  @element_sequence = []
50
50
  @content_mapping = nil
51
51
  @raw_mapping = nil
52
+ @all_attributes_rule = nil
52
53
  @mixed_content = false
53
54
  @xml_space = nil
54
55
  @format = :xml
@@ -875,6 +876,26 @@ module Lutaml
875
876
  )
876
877
  end
877
878
 
879
+ # Capture every attribute not claimed by an explicit map_attribute
880
+ # (and matching an optional pattern) into a Hash attribute, and emit
881
+ # it back as attributes after the mapped ones on serialization —
882
+ # the remainder semantics of map_all, for attribute families
883
+ # (lutaml-model#1031): HTML data-*, ARIA aria-*, on* handlers.
884
+ #
885
+ # @param to [Symbol] the Hash attribute name
886
+ # @param matching [Regexp, nil] optional name filter; nil captures
887
+ # every unclaimed attribute
888
+ #
889
+ # @example
890
+ # xml do
891
+ # element "mn"
892
+ # map_attribute "name", to: :name
893
+ # map_all_attributes to: :other_attributes, matching: /\A(data-|aria-|on)/
894
+ # end
895
+ def map_all_attributes(to:, matching: nil)
896
+ @all_attributes_rule = AllAttributesMapping.new(to, matching)
897
+ end
898
+
878
899
  def sequence(&block)
879
900
  @element_sequence << ::Lutaml::Model::Sequence.new(self,
880
901
  format: :xml).tap do |s|
@@ -1681,5 +1702,21 @@ module Lutaml
1681
1702
  self.class.new(@target.dup, @to)
1682
1703
  end
1683
1704
  end
1705
+
1706
+ # Wildcard attribute-family mapping (#1031): the target Hash collects
1707
+ # every attribute no explicit map_attribute claims and that matches
1708
+ # the optional pattern; serialization emits each pair as an attribute.
1709
+ class AllAttributesMapping
1710
+ attr_reader :to, :matching
1711
+
1712
+ def initialize(to, matching)
1713
+ @to = to
1714
+ @matching = matching
1715
+ end
1716
+
1717
+ def dup
1718
+ self.class.new(@to, @matching)
1719
+ end
1720
+ end
1684
1721
  end
1685
1722
  end
@@ -503,11 +503,50 @@ module Lutaml
503
503
  apply_processing_instruction_deserialization(doc, instance, xml_mapping,
504
504
  effective_register)
505
505
 
506
+ apply_all_attributes_deserialization(doc, instance, xml_mapping)
507
+
506
508
  run_consolidation(instance, effective_register, instance_is_serialize)
507
509
 
508
510
  instance
509
511
  end
510
512
 
513
+ # #1031: wildcard attribute families. After the explicit attribute
514
+ # rules have consumed their names, every remaining unprefixed
515
+ # attribute matching the rule's pattern lands in the target Hash —
516
+ # names claimed by explicit rules never leak into it.
517
+ def apply_all_attributes_deserialization(doc, instance, xml_mapping)
518
+ rule = xml_mapping.all_attributes_rule
519
+ return if rule.nil?
520
+
521
+ element = doc.is_a?(::Lutaml::Xml::XmlElement) ? doc : doc.root
522
+ return if element.nil?
523
+
524
+ claimed = attribute_names_claimed_by_rules(xml_mapping)
525
+ remainder = {}
526
+ element.attributes.each_value do |attr|
527
+ name = attr.unprefixed_name.to_s
528
+ next if claimed.include?(name)
529
+ next if rule.matching && !rule.matching.match?(name)
530
+
531
+ remainder[name] = attr.value
532
+ end
533
+ return if remainder.empty?
534
+
535
+ instance.public_send(:"#{rule.to}=", remainder)
536
+ instance.value_set_for(rule.to) if instance.is_a?(::Lutaml::Model::Serialize)
537
+ end
538
+
539
+ # Attribute names claimed by explicit map_attribute rules, plus the
540
+ # engine-handled schemaLocation (which renders through
541
+ # raw_schema_location, not the remainder).
542
+ def attribute_names_claimed_by_rules(xml_mapping)
543
+ names = %w[schemaLocation]
544
+ xml_mapping.mapping_attributes_hash.each_value do |rule_or_array|
545
+ Array(rule_or_array).each { |rule| names << rule.name.to_s }
546
+ end
547
+ names
548
+ end
549
+
511
550
  def validate_document!(doc, options)
512
551
  return unless doc.is_a?(Array)
513
552
 
@@ -101,6 +101,9 @@ module Lutaml
101
101
  apply_standard_rules(root, model_instance, options)
102
102
  end
103
103
 
104
+ # The wildcard attribute family emits after the mapped attributes
105
+ apply_all_attributes_mappings(root, model_instance, mapping)
106
+
104
107
  root
105
108
  end
106
109
 
@@ -242,6 +245,27 @@ module Lutaml
242
245
  end
243
246
 
244
247
  # Check if element order should be used for serialization
248
+
249
+ # #1031: emit the wildcard attribute family after the mapped
250
+ # attributes. Attributes render in insertion order; the remainder
251
+ # follows the explicitly mapped names.
252
+ def apply_all_attributes_mappings(root, model_instance, mapping)
253
+ rule = mapping.all_attributes_rule
254
+ return if rule.nil?
255
+
256
+ value = model_instance.public_send(rule.to)
257
+ return if value.nil? || !value.is_a?(::Hash)
258
+
259
+ value.each do |name, attr_value|
260
+ next if attr_value.nil?
261
+
262
+ root.add_attribute(
263
+ ::Lutaml::Xml::DataModel::XmlAttribute.new(name.to_s,
264
+ attr_value.to_s, nil),
265
+ )
266
+ end
267
+ end
268
+
245
269
  #
246
270
  # @param model_instance [Object] The model instance
247
271
  # @param mapping [Xml::Mapping] The mapping
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # #1031: wildcard attribute families — every attribute no explicit
6
+ # map_attribute claims and that matches the pattern lands in a Hash and
7
+ # is emitted back after the mapped attributes on serialization.
8
+ RSpec.describe "map_all_attributes" do
9
+ let(:model_class) do
10
+ Class.new(Lutaml::Model::Serializable) do
11
+ attribute :name, :string
12
+ attribute :data_attributes, :hash
13
+
14
+ xml do
15
+ element "mn"
16
+ map_attribute "name", to: :name
17
+ map_all_attributes to: :data_attributes, matching: /\A(data-|aria-|on)/
18
+ end
19
+ end
20
+ end
21
+
22
+ let(:stubbed) do
23
+ stub_const("MapAllAttributesModel", model_class)
24
+ MapAllAttributesModel
25
+ end
26
+
27
+ it "captures unclaimed, pattern-matching attributes into the Hash" do
28
+ xml = '<mn name="x" data-title="T" onclick="f()" aria-label="L" unre="nope"/>'
29
+ model = stubbed.from_xml(xml)
30
+
31
+ expect(model.name).to eq("x")
32
+ expect(model.data_attributes).to eq(
33
+ "data-title" => "T", "onclick" => "f()", "aria-label" => "L",
34
+ )
35
+ end
36
+
37
+ it "never lets mapped names leak into the remainder" do
38
+ xml = '<mn name="x" data-name="D"/>'
39
+ model = stubbed.from_xml(xml)
40
+
41
+ expect(model.data_attributes).to eq("data-name" => "D")
42
+ end
43
+
44
+ it "emits the remainder after the mapped attributes on serialization" do
45
+ model = stubbed.from_xml(
46
+ '<mn name="x" data-title="T" onclick="f()"/>',
47
+ )
48
+ out = model.to_xml
49
+
50
+ expect(out).to include('name="x"')
51
+ expect(out).to include('data-title="T"')
52
+ expect(out).to include('onclick="f()"')
53
+ expect(out.index("name=")).to be < out.index("data-title=")
54
+ end
55
+
56
+ it "round-trips losslessly" do
57
+ xml = '<mn name="x" data-title="T" onclick="f()" aria-label="L"/>'
58
+ model = stubbed.from_xml(xml)
59
+ round_tripped = stubbed.from_xml(model.to_xml)
60
+
61
+ expect(round_tripped.data_attributes).to eq(model.data_attributes)
62
+ expect(round_tripped.name).to eq(model.name)
63
+ end
64
+
65
+ it "emits nothing when the remainder is empty" do
66
+ model = stubbed.from_xml('<mn name="x"/>')
67
+ out = model.to_xml
68
+
69
+ expect(out).not_to include("data-")
70
+ end
71
+
72
+ it "captures every unclaimed attribute when no pattern is given" do
73
+ klass = Class.new(Lutaml::Model::Serializable) do
74
+ attribute :name, :string
75
+ attribute :other_attributes, :hash
76
+
77
+ xml do
78
+ element "el"
79
+ map_attribute "name", to: :name
80
+ map_all_attributes to: :other_attributes
81
+ end
82
+ end
83
+ stub_const("MapAllNoPatternModel", klass)
84
+
85
+ model = klass.from_xml('<el name="x" zeta="1"/>')
86
+
87
+ expect(model.other_attributes).to eq("zeta" => "1")
88
+ end
89
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # A class that defines its own initialize is built through it by the
6
+ # from_* walk, and the walk hands it the register exactly as
7
+ # `new(lutaml_register:)` did before the compiled seed existed: a
8
+ # constructor that tells a blank construction from a caller's by that
9
+ # keyword sees the same call either way.
10
+ RSpec.describe "Walk construction of a custom-initialize class" do
11
+ let(:model) do
12
+ Class.new(Lutaml::Model::Serializable) do
13
+ attribute :name, :string
14
+
15
+ json do
16
+ map "name", to: :name
17
+ end
18
+
19
+ class << self
20
+ attr_accessor :constructions
21
+ end
22
+
23
+ def initialize(*args, **kwargs)
24
+ self.class.constructions << [args, kwargs.keys]
25
+ @internal = :ready
26
+ super
27
+ end
28
+
29
+ attr_reader :internal
30
+ end
31
+ end
32
+
33
+ before do
34
+ stub_const("WalkConstructorModel", model)
35
+ model.constructions = []
36
+ end
37
+
38
+ it "runs the class's own initialize" do
39
+ expect(model.from_json('{"name":"a"}').internal).to eq(:ready)
40
+ end
41
+
42
+ it "passes the register, as the walk did through new(lutaml_register:)" do
43
+ parsed = model.from_json('{"name":"a"}')
44
+
45
+ expect(model.constructions).to eq([[[], [:lutaml_register]]])
46
+ expect(parsed.name).to eq("a")
47
+ end
48
+ 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.136
4
+ version: 0.8.138
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-26 00:00:00.000000000 Z
11
+ date: 2026-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -1796,6 +1796,7 @@ files:
1796
1796
  - spec/lutaml/model/liquid/indexed_access_spec.rb
1797
1797
  - spec/lutaml/model/liquid_compatibility_spec.rb
1798
1798
  - spec/lutaml/model/logger_spec.rb
1799
+ - spec/lutaml/model/map_all_attributes_spec.rb
1799
1800
  - spec/lutaml/model/map_all_spec.rb
1800
1801
  - spec/lutaml/model/map_content_spec.rb
1801
1802
  - spec/lutaml/model/map_unmapped_spec.rb
@@ -1912,6 +1913,7 @@ files:
1912
1913
  - spec/lutaml/model/value_map_spec.rb
1913
1914
  - spec/lutaml/model/value_policy_spec.rb
1914
1915
  - spec/lutaml/model/value_transformation_comprehensive_spec.rb
1916
+ - spec/lutaml/model/walk_constructor_spec.rb
1915
1917
  - spec/lutaml/model/with_child_mapping_spec.rb
1916
1918
  - spec/lutaml/model/writer_assigned_attribute_spec.rb
1917
1919
  - spec/lutaml/model/xml_decoupling_spec.rb