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.
- checksums.yaml +4 -4
- data/.gitignore +0 -1
- data/CLAUDE.md +18 -1
- data/Gemfile +2 -1
- data/Gemfile.lock +327 -0
- data/Rakefile +78 -1
- data/TODO.VISION.md +123 -0
- data/TODO.cleanup.md +43 -0
- data/TODO.conformance.md +99 -0
- data/TODO.docs-spa.md +46 -0
- data/TODO.interop.md +52 -0
- data/TODO.performance.md +80 -0
- data/TODO.som.md +47 -0
- data/benchmark/engines.rb +77 -0
- data/config/validation.yml +4 -0
- data/docs/CONFORMANCE.adoc +43 -0
- data/docs/JSON_SCHEMA.adoc +42 -0
- data/docs/superpowers/plans/2026-10-20-moxml-leptris-migration.md +735 -0
- data/docs/superpowers/specs/2026-10-20-moxml-leptris-migration-design.md +169 -0
- data/docs/superpowers/specs/2026-10-21-rule-engine-wiring-design.md +84 -0
- data/lib/lutaml/xsd/conformance/runner.rb +169 -0
- data/lib/lutaml/xsd/json_schema_exporter.rb +240 -0
- data/lib/lutaml/xsd/package_builder.rb +2 -2
- data/lib/lutaml/xsd/spa/schema_serializer.rb +12 -7
- data/lib/lutaml/xsd/validation/engine_context.rb +190 -0
- data/lib/lutaml/xsd/validation/rule_engine.rb +36 -18
- data/lib/lutaml/xsd/validation/rule_registry.rb +34 -0
- data/lib/lutaml/xsd/validation/rules/attribute_validation_rule.rb +126 -17
- data/lib/lutaml/xsd/validation/rules/content_model_validation_rule.rb +304 -194
- data/lib/lutaml/xsd/validation/rules/element_structure_rule.rb +25 -15
- data/lib/lutaml/xsd/validation/rules/identity_constraints_rule.rb +134 -0
- data/lib/lutaml/xsd/validation/rules/occurrence_validation_rule.rb +76 -25
- data/lib/lutaml/xsd/validation/rules/type_validation_rule.rb +134 -17
- data/lib/lutaml/xsd/validation/schema_definition_resolver.rb +254 -0
- data/lib/lutaml/xsd/validation/validation_configuration.rb +1 -0
- data/lib/lutaml/xsd/validation/validation_job.rb +70 -3
- data/lib/lutaml/xsd/validation/validation_rule.rb +44 -5
- data/lib/lutaml/xsd/validation/validator.rb +2 -1
- data/lib/lutaml/xsd/validation/xml_element.rb +2 -2
- data/lib/lutaml/xsd/version.rb +1 -1
- data/lib/lutaml/xsd.rb +14 -0
- data/lutaml-xsd.gemspec +2 -2
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
85
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
#
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
356
|
+
max_length_facet = Array(restriction.max_length).first
|
|
357
|
+
return unless max_length_facet
|
|
272
358
|
|
|
273
|
-
max_length =
|
|
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
|
-
|
|
387
|
-
|
|
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
|