lutaml-xsd 1.2.95 → 1.2.97

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: a752804ef8cfcbbe98845ffcfcbd8830d2c37020d392d34524795f645351cef9
4
- data.tar.gz: 467cd19ffcc587cbdc4007e614b02f2f299a26917c684e997138fa5116686a7b
3
+ metadata.gz: 74cc0c3c59a0cb1a80968d340ccf4d8be8b7d9ce9d1257c7e59044ebd85bc5b0
4
+ data.tar.gz: 453526a2baaeb84186acb8f3d84b573ddaeb988af120acf601bd2e621cd159e2
5
5
  SHA512:
6
- metadata.gz: 904b2740eaa42225aa419c6a27586cc115fbcba3efaa4e4fe987b979f7139bd00e5e96007f094f65b5631a8383bdd12d0e50eaef82482733662d2dcda8658507
7
- data.tar.gz: 545c68eb6ce9cda1d4b834d3bf608974d3da82e774fee7465918ba39d5c2d7183892c0a314c3314d15f68e8f20a6af27e264fb752f5023008db7a5cfa84be512
6
+ metadata.gz: 67c01245e71827ab4682e6dda886b296586d96960b5e814b1b1e9ccbdee53d4fc33ee95ed705b90730e636f0f2c0ea5a7f325809072a7e0a0bd79d0928b91a79
7
+ data.tar.gz: 3246ef0bfd177d2019fa3fd08b01409647d2d2d71c4bb8757e108a55c9374f72684f258e46419bece176a03f94a24a3c44c11ec484b38f9fe60e4114099f022e
data/TODO.conformance.md CHANGED
@@ -525,7 +525,40 @@ tuple keys, node-set field violations and absent-field rows handled, cross-eleme
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
527
  (98.28% restored, zero regressions vs the 406 baseline). The
528
+ - IDREF-RESOLUTION ROUND (2026-11-07): +1 IC (idZ012; suite
529
+ 4620->4621 = 98.40%, zero regressions, rspec green). Every
530
+ IDREF/IDREFS value must match an ID in the document (XSD 1.0
531
+ §3.3.3) — the check did not exist at all. IdrefResolutionRule
532
+ (identity category, root-scoped two-phase scan beside
533
+ IdUniquenessRule) collects every document ID (element text +
534
+ attributes), then checks IDREF/IDREFS values from instance
535
+ attributes, element text, AND the declaration's fixed/default
536
+ supply when the instance omits the attribute — a defaulted IDREFS
537
+ still references (idZ012: drefs type="xs:IDREFS" default="abc"
538
+ against a document with no ID). IDREFS tokenizes; every token must
539
+ resolve; the two-phase order makes forward references valid.
540
+ Remaining IC singleton: idZ015 (a skip-wildcard-governed attribute
541
+ supplies no IC field value).
542
+
528
543
  407 wins ride again when #47 and #529 land. NET: no score
544
+ - ID-VALUE-CONSTRAINT ROUND (2026-11-07): +1 IC (idZ011; suite
545
+ 4619->4620 = 98.38%, zero regressions, rspec green). Value
546
+ constraints (fixed/default) are FORBIDDEN on attribute declarations
547
+ typed xs:ID or derived from it (XSD 1.0 §3.2.6) — the schema is
548
+ uncompilable, so every instance reached is invalid regardless of
549
+ instance use. The attribute rule's declaration-scoped step 1c
550
+ checks the builtin-prefixed reference, ref-resolved declarations,
551
+ and the inline/named restriction base chain (depth 6); the
552
+ idZ011_a companion (xs:STRING default) stays clean. Family probes:
553
+ Attribute 113/113 clean, AttributeGroup 14 = the parked moxml#522
554
+ set unchanged. PROBE LESSON (repeat): single-process family probes
555
+ CRAWL after ~50 engine-heavy cases (engine-state accumulation;
556
+ enumeration order is unstable so the hang point moves) — the case
557
+ runs alone in <1s; batched methodology or the full measure is the
558
+ only reliable family signal at this scale. Remaining IC
559
+ singletons: idZ012 (IDREFS default references no ID), idZ015 (a
560
+ skip-wildcard-governed attribute supplies no IC field value).
561
+
529
562
  change; the pin protects the register.
530
563
  - NON-SIMPLE-FIELD ROUND (2026-11-07): +2 IC (idK012, idZ010; suite
531
564
  4617->4619 = 98.36%, zero regressions, rspec green). An identity
@@ -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
@@ -106,6 +106,13 @@ module Lutaml
106
106
  xml_attributes, collector,
107
107
  wildcards: wildcards)
108
108
 
109
+ # Step 1c: an xs:ID declaration cannot carry a value
110
+ # constraint — the schema itself is uncompilable, so the
111
+ # instance is invalid wherever the declaration is reached
112
+ # (XSD 1.0 §3.2.6, idZ011).
113
+ validate_id_value_constraints(schema_attributes, collector,
114
+ xml_element, repository)
115
+
109
116
  # Step 2: Validate attribute values
110
117
  validate_attribute_values(xml_element, resolved_attributes,
111
118
  xml_attributes, collector,
@@ -436,6 +443,73 @@ module Lutaml
436
443
  end
437
444
  end
438
445
 
446
+ # An xs:ID declaration with fixed or default is an illegal
447
+ # value constraint (XSD 1.0 §3.2.6): the type definition is
448
+ # or is derived from xs:ID. Declaration-scoped — fires on the
449
+ # element's type regardless of instance use (idZ011).
450
+ #
451
+ # @return [void]
452
+ def validate_id_value_constraints(schema_attrs, collector,
453
+ xml_element, repository)
454
+ schema_attrs.each do |schema_attr|
455
+ next if schema_attr.fixed.to_s.empty? &&
456
+ schema_attr.default.to_s.empty?
457
+ next unless id_typed_attribute?(schema_attr, repository)
458
+
459
+ report_error(
460
+ collector,
461
+ code: "id_value_constraint_violation",
462
+ message: "Attribute '#{schema_attr.name}' is typed " \
463
+ "xs:ID (or derived from it) and cannot carry " \
464
+ "a fixed or default value constraint",
465
+ location: xml_element.xpath,
466
+ context: { attribute: schema_attr.name },
467
+ )
468
+ end
469
+ end
470
+
471
+ # Whether the declaration's type is xs:ID or derives from it:
472
+ # a builtin-prefixed reference, or a restriction base chain
473
+ # (inline or named) whose terminal is xs:ID.
474
+ #
475
+ # @return [Boolean]
476
+ def id_typed_attribute?(schema_attr, repository)
477
+ return true if builtin_id_type?(schema_attr.type)
478
+
479
+ ref = schema_attr.ref.to_s
480
+ if ref.match?(/\S/) && repository
481
+ resolved = repository.resolve_attribute(ref.split(":").last,
482
+ nil)
483
+ return true if resolved && builtin_id_type?(resolved.type)
484
+ end
485
+
486
+ named = schema_attr.type.to_s.split(":").last
487
+ chain = schema_attr.simple_type ||
488
+ (named && repository&.resolve_named_type(named))
489
+ id_derived_chain?(chain, repository, 0)
490
+ end
491
+
492
+ # @return [Boolean]
493
+ def builtin_id_type?(reference)
494
+ parts = reference.to_s.split(":")
495
+ %w[xs xsd].include?(parts.first) && parts.last == "ID"
496
+ end
497
+
498
+ # @param type [Lutaml::Xml::Schema::Xsd::SimpleType, nil]
499
+ # @return [Boolean]
500
+ def id_derived_chain?(type, repository, depth)
501
+ return false if depth > 6 || type.nil?
502
+
503
+ base = type.restriction&.base.to_s
504
+ return true if builtin_id_type?(base)
505
+ return false if base.empty?
506
+
507
+ id_derived_chain?(
508
+ repository&.resolve_named_type(base.split(":").last),
509
+ repository, depth + 1
510
+ )
511
+ end
512
+
439
513
  # Check if attribute is required
440
514
  #
441
515
  # @param schema_attr [Lutaml::Xsd::Attribute] Schema attribute
@@ -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.95"
5
+ VERSION = "1.2.97"
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.95
4
+ version: 1.2.97
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