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.
@@ -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.respond_to?(:complex_types) || schema.respond_to?(:complex_type)
578
+ return [] unless schema.is_a?(Lutaml::Xml::Schema::Xsd::Schema)
579
579
 
580
- types = schema.respond_to?(:complex_types) ? schema.complex_types : schema.complex_type
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.respond_to?(:element) && schema.element
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.respond_to?(:complex_type) && element.complex_type
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.respond_to?(:abstract) ? element.complex_type.abstract : false,
619
- mixed: element.complex_type.respond_to?(:mixed) ? element.complex_type.mixed : false,
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 [] if !model.respond_to?(:choice) || !model.choice
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 [] if !model.respond_to?(:sequence) || !model.sequence
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.respond_to?(:groups) || schema.respond_to?(:group)
883
+ return [] unless schema.groups
884
884
 
885
- grps = schema.respond_to?(:groups) ? schema.groups : schema.group
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.respond_to?(:ref) ? model.ref : nil,
1085
- name: model.respond_to?(:name) ? model.name : nil,
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 [] if !model.respond_to?(:group) || !model.group
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 model (handles extension nesting)
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
- # Used for both direct attribute groups and those inside content model extensions.
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
- if model.respond_to?(:attribute_group) && model.attribute_group && !model.attribute_group.empty?
1123
- groups = model.attribute_group.is_a?(Array) ? model.attribute_group : [model.attribute_group]
1124
- refs.concat(groups)
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.respond_to?(:element)
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.respond_to?(:any_attribute)
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/element/
101
- # any/group particles plus, for extensions, the base type's
102
- # particles first. Cycle-guarded. Particles are kept whole —
103
- # nested containers are expanded by the walk's matching, which
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
- %i[sequence choice all element any group].each do |kind|
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.respond_to?(:complex_content) && type.complex_content
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
- end
139
- %i[sequence choice all element any group].each do |kind|
140
- next unless extension.respond_to?(kind)
141
-
142
- Array(extension.public_send(kind)).each do |particle|
143
- next unless particle
144
-
145
- if particle.is_a?(Lutaml::Xsd::Sequence)
146
- particles.concat(collect_particles(particle))
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
- if content_model.respond_to?(:resolved_element_order) &&
394
- content_model.resolved_element_order.to_a.any?
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
- 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 }
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).map do |field|
109
- field.respond_to?(:xpath) ? field.xpath : field.to_s
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
- first.respond_to?(:value) && first.value ? first.value : first.text
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
- PARTICLE_KINDS = %i[sequence choice all].freeze
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
- return nil unless type.respond_to?(:sequence) || type.respond_to?(:choice) || type.respond_to?(:all)
130
-
131
- direct = Array(type.sequence).first ||
132
- Array(type.choice).first || Array(type.all).first
133
- return direct if direct
134
-
135
- # Derived types keep their particles under complex_content.
136
- if type.respond_to?(:complex_content) && type.complex_content
137
- content = type.complex_content
138
- part = Array(content.extension).first ||
139
- Array(content.restriction).first
140
- return particle_for_type(part) if part
141
- end
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
- nil
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
- PARTICLE_KINDS.each do |kind|
220
- next unless node.respond_to?(kind) # optional particle collections
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
- next unless particle
224
-
225
- Array(particle.element).each(&)
226
- PARTICLE_KINDS.each do |nested|
227
- next unless particle.respond_to?(nested)
228
-
229
- each_particle_element(particle.public_send(nested), &)
230
- end
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
- return unless node.respond_to?(:complex_content) && node.complex_content
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 = node.complex_content.public_send(kind)
267
+ part = content.public_send(kind)
238
268
  each_particle_element(part, &) if part
239
269
  end
240
270
  end