diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c34b8a993..1172a9db7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2026-04-30 07:27:20 UTC using RuboCop version 1.86.0. +# on 2026-05-01 09:59:35 UTC using RuboCop version 1.86.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -11,126 +11,150 @@ Gemspec/RequiredRubyVersion: Exclude: - 'lutaml-model.gemspec' -# Offense count: 20 +# Offense count: 15 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: with_first_argument, with_fixed_indentation Layout/ArgumentAlignment: Exclude: - - 'lib/lutaml/model/schema/schema_builder/nokogiri.rb' - - 'lib/lutaml/model/schema/schema_builder/oga.rb' - - 'lib/lutaml/xml/adapter/nokogiri_adapter.rb' - - 'lib/lutaml/xml/adapter/oga_adapter.rb' - - 'lib/lutaml/xml/adapter/ox_adapter.rb' - - 'lib/lutaml/xml/adapter_element.rb' - - 'lib/lutaml/xml/data_model.rb' - - 'lib/lutaml/xml/schema/builder/nokogiri.rb' - - 'lib/lutaml/xml/schema/builder/oga.rb' - - 'lib/lutaml/xml/schema/relaxng_schema.rb' - - 'lib/lutaml/xml/transformation.rb' - - 'spec/lutaml/model/processing_instruction_spec.rb' + - 'lib/lutaml/model/configuration.rb' + - 'lib/lutaml/model/instrumentation.rb' + - 'lib/lutaml/xml/schema/xsd/schema.rb' + - 'lib/lutaml/xml/schema/xsd/schema_path.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/complex_type_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/containers_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: IndentationWidth. -Layout/AssignmentIndentation: +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: with_first_element, with_fixed_indentation +Layout/ArrayAlignment: Exclude: - - 'lib/lutaml/xml/adapter/ox_adapter.rb' + - 'lib/lutaml/xml/schema/xsd/base.rb' -# Offense count: 2 +# Offense count: 40 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleAlignWith. # SupportedStylesAlignWith: either, start_of_block, start_of_line Layout/BlockAlignment: Exclude: - - 'lib/lutaml/xml/adapter_element.rb' - - 'lib/lutaml/xml/transformation.rb' + - 'lib/lutaml/xml/schema/xsd/complex_type.rb' + - 'lib/lutaml/xml/schema/xsd/element.rb' + - 'lib/lutaml/xml/schema/xsd/schema.rb' + - 'spec/lutaml/key_value/transformation_spec.rb' + - 'spec/lutaml/model/cached_type_resolver_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/attribute_and_group_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/complex_type_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/containers_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb' -# Offense count: 2 +# Offense count: 32 # This cop supports safe autocorrection (--autocorrect). Layout/BlockEndNewline: Exclude: - - 'lib/lutaml/xml/adapter_element.rb' - - 'lib/lutaml/xml/transformation.rb' + - 'lib/lutaml/xml/schema/xsd/complex_type.rb' + - 'lib/lutaml/xml/schema/xsd/element.rb' + - 'lib/lutaml/xml/schema/xsd/schema.rb' + - 'spec/lutaml/key_value/transformation_spec.rb' + - 'spec/lutaml/model/cached_type_resolver_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/attribute_and_group_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/complex_type_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/containers_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb' -# Offense count: 1 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Layout/ClosingParenthesisIndentation: Exclude: - - 'lib/lutaml/xml/adapter/ox_adapter.rb' + - 'spec/lutaml/model/cached_type_resolver_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). -Layout/EmptyLineAfterGuardClause: +Layout/ElseAlignment: Exclude: - - 'lib/lutaml/xml/transformation.rb' + - 'lib/lutaml/xml/schema/xsd/schema_path.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleAlignWith. +# SupportedStylesAlignWith: keyword, variable, start_of_line +Layout/EndAlignment: + Exclude: + - 'lib/lutaml/xml/schema/xsd/schema_path.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses Layout/FirstArgumentIndentation: Exclude: - - 'lib/lutaml/xml/adapter/ox_adapter.rb' - -# Offense count: 11 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. -# SupportedHashRocketStyles: key, separator, table -# SupportedColonStyles: key, separator, table -# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit -Layout/HashAlignment: - Exclude: - - 'lib/lutaml/model/schema/schema_builder/nokogiri.rb' - - 'lib/lutaml/model/schema/schema_builder/oga.rb' - - 'lib/lutaml/xml/adapter_element.rb' - - 'lib/lutaml/xml/schema/builder/nokogiri.rb' - - 'lib/lutaml/xml/schema/builder/oga.rb' - - 'lib/lutaml/xml/schema/relaxng_schema.rb' + - 'spec/lutaml/model/cached_type_resolver_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb' -# Offense count: 4 +# Offense count: 58 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns. # SupportedStylesAlignWith: start_of_line, relative_to_receiver Layout/IndentationWidth: Exclude: - - 'lib/lutaml/xml/adapter_element.rb' - - 'lib/lutaml/xml/transformation.rb' + - 'lib/lutaml/xml/schema/xsd/complex_type.rb' + - 'lib/lutaml/xml/schema/xsd/element.rb' + - 'lib/lutaml/xml/schema/xsd/schema.rb' + - 'lib/lutaml/xml/schema/xsd/schema_path.rb' + - 'spec/lutaml/key_value/transformation_spec.rb' + - 'spec/lutaml/model/cached_type_resolver_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/attribute_and_group_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/complex_type_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/containers_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb' -# Offense count: 3191 +# Offense count: 3236 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https Layout/LineLength: Enabled: false -# Offense count: 1 +# Offense count: 8 +# This cop supports safe autocorrection (--autocorrect). +Layout/MultilineBlockLayout: + Exclude: + - 'spec/lutaml/xml/schema/xsd/liquid_methods/attribute_and_group_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/complex_type_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/containers_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb' + +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: symmetrical, new_line, same_line Layout/MultilineMethodCallBraceLayout: Exclude: - - 'lib/lutaml/xml/adapter/ox_adapter.rb' + - 'spec/lutaml/model/cached_type_resolver_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb' -# Offense count: 17 +# Offense count: 24 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowInHeredoc. Layout/TrailingWhitespace: Exclude: - - 'lib/lutaml/model/schema/schema_builder/nokogiri.rb' - - 'lib/lutaml/model/schema/schema_builder/oga.rb' - - 'lib/lutaml/xml/adapter/nokogiri_adapter.rb' - - 'lib/lutaml/xml/adapter/oga_adapter.rb' - - 'lib/lutaml/xml/adapter/ox_adapter.rb' - - 'lib/lutaml/xml/adapter_element.rb' - - 'lib/lutaml/xml/data_model.rb' - - 'lib/lutaml/xml/schema/builder/nokogiri.rb' - - 'lib/lutaml/xml/schema/builder/oga.rb' - - 'lib/lutaml/xml/schema/relaxng_schema.rb' - - 'lib/lutaml/xml/transformation.rb' - - 'spec/lutaml/model/processing_instruction_spec.rb' + - 'lib/lutaml/model/configuration.rb' + - 'lib/lutaml/model/instrumentation.rb' + - 'lib/lutaml/xml/schema/xsd/base.rb' + - 'lib/lutaml/xml/schema/xsd/complex_type.rb' + - 'lib/lutaml/xml/schema/xsd/schema.rb' + - 'lib/lutaml/xml/schema/xsd/schema_path.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/attribute_and_group_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/complex_type_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/containers_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb' -# Offense count: 20 +# Offense count: 21 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: @@ -221,18 +245,18 @@ Lint/UnusedMethodArgument: - 'lib/lutaml/xml/unqualified_inheritance_strategy.rb' - 'spec/support/xml/xsd/code_example_validator.rb' -# Offense count: 2 +# Offense count: 3 Lint/UselessConstantScoping: Exclude: - 'lib/lutaml/xml/adapter/nokogiri_adapter.rb' - 'lib/lutaml/xml/mapping_rule.rb' -# Offense count: 329 +# Offense count: 336 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max. Metrics/AbcSize: Enabled: false -# Offense count: 55 +# Offense count: 54 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. # AllowedMethods: refine Metrics/BlockLength: @@ -243,12 +267,12 @@ Metrics/BlockLength: Metrics/BlockNesting: Max: 6 -# Offense count: 287 +# Offense count: 297 # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/CyclomaticComplexity: Enabled: false -# Offense count: 534 +# Offense count: 543 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Max: 514 @@ -259,7 +283,7 @@ Metrics/ParameterLists: Max: 24 MaxOptionalParameters: 5 -# Offense count: 244 +# Offense count: 251 # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/PerceivedComplexity: Enabled: false @@ -346,13 +370,13 @@ RSpec/BeforeAfterAll: - 'spec/lutaml/xml/fpi_namespace_spec.rb' - 'spec/lutaml/xml/namespace_spec.rb' -# Offense count: 99 +# Offense count: 100 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: Enabled: false -# Offense count: 76 +# Offense count: 79 # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: Enabled: false @@ -363,7 +387,7 @@ RSpec/DescribeMethod: - 'spec/lutaml/xml/schema/xsd/schema_helper_methods_spec.rb' - 'spec/lutaml/xml/serializable_namespace_spec.rb' -# Offense count: 1127 +# Offense count: 1155 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 68 @@ -407,7 +431,7 @@ RSpec/InstanceVariable: - 'spec/lutaml/xml/mapping_finalization_guard_spec.rb' - 'spec/lutaml/xml/namespace_aware_parsing_spec.rb' -# Offense count: 24 +# Offense count: 25 RSpec/LeakyConstantDeclaration: Exclude: - 'spec/lutaml/key_value/transformation_spec.rb' @@ -438,11 +462,11 @@ RSpec/MultipleDescribes: - 'spec/lutaml/xml/namespace_resolution_strategy_spec.rb' - 'spec/lutaml/xml/xml_space_type_spec.rb' -# Offense count: 1267 +# Offense count: 1300 RSpec/MultipleExpectations: Max: 21 -# Offense count: 187 +# Offense count: 189 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: Max: 15 @@ -486,7 +510,7 @@ RSpec/RepeatedExampleGroupDescription: Exclude: - 'spec/lutaml/model/mixed_content_spec.rb' -# Offense count: 28 +# Offense count: 32 # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector. # SupportedInflectors: default, active_support RSpec/SpecFilePathFormat: @@ -510,7 +534,7 @@ Security/MarshalLoad: Exclude: - 'scripts-xmi-profile/profile_xmi.rb' -# Offense count: 2 +# Offense count: 46 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces @@ -519,8 +543,16 @@ Security/MarshalLoad: # AllowedMethods: lambda, proc, it Style/BlockDelimiters: Exclude: - - 'lib/lutaml/xml/adapter_element.rb' - - 'lib/lutaml/xml/transformation.rb' + - 'lib/lutaml/xml/schema/xsd/complex_type.rb' + - 'lib/lutaml/xml/schema/xsd/element.rb' + - 'lib/lutaml/xml/schema/xsd/schema.rb' + - 'spec/lutaml/key_value/transformation_spec.rb' + - 'spec/lutaml/model/cached_type_resolver_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/attribute_and_group_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/complex_type_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/containers_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb' + - 'spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -571,11 +603,17 @@ Style/MixinUsage: - 'bench/bench_uniword.rb' - 'bench/bench_xmi.rb' -# Offense count: 2 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/MultilineIfModifier: Exclude: - - 'lib/lutaml/xml/adapter/ox_adapter.rb' + - 'lib/lutaml/xml/schema/xsd/schema_path.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/MultilineTernaryOperator: + Exclude: + - 'lib/lutaml/xml/schema/xsd/schema_path.rb' # Offense count: 12 # Configuration parameters: AllowedClasses. diff --git a/README.adoc b/README.adoc index 12faa27eb..ece1b14d8 100644 --- a/README.adoc +++ b/README.adoc @@ -16473,6 +16473,124 @@ The XSD parser supports the following XSD constructs: | - |=== +[[xsd-liquid-helpers]] +=== Liquid template helpers + +Parsed XSD objects expose helper methods designed for use with Liquid +templates (see <>). These helpers resolve +cross-references, flatten nested content models, and provide sorted +access to schema components. + +[cols="1,4"] +|=== +| Method | Description + +| `Schema#elements_sorted_by_name` +| Global elements sorted alphabetically by name. + +| `Schema#complex_types_sorted_by_name` +| Complex type definitions sorted by name. + +| `Schema#attribute_groups_sorted_by_name` +| Attribute group definitions sorted by name. + +| `Schema#simple_types_sorted_by_name` +| Simple type definitions sorted by name. + +| `Schema#attributes_sorted_by_name` +| Global attribute declarations sorted by name. + +| `Element#used_by` +| Complex types whose content model references this element. + +| `Element#attributes` +| Attributes from the element's referenced complex type. + +| `Element#child_elements` +| Child elements from the element's referenced complex type. + +| `Element#referenced_type` +| Resolved type name after following `ref`. + +| `Element#referenced_name` +| Effective element name after resolving `ref`. + +| `Element#referenced_object` +| The resolved XSD object behind a `ref`. + +| `Element#referenced_complex_type` +| Complex type referenced by the element's `type` attribute. + +| `ComplexType#used_by` +| Elements (root-level and nested) that reference this complex type. + +| `ComplexType#attribute_elements` +| Flattened attributes including those from attribute groups and extensions. + +| `ComplexType#child_elements` +| All nested element declarations from sequences, choices, and groups. + +| `ComplexType#direct_child_elements` +| Direct child elements, excluding attributes, attribute groups, and annotations. + +| `Attribute#cardinality` +| `"1"` for required, `"0..1"` for optional. + +| `Attribute#referenced_type` +| Resolved type name after following `ref`. + +| `Attribute#referenced_name` +| Effective attribute name after resolving `ref`. + +| `AttributeGroup#used_by` +| Complex types that reference this attribute group. + +| `AttributeGroup#attribute_elements` +| Flattened list of attributes from the group and nested groups. + +| `Sequence#child_elements` +| Recursively collected child elements. + +| `Choice#child_elements` +| Recursively collected child elements. + +| `Group#child_elements` +| Child elements from the resolved group definition. + +| `SimpleContent#attribute_elements` +| Attributes inherited from the base type plus extension attributes. + +| `SimpleContent#base_type` +| Base type resolved from inline, extension, or restriction declarations. +|=== + +.Base-level helpers available on all XSD model objects +[cols="1,4"] +|=== +| Method | Description + +| `resolved_element_order` +| Elements in document order, with references resolved. + +| `unresolvable_items` +| Elements whose `ref` cannot be resolved (broken cross-references). + +| `any?`, `all?`, `choice?`, `element?`, `sequence?`, `attribute?`, `annotation?` +| Content model type predicates. + +| `simple_content?`, `complex_content?`, `attribute_group?` +| Content model type predicates. + +| `min_occurrences`, `max_occurrences` +| Occurrence bounds as integers. + +| `target_prefix` +| Namespace prefix of the containing schema. + +| `to_formatted_xml` +| Pretty-printed XML output. +|=== + [[xsd-limitations]] === Limitations diff --git a/docs/_references/xsd-schema-parsing.adoc b/docs/_references/xsd-schema-parsing.adoc index eb53a5ae6..981aba480 100644 --- a/docs/_references/xsd-schema-parsing.adoc +++ b/docs/_references/xsd-schema-parsing.adoc @@ -932,6 +932,124 @@ The validator performs the following checks: * Version compatibility (XSD 1.0 vs 1.1 features) +== Liquid template helpers + +Parsed XSD objects expose helper methods designed for use with Liquid +templates (see link:../_guides/liquid-templates.adoc[Liquid Templates]). +These helpers resolve cross-references, flatten nested content models, +and provide sorted access to schema components. + +[cols="1,4"] +|=== +| Method | Description + +| `Schema#elements_sorted_by_name` +| Global elements sorted alphabetically by name. + +| `Schema#complex_types_sorted_by_name` +| Complex type definitions sorted by name. + +| `Schema#attribute_groups_sorted_by_name` +| Attribute group definitions sorted by name. + +| `Schema#simple_types_sorted_by_name` +| Simple type definitions sorted by name. + +| `Schema#attributes_sorted_by_name` +| Global attribute declarations sorted by name. + +| `Element#used_by` +| Complex types whose content model references this element. + +| `Element#attributes` +| Attributes from the element's referenced complex type. + +| `Element#child_elements` +| Child elements from the element's referenced complex type. + +| `Element#referenced_type` +| Resolved type name after following `ref`. + +| `Element#referenced_name` +| Effective element name after resolving `ref`. + +| `Element#referenced_object` +| The resolved XSD object behind a `ref`. + +| `Element#referenced_complex_type` +| Complex type referenced by the element's `type` attribute. + +| `ComplexType#used_by` +| Elements (root-level and nested) that reference this complex type. + +| `ComplexType#attribute_elements` +| Flattened attributes including those from attribute groups and extensions. + +| `ComplexType#child_elements` +| All nested element declarations from sequences, choices, and groups. + +| `ComplexType#direct_child_elements` +| Direct child elements, excluding attributes, attribute groups, and annotations. + +| `Attribute#cardinality` +| `"1"` for required, `"0..1"` for optional. + +| `Attribute#referenced_type` +| Resolved type name after following `ref`. + +| `Attribute#referenced_name` +| Effective attribute name after resolving `ref`. + +| `AttributeGroup#used_by` +| Complex types that reference this attribute group. + +| `AttributeGroup#attribute_elements` +| Flattened list of attributes from the group and nested groups. + +| `Sequence#child_elements` +| Recursively collected child elements. + +| `Choice#child_elements` +| Recursively collected child elements. + +| `Group#child_elements` +| Child elements from the resolved group definition. + +| `SimpleContent#attribute_elements` +| Attributes inherited from the base type plus extension attributes. + +| `SimpleContent#base_type` +| Base type resolved from inline, extension, or restriction declarations. +|=== + +.Base-level helpers available on all XSD model objects +[cols="1,4"] +|=== +| Method | Description + +| `resolved_element_order` +| Elements in document order, with references resolved. + +| `unresolvable_items` +| Elements whose `ref` cannot be resolved (broken cross-references). + +| `any?`, `all?`, `choice?`, `element?`, `sequence?`, `attribute?`, `annotation?` +| Content model type predicates. + +| `simple_content?`, `complex_content?`, `attribute_group?` +| Content model type predicates. + +| `min_occurrences`, `max_occurrences` +| Occurrence bounds as integers. + +| `target_prefix` +| Namespace prefix of the containing schema. + +| `to_formatted_xml` +| Pretty-printed XML output. +|=== + + == Limitations The XSD Schema Parsing feature has the following limitations: diff --git a/lib/lutaml/model/configuration.rb b/lib/lutaml/model/configuration.rb index f9f64669e..dcdfb71db 100644 --- a/lib/lutaml/model/configuration.rb +++ b/lib/lutaml/model/configuration.rb @@ -241,7 +241,8 @@ def load_adapter_file(adapter, type) adapter_path = if Lutaml::Model::RuntimeCompatibility.opal? "lutaml/key_value/adapter/#{adapter}/#{type}" else - File.join(__dir__, "../key_value/adapter", adapter, type) + File.join(__dir__, "../key_value/adapter", adapter, + type) end require adapter_path rescue LoadError diff --git a/lib/lutaml/model/instrumentation.rb b/lib/lutaml/model/instrumentation.rb index 40c5cddf1..7875b45eb 100644 --- a/lib/lutaml/model/instrumentation.rb +++ b/lib/lutaml/model/instrumentation.rb @@ -168,7 +168,8 @@ def memory_usage return nil unless defined?(GC) GC.start if GC.respond_to?(:compact) - IO.popen(["ps", "-o", "rss=", "-p", Process.pid.to_s], &:read).to_i * 1024 + IO.popen(["ps", "-o", "rss=", "-p", Process.pid.to_s], + &:read).to_i * 1024 rescue StandardError nil end diff --git a/lib/lutaml/model/schema/schema_builder/nokogiri.rb b/lib/lutaml/model/schema/schema_builder/nokogiri.rb index c8a834394..4b0165a4e 100644 --- a/lib/lutaml/model/schema/schema_builder/nokogiri.rb +++ b/lib/lutaml/model/schema/schema_builder/nokogiri.rb @@ -1,29 +1,38 @@ # frozen_string_literal: true -require "moxml" +require "nokogiri" module Lutaml module Model module Schema class SchemaBuilder - # Moxml-based adapter for XSD schema generation (Nokogiri backend) - # Uses Moxml::Builder's method_missing DSL for element creation. + # Nokogiri adapter for XSD schema generation + # Wraps Nokogiri::XML::Builder to provide schema building capabilities class Nokogiri attr_reader :builder def initialize(options = {}, &block) - @encoding = options[:encoding] || "UTF-8" - @builder = Moxml::Builder.new(Moxml.new) - - block&.call(@builder) + encoding = options[:encoding] || "UTF-8" + @builder = if block + ::Nokogiri::XML::Builder.new(encoding: encoding, +&block) + else + ::Nokogiri::XML::Builder.new(encoding: encoding) + end end # Generate the XSD schema XML string + # @param options [Hash] formatting options + # @option options [Boolean] :pretty Pretty print with indentation + # @option options [Integer] :indent Number of spaces for indentation (default: 2) + # @return [String] XSD XML string def to_xml(options = {}) - indent = options[:pretty] ? (options[:indent] || 2) : 0 - decl = "\n" - "#{decl}#{@builder.document.root.to_xml(declaration: false, - indent: indent, expand_empty: false)}\n" + if options[:pretty] + indent = options[:indent] || 2 + builder.to_xml(indent: indent) + else + builder.to_xml + end end end end diff --git a/lib/lutaml/model/schema/schema_builder/oga.rb b/lib/lutaml/model/schema/schema_builder/oga.rb index 2c5c01417..1a9accf10 100644 --- a/lib/lutaml/model/schema/schema_builder/oga.rb +++ b/lib/lutaml/model/schema/schema_builder/oga.rb @@ -1,31 +1,36 @@ # frozen_string_literal: true -require "moxml" +require "oga" module Lutaml module Model module Schema class SchemaBuilder - # Moxml-based adapter for XSD schema generation (Oga backend) - # Uses Moxml::Builder's method_missing DSL for element creation. + # Oga adapter for XSD schema generation + # Wraps Oga XML building to provide schema building capabilities class Oga - attr_reader :builder + attr_reader :builder, :document - def initialize(options = {}, &block) + def initialize(options = {}) @encoding = options[:encoding] || "UTF-8" - @builder = Moxml::Builder.new(Moxml.new) + @document = ::Oga::XML::Document.new + @builder = OgaBuilderWrapper.new(@document, @encoding) - block&.call(@builder) + # Execute the block if provided + yield(@builder) if block_given? end # Generate the XSD schema XML string + # @param options [Hash] formatting options + # @option options [Boolean] :pretty Pretty print with indentation (note: Oga doesn't support pretty printing directly) + # @return [String] XSD XML string def to_xml(_options = {}) - xml = @builder.document.root.to_xml(declaration: false, - expand_empty: false) + xml = @document.to_xml + # Add XML declaration with encoding "\n#{xml}" end - # Forward all other methods to the builder + # Forward all other methods to the builder wrapper def method_missing(method_name, ...) @builder.public_send(method_name, ...) end @@ -33,6 +38,53 @@ def method_missing(method_name, ...) def respond_to_missing?(method_name, include_private = false) @builder.respond_to?(method_name, include_private) || super end + + # Wrapper that provides a Nokogiri-like API for Oga + class OgaBuilderWrapper + def initialize(document, encoding) + @document = document + @encoding = encoding + @current_element = nil + end + + # Create a method that acts like Nokogiri's builder DSL + # e.g., xml.schema { } or xml.element { } + def method_missing(method_name, *args) + attributes = args.first || {} + + # Create the element + element = ::Oga::XML::Element.new(name: method_name.to_s) + + # Add attributes + attributes.each do |name, value| + element.attributes << ::Oga::XML::Attribute.new( + name: name.to_s, + value: value.to_s, + ) + end + + # Add to document or current element + if @current_element + @current_element.children << element + else + @document.children << element + end + + # If block given, process nested elements + if block_given? + parent = @current_element + @current_element = element + yield + @current_element = parent + end + + element + end + + def respond_to_missing?(_method_name, _include_private = false) + true # Accept any method for element creation + end + end end end end diff --git a/lib/lutaml/model/schema/xml_compiler.rb b/lib/lutaml/model/schema/xml_compiler.rb index 10e9262e7..fc4083b47 100644 --- a/lib/lutaml/model/schema/xml_compiler.rb +++ b/lib/lutaml/model/schema/xml_compiler.rb @@ -57,6 +57,7 @@ module XmlCompiler "space" => "Lutaml::Xml::W3c::XmlSpaceType", "base" => "Lutaml::Xml::W3c::XmlBaseType", }.freeze + XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace" def to_models(schema, options = {}) as_models(schema, options: options) @@ -141,8 +142,6 @@ def create_file(name, content, dir) end def require_classes(classes_hash) - return if Lutaml::Model::RuntimeCompatibility.opal? - Dir.mktmpdir do |dir| # Create subdirectory for class files (matches autoload path in registry) module_subdir = "generatedmodels" @@ -223,6 +222,8 @@ def schema_to_models(schemas) when Lutaml::Xml::Schema::Xsd::Element @elements[item_name] = setup_element(order_item) when Lutaml::Xml::Schema::Xsd::Attribute + next if xml_defined_attribute?(schema, item_name) + @attributes[item_name] = setup_attribute(order_item) when Lutaml::Xml::Schema::Xsd::AttributeGroup @attribute_groups[item_name] = @@ -570,6 +571,11 @@ def collect_namespaces(schemas, options) @namespace_classes[ns_class.class_name] = ns_class end end + + def xml_defined_attribute?(schema, item_name) + schema.target_namespace == XML_NAMESPACE_URI && + XML_DEFINED_ATTRIBUTES.key?(item_name) + end end end end diff --git a/lib/lutaml/xml/schema.rb b/lib/lutaml/xml/schema.rb index a0709867c..ecd979682 100644 --- a/lib/lutaml/xml/schema.rb +++ b/lib/lutaml/xml/schema.rb @@ -3,14 +3,11 @@ module Lutaml module Xml module Schema - Lutaml::Model::RuntimeCompatibility.autoload_native( - self, - Xsd: "#{__dir__}/schema/xsd", - XsdSchema: "#{__dir__}/schema/xsd_schema", - RelaxngSchema: "#{__dir__}/schema/relaxng_schema", - Builder: "#{__dir__}/schema/builder", - BuiltinTypes: "#{__dir__}/schema/builtin_types", - ) + autoload :Xsd, "#{__dir__}/schema/xsd" + autoload :XsdSchema, "#{__dir__}/schema/xsd_schema" + autoload :RelaxngSchema, "#{__dir__}/schema/relaxng_schema" + autoload :Builder, "#{__dir__}/schema/builder" + autoload :BuiltinTypes, "#{__dir__}/schema/builtin_types" end end end diff --git a/lib/lutaml/xml/schema/builder.rb b/lib/lutaml/xml/schema/builder.rb index 7aeab18f7..f220a3e14 100644 --- a/lib/lutaml/xml/schema/builder.rb +++ b/lib/lutaml/xml/schema/builder.rb @@ -4,14 +4,15 @@ module Lutaml module Xml module Schema # SchemaBuilder provides an adapter-agnostic interface for XSD schema generation - # Uses moxml for all XML construction, with adapter-specific backends for - # Nokogiri and Oga. When the configured XML adapter is Ox or REXML, we - # default to Nokogiri for schema generation. + # It wraps XML builders (Nokogiri, Oga) to generate XSD documents + # + # NOTE: Schema generation is separate from XML parsing. While the XML parsing + # adapters (Nokogiri, Oga, Ox, REXML) handle reading/writing XML documents, + # schema generation requires an XML builder API which is only implemented for + # Nokogiri and Oga. When the configured XML adapter is Ox or REXML, we use + # Nokogiri for schema generation since it has the most complete builder API. class Builder - Lutaml::Model::RuntimeCompatibility.autoload_native( - self, - Nokogiri: "#{__dir__}/builder/nokogiri", - ) + autoload :Nokogiri, "#{__dir__}/builder/nokogiri" autoload :Oga, "#{__dir__}/builder/oga" attr_reader :builder, :adapter_type diff --git a/lib/lutaml/xml/schema/builder/nokogiri.rb b/lib/lutaml/xml/schema/builder/nokogiri.rb index baa123107..3cc84c468 100644 --- a/lib/lutaml/xml/schema/builder/nokogiri.rb +++ b/lib/lutaml/xml/schema/builder/nokogiri.rb @@ -1,22 +1,24 @@ # frozen_string_literal: true -require "moxml" +require "nokogiri" module Lutaml module Xml module Schema class Builder - # Moxml-based adapter for XSD schema generation (Nokogiri backend) - # Uses moxml's document/element API with a method_missing DSL - # that mirrors Nokogiri::XML::Builder's interface. + # Nokogiri adapter for XSD schema generation + # Wraps Nokogiri::XML::Builder to provide schema building capabilities class Nokogiri attr_reader :builder def initialize(options = {}, &block) - @encoding = options[:encoding] || "UTF-8" - @builder = Moxml::Builder.new(Moxml.new) - - block&.call(@builder) + encoding = options[:encoding] || "UTF-8" + @builder = if block + ::Nokogiri::XML::Builder.new(encoding: encoding, +&block) + else + ::Nokogiri::XML::Builder.new(encoding: encoding) + end end # Generate the XSD schema XML string @@ -25,10 +27,12 @@ def initialize(options = {}, &block) # @option options [Integer] :indent Number of spaces for indentation (default: 2) # @return [String] XSD XML string def to_xml(options = {}) - indent = options[:pretty] ? (options[:indent] || 2) : 0 - decl = "\n" - "#{decl}#{@builder.document.root.to_xml(declaration: false, - indent: indent, expand_empty: false)}\n" + if options[:pretty] + indent = options[:indent] || 2 + @builder.to_xml(indent: indent) + else + @builder.to_xml + end end end end diff --git a/lib/lutaml/xml/schema/builder/oga.rb b/lib/lutaml/xml/schema/builder/oga.rb index 863b21625..d9c1ce1f5 100644 --- a/lib/lutaml/xml/schema/builder/oga.rb +++ b/lib/lutaml/xml/schema/builder/oga.rb @@ -1,29 +1,36 @@ # frozen_string_literal: true -require "moxml" +require "oga" module Lutaml module Xml module Schema class Builder - # Moxml-based adapter for XSD schema generation (Oga backend) - # Uses Moxml::Builder's method_missing DSL for element creation. + # Oga adapter for XSD schema generation + # Wraps Oga XML building to provide schema building capabilities class Oga - def initialize(options = {}, &block) + attr_reader :document + + def initialize(options = {}) @encoding = options[:encoding] || "UTF-8" - @builder = Moxml::Builder.new(Moxml.new) + @document = ::Oga::XML::Document.new + @builder = OgaBuilderWrapper.new(@document, @encoding) - block&.call(@builder) + # Execute the block if provided + yield(@builder) if block_given? end # Generate the XSD schema XML string + # @param options [Hash] formatting options + # @option options [Boolean] :pretty Pretty print with indentation + # @return [String] XSD XML string def to_xml(_options = {}) - xml = @builder.document.root.to_xml(declaration: false, - expand_empty: false) + xml = @document.to_xml + # Add XML declaration with encoding "\n#{xml}" end - # Forward all other methods to the builder + # Forward all other methods to the builder wrapper def method_missing(method_name, ...) @builder.public_send(method_name, ...) end @@ -31,6 +38,53 @@ def method_missing(method_name, ...) def respond_to_missing?(method_name, include_private = false) @builder.respond_to?(method_name, include_private) || super end + + # Wrapper that provides a Nokogiri-like API for Oga + class OgaBuilderWrapper + def initialize(document, encoding) + @document = document + @encoding = encoding + @current_element = nil + end + + # Create a method that acts like Nokogiri's builder DSL + # e.g., xml.schema { } or xml.element { } + def method_missing(method_name, *args) + attributes = args.first || {} + + # Create the element + element = ::Oga::XML::Element.new(name: method_name.to_s) + + # Add attributes + attributes.each do |name, value| + element.attributes << ::Oga::XML::Attribute.new( + name: name.to_s, + value: value.to_s, + ) + end + + # Add to document or current element + if @current_element + @current_element.children << element + else + @document.children << element + end + + # If block given, process nested elements + if block_given? + parent = @current_element + @current_element = element + yield + @current_element = parent + end + + element + end + + def respond_to_missing?(_method_name, _include_private = false) + true # Accept any method for element creation + end + end end end end diff --git a/lib/lutaml/xml/schema/relaxng_schema.rb b/lib/lutaml/xml/schema/relaxng_schema.rb index 0c8e5031e..2ea1a32ae 100644 --- a/lib/lutaml/xml/schema/relaxng_schema.rb +++ b/lib/lutaml/xml/schema/relaxng_schema.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "moxml" +require "nokogiri" module Lutaml module Xml @@ -13,17 +13,14 @@ class RelaxngSchema def self.generate(klass, options = {}) register = extract_register_from(klass) - xml = Moxml::Builder.new(Moxml.new) - - xml.grammar(xmlns: "http://relaxng.org/ns/structure/1.0") do - generate_start(xml, klass) - generate_define(xml, klass, register) + builder = ::Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml| + xml.grammar(xmlns: "http://relaxng.org/ns/structure/1.0") do + generate_start(xml, klass) + generate_define(xml, klass, register) + end end - indent = options[:pretty] ? 2 : 0 - decl = "\n" - "#{decl}#{xml.document.root.to_xml(declaration: false, - indent: indent, expand_empty: false)}\n" + options[:pretty] ? builder.to_xml(indent: 2) : builder.to_xml end def self.generate_start(xml, klass) diff --git a/lib/lutaml/xml/schema/xsd.rb b/lib/lutaml/xml/schema/xsd.rb index ee51da78a..87f45b522 100644 --- a/lib/lutaml/xml/schema/xsd.rb +++ b/lib/lutaml/xml/schema/xsd.rb @@ -5,8 +5,8 @@ # dependency since lutaml/xml.rb requires lutaml/model. The adapter type # is already set by lutaml/xml.rb at the end. -adapter = Lutaml::Model::RuntimeCompatibility.opal? ? :oga : :nokogiri -Lutaml::Model::Config.xml_adapter_type ||= adapter +adapter = RUBY_ENGINE == "opal" ? :oga : :nokogiri +Lutaml::Model::Config.xml_adapter_type = adapter unless defined?(Lutaml::Model::Config.xml_adapter_type) # Require the XsdNamespace class for XSD schema support require_relative "xsd_namespace" @@ -33,9 +33,9 @@ module Xsd Glob Group Import Include Key Keyref Length List MaxExclusive MaxInclusive MaxLength MinExclusive MinInclusive MinLength Notation Pattern Redefine RestrictionComplexContent - RestrictionSimpleContent RestrictionSimpleType Schema Selector - Sequence SimpleContent SimpleType TotalDigits Union Unique WhiteSpace - SchemaFileValidationResults + RestrictionSimpleContent RestrictionSimpleType Schema SchemaPath + Selector Sequence SimpleContent SimpleType TotalDigits Union Unique + WhiteSpace SchemaFileValidationResults ].freeze # Autoload all XSD model files (lazy loading) @@ -75,6 +75,7 @@ module Xsd autoload :Field, "#{__dir__}/xsd/field" autoload :FractionDigits, "#{__dir__}/xsd/fraction_digits" autoload :Glob, "#{__dir__}/xsd/glob" + autoload :SchemaPath, "#{__dir__}/xsd/schema_path" autoload :Group, "#{__dir__}/xsd/group" autoload :Import, "#{__dir__}/xsd/import" autoload :Include, "#{__dir__}/xsd/include" @@ -121,12 +122,13 @@ def register_model(klass, id) register.register_model(klass, id: id) # Also register in default context so XSD types can be resolved - # during XML parsing + # during XML parsing. default_ctx = Lutaml::Model::GlobalContext.default_context unless default_ctx.registry.registered?(id) default_ctx.registry.register(id, klass) end - # Also register by class name for string resolution + + # Also register by class name for string resolution. klass_name = klass.to_s unless default_ctx.registry.registered?(klass_name.to_sym) default_ctx.registry.register(klass_name.to_sym, klass) @@ -150,14 +152,12 @@ def parse(xsd, location: nil, nested_schema: false, register: nil, end register ||= self.register - # Accumulate schemas across parse() calls. When parsing multiple - # entrypoints (e.g., urbanFunction.xsd, urbanObject.xsd), each may - # import shared schemas. The schema_by_location_or_instance method - # checks processed_schemas first, so duplicates are avoided by reuse. - Glob.schema_mappings = schema_mappings - Glob.path_or_url(location) - Schema.from_xml(xsd, register: register) + Schema.reset_processed_schemas unless nested_schema + + Glob.with_location(location) do + Schema.from_xml(xsd, register: register).assign_root! + end end end end diff --git a/lib/lutaml/xml/schema/xsd/attribute.rb b/lib/lutaml/xml/schema/xsd/attribute.rb index f85e4d9c6..a520693f3 100644 --- a/lib/lutaml/xml/schema/xsd/attribute.rb +++ b/lib/lutaml/xml/schema/xsd/attribute.rb @@ -35,17 +35,40 @@ class Attribute < Base map_element :simpleType, to: :simple_type end - # liquid do + liquid do + map "cardinality", to: :cardinality + map "referenced_name", to: :referenced_name + map "referenced_type", to: :referenced_type + map "referenced_object", to: :referenced_object + end - # map "cardinality", to: :cardinality + # Translate XSD attribute `use` semantics into the cardinality + # strings expected by Liquid templates. + def cardinality + case use + when "required" then "1" + when "optional" then "0..1" + end + end - # map "referenced_name", to: :referenced_name + # Resolve the effective type, following `ref` when needed. + def referenced_type + referenced_object&.type + end - # map "referenced_type", to: :referenced_type + # Resolve the effective attribute name, falling back to the raw + # `ref` when the target cannot be found. + def referenced_name + referenced_object&.name || ref + end - # map "referenced_object", to: :referenced_object + # Return the attribute itself when it is named, otherwise resolve + # the root-level attribute referenced by `ref`. + def referenced_object + return self if name - # end + find_object(xsd_root.attribute) + end Lutaml::Xml::Schema::Xsd.register_model(self, :attribute) end diff --git a/lib/lutaml/xml/schema/xsd/attribute_group.rb b/lib/lutaml/xml/schema/xsd/attribute_group.rb index 3866a766c..ebd28fe06 100644 --- a/lib/lutaml/xml/schema/xsd/attribute_group.rb +++ b/lib/lutaml/xml/schema/xsd/attribute_group.rb @@ -29,15 +29,57 @@ class AttributeGroup < Base map_element :attributeGroup, to: :attribute_group end - # liquid do + liquid do + map "used_by", to: :used_by + map "referenced_object", to: :referenced_object + map "attribute_elements", to: :attribute_elements + end - # map "used_by", to: :used_by + # Return complex types that reference this attribute group. + def used_by + xsd_root.complex_type.select { |type| find_used_by(type) } + end - # map "referenced_object", to: :referenced_object + # Flatten nested attribute-group references into a single list of + # attribute objects for template consumption. + def attribute_elements(array = []) + group = referenced_object + return array unless group - # map "attribute_elements", to: :attribute_elements + group.resolved_element_order&.each do |child| + case child + when AttributeGroup + if child.referenced_object + child.attribute_elements(array) + else + array << child + end + when Attribute + array << child + end + end + array + end - # end + # Recursively inspect nested content to determine whether the given + # object references this attribute group. + def find_used_by(object) + object&.resolved_element_order&.any? do |child| + if child.is_a?(AttributeGroup) + child.ref == name + else + find_used_by(child) + end + end || false + end + + # Return the attribute group itself when it is named, otherwise + # resolve the root-level group referenced by `ref`. + def referenced_object + return self if name + + find_object(xsd_root.attribute_group) + end Lutaml::Xml::Schema::Xsd.register_model(self, :attribute_group) end diff --git a/lib/lutaml/xml/schema/xsd/base.rb b/lib/lutaml/xml/schema/xsd/base.rb index 40dc1fcc6..058ace422 100644 --- a/lib/lutaml/xml/schema/xsd/base.rb +++ b/lib/lutaml/xml/schema/xsd/base.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -Lutaml::Model::RuntimeCompatibility.require_native("canon") +require "canon" module Lutaml module Xml @@ -11,32 +11,173 @@ class Base < Lutaml::Model::Serializable ELEMENT_ORDER_IGNORABLE = %w[import include].freeze def to_formatted_xml(except: []) - unless Lutaml::Model::RuntimeCompatibility.native? - raise NotImplementedError, - "XSD formatted XML output is not available under Opal" - end - Canon.format_xml( to_xml(except: except), ).gsub(XML_DECLARATION_REGEX, "") end def resolved_element_order - element_order.each_with_object(element_order.dup) do |element, array| + element_order&.each_with_object(element_order.dup) do |element, array| next delete_deletables(array, element) if deletable?(element) update_element_array(array, element) end end - # liquid do - # map "to_xml", to: :to_xml - # map "to_formatted_xml", to: :to_formatted_xml - # map "resolved_element_order", to: :resolved_element_order - # end + # Propagate the parsed schema root through the XSD object graph so + # Liquid helpers can resolve cross-object references consistently. + def assign_root!(root = self, seen = nil) + seen ||= {}.compare_by_identity + return self if seen[self] + + seen[self] = true + self.lutaml_root = root + + self.class.attributes&.each_key do |attribute_name| + assign_root_value!(public_send(attribute_name), root, seen) + end + + self + end + + def sequence? + is_a?(Sequence) + end + + def any? + is_a?(Any) + end + + def all? + is_a?(All) + end + + def choice? + is_a?(Choice) + end + + def annotation? + is_a?(Annotation) + end + + def attribute? + is_a?(Attribute) + end + + def attribute_group? + is_a?(AttributeGroup) + end + + def simple_content? + is_a?(SimpleContent) + end + + def complex_content? + is_a?(ComplexContent) + end + + def element? + is_a?(Element) + end + + def min_occurrences + return unless respond_to?(:min_occurs) + + @min_occurs&.to_i || 1 + end + + def max_occurrences + return unless respond_to?(:max_occurs) + return "*" if @max_occurs == "unbounded" + + @max_occurs&.to_i || 1 + end + + def target_prefix + xsd_root&.target_namespace_prefix + end + + def unresolvable_items(array = [], seen = nil) + seen ||= {}.compare_by_identity + return array if seen[self] + + seen[self] = true + resolved_element_order&.each do |element| + if element.respond_to?(:ref) && element.ref + if resolvable_reference?(element.ref) + element.referenced_object&.unresolvable_items(array, seen) + else + array << element + end + elsif element.respond_to?(:unresolvable_items) + element.unresolvable_items(array, seen) + end + end + array + end + + liquid do + map "any?", to: :any? + map "all?", to: :all? + map "to_xml", to: :to_xml + map "choice?", to: :choice? + map "element?", to: :element? + map "sequence?", to: :sequence? + map "attribute?", to: :attribute? + map "annotation?", to: :annotation? + map "target_prefix", to: :target_prefix + map "simple_content?", to: :simple_content? + map "min_occurrences", to: :min_occurrences + map "max_occurrences", to: :max_occurrences + map "to_formatted_xml", to: :to_formatted_xml + map "complex_content?", to: :complex_content? + map "attribute_group?", to: :attribute_group? + map "unresolvable_items", to: :unresolvable_items + map "resolved_element_order", to: :resolved_element_order + end private + # Resolve a named object from a root-level collection, taking the + # schema target prefix into account when references are qualified. + def find_object(collection, reference = ref) + find_in_collection(collection, reference) || + find_in_related_schemas(collection, reference) + end + + def reference_local_name(reference) + reference.to_s.split(":", 2).last + end + + def reference_prefix(reference) + parts = reference.to_s.split(":", 2) + parts.length == 2 ? parts.first : nil + end + + def builtin_reference?(reference) + %w[xs xsd xml].include?(reference_prefix(reference)) + end + + def reference_matches?(name, reference) + return false if name.nil? || reference.nil? + return true if name == reference + return false if builtin_reference?(reference) + + reference_local_name(reference) == name + end + + def resolvable_reference?(reference) + !builtin_reference?(reference) && !find_object_for_ref(reference).nil? + end + + def find_object_for_ref(reference) + if respond_to?(:referenced_object) + referenced_object + else + find_object(collection_for_reference(reference), reference) + end + end + def deletable?(instance) instance.text? || ELEMENT_ORDER_IGNORABLE.include?(instance.name) @@ -56,6 +197,69 @@ def update_element_array(array, instance) index += 1 end end + + def assign_root_value!(value, root, seen) + Array(value).each do |child| + next unless child.respond_to?(:assign_root!) + + child_root = child.is_a?(Schema) ? child : root + child.assign_root!(child_root, seen) + end + end + + def find_in_collection(collection, reference) + collection&.find do |object| + reference_matches?(object.name, reference) + end + end + + def find_in_related_schemas(collection, reference) + collection_name = collection_name_for(collection) + return unless collection_name + + related_schemas.each do |schema| + found = find_in_collection(schema.public_send(collection_name), + reference) + return found if found + end + + nil + end + + def collection_name_for(collection) + return unless xsd_root + + %i[element complex_type simple_type attribute group + attribute_group].find do |name| + xsd_root.public_send(name).equal?(collection) + end + end + + def related_schemas(root = xsd_root, seen = nil) + seen ||= {}.compare_by_identity + return [] unless root + return [] if seen[root] + + seen[root] = true + schemas = [] + %i[import include].each do |collection_name| + root.public_send(collection_name).each do |schema| + next unless schema.is_a?(Schema) + + schemas << schema + schemas.concat(related_schemas(schema, seen)) + end + end + schemas + end + + def collection_for_reference(_reference) + [] + end + + def xsd_root + lutaml_root || (self if is_a?(Schema)) + end end end end diff --git a/lib/lutaml/xml/schema/xsd/choice.rb b/lib/lutaml/xml/schema/xsd/choice.rb index e6c6f9811..c178d67f4 100644 --- a/lib/lutaml/xml/schema/xsd/choice.rb +++ b/lib/lutaml/xml/schema/xsd/choice.rb @@ -32,11 +32,33 @@ class Choice < Base map_element :any, to: :any end - # liquid do + liquid do + map "child_elements", to: :child_elements + end - # map "child_elements", to: :child_elements + # Walk the choice recursively and collect contained element nodes. + def child_elements(array = []) + resolved_element_order&.each do |child| + if child.is_a?(Element) + array << child + elsif child.respond_to?(:child_elements) + child.child_elements(array) + end + end + array + end - # end + # Check whether the choice references a given element name in any + # nested branch. + def find_elements_used(element_name) + resolved_element_order&.any? do |child| + if child.is_a?(Element) + reference_matches?(element_name, child.ref || child.name) + elsif child.respond_to?(:find_elements_used) + child.find_elements_used(element_name) + end + end || false + end Lutaml::Xml::Schema::Xsd.register_model(self, :choice) end diff --git a/lib/lutaml/xml/schema/xsd/complex_content.rb b/lib/lutaml/xml/schema/xsd/complex_content.rb index 0e9c7b3f3..a3794592c 100644 --- a/lib/lutaml/xml/schema/xsd/complex_content.rb +++ b/lib/lutaml/xml/schema/xsd/complex_content.rb @@ -23,6 +23,14 @@ class ComplexContent < Base map_element :restriction, to: :restriction end + # Delegate attribute collection to the complex-content extension + # when one is present. + def attribute_elements(array = []) + extension&.attribute_elements(array) + restriction&.attribute_elements(array) + array + end + Lutaml::Xml::Schema::Xsd.register_model(self, :complex_content) end end diff --git a/lib/lutaml/xml/schema/xsd/complex_type.rb b/lib/lutaml/xml/schema/xsd/complex_type.rb index 71c5b1167..689bb3733 100644 --- a/lib/lutaml/xml/schema/xsd/complex_type.rb +++ b/lib/lutaml/xml/schema/xsd/complex_type.rb @@ -5,6 +5,13 @@ module Xml module Schema module Xsd class ComplexType < Base + DIRECT_CHILD_ELEMENTS_EXCEPTION = %w[ + AttributeGroup + AnyAttribute + Annotation + Attribute + ].freeze + attribute :id, :string attribute :name, :string attribute :base, :string @@ -47,18 +54,90 @@ class ComplexType < Base map_element :complexContent, to: :complex_content end - # liquid do + liquid do + map "used_by", to: :used_by + map "child_elements", to: :child_elements + map "attribute_elements", to: :attribute_elements + map "direct_child_elements", to: :direct_child_elements + end + + # Return root-level elements and nested child elements that refer to + # this complex type by name. + def used_by + root_complex_types = xsd_root.complex_type.reject do |complex_type| + complex_type == self + end + raw_elements = xsd_root.group.flat_map(&:child_elements) + raw_elements.concat(xsd_root.element) + raw_elements.concat(root_complex_types.flat_map(&:child_elements)) + raw_elements.select do |element| + reference_matches?(name, element.type) + end + end - # map "used_by", to: :used_by + # Flatten attributes declared directly on the complex type and those + # brought in through groups and content extensions. + def attribute_elements(array = []) + array.concat(attribute) + attribute_group.each { |group| group.attribute_elements(array) } + simple_content&.attribute_elements(array) + complex_content&.attribute_elements(array) + array + end - # map "child_elements", to: :child_elements + # Return structural children while skipping attributes and other + # non-element content wrappers listed in the exception set. + def direct_child_elements(array = [], +except: DIRECT_CHILD_ELEMENTS_EXCEPTION) + resolved_element_order&.each do |child| + next if except.any? do |klass| + child.class.name.include?("::#{klass}") + end - # map "attribute_elements", to: :attribute_elements + array << child if child.resolved_element_order&.any? + end + array + end - # end + # Walk nested content containers and collect the element nodes they + # expose in document order. + def child_elements(array = []) + resolved_element_order&.each do |child| + if child.is_a?(Element) + array << child + elsif child.respond_to?(:child_elements) + child.child_elements(array) + end + end + array + end + + # Check whether a referenced element name is used anywhere within + # this complex type's nested content model. + def find_elements_used(element_name) + resolved_element_order&.any? do |child| + if child.is_a?(Element) + reference_matches?(element_name, child.ref || child.name) + elsif child.respond_to?(:find_elements_used) + child.find_elements_used(element_name) + end + end || false + end + + # Recursively inspect another object to see whether it references + # this complex type through nested attribute groups. + def find_used_by(object) + object&.resolved_element_order&.any? do |child| + if child.is_a?(AttributeGroup) + child.ref == name + else + find_used_by(child) + end + end || false + end - # Get elements from content model (sequence, choice, or all) - # @return [Array] Elements from content model + # Get elements from the primary content model (sequence, choice, or all). + # @return [Array] Elements exposed by the active content model def elements return sequence.element if sequence.respond_to?(:element) return choice.element if choice.respond_to?(:element) @@ -67,7 +146,7 @@ def elements [] end - # Convenience plural accessor for attributes + # Convenience plural accessor for attributes. alias attributes attribute Lutaml::Xml::Schema::Xsd.register_model(self, :complex_type) diff --git a/lib/lutaml/xml/schema/xsd/element.rb b/lib/lutaml/xml/schema/xsd/element.rb index 2aee3a818..bca3e7005 100644 --- a/lib/lutaml/xml/schema/xsd/element.rb +++ b/lib/lutaml/xml/schema/xsd/element.rb @@ -53,27 +53,61 @@ class Element < Base map_element :key, to: :key end - # liquid do - - # map "used_by", to: :used_by + liquid do + map "used_by", to: :used_by + map "attributes", to: :attributes + map "child_elements", to: :child_elements + map "referenced_type", to: :referenced_type + map "min_occurrences", to: :min_occurrences + map "max_occurrences", to: :max_occurrences + map "referenced_name", to: :referenced_name + map "referenced_object", to: :referenced_object + map "referenced_complex_type", to: :referenced_complex_type + end - # map "attributes", to: :attributes + # Return complex types whose nested content model references this + # element by name. + def used_by + xsd_root.complex_type.select do |object| + object.find_elements_used(name) + end + end - # map "child_elements", to: :child_elements + # Delegate attribute lookup to the complex type referenced by this + # element, if any. + def attributes + referenced_complex_type&.attribute_elements + end - # map "referenced_type", to: :referenced_type + # Delegate child-element discovery to the referenced complex type. + def child_elements(array = []) + referenced_complex_type&.child_elements(array) + end - # map "min_occurrences", to: :min_occurrences + # Return the effective element name after resolving `ref`. + def referenced_name + referenced_object&.name || ref + end - # map "max_occurrences", to: :max_occurrences + # Return the effective element type after resolving `ref`. + def referenced_type + referenced_object&.type + end - # map "referenced_name", to: :referenced_name + # Return the element itself when it is named, otherwise resolve the + # root-level element referenced by `ref`. + def referenced_object + return self if name - # map "referenced_object", to: :referenced_object + find_object(xsd_root.element) + end - # map "referenced_complex_type", to: :referenced_complex_type + # Resolve the complex type referenced by the effective element type. + def referenced_complex_type + return complex_type if complex_type - # end + find_object(xsd_root.complex_type, referenced_type) + end Lutaml::Xml::Schema::Xsd.register_model(self, :element) end diff --git a/lib/lutaml/xml/schema/xsd/extension_complex_content.rb b/lib/lutaml/xml/schema/xsd/extension_complex_content.rb index b8647b269..05eb0ebcb 100644 --- a/lib/lutaml/xml/schema/xsd/extension_complex_content.rb +++ b/lib/lutaml/xml/schema/xsd/extension_complex_content.rb @@ -35,6 +35,25 @@ class ExtensionComplexContent < Base map_element :attributeGroup, to: :attribute_group end + liquid do + map "attribute_elements", to: :attribute_elements + map "referenced_object", to: :referenced_object + end + + # Reuse the base complex type's flattened attribute list, then add + # attributes declared directly on this extension. + def attribute_elements(array = []) + referenced_object&.attribute_elements(array) + array.concat(attribute) + attribute_group.each { |group| group.attribute_elements(array) } + array + end + + # Resolve the base complex type referenced by the extension. + def referenced_object + find_object(xsd_root.complex_type, base) + end + Lutaml::Xml::Schema::Xsd.register_model(self, :extension_complex_content) end diff --git a/lib/lutaml/xml/schema/xsd/extension_simple_content.rb b/lib/lutaml/xml/schema/xsd/extension_simple_content.rb index 0949053b1..47e195748 100644 --- a/lib/lutaml/xml/schema/xsd/extension_simple_content.rb +++ b/lib/lutaml/xml/schema/xsd/extension_simple_content.rb @@ -27,11 +27,17 @@ class ExtensionSimpleContent < Base map_element :attributeGroup, to: :attribute_group end - # liquid do - - # map "attribute_elements", to: :attribute_elements + liquid do + map "attribute_elements", to: :attribute_elements + end - # end + # Flatten attributes declared directly on the extension together + # with those pulled in through attribute groups. + def attribute_elements(array = []) + array.concat(attribute) + attribute_group.each { |group| group.attribute_elements(array) } + array + end Lutaml::Xml::Schema::Xsd.register_model(self, :extension_simple_content) diff --git a/lib/lutaml/xml/schema/xsd/glob.rb b/lib/lutaml/xml/schema/xsd/glob.rb index f6cf72801..1fd3d4001 100644 --- a/lib/lutaml/xml/schema/xsd/glob.rb +++ b/lib/lutaml/xml/schema/xsd/glob.rb @@ -15,17 +15,18 @@ def schema_mappings def schema_mappings=(mappings) @schema_mappings = mappings || [] + @schema_path = SchemaPath.new(location, + schema_mappings: @schema_mappings) end def path_or_url(location) return nullify_location if location.nil? + @schema_path = SchemaPath.new(location, + schema_mappings: schema_mappings) @location = location - @url = location if location.start_with?(%r{http\w?:/{2}[^.]+}) - @path = File.expand_path(location) unless @url - rescue Errno::ENOENT - raise ::Lutaml::Xml::Schema::Xsd::Error, - "Invalid location: #{location}" + @url = @schema_path.url&.to_s + @path = @schema_path.path end def location @@ -51,84 +52,51 @@ def http_get(url) def include_schema(schema_location) return unless location? && schema_location - # Check if there's a mapping for this schema location - resolved_location = resolve_schema_location(schema_location) - - # If resolved to absolute path, use it directly - if absolute_path?(resolved_location) - unless File.exist?(resolved_location) - raise ::Lutaml::Xml::Schema::Xsd::Error, "Mapped schema file not found: #{resolved_location} " \ - "(original location: #{schema_location})" - end - return File.read(resolved_location) - end + schema_path.include_schema(schema_location) + end - # If resolved location is a URL, fetch it directly - return http_get(resolved_location) if resolved_location.match?(%r{^https?://}) + def location_for(schema_location) + return unless location? && schema_location - schema_path = schema_location_path(resolved_location) - read_schema_file(schema_path, schema_location) + schema_path.location_for(schema_location) end - private - - def absolute_path?(path) - # Unix/Linux/macOS: starts with / - # Windows: starts with drive letter (e.g., C:\) - path.start_with?("/") || path.match?(%r{^[A-Za-z]:[\\/]}) + def schema_location_path(schema_location) + schema_path.schema_location_path(schema_location) end def resolve_schema_location(schema_location) - return schema_location if schema_mappings.empty? - - # Iterate through mappings array - schema_mappings.each do |mapping| - from = mapping[:from] || mapping["from"] - to = mapping[:to] || mapping["to"] - pattern = mapping[:pattern] || mapping["pattern"] - next unless from && to - - # Exact string match when pattern is not explicitly true - if (pattern != true) && from.is_a?(String) && from == schema_location - return to - end - - # Regex match when pattern is true or from is already a Regexp - if pattern == true || from.is_a?(Regexp) - regex = from.is_a?(Regexp) ? from : Regexp.new(from) - match = schema_location.match(regex) - if match - return schema_location.gsub(regex, to) - end - end - end - - schema_location + schema_path.resolve_schema_location(schema_location) end - def schema_location_path(schema_location) - separator = "/" unless schema_location&.start_with?("/") || location&.end_with?("/") - - location_params = [location, schema_location].compact - url? ? location_params.join(separator) : File.join(location_params) + def with_location(location) + snapshot = { + location: @location, + path: @path, + url: @url, + schema_path: @schema_path, + } + path_or_url(location) + yield + ensure + @location = snapshot[:location] + @path = snapshot[:path] + @url = snapshot[:url] + @schema_path = snapshot[:schema_path] end - def read_schema_file(schema_path, original_location) - if url? - http_get(schema_path) - else - unless File.exist?(schema_path) - raise ::Lutaml::Xml::Schema::Xsd::Error, "Schema file not found: #{schema_path} " \ - "(original location: #{original_location})" - end - File.read(schema_path) - end + private + + def schema_path + @schema_path ||= SchemaPath.new(location, + schema_mappings: schema_mappings) end def nullify_location @location = nil @path = nil @url = nil + @schema_path = nil end end end diff --git a/lib/lutaml/xml/schema/xsd/group.rb b/lib/lutaml/xml/schema/xsd/group.rb index 1da11f808..9b1059f26 100644 --- a/lib/lutaml/xml/schema/xsd/group.rb +++ b/lib/lutaml/xml/schema/xsd/group.rb @@ -31,11 +31,45 @@ class Group < Base map_element :all, to: :all end - # liquid do + liquid do + map "child_elements", to: :child_elements + end + + # Walk the referenced group definition and collect its element + # children recursively. + def child_elements(array = []) + group = referenced_object + return array unless group - # map "child_elements", to: :child_elements + group.resolved_element_order&.each do |child| + if child.is_a?(Element) + array << child + elsif child.respond_to?(:child_elements) + child.child_elements(array) + end + end + array + end - # end + # Check whether the group references a given element name anywhere + # in its nested content model. + def find_elements_used(element_name) + resolved_element_order&.any? do |child| + if child.is_a?(Element) + reference_matches?(element_name, child.ref || child.name) + elsif child.respond_to?(:find_elements_used) + child.find_elements_used(element_name) + end + end || false + end + + # Return the group itself when it is named, otherwise resolve the + # root-level group referenced by `ref`. + def referenced_object + return self if name + + find_object(xsd_root.group) + end Lutaml::Xml::Schema::Xsd.register_model(self, :group) end diff --git a/lib/lutaml/xml/schema/xsd/restriction_complex_content.rb b/lib/lutaml/xml/schema/xsd/restriction_complex_content.rb index e898218c3..d89df4a91 100644 --- a/lib/lutaml/xml/schema/xsd/restriction_complex_content.rb +++ b/lib/lutaml/xml/schema/xsd/restriction_complex_content.rb @@ -35,6 +35,18 @@ class RestrictionComplexContent < Base map_element :attributeGroup, to: :attribute_group end + liquid do + map "attribute_elements", to: :attribute_elements + end + + # Flatten attributes allowed by this restriction, including any + # referenced attribute groups. + def attribute_elements(array = []) + array.concat(attribute) + attribute_group.each { |group| group.attribute_elements(array) } + array + end + Lutaml::Xml::Schema::Xsd.register_model(self, :restriction_complex_content) end diff --git a/lib/lutaml/xml/schema/xsd/restriction_simple_content.rb b/lib/lutaml/xml/schema/xsd/restriction_simple_content.rb index 56b3ac912..2b7c8b505 100644 --- a/lib/lutaml/xml/schema/xsd/restriction_simple_content.rb +++ b/lib/lutaml/xml/schema/xsd/restriction_simple_content.rb @@ -63,6 +63,18 @@ class RestrictionSimpleContent < Base map_element :length, to: :length end + liquid do + map "attribute_elements", to: :attribute_elements + end + + # Flatten attributes allowed by this restriction, including any + # referenced attribute groups. + def attribute_elements(array = []) + array.concat(attribute) + attribute_group.each { |group| group.attribute_elements(array) } + array + end + Lutaml::Xml::Schema::Xsd.register_model(self, :restriction_simple_content) end diff --git a/lib/lutaml/xml/schema/xsd/schema.rb b/lib/lutaml/xml/schema/xsd/schema.rb index d442ade7d..e24d8a0e6 100644 --- a/lib/lutaml/xml/schema/xsd/schema.rb +++ b/lib/lutaml/xml/schema/xsd/schema.rb @@ -14,6 +14,7 @@ class Schema < Base attribute :final_default, :string attribute :block_default, :string attribute :target_namespace, :string + attribute :target_namespace_prefix, :string attribute :element_form_default, :string attribute :attribute_form_default, :string attribute :imports, :import, collection: true, initialize_empty: true @@ -60,7 +61,8 @@ class Schema < Base map_attribute :attributeFormDefault, to: :attribute_form_default map_attribute :elementFormDefault, to: :element_form_default - map_attribute :targetNamespace, to: :target_namespace + map_attribute :targetNamespace, to: :target_namespace, + with: { from: :target_namespace_from } map_attribute :finalDefault, to: :final_default map_attribute :blockDefault, to: :block_default map_attribute :version, to: :version @@ -68,6 +70,16 @@ class Schema < Base map_attribute :lang, to: :lang end + liquid do + map "elements_sorted_by_name", to: :elements_sorted_by_name + map "attributes_sorted_by_name", to: :attributes_sorted_by_name + map "simple_types_sorted_by_name", to: :simple_types_sorted_by_name + map "complex_types_sorted_by_name", + to: :complex_types_sorted_by_name + map "attribute_groups_sorted_by_name", + to: :attribute_groups_sorted_by_name + end + def import_from_schema(model, value) value.each do |schema| setup_import_and_include( @@ -107,18 +119,53 @@ def include_to_schema(model, parent, _doc) end end + # Return global elements sorted alphabetically by name for stable + # Liquid iteration. + def elements_sorted_by_name + element.sort_by { |item| item.name.to_s } + end + + # Return global attributes sorted alphabetically by name. + def attributes_sorted_by_name + attribute.sort_by { |item| item.name.to_s } + end + + # Return simple types sorted alphabetically by name. + def simple_types_sorted_by_name + simple_type.sort_by { |item| item.name.to_s } + end + + # Return complex types sorted alphabetically by name. + def complex_types_sorted_by_name + complex_type.sort_by { |item| item.name.to_s } + end + + # Return attribute groups sorted alphabetically by name. + def attribute_groups_sorted_by_name + attribute_group.sort_by { |item| item.name.to_s } + end + + # Capture both the target namespace URI and the matching prefix + # discovered during XML deserialization. + def target_namespace_from(model, value, custom_args = {}) + model.target_namespace = value + namespaces = custom_args[:namespaces] || {} + model.target_namespace_prefix = + namespaces.find { |_, namespace| namespace.uri == value }&.first + end + # Find a type definition by local name # @param local_name [String] The local name of the type # @return [SimpleType, ComplexType, nil] The type definition or nil def find_type(local_name) return nil if local_name.nil? - # Search simple types - found = simple_type.find { |t| t.name == local_name } + # Search simple types first. + found = simple_type.find { |type| type.name == local_name } return found if found - # Search complex types - complex_type.find { |t| t.name == local_name } + # Fall back to complex types if no simple type matches. + complex_type.find { |type| type.name == local_name } end # Find an element definition by local name @@ -127,7 +174,7 @@ def find_type(local_name) def find_element(local_name) return nil if local_name.nil? - element.find { |e| e.name == local_name } + element.find { |element| element.name == local_name } end # Find complex type by name @@ -136,7 +183,7 @@ def find_element(local_name) def find_complex_type(name) return nil if name.nil? - complex_type.find { |t| t.name == name } + complex_type.find { |type| type.name == name } end # Find simple type by name @@ -145,7 +192,7 @@ def find_complex_type(name) def find_simple_type(name) return nil if name.nil? - simple_type.find { |t| t.name == name } + simple_type.find { |type| type.name == name } end # Quick statistics about the schema @@ -167,7 +214,7 @@ def stats # Quick validation check # @return [Boolean] True if the schema has a target namespace def valid? - # Basic validation - can be enhanced + # Basic validation: a parsed schema should declare a target namespace. !target_namespace.nil? && !target_namespace.empty? end @@ -185,12 +232,12 @@ def summary def name return nil unless target_namespace - # Extract the last part of the namespace URI as the name - # e.g., "http://example.com/test" => "test" + # Extract the last segment of the namespace URI as the schema name. + # For example, "http://example.com/test" becomes "test". target_namespace.split("/").last || target_namespace end - # Convenience plural accessors for collections + # Convenience plural accessors for common schema collections. alias elements element alias complex_types complex_type alias simple_types simple_type @@ -200,21 +247,22 @@ def name private def all_namespaces + # Aggregate the schema target namespace with imported namespaces. namespaces = [target_namespace].compact - import.each { |i| namespaces << i.namespace if i&.namespace } + import.each do |item| + namespaces << item.namespace if item&.namespace + end namespaces.uniq end def setup_import_and_include(klass, model, schema, args = {}) instance = init_instance_of(klass, schema.attributes || {}, args) annotation_object(instance, schema) - # Use builder-style append instead of << to handle frozen sentinel model.public_send("#{klass}s", instance) schema_path = instance.schema_path return if self.class.in_progress?(schema_path) || schema_path.nil? self.class.add_in_progress(schema_path) - # Use builder-style append instead of << to handle frozen sentinel model.public_send(klass, insert_in_processed_schemas(instance)) self.class.remove_in_progress(schema_path) end @@ -238,18 +286,34 @@ def insert_in_processed_schemas(instance) def schema_by_location_or_instance(instance) schema_path = instance.schema_path - return unless schema_path && Glob.location? + return unless schema_path + + location = Glob.location_for(schema_path) if Glob.location? + schema_content = if Glob.location? + instance.fetch_schema + else + schema_path_without_base(schema_path) + end + return unless schema_content self.class.processed_schemas[schema_path] || Lutaml::Xml::Schema::Xsd.parse( - instance.fetch_schema, - location: Glob.location, + schema_content, + location: location || schema_path, nested_schema: true, register: Lutaml::Xml::Schema::Xsd.register.id, schema_mappings: Glob.schema_mappings, ) end + def schema_path_without_base(schema_path) + path = SchemaPath.new(schema_path) + return unless path.absolute_path?(schema_path) || + path.absolute_url?(schema_path) + + path.include_schema(schema_path) + end + def annotation_object(instance, schema) elements = schema.children || [] annotation_key = elements.find do |element| diff --git a/lib/lutaml/xml/schema/xsd/schema_path.rb b/lib/lutaml/xml/schema/xsd/schema_path.rb new file mode 100644 index 000000000..f41365a6c --- /dev/null +++ b/lib/lutaml/xml/schema/xsd/schema_path.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +require "net/http" + +module Lutaml + module Xml + module Schema + module Xsd + class SchemaPath + URI_SCHEMES = %w[http https].freeze + WINDOWS_ABSOLUTE_PATH_REGEX = %r{\A[A-Za-z]:[\\/]} + FORWARD_SLASH = "/" + + attr_reader :location, :path, :url, :schema_mappings + + def initialize(location, schema_mappings: []) + @location = location + @schema_mappings = normalize_mappings(schema_mappings) + assign_location(location) + end + + def path? + !path.nil? + end + + def url? + !url.nil? + end + + def location? + path? || url? + end + + def include_schema(schema_location) + return unless schema_location + + resolved_location = resolve_schema_location(schema_location) + if absolute_path?(resolved_location) + return read_absolute_path( + resolved_location, + schema_location, + mapped: resolved_location != schema_location, + ) + end + return http_get(resolved_location) if absolute_url?(resolved_location) + + schema_path = schema_location_path(resolved_location) + if url? + http_get(schema_path) + else + read_absolute_path(schema_path, + schema_location) + end + end + + def location_for(schema_location) + resolved_location = resolve_schema_location(schema_location) + return resolved_location if absolute_path?(resolved_location) || absolute_url?(resolved_location) + + schema_location_path(resolved_location) + end + + def relative_path?(schema_location) + return false unless location? && schema_location + + resolved_location = resolve_schema_location(schema_location) + return false if absolute_path?(resolved_location) || absolute_url?(resolved_location) + + File.exist?(schema_location_path(resolved_location)) + end + + def schema_location_path(schema_location) + if url? + separator = FORWARD_SLASH unless url_separator_present?(schema_location) + [url.to_s, schema_location].compact.join(separator) + else + File.join([path, schema_location].compact) + end + end + + def resolve_schema_location(schema_location) + return schema_location if schema_location.nil? + return schema_location if schema_mappings.empty? + + schema_location_candidates(schema_location).each do |candidate| + mapped = mapped_schema_location(candidate) + return mapped if mapped + end + + schema_location + end + + def absolute_path?(path) + path.to_s.start_with?(FORWARD_SLASH) || + path.to_s.match?(WINDOWS_ABSOLUTE_PATH_REGEX) + end + + def absolute_url?(schema_location) + URI::DEFAULT_PARSER + .make_regexp(URI_SCHEMES) + .match?(schema_location.to_s) + end + + private + + def assign_location(location) + if absolute_url?(location) + @url = URI(extract_base_url(location)) + elsif location + expanded = File.expand_path(location) + @path = File.directory?(expanded) ? expanded : File.dirname(expanded) + end + rescue Errno::ENOENT + raise ::Lutaml::Xml::Schema::Xsd::Error, + "Invalid location: #{location}" + end + + def normalize_mappings(mappings) + return [] if mappings.nil? || mappings == {} + + Array(mappings) + end + + def schema_location_candidates(schema_location) + candidates = [schema_location] + case schema_location + when /\Ahttp:\/\// + candidates << schema_location.sub(/\Ahttp:\/\//, "https://") + when /\Ahttps:\/\// + candidates << schema_location.sub(/\Ahttps:\/\//, "http://") + end + candidates.uniq + end + + def mapped_schema_location(schema_location) + schema_mappings.each do |mapping| + from = mapping[:from] || mapping["from"] + to = mapping[:to] || mapping["to"] + pattern = mapping[:pattern] || mapping["pattern"] + next unless from && to + + if pattern != true && from.is_a?(String) && from == schema_location + return to + end + + next unless pattern == true || from.is_a?(Regexp) + + regex = from.is_a?(Regexp) ? from : Regexp.new(from) + if schema_location.match?(regex) + return schema_location.gsub(regex, + to) + end + end + + nil + end + + def read_absolute_path(path, original_location, mapped: false) + unless File.exist?(path) + message = mapped ? "Mapped schema file not found" : "Schema file not found" + raise ::Lutaml::Xml::Schema::Xsd::Error, + "#{message}: #{path} " \ + "(original location: #{original_location})" + end + + File.read(path) + end + + def http_get(uri) + Net::HTTP.get(URI.parse(uri)) + end + + def extract_base_url(uri) + parsed_uri = URI.parse(uri.to_s) + path = parsed_uri.path.to_s + last_separator_index = path.rindex(FORWARD_SLASH) + return uri unless last_separator_index + + last_segment = path[(last_separator_index + 1)..] + return uri unless last_segment&.include?(".") + + parsed_uri.path = path[0..last_separator_index] + parsed_uri.query = nil + parsed_uri.fragment = nil + parsed_uri.to_s + end + + def url_separator_present?(schema_location) + schema_location&.start_with?(FORWARD_SLASH) || + url.to_s.end_with?(FORWARD_SLASH) + end + end + end + end + end +end diff --git a/lib/lutaml/xml/schema/xsd/schema_validator.rb b/lib/lutaml/xml/schema/xsd/schema_validator.rb index 1495e05b4..66c1c6af2 100644 --- a/lib/lutaml/xml/schema/xsd/schema_validator.rb +++ b/lib/lutaml/xml/schema/xsd/schema_validator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "moxml" +require "nokogiri" module Lutaml module Xml @@ -86,7 +86,7 @@ def validate(content) # @param content [String] XML content to analyze # @return [String] Detected version ("1.0" or "1.1") def self.detect_version(content) - doc = Moxml.new.parse(content) + doc = ::Nokogiri::XML(content) root = doc.root return "1.0" unless root @@ -102,13 +102,13 @@ def self.detect_version(content) "1.0" end end - rescue Moxml::ParseError + rescue ::Nokogiri::XML::SyntaxError "1.0" # Default to 1.0 if cannot parse end # Check if document has XSD 1.1 specific features # - # @param doc [Moxml::Document] Parsed XML document + # @param doc [Nokogiri::XML::Document] Parsed XML document # @return [Boolean] true if XSD 1.1 features are present def self.has_xsd_1_1_features?(doc) # Check for XSD 1.1 specific elements @@ -149,25 +149,25 @@ def self.has_xsd_1_1_features?(doc) # @param content [String] XML content to validate # @raise [SchemaValidationError] if XML syntax is invalid def validate_xml_syntax(content) - Moxml.new.parse(content, strict: true) - rescue Moxml::ParseError => e + ::Nokogiri::XML(content, &:strict) + rescue ::Nokogiri::XML::SyntaxError => e raise SchemaValidationError, "Invalid XML syntax: #{e.message}" end # Parse XML with error handling # # @param content [String] XML content to parse - # @return [Moxml::Document] Parsed document + # @return [::Nokogiri::XML::Document] Parsed document # @raise [SchemaValidationError] if parsing fails def parse_xml(content) - Moxml.new.parse(content) - rescue Moxml::ParseError => e + ::Nokogiri::XML(content) + rescue ::Nokogiri::XML::SyntaxError => e raise SchemaValidationError, "Failed to parse XML: #{e.message}" end # Validate that root element is xs:schema # - # @param doc [Moxml::Document] Parsed XML document + # @param doc [Nokogiri::XML::Document] Parsed XML document # @raise [SchemaValidationError] if root element is not a valid schema element def validate_schema_root(doc) root = doc.root @@ -185,7 +185,7 @@ def validate_schema_root(doc) end # Check if root element has the correct namespace - namespace_uri = root.namespace_uri + namespace_uri = root.namespace&.href return if namespace_uri == XSD_NAMESPACE if namespace_uri.nil? @@ -201,7 +201,7 @@ def validate_schema_root(doc) # Validate schema version compatibility # - # @param doc [Moxml::Document] Parsed XML document + # @param doc [Nokogiri::XML::Document] Parsed XML document # @raise [SchemaValidationError] if schema uses features incompatible with target version def validate_schema_version(doc) return if @version == "1.1" # 1.1 accepts all features @@ -218,7 +218,7 @@ def validate_schema_version(doc) # Detect which XSD 1.1 features are present # - # @param doc [Moxml::Document] Parsed XML document + # @param doc [Nokogiri::XML::Document] Parsed XML document # @return [Array] List of detected XSD 1.1 features def detect_1_1_features(doc) features = [] diff --git a/lib/lutaml/xml/schema/xsd/sequence.rb b/lib/lutaml/xml/schema/xsd/sequence.rb index d6f12a5ce..02230ea82 100644 --- a/lib/lutaml/xml/schema/xsd/sequence.rb +++ b/lib/lutaml/xml/schema/xsd/sequence.rb @@ -32,11 +32,33 @@ class Sequence < Base map_element :any, to: :any end - # liquid do + liquid do + map "child_elements", to: :child_elements + end - # map "child_elements", to: :child_elements + # Walk the sequence recursively and collect contained element nodes. + def child_elements(array = []) + resolved_element_order&.each do |child| + if child.is_a?(Element) + array << child + elsif child.respond_to?(:child_elements) + child.child_elements(array) + end + end + array + end - # end + # Check whether the sequence references a given element name in any + # nested branch. + def find_elements_used(element_name) + resolved_element_order&.any? do |child| + if child.is_a?(Element) + reference_matches?(element_name, child.ref || child.name) + elsif child.respond_to?(:find_elements_used) + child.find_elements_used(element_name) + end + end || false + end Lutaml::Xml::Schema::Xsd.register_model(self, :sequence) end diff --git a/lib/lutaml/xml/schema/xsd/serialized_schema.rb b/lib/lutaml/xml/schema/xsd/serialized_schema.rb index 43c517301..c320b2d47 100644 --- a/lib/lutaml/xml/schema/xsd/serialized_schema.rb +++ b/lib/lutaml/xml/schema/xsd/serialized_schema.rb @@ -58,15 +58,18 @@ def to_schema ) # Reconstruct collections - schema.simple_type = deserialize_types( - data["simple_types"], :simple_type - ) - schema.complex_type = deserialize_types( - data["complex_types"], :complex_type - ) - schema.element = deserialize_elements(data["elements"]) - schema.attribute_group = deserialize_attribute_groups(data["attribute_groups"]) - schema.group = deserialize_groups(data["groups"]) + schema.instance_variable_set(:@simple_type, + deserialize_types(data["simple_types"], + :simple_type)) + schema.instance_variable_set(:@complex_type, + deserialize_types(data["complex_types"], + :complex_type)) + schema.instance_variable_set(:@element, + deserialize_elements(data["elements"])) + schema.instance_variable_set(:@attribute_group, + deserialize_attribute_groups(data["attribute_groups"])) + schema.instance_variable_set(:@group, + deserialize_groups(data["groups"])) schema end diff --git a/lib/lutaml/xml/schema/xsd/simple_content.rb b/lib/lutaml/xml/schema/xsd/simple_content.rb index 6795bc641..d93c2ce06 100644 --- a/lib/lutaml/xml/schema/xsd/simple_content.rb +++ b/lib/lutaml/xml/schema/xsd/simple_content.rb @@ -23,11 +23,35 @@ class SimpleContent < Base map_element :extension, to: :extension end - # liquid do + liquid do + map "attribute_elements", to: :attribute_elements + map "base_type", to: :base_type + end + + # Collect attributes inherited from the base complex type together + # with attributes added by the simple-content extension. + def attribute_elements(array = []) + base_complex_type&.attribute_elements(array) + extension&.attribute_elements(array) + restriction&.attribute_elements(array) + array + end - # map "attribute_elements", to: :attribute_elements + # Resolve the effective base type from inline, extension, or + # restriction declarations. + def base_type + base || + extension&.base || + restriction&.base + end + + private - # end + # Resolve the complex type that provides inherited attributes for + # this simple-content definition, if one exists. + def base_complex_type + find_object(xsd_root.complex_type, base_type) + end Lutaml::Xml::Schema::Xsd.register_model(self, :simple_content) end diff --git a/spec/lutaml/key_value/transformation_spec.rb b/spec/lutaml/key_value/transformation_spec.rb index 30b1d24aa..02a20d1d4 100644 --- a/spec/lutaml/key_value/transformation_spec.rb +++ b/spec/lutaml/key_value/transformation_spec.rb @@ -284,7 +284,9 @@ def compile_rules(_mapping_dsl) expect(transformation).to be_frozen rule = instance_double(Lutaml::Model::CompiledRule, attribute_name: :name) - expect { transformation.send(:valid_mapping?, rule, {}) }.not_to raise_error + expect do + transformation.send(:valid_mapping?, rule, {}) + end.not_to raise_error end end end diff --git a/spec/lutaml/model/cached_type_resolver_spec.rb b/spec/lutaml/model/cached_type_resolver_spec.rb index 7a28bd580..aba07a2b0 100644 --- a/spec/lutaml/model/cached_type_resolver_spec.rb +++ b/spec/lutaml/model/cached_type_resolver_spec.rb @@ -50,7 +50,9 @@ end expect(result).to eq(:outer) - expect(cache_backend.fetch_or_store(other_cache_key) { :other }).to eq(:nested) + expect(cache_backend.fetch_or_store(other_cache_key) do + :other + end).to eq(:nested) end end @@ -334,7 +336,9 @@ it "does not autoload the native ConcurrentMapCache on Opal" do hide_const("Lutaml::Model::CachedTypeResolver::ConcurrentMapCache") - load File.expand_path("../../../lib/lutaml/model/cached_type_resolver.rb", __dir__) + load File.expand_path( + "../../../lib/lutaml/model/cached_type_resolver.rb", __dir__ + ) expect(described_class.autoload?(:ConcurrentMapCache)).to be_nil end diff --git a/spec/lutaml/xml/schema/compiler_spec.rb b/spec/lutaml/xml/schema/compiler_spec.rb index 5a6269b05..789d56244 100644 --- a/spec/lutaml/xml/schema/compiler_spec.rb +++ b/spec/lutaml/xml/schema/compiler_spec.rb @@ -1,4 +1,5 @@ require "spec_helper" +require "net/http" require "lutaml/model/schema" require "support/xml/schema_helper" @@ -27,6 +28,24 @@ def strip_xml_comments(xml) current end + describe ".as_models" do + let(:xml_namespace_schema) do + <<~XSD + + + + XSD + end + + it "keeps built-in W3C XML attribute types when processing xml.xsd" do + described_class.as_models(xml_namespace_schema) + + expect(described_class.attributes["id"].type) + .to eq("Lutaml::Xml::W3c::XmlIdType") + end + end + describe ".to_models" do describe "Testing the unofficial schemas" do context "with valid xml schema, it generates the models" do diff --git a/spec/lutaml/xml/schema/xsd/glob_spec.rb b/spec/lutaml/xml/schema/xsd/glob_spec.rb index f0429ff9d..076345908 100644 --- a/spec/lutaml/xml/schema/xsd/glob_spec.rb +++ b/spec/lutaml/xml/schema/xsd/glob_spec.rb @@ -240,6 +240,13 @@ expect(described_class.path?).to be false end + it "uses the containing URL when location points to a schema file" do + described_class.path_or_url("https://example.com/schemas/root.xsd") + + expect(described_class.schema_location_path("child.xsd")) + .to eq("https://example.com/schemas/child.xsd") + end + it "handles nil location" do described_class.path_or_url(nil) expect(described_class.location?).to be false diff --git a/spec/lutaml/xml/schema/xsd/liquid_methods/attribute_and_group_spec.rb b/spec/lutaml/xml/schema/xsd/liquid_methods/attribute_and_group_spec.rb new file mode 100644 index 000000000..eb4a5cfc5 --- /dev/null +++ b/spec/lutaml/xml/schema/xsd/liquid_methods/attribute_and_group_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe "XSD attribute-based liquid helpers" do + let(:schema_xml) do + <<~XML + + + + + + + + + + + + + XML + end + + let(:schema) { Lutaml::Xml::Schema::Xsd.parse(schema_xml, validate_schema: false) } + let(:test_attr) do + schema.attribute.find do |attribute| + attribute.name == "TestAttribute" + end + end + let(:attr_group) do + schema.attribute_group.find do |group| + group.name == "TestAttributeGroup" + end + end + let(:root_type) do + schema.complex_type.find do |type| + type.name == "RootType" + end + end + + it "reports attribute cardinality and reference resolution" do + test_attr.use = "required" + + ref_attr = Lutaml::Xml::Schema::Xsd::Attribute.new( + __register: Lutaml::Xml::Schema::Xsd.register, + ) + ref_attr.ref = "TestAttribute" + ref_attr.lutaml_root = schema + + expect(test_attr.cardinality).to eq("1") + expect(ref_attr.referenced_name).to eq("TestAttribute") + expect(ref_attr.referenced_type).to eq("xs:string") + expect(test_attr.to_liquid.cardinality).to eq("1") + end + + it "finds complex types using an attribute group" do + expect(attr_group.used_by).to include(root_type) + expect(attr_group.find_used_by(root_type)).to be true + end + + it "expands attribute groups into their attributes" do + expect(attr_group.attribute_elements.map(&:name)) + .to contain_exactly("GroupAttr1", "GroupAttr2") + end + + it "resolves referenced attribute groups" do + ref_group = Lutaml::Xml::Schema::Xsd::AttributeGroup.new( + __register: Lutaml::Xml::Schema::Xsd.register, + ) + ref_group.ref = "TestAttributeGroup" + ref_group.lutaml_root = schema + + expect(ref_group.referenced_object).to eq(attr_group) + end +end diff --git a/spec/lutaml/xml/schema/xsd/liquid_methods/complex_type_spec.rb b/spec/lutaml/xml/schema/xsd/liquid_methods/complex_type_spec.rb new file mode 100644 index 000000000..3874376f0 --- /dev/null +++ b/spec/lutaml/xml/schema/xsd/liquid_methods/complex_type_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe Lutaml::Xml::Schema::Xsd::ComplexType do + let(:schema_xml) do + <<~XML + + + + + + + + + + + + + + + + + + + + + + + + + + + XML + end + + let(:schema) { Lutaml::Xml::Schema::Xsd.parse(schema_xml, validate_schema: false) } + let(:root_type) do + schema.complex_type.find do |complex_type| + complex_type.name == "RootType" + end + end + + it "finds elements that use the complex type" do + expect(root_type.used_by.any? do |element| + element.type == "RootType" + end).to be true + end + + it "collects attributes from the type and attribute groups" do + expect(root_type.attribute_elements.map(&:name)).to include("RootAttr", + "GroupAttr1") + end + + it "returns direct and nested child elements" do + expect(root_type.direct_child_elements.map(&:class)) + .to include(Lutaml::Xml::Schema::Xsd::Sequence) + expect(root_type.child_elements.map(&:referenced_name)) + .to include("DirectChild", "ChoiceElement1", "GroupElement1") + end + + it "detects element usage inside nested structures" do + expect(root_type.find_elements_used("DirectChild")).to be true + expect(root_type.find_elements_used("NonExistent")).to be false + end + + it "collects attributes from simple content extensions" do + simple_content_schema = <<~XML + + + + + + + + + + XML + + parsed = Lutaml::Xml::Schema::Xsd.parse(simple_content_schema, + validate_schema: false) + expect(parsed.complex_type.first.attribute_elements.map(&:name)).to include("ExtendedAttr") + end + + it "collects attributes from complex content extensions" do + complex_content_schema = <<~XML + + + + + + + + + + + + + + + + + + + + + + XML + + parsed = Lutaml::Xml::Schema::Xsd.parse(complex_content_schema, + validate_schema: false) + extended_type = parsed.complex_type.find do |type| + type.name == "ExtendedType" + end + + expect(extended_type.attribute_elements.map(&:name)) + .to include("BaseAttr", "ExtendedAttr", "ExtendedGroupAttr") + end + + it "collects attributes from complex content restrictions" do + complex_content_schema = <<~XML + + + + + + + + + + + + + + + + + + + + + + + + + XML + + parsed = Lutaml::Xml::Schema::Xsd.parse(complex_content_schema, + validate_schema: false) + restricted_type = parsed.complex_type.find do |type| + type.name == "RestrictedType" + end + + expect(restricted_type.attribute_elements.map(&:name)) + .to include("RestrictedAttr", "RestrictedGroupAttr") + end +end diff --git a/spec/lutaml/xml/schema/xsd/liquid_methods/containers_spec.rb b/spec/lutaml/xml/schema/xsd/liquid_methods/containers_spec.rb new file mode 100644 index 000000000..f99f1a665 --- /dev/null +++ b/spec/lutaml/xml/schema/xsd/liquid_methods/containers_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe "XSD container liquid helpers" do + let(:schema_xml) do + <<~XML + + + + + + + + + + + + + + + + + + + + + + + + + + + + XML + end + + let(:schema) { Lutaml::Xml::Schema::Xsd.parse(schema_xml, validate_schema: false) } + let(:root_type) do + schema.complex_type.find do |complex_type| + complex_type.name == "RootType" + end + end + let(:sequence) { root_type.sequence } + let(:choice) { root_type.sequence.choice.first } + let(:group) { schema.group.find { |item| item.name == "TestGroup" } } + + it "collects child elements from sequences" do + expect(sequence.child_elements.map(&:referenced_name)) + .to include("DirectChild", "ChoiceElement1", "ChoiceElement2", + "NestedSeqElement", "GroupElement1", "GroupElement2") + end + + it "collects child elements from choices" do + expect(choice.child_elements.map(&:referenced_name)) + .to include("ChoiceElement1", "ChoiceElement2") + end + + it "collects child elements from groups" do + expect(group.child_elements.map(&:referenced_name)) + .to contain_exactly("GroupElement1", "GroupElement2") + end + + it "detects referenced elements within nested containers" do + expect(sequence.find_elements_used("DirectChild")).to be true + expect(choice.find_elements_used("ChoiceElement1")).to be true + expect(group.find_elements_used("GroupElement1")).to be true + end + + it "returns missing references as unresolvable instead of raising" do + missing_ref_schema = <<~XML + + + + + + + + XML + + parsed = Lutaml::Xml::Schema::Xsd.parse(missing_ref_schema, + validate_schema: false) + missing_type = parsed.complex_type.find do |type| + type.name == "MissingReferenceType" + end + + expect { missing_type.unresolvable_items }.not_to raise_error + expect(missing_type.unresolvable_items.map(&:ref)).to include("test:MissingElement") + end + + it "resolves referenced groups" do + ref_group = Lutaml::Xml::Schema::Xsd::Group.new( + __register: Lutaml::Xml::Schema::Xsd.register, + ) + ref_group.ref = "TestGroup" + ref_group.lutaml_root = schema + + expect(ref_group.referenced_object).to eq(group) + end +end diff --git a/spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb b/spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb new file mode 100644 index 000000000..4b9b8964b --- /dev/null +++ b/spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe Lutaml::Xml::Schema::Xsd::Element do + let(:schema_xml) do + <<~XML + + + + + + + + + + + + + + + + + + + + + + + + + XML + end + + let(:schema) { Lutaml::Xml::Schema::Xsd.parse(schema_xml, validate_schema: false) } + let(:root_element) do + schema.element.find do |element| + element.name == "RootElement" + end + end + let(:referenced_element) do + schema.element.find do |element| + element.name == "ReferencedElement" + end + end + let(:simple_element) do + schema.element.find do |element| + element.name == "SimpleElement" + end + end + + it "assigns the parsed schema as the root context" do + expect(root_element.lutaml_root).to eq(schema) + end + + it "returns attributes from the referenced complex type" do + expect(root_element.attributes.map(&:name)).to include("RootAttr") + end + + it "returns child elements from the referenced complex type" do + expect(root_element.child_elements.map(&:name)).to include("DirectChild") + end + + it "returns child data from inline anonymous complex types" do + inline_schema_xml = <<~XML + + + + + + + + + + + XML + + parsed = Lutaml::Xml::Schema::Xsd.parse(inline_schema_xml, + validate_schema: false) + inline_root = parsed.element.find { |element| element.name == "InlineRoot" } + + expect(inline_root.attributes.map(&:name)).to include("InlineAttr") + expect(inline_root.child_elements.map(&:name)).to include("InlineChild") + end + + it "returns nil child data when no complex type is referenced" do + expect(simple_element.attributes).to be_nil + expect(simple_element.child_elements).to be_nil + end + + it "resolves referenced objects and types" do + ref_element = described_class.new(__register: Lutaml::Xml::Schema::Xsd.register) + ref_element.ref = "ReferencedElement" + ref_element.lutaml_root = schema + + expect(ref_element.referenced_name).to eq("ReferencedElement") + expect(ref_element.referenced_type).to eq("xs:string") + expect(ref_element.referenced_object).to eq(referenced_element) + end + + it "resolves referenced complex types" do + expect(root_element.referenced_complex_type.name).to eq("RootType") + expect(simple_element.referenced_complex_type).to be_nil + end + + it "finds complex types that use the element" do + expect(root_element.used_by.map(&:name)).to include("UsesRootElement") + end + + it "finds complex types that use a qualified element reference" do + qualified_schema_xml = <<~XML + + + + + + + + + XML + + parsed = Lutaml::Xml::Schema::Xsd.parse(qualified_schema_xml, + validate_schema: false) + qualified_root = parsed.element.find do |element| + element.name == "QualifiedRoot" + end + + expect(qualified_root.used_by.map(&:name)).to include("UsesQualifiedRoot") + end + + it "exposes helper methods through Liquid" do + expect(root_element.to_liquid.referenced_name).to eq("RootElement") + end +end diff --git a/spec/lutaml/xml/schema/xsd/liquid_methods/schema_spec.rb b/spec/lutaml/xml/schema/xsd/liquid_methods/schema_spec.rb new file mode 100644 index 000000000..8452093ed --- /dev/null +++ b/spec/lutaml/xml/schema/xsd/liquid_methods/schema_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe Lutaml::Xml::Schema::Xsd::Schema do + let(:schema_xml) do + <<~XML + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + XML + end + + let(:schema) { Lutaml::Xml::Schema::Xsd.parse(schema_xml, validate_schema: false) } + + it "sorts elements by name" do + expect(schema.elements_sorted_by_name.map(&:name)) + .to eq(%w[AlphaElement BetaElement GammaElement]) + end + + it "sorts elements with nil names without raising" do + anonymous_element = Lutaml::Xml::Schema::Xsd::Element.new( + __register: Lutaml::Xml::Schema::Xsd.register, + ) + schema.element << anonymous_element + + expect { schema.elements_sorted_by_name }.not_to raise_error + expect(schema.elements_sorted_by_name.first.name).to be_nil + end + + it "sorts complex types by name" do + expect(schema.complex_types_sorted_by_name.map(&:name)) + .to eq(%w[AlphaType BetaType WidgetType]) + end + + it "sorts attribute groups by name" do + expect(schema.attribute_groups_sorted_by_name.map(&:name)) + .to eq(%w[BaseAttributes CoreAttributes ZetaAttributes]) + end + + it "exposes sorting helpers through Liquid" do + expect(schema.to_liquid.elements_sorted_by_name.map(&:name)) + .to eq(%w[AlphaElement BetaElement GammaElement]) + end +end diff --git a/spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb b/spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb new file mode 100644 index 000000000..5722a236f --- /dev/null +++ b/spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe "XSD simple content liquid helpers" do + let(:schema_xml) do + <<~XML + + + + + + + + + + + + + + + + + + + + + + + + + + + + + XML + end + + let(:schema) { Lutaml::Xml::Schema::Xsd.parse(schema_xml, validate_schema: false) } + let(:complex_types) { schema.complex_type } + let(:simple_content) do + complex_types.find do |complex_type| + complex_type.name == "SimpleContentType" + end.simple_content + end + + it "returns attributes from the extension and attribute groups" do + expect(simple_content.attribute_elements.map(&:name)).to include( + "ExtendedAttr", "GroupAttr1" + ) + expect(simple_content.to_liquid.attribute_elements.map(&:name)).to include( + "ExtendedAttr", "GroupAttr1" + ) + end + + it "resolves base type from extension, restriction, or inline base" do + with_base = complex_types.find do |complex_type| + complex_type.name == "SimpleContentWithBase" + end + with_restriction = complex_types.find do |complex_type| + complex_type.name == "SimpleContentWithRestriction" + end + + expect(simple_content.base_type).to eq("xs:string") + expect(with_base.simple_content.base_type).to eq("xs:int") + expect(with_restriction.simple_content.base_type).to eq("xs:string") + end + + it "returns attributes from simple content restrictions" do + with_restriction = complex_types.find do |type| + type.name == "SimpleContentWithRestriction" + end + + expect(with_restriction.simple_content.attribute_elements.map(&:name)) + .to include("RestrictedAttr", "GroupAttr1") + end +end diff --git a/spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb b/spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb index c28d17309..edda68615 100644 --- a/spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb +++ b/spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb @@ -2,6 +2,8 @@ require_relative "spec_helper" require "lutaml/xml/schema/xsd" +require "fileutils" +require "tmpdir" RSpec.describe "Schema mapping integration" do let(:fixtures_dir) do @@ -231,6 +233,16 @@ to: File.join(fixtures_dir, 'smil20/\1.xsd') }, # 19-21. URL mappings + { from: %r{https?://schemas\.opengis\.net/citygml/(.+\.xsd)$}, + to: File.join(fixtures_dir, 'citygml/\1') }, + { from: %r{https?://schemas\.opengis\.net/gml/3\.1\.1/base/gml\.xsd$}, + to: File.join(fixtures_dir, + "codesynthesis-gml-3.2.1/gml/3.2.1/gml.xsd") }, + { from: %r{https?://schemas\.opengis\.net/gml/3\.1\.1/smil/(.+\.xsd)$}, + to: File.join(fixtures_dir, 'smil20/\1') }, + { from: %r{https?://schemas\.opengis\.net/gml/3\.2\.1/(.+\.xsd)$}, + to: File.join(fixtures_dir, + 'codesynthesis-gml-3.2.1/gml/3.2.1/\1') }, { from: %r{https://schemas\.isotc211\.org/(.+)}, to: File.join(fixtures_dir, 'codesynthesis-gml-3.2.1/iso/\1') }, { from: %r{(?:\.\./)+(\d{5}/.+\.xsd)$}, @@ -565,6 +577,53 @@ end describe "nested schema imports with mappings" do + it "resolves nested includes relative to the imported schema location" do + Dir.mktmpdir do |dir| + nested_dir = File.join(dir, "nested") + FileUtils.mkdir_p(nested_dir) + + root_path = File.join(dir, "root.xsd") + imported_path = File.join(nested_dir, "imported.xsd") + leaf_path = File.join(nested_dir, "leaf.xsd") + + File.write(root_path, <<~XSD) + + + + + XSD + + File.write(imported_path, <<~XSD) + + + + + XSD + + File.write(leaf_path, <<~XSD) + + + + + XSD + + parsed = Lutaml::Xml::Schema::Xsd.parse( + File.read(root_path), + location: root_path, + validate_schema: false, + ) + + imported_schema = parsed.import.first + included_schema = imported_schema.include.first + + expect(included_schema.element.map(&:name)).to include("LeafElement") + end + end + it "resolves nested imports using mappings" do # Use CityGML building schema which imports cityGMLBase citygml_building_path = File.join(fixtures_dir, @@ -575,6 +634,16 @@ # Map cityGMLBase.xsd { from: "../../2.0/cityGMLBase.xsd", to: File.join(fixtures_dir, "citygml/2.0/cityGMLBase.xsd") }, + # Map CityGML schemas via HTTP URL + { from: %r{https?://schemas\.opengis\.net/citygml/(.+\.xsd)$}, + to: File.join(fixtures_dir, 'citygml/\1') }, + # Map GML 3.1.1 entry point used by CityGML fixtures + { from: %r{https?://schemas\.opengis\.net/gml/3\.1\.1/base/gml\.xsd$}, + to: File.join(fixtures_dir, + "codesynthesis-gml-3.2.1/gml/3.2.1/gml.xsd") }, + # Map SMIL files imported by GML style schemas + { from: %r{https?://schemas\.opengis\.net/gml/3\.1\.1/smil/(.+\.xsd)$}, + to: File.join(fixtures_dir, 'smil20/\1') }, # Map GML schemas via HTTP URL { from: %r{http://schemas\.opengis\.net/gml/3\.2\.1/(.+\.xsd)$}, to: File.join(fixtures_dir, 'codesynthesis-gml-3.2.1/gml/3.2.1/\1') },