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
@@ -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 = Nokogiri::XML(File.read(original_file_path))
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.href == xsd_ns
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 [Nokogiri::Document] The parsed XSD document
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.href == target_ns && nd.prefix
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
- node&.to_xml(indent: 2)
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
- rules.each do |rule|
50
- execute_rule(rule, document, repository)
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 document [XmlDocument] The XML document
79
- # @param repository [SchemaRepository] The schema repository
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, document, repository)
93
+ def execute_rule(rule, xml_element, schema_argument, resolver)
82
94
  return unless rule.enabled?
83
95
 
84
- rule.validate(document, repository, @collector)
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
- enabled_rules = @registry.all_rules(enabled_only: true)
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 = collect_schema_attributes(schema_element)
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
- # TODO: Resolve type reference from repository
120
- nil
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
- # Check for wildcard (anyAttribute)
320
- next if has_any_attribute?(schema_attrs)
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
- # Check if schema allows any attribute (wildcard)
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 has_any_attribute?(_schema_attrs)
342
- # TODO: Check for anyAttribute in schema
343
- false
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