lutaml-xsd 1.2.0 → 1.2.2
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/Gemfile.lock +45 -45
- data/TODO.cleanup.md +12 -2
- data/TODO.conformance.md +37 -35
- data/lib/lutaml/xsd/json_schema_exporter.rb +35 -8
- data/lib/lutaml/xsd/rng_to_xsd_converter.rb +67 -46
- data/lib/lutaml/xsd/schema_repository/type_index.rb +2 -2
- data/lib/lutaml/xsd/spa/schema_serializer.rb +22 -25
- data/lib/lutaml/xsd/validation/rules/attribute_validation_rule.rb +27 -1
- data/lib/lutaml/xsd/validation/rules/content_model_validation_rule.rb +28 -23
- data/lib/lutaml/xsd/validation/rules/identity_constraints_rule.rb +28 -5
- data/lib/lutaml/xsd/validation/schema_definition_resolver.rb +57 -27
- data/lib/lutaml/xsd/validation/unique_particle_attribution.rb +302 -0
- data/lib/lutaml/xsd/validation/validation_rule.rb +5 -5
- data/lib/lutaml/xsd/version.rb +1 -1
- data/lutaml-xsd.gemspec +1 -1
- metadata +5 -4
|
@@ -575,9 +575,9 @@ module Lutaml
|
|
|
575
575
|
# @return [Array<Hash>] Serialized complex types (sorted alphabetically by name)
|
|
576
576
|
def serialize_complex_types(schema, prefix = nil, schema_source = nil,
|
|
577
577
|
file_path = nil)
|
|
578
|
-
return [] unless schema.
|
|
578
|
+
return [] unless schema.is_a?(Lutaml::Xml::Schema::Xsd::Schema)
|
|
579
579
|
|
|
580
|
-
types = schema.
|
|
580
|
+
types = schema.complex_types
|
|
581
581
|
serialized = types.map.with_index do |type, index|
|
|
582
582
|
serialize_complex_type(
|
|
583
583
|
type, index, prefix, schema_source, file_path
|
|
@@ -598,13 +598,13 @@ file_path = nil)
|
|
|
598
598
|
# @param file_path [String, nil] Optional file path for diagram generation
|
|
599
599
|
# @return [Array<Hash>] Serialized inline complex types
|
|
600
600
|
def serialize_inline_complex_types(schema, prefix = nil, _schema_source = nil, _file_path = nil)
|
|
601
|
-
return [] unless schema.
|
|
601
|
+
return [] unless schema.element
|
|
602
602
|
|
|
603
603
|
elements = schema.element.is_a?(Array) ? schema.element : [schema.element]
|
|
604
604
|
inline_types = []
|
|
605
605
|
|
|
606
606
|
elements.compact.each do |element|
|
|
607
|
-
next unless element.
|
|
607
|
+
next unless element.complex_type
|
|
608
608
|
next if element.complex_type.name # Skip if already named (top-level)
|
|
609
609
|
|
|
610
610
|
# Generate a synthetic name for the inline type
|
|
@@ -615,8 +615,8 @@ file_path = nil)
|
|
|
615
615
|
name: inline_name,
|
|
616
616
|
base: nil,
|
|
617
617
|
content_model: extract_content_model(element.complex_type),
|
|
618
|
-
abstract: element.complex_type.
|
|
619
|
-
mixed: element.complex_type.
|
|
618
|
+
abstract: element.complex_type.abstract || false,
|
|
619
|
+
mixed: element.complex_type.mixed || false,
|
|
620
620
|
attributes: serialize_type_attributes(element.complex_type),
|
|
621
621
|
elements: serialize_type_elements(element.complex_type),
|
|
622
622
|
choice: serialize_choice(element.complex_type.choice),
|
|
@@ -705,7 +705,7 @@ schema_source = nil, file_path = nil)
|
|
|
705
705
|
# @param model [Object] Model that may contain choice
|
|
706
706
|
# @return [Array<Hash>] Serialized choices
|
|
707
707
|
def serialize_choices(model)
|
|
708
|
-
return []
|
|
708
|
+
return [] unless model.choice
|
|
709
709
|
|
|
710
710
|
model_choices = if model.choice.is_a?(Array)
|
|
711
711
|
model.choice
|
|
@@ -743,7 +743,7 @@ schema_source = nil, file_path = nil)
|
|
|
743
743
|
# @param model [Object] Model that may contain sequence
|
|
744
744
|
# @return [Array<Hash>] Serialized sequences
|
|
745
745
|
def serialize_sequences(model)
|
|
746
|
-
return []
|
|
746
|
+
return [] unless model.sequence
|
|
747
747
|
|
|
748
748
|
model_sequences = if model.sequence.is_a?(Array)
|
|
749
749
|
model.sequence
|
|
@@ -880,9 +880,9 @@ schema_source = nil, file_path = nil)
|
|
|
880
880
|
# @param schema [Schema] Schema object
|
|
881
881
|
# @return [Array<Hash>] Serialized groups
|
|
882
882
|
def serialize_groups(schema, prefix = nil)
|
|
883
|
-
return [] unless schema.
|
|
883
|
+
return [] unless schema.groups
|
|
884
884
|
|
|
885
|
-
grps = schema.
|
|
885
|
+
grps = schema.groups
|
|
886
886
|
grps.map.with_index do |group, index|
|
|
887
887
|
# serialize_group(group, index, prefix)
|
|
888
888
|
serialize_type_group(group, index, prefix)
|
|
@@ -1081,8 +1081,8 @@ source = nil)
|
|
|
1081
1081
|
|
|
1082
1082
|
{
|
|
1083
1083
|
id: group_id(index, model, prefix),
|
|
1084
|
-
ref: model.
|
|
1085
|
-
name: model.
|
|
1084
|
+
ref: model.ref,
|
|
1085
|
+
name: model.name,
|
|
1086
1086
|
occurs: {
|
|
1087
1087
|
min: model.min_occurs || 1,
|
|
1088
1088
|
max: model.max_occurs || 1,
|
|
@@ -1098,7 +1098,7 @@ source = nil)
|
|
|
1098
1098
|
# @param model [Object] Model that may contain group
|
|
1099
1099
|
# @return [Array<Hash>] Serialized groups
|
|
1100
1100
|
def serialize_type_groups(model)
|
|
1101
|
-
return []
|
|
1101
|
+
return [] unless model.group
|
|
1102
1102
|
|
|
1103
1103
|
model_groups = if model.group.is_a?(Array)
|
|
1104
1104
|
model.group
|
|
@@ -1110,22 +1110,18 @@ source = nil)
|
|
|
1110
1110
|
end
|
|
1111
1111
|
end
|
|
1112
1112
|
|
|
1113
|
-
# Collect attribute group references from a
|
|
1113
|
+
# Collect attribute group references from a content wrapper
|
|
1114
|
+
# (simpleContent/complexContent — both declare extension and
|
|
1115
|
+
# restriction); the extensions carry the attribute groups.
|
|
1114
1116
|
#
|
|
1115
|
-
#
|
|
1116
|
-
#
|
|
1117
|
-
# @param model [Object] Any object that may have attribute_group or extension
|
|
1117
|
+
# @param model [SimpleContent, ComplexContent] Content wrapper
|
|
1118
1118
|
# @return [Array<Object>] Collected attribute group reference objects
|
|
1119
1119
|
def collect_attribute_group_refs(model)
|
|
1120
1120
|
refs = []
|
|
1121
1121
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
refs.concat(
|
|
1125
|
-
end
|
|
1126
|
-
|
|
1127
|
-
if model.respond_to?(:extension) && model.extension
|
|
1128
|
-
refs.concat(collect_extension_attribute_group_refs(model.extension))
|
|
1122
|
+
%i[extension restriction].each do |kind|
|
|
1123
|
+
part = model.public_send(kind)
|
|
1124
|
+
refs.concat(collect_extension_attribute_group_refs(part)) if part
|
|
1129
1125
|
end
|
|
1130
1126
|
|
|
1131
1127
|
refs
|
|
@@ -1385,7 +1381,8 @@ source = nil)
|
|
|
1385
1381
|
# @param type [ComplexType] Complex type
|
|
1386
1382
|
# @return [Array<Hash>] Serialized elements
|
|
1387
1383
|
def serialize_type_elements(type)
|
|
1388
|
-
return [] unless type.
|
|
1384
|
+
return [] unless type.is_a?(Lutaml::Xml::Schema::Xsd::Sequence) ||
|
|
1385
|
+
type.is_a?(Lutaml::Xml::Schema::Xsd::Choice)
|
|
1389
1386
|
|
|
1390
1387
|
type.element.map do |elem|
|
|
1391
1388
|
{
|
|
@@ -22,6 +22,14 @@ module Lutaml
|
|
|
22
22
|
#
|
|
23
23
|
# @see docs/JING_ALGORITHM_PORTING_GUIDE.md Attribute Validation
|
|
24
24
|
class AttributeValidationRule < ValidationRule
|
|
25
|
+
PROBE_SCHEMA = <<~XSD
|
|
26
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
27
|
+
<xs:complexType name="probe">
|
|
28
|
+
<xs:anyAttribute namespace="##local"/>
|
|
29
|
+
</xs:complexType>
|
|
30
|
+
</xs:schema>
|
|
31
|
+
XSD
|
|
32
|
+
|
|
25
33
|
CONTENT_KINDS = %i[complex_content simple_content].freeze
|
|
26
34
|
CONTENT_PART_KINDS = %i[extension restriction].freeze
|
|
27
35
|
|
|
@@ -371,6 +379,18 @@ module Lutaml
|
|
|
371
379
|
# Lutaml::Xsd::ComplexType, nil]
|
|
372
380
|
# @param repository [SchemaDefinitionResolver, nil]
|
|
373
381
|
# @return [AnyAttribute, nil]
|
|
382
|
+
# Whether the loaded lutaml-model parses xs:anyAttribute on
|
|
383
|
+
# complexType (the mapping landed after 0.8.121); probed once
|
|
384
|
+
# with a minimal schema.
|
|
385
|
+
def complex_type_wildcard_support?
|
|
386
|
+
@complex_type_wildcard_support ||= begin
|
|
387
|
+
probe = Lutaml::Xml::Schema::Xsd.parse(PROBE_SCHEMA)
|
|
388
|
+
!probe.complex_type.first.any_attribute.nil?
|
|
389
|
+
rescue StandardError
|
|
390
|
+
false
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
374
394
|
def collect_any_attribute(schema_def, repository = nil)
|
|
375
395
|
if schema_def.is_a?(Lutaml::Xml::Schema::Xsd::Element)
|
|
376
396
|
type_def = resolve_type(schema_def, repository)
|
|
@@ -378,6 +398,12 @@ module Lutaml
|
|
|
378
398
|
end
|
|
379
399
|
return nil unless schema_def
|
|
380
400
|
|
|
401
|
+
if schema_def.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType) &&
|
|
402
|
+
complex_type_wildcard_support?
|
|
403
|
+
wildcard = schema_def.any_attribute
|
|
404
|
+
return wildcard if wildcard
|
|
405
|
+
end
|
|
406
|
+
|
|
381
407
|
Array(schema_def.attribute_group).each do |group|
|
|
382
408
|
wildcard = Array(group.any_attribute).first
|
|
383
409
|
return wildcard if wildcard
|
|
@@ -389,7 +415,7 @@ module Lutaml
|
|
|
389
415
|
|
|
390
416
|
CONTENT_PART_KINDS.each do |part_kind|
|
|
391
417
|
part = Array(content.public_send(part_kind)).first
|
|
392
|
-
next unless part
|
|
418
|
+
next unless part
|
|
393
419
|
|
|
394
420
|
wildcard = Array(part.any_attribute).first
|
|
395
421
|
return wildcard if wildcard
|
|
@@ -22,6 +22,15 @@ module Lutaml
|
|
|
22
22
|
#
|
|
23
23
|
# @see docs/JING_ALGORITHM_PORTING_GUIDE.md Content Model Validation
|
|
24
24
|
class ContentModelValidationRule < ValidationRule
|
|
25
|
+
# Types whose particle accessors type_particles reads — all are
|
|
26
|
+
# declared model attributes (lutaml-model 0.8.114+).
|
|
27
|
+
COMPLEX_PARTICLE_TYPES = [
|
|
28
|
+
Lutaml::Xml::Schema::Xsd::ComplexType,
|
|
29
|
+
Lutaml::Xml::Schema::Xsd::ExtensionComplexContent,
|
|
30
|
+
Lutaml::Xml::Schema::Xsd::RestrictionComplexContent,
|
|
31
|
+
].freeze
|
|
32
|
+
COMPLEX_PARTICLE_KINDS = %i[sequence choice all group].freeze
|
|
33
|
+
|
|
25
34
|
# Rule category
|
|
26
35
|
#
|
|
27
36
|
# @return [Symbol] :structure
|
|
@@ -97,22 +106,21 @@ module Lutaml
|
|
|
97
106
|
end
|
|
98
107
|
end
|
|
99
108
|
|
|
100
|
-
# Ordered particles of a type: own sequence/choice/all/
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
109
|
+
# Ordered particles of a type: own sequence/choice/all/group
|
|
110
|
+
# particles plus, for extensions, the base type's particles
|
|
111
|
+
# first. Cycle-guarded. Particles are kept whole — nested
|
|
112
|
+
# containers are expanded by the walk's matching, which
|
|
104
113
|
# preserves their own occurrence constraints.
|
|
105
114
|
#
|
|
106
115
|
# @return [Array]
|
|
107
116
|
def type_particles(type, repository, seen)
|
|
108
117
|
return [] if type.nil? || seen.key?(type)
|
|
118
|
+
return [] unless COMPLEX_PARTICLE_TYPES.any? { |k| type.is_a?(k) }
|
|
109
119
|
|
|
110
120
|
seen[type] = true
|
|
111
121
|
particles = []
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
next unless type.respond_to?(kind)
|
|
115
|
-
|
|
123
|
+
COMPLEX_PARTICLE_KINDS.each do |kind|
|
|
116
124
|
Array(type.public_send(kind)).each do |particle|
|
|
117
125
|
next unless particle
|
|
118
126
|
|
|
@@ -127,7 +135,8 @@ module Lutaml
|
|
|
127
135
|
end
|
|
128
136
|
end
|
|
129
137
|
|
|
130
|
-
if type.
|
|
138
|
+
if type.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType) &&
|
|
139
|
+
type.complex_content
|
|
131
140
|
content = type.complex_content
|
|
132
141
|
extension = Array(content.extension).first
|
|
133
142
|
if extension
|
|
@@ -135,17 +144,15 @@ module Lutaml
|
|
|
135
144
|
base_type = base_local && repository&.resolve_named_type(base_local)
|
|
136
145
|
particles = type_particles(base_type, repository, seen) +
|
|
137
146
|
particles
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
else
|
|
148
|
-
particles << particle
|
|
147
|
+
COMPLEX_PARTICLE_KINDS.each do |kind|
|
|
148
|
+
Array(extension.public_send(kind)).each do |particle|
|
|
149
|
+
next unless particle
|
|
150
|
+
|
|
151
|
+
if particle.is_a?(Lutaml::Xsd::Sequence)
|
|
152
|
+
particles.concat(collect_particles(particle))
|
|
153
|
+
else
|
|
154
|
+
particles << particle
|
|
155
|
+
end
|
|
149
156
|
end
|
|
150
157
|
end
|
|
151
158
|
end
|
|
@@ -390,10 +397,8 @@ module Lutaml
|
|
|
390
397
|
def collect_particles(content_model)
|
|
391
398
|
# resolved_element_order preserves declaration order including
|
|
392
399
|
# xs:any particles (which child_elements drops).
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return content_model.resolved_element_order.to_a
|
|
396
|
-
end
|
|
400
|
+
ordered = content_model.resolved_element_order
|
|
401
|
+
return ordered.to_a if ordered.to_a.any?
|
|
397
402
|
|
|
398
403
|
particles = []
|
|
399
404
|
|
|
@@ -100,13 +100,33 @@ module Lutaml
|
|
|
100
100
|
def selector_xpaths(constraint)
|
|
101
101
|
return [] unless constraint.selector
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
selector = constraint.selector
|
|
104
|
+
if selector.is_a?(Lutaml::Xml::Schema::Xsd::Selector)
|
|
105
|
+
xpath_strings(selector.xpath)
|
|
106
|
+
else
|
|
107
|
+
xpath_strings(selector)
|
|
108
|
+
end
|
|
105
109
|
end
|
|
106
110
|
|
|
107
111
|
def field_xpaths(constraint)
|
|
108
|
-
Array(constraint.field).
|
|
109
|
-
field.
|
|
112
|
+
Array(constraint.field).flat_map do |field|
|
|
113
|
+
if field.is_a?(Lutaml::Xml::Schema::Xsd::Field)
|
|
114
|
+
xpath_strings(field.xpath)
|
|
115
|
+
else
|
|
116
|
+
xpath_strings(field)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Selector/field values are either the raw XPath string or the
|
|
122
|
+
# parsed xs:selector/xs:field model whose xpath attribute holds
|
|
123
|
+
# it; whitespace-separated lists yield one entry each.
|
|
124
|
+
def xpath_strings(value)
|
|
125
|
+
case value
|
|
126
|
+
when String
|
|
127
|
+
value.split(/\s+/).reject(&:empty?)
|
|
128
|
+
else
|
|
129
|
+
[]
|
|
110
130
|
end
|
|
111
131
|
end
|
|
112
132
|
|
|
@@ -114,7 +134,10 @@ module Lutaml
|
|
|
114
134
|
first = Array(node.xpath(field_xpath)).first
|
|
115
135
|
return nil unless first
|
|
116
136
|
|
|
117
|
-
|
|
137
|
+
case first
|
|
138
|
+
when Moxml::Attribute then first.value
|
|
139
|
+
else first.text
|
|
140
|
+
end
|
|
118
141
|
end
|
|
119
142
|
|
|
120
143
|
# Keyref `refer` as a string; nil when the model left it
|
|
@@ -15,7 +15,25 @@ module Lutaml
|
|
|
15
15
|
# Deliberately not resolved here (rule-engine backlog): dynamic
|
|
16
16
|
# typing via xsi:type and derivation closure.
|
|
17
17
|
class SchemaDefinitionResolver
|
|
18
|
-
|
|
18
|
+
# Nested particle accessors per container class — all are declared
|
|
19
|
+
# model attributes (lutaml-model 0.8.114+), so dispatch is by
|
|
20
|
+
# class rather than duck-typed probing. Sequence/Choice cannot
|
|
21
|
+
# nest xs:all, and only xs:all carries bare elements.
|
|
22
|
+
CONTAINER_NESTED_KINDS = {
|
|
23
|
+
Lutaml::Xml::Schema::Xsd::ComplexType => %i[sequence choice all],
|
|
24
|
+
Lutaml::Xml::Schema::Xsd::ExtensionComplexContent =>
|
|
25
|
+
%i[sequence choice all],
|
|
26
|
+
Lutaml::Xml::Schema::Xsd::RestrictionComplexContent =>
|
|
27
|
+
%i[sequence choice all],
|
|
28
|
+
Lutaml::Xml::Schema::Xsd::Sequence => %i[sequence choice],
|
|
29
|
+
Lutaml::Xml::Schema::Xsd::Choice => %i[sequence choice],
|
|
30
|
+
Lutaml::Xml::Schema::Xsd::All => [].freeze,
|
|
31
|
+
}.freeze
|
|
32
|
+
CONTAINER_LEAF_KINDS = {
|
|
33
|
+
Lutaml::Xml::Schema::Xsd::Sequence => %i[element any group],
|
|
34
|
+
Lutaml::Xml::Schema::Xsd::Choice => %i[element any group],
|
|
35
|
+
Lutaml::Xml::Schema::Xsd::All => %i[element].freeze,
|
|
36
|
+
}.freeze
|
|
19
37
|
|
|
20
38
|
def initialize(repository)
|
|
21
39
|
@repository = repository
|
|
@@ -126,21 +144,26 @@ module Lutaml
|
|
|
126
144
|
private
|
|
127
145
|
|
|
128
146
|
def particle_for_type(type)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
case type
|
|
148
|
+
when Lutaml::Xml::Schema::Xsd::ComplexType
|
|
149
|
+
direct = Array(type.sequence).first ||
|
|
150
|
+
Array(type.choice).first || Array(type.all).first
|
|
151
|
+
return direct if direct
|
|
152
|
+
|
|
153
|
+
# Derived types keep their particles under complex_content.
|
|
154
|
+
if type.complex_content
|
|
155
|
+
content = type.complex_content
|
|
156
|
+
part = Array(content.extension).first ||
|
|
157
|
+
Array(content.restriction).first
|
|
158
|
+
return particle_for_type(part) if part
|
|
159
|
+
end
|
|
142
160
|
|
|
143
|
-
|
|
161
|
+
nil
|
|
162
|
+
when Lutaml::Xml::Schema::Xsd::ExtensionComplexContent,
|
|
163
|
+
Lutaml::Xml::Schema::Xsd::RestrictionComplexContent
|
|
164
|
+
Array(type.sequence).first ||
|
|
165
|
+
Array(type.choice).first || Array(type.all).first
|
|
166
|
+
end
|
|
144
167
|
end
|
|
145
168
|
|
|
146
169
|
def schemas
|
|
@@ -216,25 +239,32 @@ module Lutaml
|
|
|
216
239
|
end
|
|
217
240
|
return if visited_particle?(node)
|
|
218
241
|
|
|
219
|
-
|
|
220
|
-
|
|
242
|
+
nested = CONTAINER_NESTED_KINDS[node.class]
|
|
243
|
+
return unless nested
|
|
221
244
|
|
|
245
|
+
nested.each do |kind|
|
|
222
246
|
Array(node.public_send(kind)).each do |particle|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
247
|
+
each_particle_element(particle, &) if particle
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
(CONTAINER_LEAF_KINDS[node.class] || []).each do |kind|
|
|
251
|
+
Array(node.public_send(kind)).each do |leaf|
|
|
252
|
+
# xs:any wildcards and xs:group references are not element
|
|
253
|
+
# declarations; the index holds Element models only.
|
|
254
|
+
yield leaf if leaf.is_a?(Lutaml::Xml::Schema::Xsd::Element)
|
|
231
255
|
end
|
|
232
256
|
end
|
|
233
257
|
|
|
234
|
-
|
|
258
|
+
walk_complex_content(node, &) if
|
|
259
|
+
node.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def walk_complex_content(node, &)
|
|
263
|
+
content = node.complex_content
|
|
264
|
+
return unless content
|
|
235
265
|
|
|
236
266
|
%i[extension restriction].each do |kind|
|
|
237
|
-
part =
|
|
267
|
+
part = content.public_send(kind)
|
|
238
268
|
each_particle_element(part, &) if part
|
|
239
269
|
end
|
|
240
270
|
end
|