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
|
@@ -5,7 +5,6 @@ require "moxml"
|
|
|
5
5
|
require "tmpdir"
|
|
6
6
|
require "fileutils"
|
|
7
7
|
require "tempfile"
|
|
8
|
-
require "nokogiri"
|
|
9
8
|
require "xsdvi"
|
|
10
9
|
require_relative "xml_instance_generator"
|
|
11
10
|
require_relative "utils/extract_enumeration"
|
|
@@ -494,6 +493,8 @@ module Lutaml
|
|
|
494
493
|
end
|
|
495
494
|
end
|
|
496
495
|
|
|
496
|
+
public
|
|
497
|
+
|
|
497
498
|
# Create a temporary XSD file that wraps a complexType/simpleType
|
|
498
499
|
# in a synthetic root element, so xsdvi can generate a diagram for it.
|
|
499
500
|
#
|
|
@@ -501,7 +502,7 @@ module Lutaml
|
|
|
501
502
|
# @param original_file_path [String] Path to the original XSD file
|
|
502
503
|
# @return [String] Path to the temporary XSD file
|
|
503
504
|
def create_type_wrapper_xsd(type_name, original_file_path)
|
|
504
|
-
doc =
|
|
505
|
+
doc = Moxml::Context.new.parse(File.read(original_file_path))
|
|
505
506
|
ns = { "xs" => "http://www.w3.org/2001/XMLSchema" }
|
|
506
507
|
schema = doc.at_xpath("//xs:schema", ns)
|
|
507
508
|
|
|
@@ -519,11 +520,11 @@ module Lutaml
|
|
|
519
520
|
# Create element with proper XSD namespace from the schema
|
|
520
521
|
xsd_ns = "http://www.w3.org/2001/XMLSchema"
|
|
521
522
|
xsd_ns_decl = schema.namespace_definitions.find do |nd|
|
|
522
|
-
nd.
|
|
523
|
+
nd.uri == xsd_ns
|
|
523
524
|
end
|
|
524
525
|
|
|
525
526
|
element_node = doc.create_element("element")
|
|
526
|
-
element_node.namespace = xsd_ns_decl
|
|
527
|
+
element_node.namespace = xsd_ns_decl if xsd_ns_decl
|
|
527
528
|
element_node["name"] = wrapper_name
|
|
528
529
|
element_node["type"] = type_ref
|
|
529
530
|
|
|
@@ -540,7 +541,7 @@ module Lutaml
|
|
|
540
541
|
# that the schema uses internally for its targetNamespace.
|
|
541
542
|
#
|
|
542
543
|
# @param type_name [String] The type name
|
|
543
|
-
# @param doc [
|
|
544
|
+
# @param doc [Moxml::Document] The parsed XSD document
|
|
544
545
|
# @param ns [Hash] Namespace mapping
|
|
545
546
|
# @return [String] The type reference string (possibly prefixed)
|
|
546
547
|
def resolve_type_prefix(type_name, doc, ns)
|
|
@@ -558,12 +559,14 @@ module Lutaml
|
|
|
558
559
|
# Find what prefix maps to the targetNamespace
|
|
559
560
|
ns_decls = schema.namespace_definitions
|
|
560
561
|
tns_prefix = ns_decls.find do |nd|
|
|
561
|
-
nd.
|
|
562
|
+
nd.uri == target_ns && nd.prefix
|
|
562
563
|
end
|
|
563
564
|
|
|
564
565
|
tns_prefix ? "#{tns_prefix.prefix}:#{type_name}" : type_name
|
|
565
566
|
end
|
|
566
567
|
|
|
568
|
+
protected
|
|
569
|
+
|
|
567
570
|
# Serialize complex types from schema
|
|
568
571
|
#
|
|
569
572
|
# @param schema [Schema] Schema object
|
|
@@ -950,7 +953,9 @@ source = nil)
|
|
|
950
953
|
"//#{type}[@#{key}='#{escaped_value}']"
|
|
951
954
|
end
|
|
952
955
|
node = doc.at_xpath(xpath)
|
|
953
|
-
|
|
956
|
+
# Element-level indentation is rejected by the leptris adapter
|
|
957
|
+
# (moxml 0.5.150+); fragments keep their source whitespace.
|
|
958
|
+
node&.to_xml
|
|
954
959
|
rescue StandardError
|
|
955
960
|
nil
|
|
956
961
|
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "moxml"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Xsd
|
|
7
|
+
module Validation
|
|
8
|
+
# EngineContext adapts Moxml::XSD (engine-native XSD validation on
|
|
9
|
+
# leptris/nokogiri) to lutaml-xsd's validation stack.
|
|
10
|
+
#
|
|
11
|
+
# Engine findings are additive: tier-1 engine scope validates laxly
|
|
12
|
+
# outside named+typed top-level elements / one-level content-model
|
|
13
|
+
# descent / lexical checks, so the Ruby rule engine stays
|
|
14
|
+
# authoritative. All query methods return nil (not false) when the
|
|
15
|
+
# engine cannot answer, and callers fall back to Ruby logic.
|
|
16
|
+
#
|
|
17
|
+
# An instance document is validated only against compiled schemas
|
|
18
|
+
# that declare its root element with a matching targetNamespace —
|
|
19
|
+
# repositories (and the global processed-schema cache) hold many
|
|
20
|
+
# unrelated schemas, and blanket validation would let every schema
|
|
21
|
+
# reject every document.
|
|
22
|
+
class EngineContext
|
|
23
|
+
ENGINE_CODE = "engine_validation"
|
|
24
|
+
|
|
25
|
+
XSD_NAMESPACE = "http://www.w3.org/2001/XMLSchema"
|
|
26
|
+
|
|
27
|
+
CompiledSchema = Struct.new(:handle, :target_namespace, :element_names)
|
|
28
|
+
|
|
29
|
+
attr_reader :context, :schemas
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# Compile repository schemas for the current adapter.
|
|
33
|
+
#
|
|
34
|
+
# @param repository [#all_schemas] Schema repository
|
|
35
|
+
# @param context [Moxml::Context] Adapter context
|
|
36
|
+
# @return [EngineContext, nil] nil when the adapter has no engine
|
|
37
|
+
def compile(repository, context: Moxml::Context.new)
|
|
38
|
+
return nil unless xsd_supported?(context)
|
|
39
|
+
|
|
40
|
+
schemas = repository.all_schemas.values.filter_map do |schema|
|
|
41
|
+
compile_schema(context, schema)
|
|
42
|
+
end
|
|
43
|
+
new(context, schemas)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Whether the adapter ships an XSD validator.
|
|
47
|
+
#
|
|
48
|
+
# @param context [Moxml::Context] Adapter context
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def xsd_supported?(context)
|
|
51
|
+
Moxml::XSD.supported?(context)
|
|
52
|
+
rescue Moxml::NotImplementedError, NameError
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Map a Moxml::XSD::ValidationError into the lutaml-xsd error type
|
|
57
|
+
#
|
|
58
|
+
# @param finding [Moxml::XSD::ValidationError]
|
|
59
|
+
# @return [Validation::ValidationError]
|
|
60
|
+
def map_finding(finding)
|
|
61
|
+
ValidationError.new(
|
|
62
|
+
code: ENGINE_CODE,
|
|
63
|
+
message: finding.message,
|
|
64
|
+
severity: :error,
|
|
65
|
+
location: finding.path,
|
|
66
|
+
line_number: finding.line,
|
|
67
|
+
context: { column: finding.column }.compact,
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def compile_schema(context, schema)
|
|
74
|
+
source = schema.to_xml
|
|
75
|
+
compiled = context.xsd(source)
|
|
76
|
+
return nil if compiled.compile_error
|
|
77
|
+
|
|
78
|
+
info = inspect_schema(context, source)
|
|
79
|
+
CompiledSchema.new(
|
|
80
|
+
compiled, info[:target_namespace], info[:element_names]
|
|
81
|
+
)
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def inspect_schema(context, source)
|
|
87
|
+
doc = context.parse(source)
|
|
88
|
+
root = doc.root
|
|
89
|
+
names = doc.xpath("/xs:schema/xs:element[@name]",
|
|
90
|
+
"xs" => XSD_NAMESPACE).map { |node| node["name"] }
|
|
91
|
+
{ target_namespace: root&.[]("targetNamespace"),
|
|
92
|
+
element_names: names }
|
|
93
|
+
rescue StandardError
|
|
94
|
+
{ target_namespace: nil, element_names: [] }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def initialize(context, schemas)
|
|
99
|
+
@context = context
|
|
100
|
+
@schemas = schemas
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @return [Boolean] true when at least one schema compiled
|
|
104
|
+
def supported?
|
|
105
|
+
!@schemas.empty?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Validate raw XML against the compiled schemas that declare the
|
|
109
|
+
# document's root element.
|
|
110
|
+
#
|
|
111
|
+
# @param xml_content [String]
|
|
112
|
+
# @return [Array<Validation::ValidationError>]
|
|
113
|
+
def findings_for(xml_content)
|
|
114
|
+
root = instance_root(xml_content)
|
|
115
|
+
return [] unless root
|
|
116
|
+
|
|
117
|
+
matching_schemas(root).flat_map do |compiled|
|
|
118
|
+
compiled.handle.validate(xml_content)
|
|
119
|
+
end.map { |finding| self.class.map_finding(finding) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Lexical check against the built-in type table ("xs:NAME" spelling).
|
|
123
|
+
#
|
|
124
|
+
# @param builtin_name [String] e.g. "xs:date"
|
|
125
|
+
# @param lexical [String] the value to check
|
|
126
|
+
# @return [Boolean, nil] nil when the engine cannot answer
|
|
127
|
+
def builtin_valid?(builtin_name, lexical)
|
|
128
|
+
Moxml::XSD.builtin_valid?(@context, builtin_name, lexical)
|
|
129
|
+
rescue StandardError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Lexical check against a schema's named simpleType.
|
|
134
|
+
#
|
|
135
|
+
# @param type_name [String]
|
|
136
|
+
# @param lexical [String]
|
|
137
|
+
# @return [Boolean, nil] nil when no compiled schema knows the type
|
|
138
|
+
def simple_valid(type_name, lexical)
|
|
139
|
+
@schemas.each do |compiled|
|
|
140
|
+
answer = compiled.handle.simple_valid?(type_name, lexical)
|
|
141
|
+
return answer
|
|
142
|
+
rescue StandardError
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Content-model check for one element's children (Moxml nodes).
|
|
149
|
+
#
|
|
150
|
+
# @param element_name [String]
|
|
151
|
+
# @param children [Array<Moxml::Node>]
|
|
152
|
+
# @return [Boolean, nil] nil when the engine cannot answer
|
|
153
|
+
def content_valid(element_name, children)
|
|
154
|
+
@schemas.each do |compiled|
|
|
155
|
+
answer = compiled.handle.content_valid?(element_name, children)
|
|
156
|
+
return answer
|
|
157
|
+
rescue StandardError
|
|
158
|
+
next
|
|
159
|
+
end
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def instance_root(xml_content)
|
|
166
|
+
@context.parse(xml_content).root
|
|
167
|
+
rescue StandardError
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def matching_schemas(root)
|
|
172
|
+
name = root.name
|
|
173
|
+
ns = root_namespace(root)
|
|
174
|
+
|
|
175
|
+
@schemas.select do |compiled|
|
|
176
|
+
compiled.element_names.include?(name) &&
|
|
177
|
+
(compiled.target_namespace.nil? ||
|
|
178
|
+
compiled.target_namespace == ns)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def root_namespace(root)
|
|
183
|
+
root.namespace&.uri
|
|
184
|
+
rescue StandardError
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "rule_registry"
|
|
4
4
|
require_relative "result_collector"
|
|
5
|
+
require_relative "schema_definition_resolver"
|
|
5
6
|
|
|
6
7
|
module Lutaml
|
|
7
8
|
module Xsd
|
|
@@ -45,9 +46,19 @@ module Lutaml
|
|
|
45
46
|
# @raise [StopValidationError] if stop-on-first-error is enabled
|
|
46
47
|
def execute_category(category, document, repository)
|
|
47
48
|
rules = @registry.rules_for_category(category)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
return if rules.empty?
|
|
50
|
+
|
|
51
|
+
resolver = SchemaDefinitionResolver.new(repository)
|
|
52
|
+
each_element(document) do |xml_element|
|
|
53
|
+
schema_def = resolver.resolve(
|
|
54
|
+
xml_element.name, xml_element.namespace_uri
|
|
55
|
+
)
|
|
56
|
+
rules.each do |rule|
|
|
57
|
+
argument = rule.schema_argument(schema_def, repository: resolver)
|
|
58
|
+
next if argument.nil? && !rule.runs_without_schema?
|
|
59
|
+
|
|
60
|
+
execute_rule(rule, xml_element, argument, resolver)
|
|
61
|
+
end
|
|
51
62
|
end
|
|
52
63
|
rescue ResultCollector::StopValidationError
|
|
53
64
|
# Stop validation early if configured
|
|
@@ -72,16 +83,19 @@ module Lutaml
|
|
|
72
83
|
# Stop validation early if configured
|
|
73
84
|
end
|
|
74
85
|
|
|
75
|
-
# Execute a single rule
|
|
86
|
+
# Execute a single rule for one instance element
|
|
76
87
|
#
|
|
77
88
|
# @param rule [ValidationRule] The rule to execute
|
|
78
|
-
# @param
|
|
79
|
-
# @param
|
|
89
|
+
# @param xml_element [XmlElement] The instance element
|
|
90
|
+
# @param schema_argument [Object, nil] What the rule validates against
|
|
91
|
+
# @param resolver [SchemaDefinitionResolver] Named-type resolver
|
|
80
92
|
# @return [void]
|
|
81
|
-
def execute_rule(rule,
|
|
93
|
+
def execute_rule(rule, xml_element, schema_argument, resolver)
|
|
82
94
|
return unless rule.enabled?
|
|
83
95
|
|
|
84
|
-
rule.validate(
|
|
96
|
+
rule.validate(
|
|
97
|
+
xml_element, schema_argument, @collector, repository: resolver
|
|
98
|
+
)
|
|
85
99
|
rescue StandardError => e
|
|
86
100
|
handle_rule_error(rule, e)
|
|
87
101
|
end
|
|
@@ -106,16 +120,8 @@ module Lutaml
|
|
|
106
120
|
# @param repository [SchemaRepository] The schema repository
|
|
107
121
|
# @return [void]
|
|
108
122
|
def execute_enabled(document, repository)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# Group by category and execute in order
|
|
112
|
-
categories = ValidationRule::CATEGORIES
|
|
113
|
-
|
|
114
|
-
categories.each do |category|
|
|
115
|
-
category_rules = enabled_rules.select { |r| r.category == category }
|
|
116
|
-
category_rules.each do |rule|
|
|
117
|
-
execute_rule(rule, document, repository)
|
|
118
|
-
end
|
|
123
|
+
ValidationRule::CATEGORIES.each do |category|
|
|
124
|
+
execute_category(category, document, repository)
|
|
119
125
|
end
|
|
120
126
|
rescue ResultCollector::StopValidationError
|
|
121
127
|
# Stop validation early if configured
|
|
@@ -164,6 +170,18 @@ module Lutaml
|
|
|
164
170
|
|
|
165
171
|
private
|
|
166
172
|
|
|
173
|
+
# Depth-first walk over instance elements.
|
|
174
|
+
#
|
|
175
|
+
# @param node [XmlDocument, XmlElement] Start node
|
|
176
|
+
# @yieldparam element [XmlElement]
|
|
177
|
+
def each_element(node, &)
|
|
178
|
+
element = node.is_a?(XmlDocument) ? node.root_element : node
|
|
179
|
+
return unless element
|
|
180
|
+
|
|
181
|
+
yield element
|
|
182
|
+
element.children.each { |child| each_element(child, &) }
|
|
183
|
+
end
|
|
184
|
+
|
|
167
185
|
# Handle errors that occur during rule execution
|
|
168
186
|
#
|
|
169
187
|
# @param rule [ValidationRule] The rule that failed
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "validation_rule"
|
|
4
|
+
require_relative "rules/element_structure_rule"
|
|
5
|
+
require_relative "rules/type_validation_rule"
|
|
6
|
+
require_relative "rules/attribute_validation_rule"
|
|
7
|
+
require_relative "rules/occurrence_validation_rule"
|
|
8
|
+
require_relative "rules/content_model_validation_rule"
|
|
9
|
+
require_relative "rules/identity_constraints_rule"
|
|
4
10
|
|
|
5
11
|
module Lutaml
|
|
6
12
|
module Xsd
|
|
@@ -11,6 +17,9 @@ module Lutaml
|
|
|
11
17
|
# It organizes rules by category and provides methods to register,
|
|
12
18
|
# retrieve, and filter rules based on configuration.
|
|
13
19
|
#
|
|
20
|
+
# {RuleRegistry.default} builds the production rule set that the
|
|
21
|
+
# Validator and the conformance harness share.
|
|
22
|
+
#
|
|
14
23
|
# @example Create and populate a registry
|
|
15
24
|
# registry = RuleRegistry.new(config)
|
|
16
25
|
# registry.register(ElementStructureRule.new)
|
|
@@ -31,6 +40,31 @@ module Lutaml
|
|
|
31
40
|
@rules_by_category = Hash.new { |h, k| h[k] = [] }
|
|
32
41
|
end
|
|
33
42
|
|
|
43
|
+
# Production rule set, gated by the validation feature flags.
|
|
44
|
+
#
|
|
45
|
+
# @param config [ValidationConfiguration]
|
|
46
|
+
# @return [RuleRegistry]
|
|
47
|
+
def self.default(config)
|
|
48
|
+
registry = new(config)
|
|
49
|
+
registry.register(Rules::ElementStructureRule.new)
|
|
50
|
+
if config.feature_enabled?(:validate_types)
|
|
51
|
+
registry.register(Rules::TypeValidationRule.new)
|
|
52
|
+
end
|
|
53
|
+
if config.feature_enabled?(:validate_attributes)
|
|
54
|
+
registry.register(Rules::AttributeValidationRule.new)
|
|
55
|
+
end
|
|
56
|
+
if config.feature_enabled?(:validate_occurrences)
|
|
57
|
+
registry.register(Rules::OccurrenceValidationRule.new)
|
|
58
|
+
end
|
|
59
|
+
if config.feature_enabled?(:validate_content_models)
|
|
60
|
+
registry.register(Rules::ContentModelValidationRule.new)
|
|
61
|
+
end
|
|
62
|
+
if config.feature_enabled?(:validate_identity_constraints)
|
|
63
|
+
registry.register(Rules::IdentityConstraintsRule.new)
|
|
64
|
+
end
|
|
65
|
+
registry
|
|
66
|
+
end
|
|
67
|
+
|
|
34
68
|
# Register a validation rule
|
|
35
69
|
#
|
|
36
70
|
# @param rule [ValidationRule] The rule to register
|
|
@@ -22,6 +22,9 @@ module Lutaml
|
|
|
22
22
|
#
|
|
23
23
|
# @see docs/JING_ALGORITHM_PORTING_GUIDE.md Attribute Validation
|
|
24
24
|
class AttributeValidationRule < ValidationRule
|
|
25
|
+
CONTENT_KINDS = %i[complex_content simple_content].freeze
|
|
26
|
+
CONTENT_PART_KINDS = %i[extension restriction].freeze
|
|
27
|
+
|
|
25
28
|
# Rule category
|
|
26
29
|
#
|
|
27
30
|
# @return [Symbol] :constraint
|
|
@@ -47,11 +50,14 @@ module Lutaml
|
|
|
47
50
|
# The schema definition
|
|
48
51
|
# @param collector [ResultCollector] Collector for validation results
|
|
49
52
|
# @return [void]
|
|
50
|
-
def validate(xml_element, schema_element, collector
|
|
53
|
+
def validate(xml_element, schema_element, collector,
|
|
54
|
+
repository: nil)
|
|
51
55
|
return unless schema_element
|
|
52
56
|
|
|
53
|
-
schema_attributes =
|
|
57
|
+
schema_attributes =
|
|
58
|
+
collect_schema_attributes(schema_element, repository)
|
|
54
59
|
xml_attributes = xml_element.attributes
|
|
60
|
+
wildcard = collect_any_attribute(schema_element, repository)
|
|
55
61
|
|
|
56
62
|
# Step 1: Check required attributes are present
|
|
57
63
|
validate_required_attributes(xml_element, schema_attributes,
|
|
@@ -63,7 +69,9 @@ module Lutaml
|
|
|
63
69
|
|
|
64
70
|
# Step 3: Check for unexpected attributes
|
|
65
71
|
validate_no_unexpected_attributes(xml_element, schema_attributes,
|
|
66
|
-
xml_attributes, collector
|
|
72
|
+
xml_attributes, collector,
|
|
73
|
+
wildcard: wildcard,
|
|
74
|
+
repository: repository)
|
|
67
75
|
end
|
|
68
76
|
|
|
69
77
|
private
|
|
@@ -73,13 +81,13 @@ module Lutaml
|
|
|
73
81
|
# @param schema_def [Lutaml::Xsd::Element, Lutaml::Xsd::ComplexType]
|
|
74
82
|
# Schema definition
|
|
75
83
|
# @return [Array<Lutaml::Xsd::Attribute>]
|
|
76
|
-
def collect_schema_attributes(schema_def)
|
|
84
|
+
def collect_schema_attributes(schema_def, repository = nil)
|
|
77
85
|
attributes = []
|
|
78
86
|
|
|
79
87
|
# For elements, get attributes from type
|
|
80
88
|
if schema_def.is_a?(Lutaml::Xml::Schema::Xsd::Element)
|
|
81
|
-
type_def = resolve_type(schema_def)
|
|
82
|
-
return collect_schema_attributes(type_def) if type_def
|
|
89
|
+
type_def = resolve_type(schema_def, repository)
|
|
90
|
+
return collect_schema_attributes(type_def, repository) if type_def
|
|
83
91
|
end
|
|
84
92
|
|
|
85
93
|
# For complex types, collect attributes
|
|
@@ -113,11 +121,12 @@ module Lutaml
|
|
|
113
121
|
#
|
|
114
122
|
# @param element [Lutaml::Xsd::Element] Schema element
|
|
115
123
|
# @return [Lutaml::Xsd::ComplexType, nil]
|
|
116
|
-
def resolve_type(element)
|
|
124
|
+
def resolve_type(element, repository = nil)
|
|
117
125
|
return element.complex_type if element.complex_type
|
|
126
|
+
return nil unless element.type
|
|
118
127
|
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
resolved = repository&.resolve_type(element)
|
|
129
|
+
resolved if resolved.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType)
|
|
121
130
|
end
|
|
122
131
|
|
|
123
132
|
# Expand attribute group reference
|
|
@@ -301,9 +310,12 @@ module Lutaml
|
|
|
301
310
|
# @param schema_attrs [Array<Lutaml::Xsd::Attribute>] Schema attrs
|
|
302
311
|
# @param xml_attrs [Array<XmlAttribute>] XML attributes
|
|
303
312
|
# @param collector [ResultCollector] Result collector
|
|
313
|
+
# @param wildcard [AnyAttribute, nil] xs:anyAttribute declaration
|
|
314
|
+
# @param repository [SchemaDefinitionResolver, nil]
|
|
304
315
|
# @return [void]
|
|
305
316
|
def validate_no_unexpected_attributes(xml_element, schema_attrs,
|
|
306
|
-
xml_attrs, collector
|
|
317
|
+
xml_attrs, collector,
|
|
318
|
+
wildcard: nil, repository: nil)
|
|
307
319
|
xml_attrs.each do |xml_attr|
|
|
308
320
|
# Skip xmlns attributes
|
|
309
321
|
next if xml_attr.name == "xmlns" ||
|
|
@@ -316,8 +328,23 @@ module Lutaml
|
|
|
316
328
|
schema_attr = find_schema_attribute(schema_attrs, xml_attr)
|
|
317
329
|
next if schema_attr
|
|
318
330
|
|
|
319
|
-
|
|
320
|
-
|
|
331
|
+
if wildcard &&
|
|
332
|
+
wildcard_permits_attribute?(wildcard, xml_element,
|
|
333
|
+
xml_attr, repository)
|
|
334
|
+
if wildcard.process_contents.to_s == "strict" &&
|
|
335
|
+
!attribute_declared?(xml_attr, repository)
|
|
336
|
+
report_error(
|
|
337
|
+
collector,
|
|
338
|
+
code: "wildcard_undeclared_attribute",
|
|
339
|
+
message: "Attribute '#{xml_attr.qualified_name}' " \
|
|
340
|
+
"matches xs:anyAttribute with " \
|
|
341
|
+
"processContents=strict but is not declared",
|
|
342
|
+
location: xml_element.xpath,
|
|
343
|
+
context: { attribute: xml_attr.qualified_name },
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
next
|
|
347
|
+
end
|
|
321
348
|
|
|
322
349
|
report_error(
|
|
323
350
|
collector,
|
|
@@ -334,13 +361,95 @@ module Lutaml
|
|
|
334
361
|
end
|
|
335
362
|
end
|
|
336
363
|
|
|
337
|
-
#
|
|
364
|
+
# The xs:anyAttribute wildcard declared on this type, where the
|
|
365
|
+
# parse layer exposes one (attribute groups and content-model
|
|
366
|
+
# extension/restriction do). complexType-level wildcards are not
|
|
367
|
+
# mapped upstream yet, so types carrying one keep the
|
|
368
|
+
# pre-wildcard behavior for undeclared attributes.
|
|
369
|
+
#
|
|
370
|
+
# @param schema_def [Lutaml::Xml::Schema::Xsd::Element,
|
|
371
|
+
# Lutaml::Xsd::ComplexType, nil]
|
|
372
|
+
# @param repository [SchemaDefinitionResolver, nil]
|
|
373
|
+
# @return [AnyAttribute, nil]
|
|
374
|
+
def collect_any_attribute(schema_def, repository = nil)
|
|
375
|
+
if schema_def.is_a?(Lutaml::Xml::Schema::Xsd::Element)
|
|
376
|
+
type_def = resolve_type(schema_def, repository)
|
|
377
|
+
return collect_any_attribute(type_def, repository) if type_def
|
|
378
|
+
end
|
|
379
|
+
return nil unless schema_def
|
|
380
|
+
|
|
381
|
+
Array(schema_def.attribute_group).each do |group|
|
|
382
|
+
wildcard = Array(group.any_attribute).first
|
|
383
|
+
return wildcard if wildcard
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
CONTENT_KINDS.each do |content_kind|
|
|
387
|
+
content = schema_def.public_send(content_kind)
|
|
388
|
+
next unless content
|
|
389
|
+
|
|
390
|
+
CONTENT_PART_KINDS.each do |part_kind|
|
|
391
|
+
part = Array(content.public_send(part_kind)).first
|
|
392
|
+
next unless part.respond_to?(:any_attribute)
|
|
393
|
+
|
|
394
|
+
wildcard = Array(part.any_attribute).first
|
|
395
|
+
return wildcard if wildcard
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
nil
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Whether an xs:anyAttribute wildcard's namespace constraint
|
|
403
|
+
# admits the attribute (attributes default to unqualified, so
|
|
404
|
+
# ##local is the common admission).
|
|
338
405
|
#
|
|
339
|
-
# @param schema_attrs [Array<Lutaml::Xsd::Attribute>] Schema attrs
|
|
340
406
|
# @return [Boolean]
|
|
341
|
-
def
|
|
342
|
-
|
|
343
|
-
|
|
407
|
+
def wildcard_permits_attribute?(wildcard, parent, xml_attr,
|
|
408
|
+
repository)
|
|
409
|
+
ns = xml_attr.namespace_uri.to_s
|
|
410
|
+
target = content_target_namespace(parent, repository).to_s
|
|
411
|
+
case wildcard.namespace.to_s
|
|
412
|
+
when "##any", ""
|
|
413
|
+
true
|
|
414
|
+
when "##other"
|
|
415
|
+
!ns.empty? && ns != target
|
|
416
|
+
when "##local"
|
|
417
|
+
ns.empty?
|
|
418
|
+
when "##targetNamespace"
|
|
419
|
+
ns == target
|
|
420
|
+
else
|
|
421
|
+
# space-separated namespace list (##targetNamespace ##local
|
|
422
|
+
# allowed)
|
|
423
|
+
wildcard.namespace.to_s.split(/\s+/).any? do |entry|
|
|
424
|
+
case entry
|
|
425
|
+
when "##local" then ns.empty?
|
|
426
|
+
when "##targetNamespace" then ns == target
|
|
427
|
+
else entry == ns
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Whether a strict-wildcard attribute resolves to a global
|
|
434
|
+
# declaration in the repository.
|
|
435
|
+
#
|
|
436
|
+
# @return [Boolean]
|
|
437
|
+
def attribute_declared?(xml_attr, repository)
|
|
438
|
+
return true if repository.nil?
|
|
439
|
+
|
|
440
|
+
!repository.resolve_attribute(xml_attr.name,
|
|
441
|
+
xml_attr.namespace_uri).nil?
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Target namespace of the schema owning the parent's declaration.
|
|
445
|
+
#
|
|
446
|
+
# @return [String, nil]
|
|
447
|
+
def content_target_namespace(parent, repository)
|
|
448
|
+
return nil unless repository
|
|
449
|
+
|
|
450
|
+
declaration = repository.resolve(parent.name,
|
|
451
|
+
parent.namespace_uri)
|
|
452
|
+
repository.target_namespace_for(declaration) if declaration
|
|
344
453
|
end
|
|
345
454
|
|
|
346
455
|
# Suggest similar attribute name
|