lutaml-xsd 1.1.13 → 1.2.0

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/CLAUDE.md +18 -1
  4. data/Gemfile +2 -1
  5. data/Gemfile.lock +327 -0
  6. data/Rakefile +78 -1
  7. data/TODO.VISION.md +123 -0
  8. data/TODO.cleanup.md +43 -0
  9. data/TODO.conformance.md +99 -0
  10. data/TODO.docs-spa.md +46 -0
  11. data/TODO.interop.md +52 -0
  12. data/TODO.performance.md +80 -0
  13. data/TODO.som.md +47 -0
  14. data/benchmark/engines.rb +77 -0
  15. data/config/validation.yml +4 -0
  16. data/docs/CONFORMANCE.adoc +43 -0
  17. data/docs/JSON_SCHEMA.adoc +42 -0
  18. data/docs/superpowers/plans/2026-10-20-moxml-leptris-migration.md +735 -0
  19. data/docs/superpowers/specs/2026-10-20-moxml-leptris-migration-design.md +169 -0
  20. data/docs/superpowers/specs/2026-10-21-rule-engine-wiring-design.md +84 -0
  21. data/lib/lutaml/xsd/conformance/runner.rb +169 -0
  22. data/lib/lutaml/xsd/json_schema_exporter.rb +240 -0
  23. data/lib/lutaml/xsd/package_builder.rb +2 -2
  24. data/lib/lutaml/xsd/spa/schema_serializer.rb +12 -7
  25. data/lib/lutaml/xsd/validation/engine_context.rb +190 -0
  26. data/lib/lutaml/xsd/validation/rule_engine.rb +36 -18
  27. data/lib/lutaml/xsd/validation/rule_registry.rb +34 -0
  28. data/lib/lutaml/xsd/validation/rules/attribute_validation_rule.rb +126 -17
  29. data/lib/lutaml/xsd/validation/rules/content_model_validation_rule.rb +304 -194
  30. data/lib/lutaml/xsd/validation/rules/element_structure_rule.rb +25 -15
  31. data/lib/lutaml/xsd/validation/rules/identity_constraints_rule.rb +134 -0
  32. data/lib/lutaml/xsd/validation/rules/occurrence_validation_rule.rb +76 -25
  33. data/lib/lutaml/xsd/validation/rules/type_validation_rule.rb +134 -17
  34. data/lib/lutaml/xsd/validation/schema_definition_resolver.rb +254 -0
  35. data/lib/lutaml/xsd/validation/validation_configuration.rb +1 -0
  36. data/lib/lutaml/xsd/validation/validation_job.rb +70 -3
  37. data/lib/lutaml/xsd/validation/validation_rule.rb +44 -5
  38. data/lib/lutaml/xsd/validation/validator.rb +2 -1
  39. data/lib/lutaml/xsd/validation/xml_element.rb +2 -2
  40. data/lib/lutaml/xsd/version.rb +1 -1
  41. data/lib/lutaml/xsd.rb +14 -0
  42. data/lutaml-xsd.gemspec +2 -2
  43. metadata +27 -8
@@ -48,7 +48,8 @@ module Lutaml
48
48
  # @param collector [ResultCollector] Collector for validation
49
49
  # results
50
50
  # @return [void]
51
- def validate(xml_element, schema_element, collector)
51
+ def validate(xml_element, schema_element, collector,
52
+ repository: nil)
52
53
  # Step 1: Validate element declaration exists
53
54
  unless validate_element_exists(xml_element, schema_element,
54
55
  collector)
@@ -59,7 +60,19 @@ module Lutaml
59
60
  validate_element_name(xml_element, schema_element, collector)
60
61
 
61
62
  # Step 3: Validate namespace correctness
62
- validate_namespace(xml_element, schema_element, collector)
63
+ validate_namespace(xml_element, schema_element, collector,
64
+ repository: repository)
65
+ end
66
+
67
+ # Structure checks run only for declarations the resolver could
68
+ # match: unresolved names also cover refs/substitution groups the
69
+ # resolver does not model yet, so treating them as errors here
70
+ # would misfire. Undeclared-element detection is tracked in
71
+ # TODO.conformance.md.
72
+ #
73
+ # @return [Boolean]
74
+ def runs_without_schema?
75
+ false
63
76
  end
64
77
 
65
78
  private
@@ -117,8 +130,9 @@ module Lutaml
117
130
  # @param schema_element [Lutaml::Xsd::Element] Schema element
118
131
  # @param collector [ResultCollector] Result collector
119
132
  # @return [void]
120
- def validate_namespace(xml_element, schema_element, collector)
121
- expected_ns = resolve_expected_namespace(schema_element)
133
+ def validate_namespace(xml_element, schema_element, collector,
134
+ repository: nil)
135
+ expected_ns = resolve_expected_namespace(schema_element, repository)
122
136
  actual_ns = xml_element.namespace_uri
123
137
 
124
138
  # Both nil is ok (no namespace)
@@ -152,21 +166,17 @@ module Lutaml
152
166
  xml_element.name == schema_element.name
153
167
  end
154
168
 
155
- # Resolve expected namespace from schema element
169
+ # Resolve expected namespace for a schema declaration via the
170
+ # resolver's index (parsed declarations carry no owner-schema
171
+ # link).
156
172
  #
157
173
  # @param schema_element [Lutaml::Xsd::Element] Schema element
174
+ # @param repository [SchemaDefinitionResolver, nil]
158
175
  # @return [String, nil]
159
- def resolve_expected_namespace(schema_element)
160
- # Check if element has explicit targetNamespace
161
- return schema_element.target_namespace if
162
- schema_element.target_namespace
163
-
164
- # Check parent schema's targetNamespace
165
- if schema_element.schema&.target_namespace
166
- return schema_element.schema.target_namespace
167
- end
176
+ def resolve_expected_namespace(schema_element, repository)
177
+ return nil unless repository
168
178
 
169
- nil
179
+ repository.target_namespace_for(schema_element)
170
180
  end
171
181
 
172
182
  # Normalize namespace (treat empty string as nil)
@@ -0,0 +1,134 @@
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
+ # Validates xs:key / xs:unique / xs:keyref identity constraints on
10
+ # an instance element.
11
+ #
12
+ # Selector and field XPaths evaluate against the instance subtree
13
+ # with unprefixed XPath semantics (no-namespace matching), which
14
+ # covers the common no-targetNamespace and local-name selector
15
+ # shapes. Scope: constraints declared on the element itself;
16
+ # cross-element keyref scopes and recursive selector Depth are
17
+ # backlog items (TODO.conformance.md).
18
+ class IdentityConstraintsRule < ValidationRule
19
+ def category
20
+ :identity
21
+ end
22
+
23
+ def description
24
+ "Validates key, keyref and unique identity constraints"
25
+ end
26
+
27
+ def validate(xml_element, schema_element, collector,
28
+ repository: nil) # rubocop:disable Lint/UnusedMethodArgument
29
+ # `repository` is part of the element-scoped dispatch contract;
30
+ # identity constraints resolve names against the declaration.
31
+ return unless schema_element.is_a?(Lutaml::Xml::Schema::Xsd::Element)
32
+
33
+ collected = {}
34
+
35
+ Array(schema_element.key).each do |constraint|
36
+ values = collect_values(xml_element, constraint)
37
+ report_duplicates(collector, xml_element, constraint, values,
38
+ "duplicate_key_violation")
39
+ collected[constraint.name] = values if constraint.name
40
+ end
41
+
42
+ Array(schema_element.unique).each do |constraint|
43
+ values = collect_values(xml_element, constraint)
44
+ report_duplicates(collector, xml_element, constraint, values,
45
+ "duplicate_unique_violation")
46
+ collected[constraint.name] = values if constraint.name
47
+ end
48
+
49
+ Array(schema_element.keyref).each do |constraint|
50
+ referred = refer_name(constraint) && collected[refer_name(constraint)]
51
+ next if referred.nil?
52
+
53
+ collect_values(xml_element, constraint).each do |value|
54
+ next if referred.include?(value)
55
+
56
+ report_error(
57
+ collector,
58
+ code: "keyref_violation",
59
+ message: "Value '#{value}' does not match any " \
60
+ "'#{refer_name(constraint)}' key",
61
+ location: xml_element.xpath,
62
+ context: { value: value, refer: refer_name(constraint) },
63
+ )
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def collect_values(xml_element, constraint)
71
+ selector_xpaths(constraint).flat_map do |selector|
72
+ Array(xml_element.moxml_element.xpath(selector)).map do |node|
73
+ field_xpaths(constraint).filter_map do |field|
74
+ field_value(node, field)
75
+ end
76
+ end
77
+ end.flatten.compact
78
+ end
79
+
80
+ def report_duplicates(collector, xml_element, constraint, values,
81
+ code)
82
+ values.group_by(&:itself).each do |value, group|
83
+ next unless group.length > 1
84
+
85
+ report_error(
86
+ collector,
87
+ code: code,
88
+ message: "Duplicate value '#{value}' for " \
89
+ "'#{constraint.name}'",
90
+ location: xml_element.xpath,
91
+ context: {
92
+ value: value,
93
+ constraint: constraint.name,
94
+ occurrences: group.length,
95
+ },
96
+ )
97
+ end
98
+ end
99
+
100
+ def selector_xpaths(constraint)
101
+ return [] unless constraint.selector
102
+
103
+ xpaths = constraint.selector.respond_to?(:xpath) ? constraint.selector.xpath : constraint.selector
104
+ Array(xpaths).map { |x| x.respond_to?(:xpath) ? x.xpath : x.to_s }
105
+ end
106
+
107
+ def field_xpaths(constraint)
108
+ Array(constraint.field).map do |field|
109
+ field.respond_to?(:xpath) ? field.xpath : field.to_s
110
+ end
111
+ end
112
+
113
+ def field_value(node, field_xpath)
114
+ first = Array(node.xpath(field_xpath)).first
115
+ return nil unless first
116
+
117
+ first.respond_to?(:value) && first.value ? first.value : first.text
118
+ end
119
+
120
+ # Keyref `refer` as a string; nil when the model left it
121
+ # unmapped (UninitializedClass — a lutaml-model mapping gap for
122
+ # `map_attribute :refer`).
123
+ def refer_name(constraint)
124
+ refer = constraint.refer
125
+ return nil unless refer.is_a?(String)
126
+
127
+ text = refer.split(":").last
128
+ text.to_s.empty? ? nil : text
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -46,7 +46,8 @@ module Lutaml
46
46
  # Lutaml::Xsd::Choice, Lutaml::Xsd::All] The schema particle
47
47
  # @param collector [ResultCollector] Collector for validation results
48
48
  # @return [void]
49
- def validate(parent_element, schema_particle, collector)
49
+ def validate(parent_element, schema_particle, collector,
50
+ repository: nil)
50
51
  return unless schema_particle
51
52
  return unless parent_element
52
53
 
@@ -54,19 +55,31 @@ module Lutaml
54
55
  case schema_particle
55
56
  when Lutaml::Xsd::Element
56
57
  validate_element_occurrence(parent_element, schema_particle,
57
- collector)
58
+ collector, repository: repository)
58
59
  when Lutaml::Xsd::Sequence
59
60
  validate_sequence_occurrences(parent_element, schema_particle,
60
- collector)
61
+ collector, repository: repository)
61
62
  when Lutaml::Xsd::Choice
62
63
  validate_choice_occurrences(parent_element, schema_particle,
63
- collector)
64
+ collector, repository: repository)
64
65
  when Lutaml::Xsd::All
65
66
  validate_all_occurrences(parent_element, schema_particle,
66
- collector)
67
+ collector, repository: repository)
67
68
  end
68
69
  end
69
70
 
71
+ # This rule validates the element's children against the
72
+ # content-model particle of the element's type.
73
+ #
74
+ # @param schema_def [Object, nil] Resolved schema declaration
75
+ # @param repository [SchemaDefinitionResolver, nil]
76
+ # @return [Object, nil]
77
+ def schema_argument(schema_def, repository: nil)
78
+ return nil unless schema_def && repository
79
+
80
+ repository.content_model_for(schema_def)
81
+ end
82
+
70
83
  private
71
84
 
72
85
  # Validate element occurrence
@@ -76,13 +89,14 @@ module Lutaml
76
89
  # @param collector [ResultCollector] Result collector
77
90
  # @return [void]
78
91
  def validate_element_occurrence(parent_element, schema_element,
79
- collector)
92
+ collector, repository: nil)
80
93
  min_occurs = parse_occurs(schema_element.min_occurs, 1)
81
94
  max_occurs = parse_max_occurs(schema_element.max_occurs, 1)
82
95
 
83
96
  # Count actual occurrences
84
- actual_count = count_element_occurrences(parent_element,
85
- schema_element)
97
+ actual_count = count_element_occurrences(
98
+ parent_element, schema_element, repository
99
+ )
86
100
 
87
101
  # Check minimum occurrence
88
102
  validate_min_occurs(parent_element, schema_element, actual_count,
@@ -100,15 +114,17 @@ module Lutaml
100
114
  # @param collector [ResultCollector] Result collector
101
115
  # @return [void]
102
116
  def validate_sequence_occurrences(parent_element, sequence,
103
- collector)
117
+ collector, repository: nil)
104
118
  return if sequence.element.empty?
105
119
 
106
120
  Array(sequence.element).each do |element|
107
- validate_element_occurrence(parent_element, element, collector)
121
+ validate_element_occurrence(parent_element, element, collector,
122
+ repository: repository)
108
123
  end
109
124
 
110
125
  # Also check nested sequences, choices, groups
111
- validate_nested_particles(parent_element, sequence, collector)
126
+ validate_nested_particles(parent_element, sequence, collector,
127
+ repository: repository)
112
128
  end
113
129
 
114
130
  # Validate choice particle occurrences
@@ -117,13 +133,15 @@ module Lutaml
117
133
  # @param choice [Lutaml::Xsd::Choice] Schema choice
118
134
  # @param collector [ResultCollector] Result collector
119
135
  # @return [void]
120
- def validate_choice_occurrences(parent_element, choice, collector)
136
+ def validate_choice_occurrences(parent_element, choice, collector,
137
+ repository: nil)
121
138
  min_occurs = parse_occurs(choice.min_occurs, 1)
122
139
  max_occurs = parse_max_occurs(choice.max_occurs, 1)
123
140
 
124
141
  # For choice, at least one alternative should be present
125
142
  # Count how many alternatives are present
126
- present_count = count_choice_alternatives(parent_element, choice)
143
+ present_count = count_choice_alternatives(parent_element, choice,
144
+ repository)
127
145
 
128
146
  if present_count < min_occurs
129
147
  report_error(
@@ -160,13 +178,15 @@ module Lutaml
160
178
  # @param all [Lutaml::Xsd::All] Schema all
161
179
  # @param collector [ResultCollector] Result collector
162
180
  # @return [void]
163
- def validate_all_occurrences(parent_element, all, collector)
181
+ def validate_all_occurrences(parent_element, all, collector,
182
+ repository: nil)
164
183
  return if all.element.empty?
165
184
 
166
185
  # In xs:all, each element can appear 0 or 1 times
167
186
  # (or based on its minOccurs/maxOccurs)
168
187
  Array(all.element).each do |element|
169
- validate_element_occurrence(parent_element, element, collector)
188
+ validate_element_occurrence(parent_element, element, collector,
189
+ repository: repository)
170
190
  end
171
191
  end
172
192
 
@@ -176,18 +196,21 @@ module Lutaml
176
196
  # @param particle [Object] Schema particle
177
197
  # @param collector [ResultCollector] Result collector
178
198
  # @return [void]
179
- def validate_nested_particles(parent_element, particle, collector)
199
+ def validate_nested_particles(parent_element, particle, collector,
200
+ repository: nil)
180
201
  # Check for nested sequences
181
202
  if particle.sequence
182
203
  Array(particle.sequence).each do |seq|
183
- validate_sequence_occurrences(parent_element, seq, collector)
204
+ validate_sequence_occurrences(parent_element, seq, collector,
205
+ repository: repository)
184
206
  end
185
207
  end
186
208
 
187
209
  # Check for nested choices
188
210
  if particle.choice
189
211
  Array(particle.choice).each do |choice|
190
- validate_choice_occurrences(parent_element, choice, collector)
212
+ validate_choice_occurrences(parent_element, choice, collector,
213
+ repository: repository)
191
214
  end
192
215
  end
193
216
 
@@ -204,27 +227,50 @@ module Lutaml
204
227
  # @param parent_element [XmlElement] Parent element
205
228
  # @param schema_element [Lutaml::Xsd::Element] Schema element
206
229
  # @return [Integer]
207
- def count_element_occurrences(parent_element, schema_element)
208
- element_name = schema_element.name
209
- target_ns = resolve_target_namespace(schema_element)
230
+ def count_element_occurrences(parent_element, schema_element,
231
+ repository = nil)
232
+ element_name = schema_element.name ||
233
+ schema_element.ref.to_s.split(":").last
234
+ target_ns = resolve_target_namespace(schema_element, repository)
210
235
 
211
236
  parent_element.children.count do |child|
212
- child.name == element_name &&
237
+ name_matches = child.name == element_name &&
213
238
  namespaces_match?(child.namespace_uri, target_ns)
239
+ name_matches ||
240
+ substitution_member?(child, schema_element, repository)
214
241
  end
215
242
  end
216
243
 
244
+ # Whether the child's declaration is a member of the
245
+ # substitution group headed by the particle's name/ref.
246
+ #
247
+ # @return [Boolean]
248
+ def substitution_member?(child, schema_element, repository)
249
+ return false unless repository
250
+
251
+ head = schema_element.ref
252
+ return false if head.to_s.empty?
253
+
254
+ declaration = repository.resolve(child.name, child.namespace_uri)
255
+ return false unless declaration
256
+
257
+ declaration.substitution_group.to_s.split(":").last ==
258
+ head.to_s.split(":").last
259
+ end
260
+
217
261
  # Count how many choice alternatives are present
218
262
  #
219
263
  # @param parent_element [XmlElement] Parent element
220
264
  # @param choice [Lutaml::Xsd::Choice] Schema choice
221
265
  # @return [Integer]
222
- def count_choice_alternatives(parent_element, choice)
266
+ def count_choice_alternatives(parent_element, choice,
267
+ repository = nil)
223
268
  return 0 if choice.element.empty?
224
269
 
225
270
  alternatives = Array(choice.element)
226
271
  alternatives.count do |alt_element|
227
- count_element_occurrences(parent_element, alt_element).positive?
272
+ count_element_occurrences(parent_element, alt_element,
273
+ repository).positive?
228
274
  end
229
275
  end
230
276
 
@@ -312,7 +358,12 @@ module Lutaml
312
358
  #
313
359
  # @param schema_element [Lutaml::Xsd::Element] Schema element
314
360
  # @return [String, nil]
315
- def resolve_target_namespace(schema_element)
361
+ def resolve_target_namespace(schema_element, repository = nil)
362
+ # Inline declarations carry no namespace of their own; the
363
+ # resolver knows the owning schema's target namespace.
364
+ from_index = repository&.target_namespace_for(schema_element)
365
+ return from_index unless from_index.nil?
366
+
316
367
  schema_element.target_namespace
317
368
  end
318
369
 
@@ -46,10 +46,28 @@ module Lutaml
46
46
  # @param collector [ResultCollector] Collector for validation
47
47
  # results
48
48
  # @return [void]
49
- def validate(xml_element, schema_element, collector)
49
+ def validate(xml_element, schema_element, collector,
50
+ repository: nil)
50
51
  return unless schema_element
51
52
 
52
- type_def = resolve_element_type(schema_element)
53
+ return if xsi_nil_handled?(xml_element, schema_element, collector)
54
+
55
+ type_def = resolve_element_type(schema_element, repository)
56
+ if xsi_type_name(xml_element)
57
+ type_def = resolve_xsi_type(xsi_type_name(xml_element),
58
+ repository)
59
+ unless type_def
60
+ report_error(
61
+ collector,
62
+ code: "xsi_type_unknown",
63
+ message: "xsi:type '#{xsi_type_name(xml_element)}' does " \
64
+ "not resolve to a known type definition",
65
+ location: xml_element.xpath,
66
+ context: { xsi_type: xsi_type_name(xml_element) },
67
+ )
68
+ return
69
+ end
70
+ end
53
71
  return unless type_def
54
72
 
55
73
  case type_def
@@ -65,11 +83,67 @@ module Lutaml
65
83
 
66
84
  private
67
85
 
86
+ # xsi:nil handling: allowed only on nillable declarations, and a
87
+ # nil element must be empty. Returns true when the element is nil
88
+ # (nothing further to validate).
89
+ #
90
+ # @return [Boolean]
91
+ def xsi_nil_handled?(xml_element, schema_element, collector)
92
+ nil_attr = xsi_attribute(xml_element, "nil")
93
+ return false unless nil_attr&.value.to_s.match?(/\A(true|1)\z/)
94
+
95
+ unless schema_element.nillable.to_s.match?(/\A(true|1)\z/)
96
+ report_error(
97
+ collector,
98
+ code: "xsi_nil_not_allowed",
99
+ message: "Element '#{xml_element.name}' is not nillable " \
100
+ "but carries xsi:nil",
101
+ location: xml_element.xpath,
102
+ context: { element: xml_element.name },
103
+ )
104
+ return true
105
+ end
106
+
107
+ return true if xml_element.children.empty?
108
+
109
+ report_error(
110
+ collector,
111
+ code: "xsi_nil_with_content",
112
+ message: "Element '#{xml_element.name}' has xsi:nil but " \
113
+ "carries content",
114
+ location: xml_element.xpath,
115
+ context: { element: xml_element.name },
116
+ )
117
+ true
118
+ end
119
+
120
+ # The xsi:type attribute value (local part), or nil.
121
+ #
122
+ # @param xml_element [XmlElement]
123
+ # @return [String, nil]
124
+ def xsi_type_name(xml_element)
125
+ attr = xsi_attribute(xml_element, "type")
126
+ return nil unless attr&.value.to_s.match?(/\S/)
127
+
128
+ attr.value.to_s.split(":").last
129
+ end
130
+
131
+ # Resolve an xsi:type local name to a named type definition.
132
+ #
133
+ # @param local_name [String]
134
+ # @param repository [SchemaDefinitionResolver, nil]
135
+ # @return [Object, nil]
136
+ def resolve_xsi_type(local_name, repository)
137
+ return nil unless repository
138
+
139
+ repository.resolve_named_type(local_name)
140
+ end
141
+
68
142
  # Resolve the type definition for an element
69
143
  #
70
144
  # @param schema_element [Lutaml::Xsd::Element] Schema element
71
145
  # @return [Lutaml::Xsd::SimpleType, Lutaml::Xsd::ComplexType, nil]
72
- def resolve_element_type(schema_element)
146
+ def resolve_element_type(schema_element, repository = nil)
73
147
  # Check for inline type definition
74
148
  return schema_element.simple_type if
75
149
  schema_element.simple_type
@@ -79,8 +153,9 @@ module Lutaml
79
153
 
80
154
  # Check for type reference
81
155
  if schema_element.type
82
- # TODO: Resolve type from repository
83
- # For now, return the type reference as-is
156
+ resolved = repository&.resolve_type(schema_element)
157
+ return resolved if resolved
158
+
84
159
  return schema_element.type
85
160
  end
86
161
 
@@ -154,9 +229,16 @@ module Lutaml
154
229
  # @param collector [ResultCollector] Result collector
155
230
  # @return [void]
156
231
  def validate_base_type(value, base_type, xml_element, collector)
157
- # TODO: Implement base type validation
158
- # This should use BaseTypeValidator registry
159
- # For now, skip validation
232
+ answer = engine_builtin_answer(base_type, value)
233
+ return if answer.nil? || answer
234
+
235
+ report_error(
236
+ collector,
237
+ code: "type_validation_error",
238
+ message: "'#{value}' is not a valid #{base_type}",
239
+ location: xml_element.xpath,
240
+ context: { value: value, type: base_type },
241
+ )
160
242
  end
161
243
 
162
244
  # Validate facets
@@ -229,9 +311,11 @@ module Lutaml
229
311
  collector)
230
312
  value_length = value.length
231
313
 
232
- # Check exact length
233
- if restriction.length
234
- expected_length = restriction.length.value.to_i
314
+ # Facet collections are arrays; an empty array must not be
315
+ # treated as a present facet.
316
+ length_facet = Array(restriction.length).first
317
+ if length_facet
318
+ expected_length = length_facet.value.to_i
235
319
  if value_length != expected_length
236
320
  report_error(
237
321
  collector,
@@ -249,8 +333,9 @@ module Lutaml
249
333
  end
250
334
 
251
335
  # Check minimum length
252
- if restriction.min_length
253
- min_length = restriction.min_length.value.to_i
336
+ min_length_facet = Array(restriction.min_length).first
337
+ if min_length_facet
338
+ min_length = min_length_facet.value.to_i
254
339
  if value_length < min_length
255
340
  report_error(
256
341
  collector,
@@ -268,9 +353,10 @@ module Lutaml
268
353
  end
269
354
 
270
355
  # Check maximum length
271
- return unless restriction.max_length
356
+ max_length_facet = Array(restriction.max_length).first
357
+ return unless max_length_facet
272
358
 
273
- max_length = restriction.max_length.value.to_i
359
+ max_length = max_length_facet.value.to_i
274
360
  return unless value_length > max_length
275
361
 
276
362
  report_error(
@@ -378,13 +464,44 @@ module Lutaml
378
464
 
379
465
  # Validate built-in type
380
466
  #
467
+ # The engine answers builtin lexical checks when the adapter ships
468
+ # an XSD engine; otherwise validation stays unimplemented here
469
+ # (matching historical behavior).
470
+ #
381
471
  # @param xml_element [XmlElement] The element
382
472
  # @param type_name [String] The type name
383
473
  # @param collector [ResultCollector] Result collector
384
474
  # @return [void]
385
475
  def validate_builtin_type(xml_element, type_name, collector)
386
- # TODO: Implement built-in type validation using BaseTypeValidator
387
- # For now, skip validation
476
+ answer = engine_builtin_answer(type_name, xml_element.text_content)
477
+ return if answer.nil?
478
+ return if answer
479
+
480
+ report_error(
481
+ collector,
482
+ code: "type_validation_error",
483
+ message: "'#{xml_element.text_content}' is not a valid " \
484
+ "#{type_name}",
485
+ location: xml_element.xpath,
486
+ context: {
487
+ value: xml_element.text_content,
488
+ type: type_name,
489
+ },
490
+ )
491
+ end
492
+
493
+ # Engine answer for a builtin lexical check, nil when unavailable
494
+ #
495
+ # @param type_name [String] XSD type name (any prefix spelling)
496
+ # @param value [String, nil] The lexical value
497
+ # @return [Boolean, nil]
498
+ def engine_builtin_answer(type_name, value)
499
+ engine = @options[:engine_context]
500
+ return unless engine&.supported?
501
+
502
+ engine.builtin_valid?(
503
+ "xs:#{type_name.to_s.split(':').last}", value
504
+ )
388
505
  end
389
506
  end
390
507
  end