lutaml-xsd 1.2.96 → 1.2.98

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: b47e6206406d15078ca3896061eb50d415a599a3a6fe62df9a59d191c31f410a
4
- data.tar.gz: d60605597341f39c26d19ac3bb8d88e6a0ff06d4f7ad03c9504d6903fc6a6ca9
3
+ metadata.gz: e129470c8c939e17b19e748ffaebf593f7f3909876fbe05e1b733e1129f448ba
4
+ data.tar.gz: '03015085efe53c4dca6dec3bc5115d9081b42a24c2f758b19c4c3424b672521f'
5
5
  SHA512:
6
- metadata.gz: 231da0fb555d2591583462fa173efef19ee7835938aab19b52a3dc637a2e1c62a2e97f428fe492bea6b7b6891da3af45602a8ebf7112c1733d914098e32a3c38
7
- data.tar.gz: baa25447cd81057253277cf53348399487b15a2d8b5735e3531a57f6f87d7dec4dc12bfe0a3e2c87f80a20f9580c094b823616b093c5764f95789a1b0724d373
6
+ metadata.gz: 4e1be4de4f925cb152fa5f69e2cb57fb42d9337ab37628b71314ab0e3bc7a5d10da9cf829173a78303c0e8e495682286bb9ecf4bde0d72056324d6e572d309cc
7
+ data.tar.gz: ae47a5ca4b9bc803c7e7c35aaeb0a9e8a35e9610bddacdd82d14b269310863f91d64a9f59f1cd9f0014ea72dd0040f975098feccd30914fc6e7cf053845a9b91
data/TODO.conformance.md CHANGED
@@ -524,7 +524,42 @@ tuple keys, node-set field violations and absent-field rows handled, cross-eleme
524
524
  series — filed leptris-rs#47; moxml 0.6.3 regressed 5 attribute
525
525
  cases (addB059/196c/197c, attO009/011) — filed moxml#529. THE
526
526
  STACK IS PINNED BACK to the known-good 1.9.406.0/0.6.2/0.8.161
527
+ - SKIP-WILDCARD-FIELD ROUND (2026-11-07): +1 IC (idZ015; suite
528
+ 4621->4622 = 98.42%, zero regressions, rspec green). THE LAST IC
529
+ SINGLETON — the idK/idZ family is now CLEAN (all six roots fixed
530
+ across the four rounds). An attribute admitted by a
531
+ processContents=skip wildcard is NOT ASSESSED, so it supplies no
532
+ identity-constraint field value: the field treats it as absent and
533
+ a key whose fields do not all evaluate is violated (idZ015: foo's
534
+ ##any skip anyAttribute + key field @bar; xmllint: "Not all fields
535
+ of key identity-constraint evaluate to a node"). The skip check
536
+ walks the selector node's declaration anyAttribute wildcards
537
+ (own + content parts + attributeGroup members, depth 3) and the
538
+ namespace constraint reads the attribute's URI from the MOXML
539
+ element wrapper — xpath result attributes are ADAPTER objects
540
+ (leptris ResultAttr carries no namespace). GOTCHA: the first
541
+ implementation read node.namespace and CRASHED under leptris —
542
+ the suite probe "passed" as CRASH-AS-INVALID, caught by writing
543
+ the rule-level spec FIRST (rule_execution_error instead of the
544
+ finding). Suite verdicts from crashes are not fixes; assert the
545
+ finding code.
546
+
527
547
  (98.28% restored, zero regressions vs the 406 baseline). The
548
+ - IDREF-RESOLUTION ROUND (2026-11-07): +1 IC (idZ012; suite
549
+ 4620->4621 = 98.40%, zero regressions, rspec green). Every
550
+ IDREF/IDREFS value must match an ID in the document (XSD 1.0
551
+ §3.3.3) — the check did not exist at all. IdrefResolutionRule
552
+ (identity category, root-scoped two-phase scan beside
553
+ IdUniquenessRule) collects every document ID (element text +
554
+ attributes), then checks IDREF/IDREFS values from instance
555
+ attributes, element text, AND the declaration's fixed/default
556
+ supply when the instance omits the attribute — a defaulted IDREFS
557
+ still references (idZ012: drefs type="xs:IDREFS" default="abc"
558
+ against a document with no ID). IDREFS tokenizes; every token must
559
+ resolve; the two-phase order makes forward references valid.
560
+ Remaining IC singleton: idZ015 (a skip-wildcard-governed attribute
561
+ supplies no IC field value).
562
+
528
563
  407 wins ride again when #47 and #529 land. NET: no score
529
564
  - ID-VALUE-CONSTRAINT ROUND (2026-11-07): +1 IC (idZ011; suite
530
565
  4619->4620 = 98.38%, zero regressions, rspec green). Value
@@ -8,6 +8,7 @@ require_relative "rules/occurrence_validation_rule"
8
8
  require_relative "rules/content_model_validation_rule"
9
9
  require_relative "rules/identity_constraints_rule"
10
10
  require_relative "rules/id_uniqueness_rule"
11
+ require_relative "rules/idref_resolution_rule"
11
12
 
12
13
  module Lutaml
13
14
  module Xsd
@@ -63,6 +64,7 @@ module Lutaml
63
64
  if config.feature_enabled?(:validate_identity_constraints)
64
65
  registry.register(Rules::IdentityConstraintsRule.new)
65
66
  registry.register(Rules::IdUniquenessRule.new)
67
+ registry.register(Rules::IdrefResolutionRule.new)
66
68
  end
67
69
  registry
68
70
  end
@@ -128,6 +128,14 @@ module Lutaml
128
128
  ambiguous = true if matched.size > 1
129
129
  values << if matched.first.nil?
130
130
  field_supply_value(node, field, repository)
131
+ elsif !matched.first.is_a?(Moxml::Element) &&
132
+ skip_governed_attribute?(node, matched.first,
133
+ repository)
134
+ # A skip-wildcard-governed attribute is not
135
+ # assessed, so it supplies no identity-
136
+ # constraint value: the field treats it as
137
+ # absent (idZ015).
138
+ nil
131
139
  elsif matched.first.is_a?(Moxml::Element) &&
132
140
  field_node_non_simple?(
133
141
  matched.first, constraint, raws[index],
@@ -337,6 +345,104 @@ module Lutaml
337
345
  repository.declared_attribute_supply(parent_type, attr_local)
338
346
  end
339
347
 
348
+ # Whether the instance attribute is governed by a SKIP
349
+ # wildcard on the selector node's declaration: a skip-
350
+ # governed attribute is not assessed, so it supplies no
351
+ # identity-constraint field value (idZ015).
352
+ #
353
+ # @return [Boolean]
354
+ def skip_governed_attribute?(element_node, attribute_node,
355
+ repository)
356
+ declaration = repository&.resolve(element_node.name,
357
+ element_node.namespace_uri)
358
+ return false unless declaration
359
+
360
+ type = declaration.complex_type ||
361
+ repository.resolve_type(declaration)
362
+ return false unless type.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType)
363
+
364
+ target_ns = repository.target_namespace_for(declaration)
365
+ attribute_ns = attribute_namespace(element_node, attribute_node)
366
+ attribute_skip_wildcards(type, repository, 0).any? do |wildcard|
367
+ wildcard_process_mode(wildcard) == "skip" &&
368
+ wildcard_admits_namespace?(wildcard, attribute_ns, target_ns)
369
+ end
370
+ end
371
+
372
+ # The attribute's namespace URI: the xpath result nodes are
373
+ # adapter objects (leptris ResultAttr carries no namespace),
374
+ # so the name-matched attribute on the MOXML element wrapper
375
+ # resolves it.
376
+ #
377
+ # @return [String]
378
+ def attribute_namespace(element_node, attribute_node)
379
+ found = element_node.attributes.find do |candidate|
380
+ candidate.name == attribute_node.name
381
+ end
382
+ found&.namespace&.uri.to_s
383
+ end
384
+
385
+ # The anyAttribute wildcards reachable from the type: its own,
386
+ # content-part bodies, and attributeGroup members (nested
387
+ # refs depth-guarded).
388
+ #
389
+ # @return [Array<Lutaml::Xml::Schema::Xsd::AnyAttribute>]
390
+ def attribute_skip_wildcards(type, repository, depth)
391
+ return [] if depth > 3
392
+
393
+ wildcards = Array(type.any_attribute)
394
+ Array(type.attribute_group).each do |group|
395
+ wildcards.concat(Array(group.any_attribute))
396
+ local = (group.ref || group.name).to_s.split(":").last
397
+ declaration = local && repository.resolve_attribute_group(local)
398
+ if declaration
399
+ wildcards.concat(attribute_skip_wildcards(declaration,
400
+ repository,
401
+ depth + 1))
402
+ end
403
+ end
404
+ content_kinds = %i[complex_content simple_content]
405
+ part_kinds = %i[extension restriction]
406
+ content_kinds.each do |content_kind|
407
+ content = type.public_send(content_kind)
408
+ next unless content
409
+
410
+ part_kinds.each do |part_kind|
411
+ Array(content.public_send(part_kind)).each do |part|
412
+ wildcards.concat(Array(part.any_attribute))
413
+ end
414
+ end
415
+ end
416
+ wildcards
417
+ end
418
+
419
+ # Whether the wildcard's namespace constraint admits the
420
+ # attribute's namespace (mirrors WildcardScope#wildcard_permits?
421
+ # for attribute nodes, against the declaration's target
422
+ # namespace).
423
+ #
424
+ # @return [Boolean]
425
+ def wildcard_admits_namespace?(wildcard, uri, target_ns)
426
+ case wildcard.namespace.to_s
427
+ when "##any", ""
428
+ true
429
+ when "##other"
430
+ !uri.empty? && uri != target_ns.to_s
431
+ when "##local"
432
+ uri.empty?
433
+ when "##targetNamespace"
434
+ uri == target_ns.to_s
435
+ else
436
+ wildcard.namespace.to_s.split(/\s+/).any? do |entry|
437
+ case entry
438
+ when "##local" then uri.empty?
439
+ when "##targetNamespace" then uri == target_ns.to_s
440
+ else entry == uri
441
+ end
442
+ end
443
+ end
444
+ end
445
+
340
446
  # Whether an element field's node supplies no identity value:
341
447
  # it has element children, is declared non-simple (complex
342
448
  # content or xsd:anyType — the typeless default), or carries a
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../validation_rule"
4
+
5
+ module Lutaml
6
+ module Xsd
7
+ module Validation
8
+ module Rules
9
+ # IdrefResolutionRule enforces built-in xs:IDREF(S) resolution
10
+ #
11
+ # XSD 1.0 §3.3.3: every IDREF/IDREFS value must match the value
12
+ # of some ID in the document. Values come from instance
13
+ # attributes, element text typed IDREF(S), AND a declaration's
14
+ # fixed/default supply when the instance omits the attribute —
15
+ # a defaulted IDREFS still references (idZ012: drefs
16
+ # type="xs:IDREFS" default="abc" against a document declaring
17
+ # no ID). IDREFS values tokenize on whitespace; every token
18
+ # must resolve. The scan is two-phase (all IDs first, then all
19
+ # references), so forward references resolve.
20
+ class IdrefResolutionRule < ValidationRule
21
+ def category
22
+ :identity
23
+ end
24
+
25
+ def description
26
+ "Validates built-in xs:IDREF(S) values resolve to document IDs"
27
+ end
28
+
29
+ def validate(xml_element, schema_element, collector,
30
+ repository: nil)
31
+ return unless repository
32
+ return unless
33
+ schema_element.is_a?(Lutaml::Xml::Schema::Xsd::Element)
34
+
35
+ # The walk invokes the rule per element and per category;
36
+ # the document is scanned exactly once, from the first
37
+ # (root) invocation — element wrappers are not
38
+ # identity-stable across passes.
39
+ return if @scanned
40
+
41
+ @scanned = true
42
+ ids = collect_ids(xml_element, repository)
43
+ check_references(xml_element, ids, collector, repository)
44
+ end
45
+
46
+ private
47
+
48
+ # Phase 1: every ID value in the document (element text and
49
+ # attributes). Mirrors IdUniquenessRule's walk — IDs come
50
+ # from instances only; a declaration cannot supply an ID that
51
+ # the document does not contain.
52
+ #
53
+ # @return [Set<String>]
54
+ def collect_ids(element, repository)
55
+ ids = []
56
+ declaration = repository.resolve(element.name,
57
+ element.namespace_uri)
58
+ if declaration
59
+ collect_element_ids(element, declaration, ids, repository)
60
+ collect_attribute_ids(element, declaration, ids, repository)
61
+ end
62
+
63
+ element.children.each do |child|
64
+ ids.concat(collect_ids(child, repository))
65
+ end
66
+ ids.compact
67
+ end
68
+
69
+ # @return [void]
70
+ def collect_element_ids(element, declaration, ids, repository)
71
+ return unless builtin_typed?(declaration, repository, "ID")
72
+
73
+ # ID/IDREF(S) collapse whitespace (token derivation) — the
74
+ # value space is the collapsed form, never the raw lexical
75
+ # (dtZ107447: " John " both sides).
76
+ ids.concat(element.text_content.to_s.split)
77
+ end
78
+
79
+ # @return [void]
80
+ def collect_attribute_ids(element, declaration, ids, repository)
81
+ typed_attributes(declaration, repository, "ID").each do |attr|
82
+ instance = element.attributes.find do |candidate|
83
+ candidate.name == attr.name
84
+ end
85
+ ids.concat(instance.value.to_s.split) if instance
86
+ end
87
+ end
88
+
89
+ # Phase 2: every IDREF(S) value the document carries or its
90
+ # declarations SUPPLY, each token matched against the IDs.
91
+ #
92
+ # @return [void]
93
+ def check_references(element, ids, collector, repository)
94
+ declaration = repository.resolve(element.name,
95
+ element.namespace_uri)
96
+ if declaration
97
+ check_element_reference(element, declaration, ids, collector,
98
+ repository)
99
+ check_attribute_references(element, declaration, ids,
100
+ collector, repository)
101
+ end
102
+
103
+ element.children.each do |child|
104
+ check_references(child, ids, collector, repository)
105
+ end
106
+ end
107
+
108
+ # @return [void]
109
+ def check_element_reference(element, declaration, ids, collector,
110
+ repository)
111
+ return unless
112
+ builtin_typed?(declaration, repository, "IDREF") ||
113
+ builtin_typed?(declaration, repository, "IDREFS")
114
+
115
+ reference_values(element.text_content.to_s).each do |value|
116
+ next if ids.include?(value)
117
+
118
+ report_unresolved(value, element, collector, "element")
119
+ end
120
+ end
121
+
122
+ # Instance values, then the declaration's fixed/default supply
123
+ # for ABSENT attributes — a defaulted IDREFS still references
124
+ # (idZ012).
125
+ #
126
+ # @return [void]
127
+ def check_attribute_references(element, declaration, ids, collector,
128
+ repository)
129
+ typed_attributes(declaration, repository, "IDREF").each do |attr|
130
+ report_tokens(element, attr, ids, collector,
131
+ instance_value(element, attr))
132
+ end
133
+ typed_attributes(declaration, repository, "IDREFS").each do |attr|
134
+ report_tokens(element, attr, ids, collector,
135
+ instance_value(element, attr))
136
+ end
137
+ end
138
+
139
+ # @return [String, nil]
140
+ def instance_value(element, attr)
141
+ instance = element.attributes.find do |candidate|
142
+ candidate.name == attr.name
143
+ end
144
+ instance&.value || attr.fixed || attr.default
145
+ end
146
+
147
+ # @return [void]
148
+ def report_tokens(element, attr, ids, collector, supplied)
149
+ reference_values(supplied).each do |value|
150
+ next if ids.include?(value)
151
+
152
+ report_unresolved(value, element, collector,
153
+ "attribute '#{attr.name}'")
154
+ end
155
+ end
156
+
157
+ # IDREF is a single value; IDREFS tokenizes on whitespace.
158
+ #
159
+ # @return [Array<String>]
160
+ def reference_values(supplied)
161
+ supplied.to_s.strip.empty? ? [] : supplied.to_s.split
162
+ end
163
+
164
+ # @return [void]
165
+ def report_unresolved(value, xml_element, collector, subject)
166
+ report_error(
167
+ collector,
168
+ code: "idref_violation",
169
+ message: "#{subject} has IDREF value '#{value}' that " \
170
+ "does not match any ID in the document",
171
+ location: xml_element.xpath,
172
+ context: { value: value },
173
+ )
174
+ end
175
+
176
+ # Whether the declaration's type is the named builtin — a
177
+ # direct reference or an inline restriction of it.
178
+ #
179
+ # @return [Boolean]
180
+ def builtin_typed?(declaration, _repository, name)
181
+ builtin_reference?(declaration.type.to_s, name) ||
182
+ builtin_reference?(
183
+ declaration.simple_type&.restriction&.base.to_s, name
184
+ )
185
+ end
186
+
187
+ # @return [Boolean]
188
+ def builtin_reference?(reference, name)
189
+ parts = reference.split(":")
190
+ parts.size == 2 && %w[xs xsd].include?(parts.first) &&
191
+ parts.last == name
192
+ end
193
+
194
+ # The declaration's own attribute particles typed the builtin:
195
+ # direct attributes, refs resolved to their declarations, and
196
+ # attributeGroup members.
197
+ #
198
+ # @return [Array<Lutaml::Xml::Schema::Xsd::Attribute>]
199
+ def typed_attributes(schema_element, repository, name)
200
+ type = schema_element.complex_type ||
201
+ repository.resolve_type(schema_element)
202
+ return [] unless type.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType)
203
+
204
+ particles = Array(type.attribute)
205
+ Array(type.attribute_group).each do |group|
206
+ declaration = repository.resolve_attribute_group(
207
+ (group.ref || group.name).to_s.split(":").last,
208
+ )
209
+ particles.concat(Array(declaration&.attribute))
210
+ end
211
+
212
+ particles.filter_map do |particle|
213
+ resolved = particle
214
+ unless particle.ref.to_s.empty?
215
+ resolved = repository.resolve_attribute(
216
+ particle.ref.split(":").last, nil
217
+ )
218
+ end
219
+ next nil unless resolved && builtin_typed?(resolved, repository,
220
+ name)
221
+
222
+ resolved
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Xsd
5
- VERSION = "1.2.96"
5
+ VERSION = "1.2.98"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-xsd
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.96
4
+ version: 1.2.98
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -578,6 +578,7 @@ files:
578
578
  - lib/lutaml/xsd/validation/rules/element_structure_rule.rb
579
579
  - lib/lutaml/xsd/validation/rules/id_uniqueness_rule.rb
580
580
  - lib/lutaml/xsd/validation/rules/identity_constraints_rule.rb
581
+ - lib/lutaml/xsd/validation/rules/idref_resolution_rule.rb
581
582
  - lib/lutaml/xsd/validation/rules/occurrence_validation_rule.rb
582
583
  - lib/lutaml/xsd/validation/rules/type_validation_rule.rb
583
584
  - lib/lutaml/xsd/validation/schema_definition_resolver.rb