Class: Lutaml::Xsd::SchemaRepository

Inherits:
Model::Serializable
  • Object
show all
Defined in:
lib/lutaml/xsd/schema_repository.rb,
lib/lutaml/xsd/schema_repository/type_index.rb,
lib/lutaml/xsd/schema_repository/namespace_registry.rb,
lib/lutaml/xsd/schema_repository/qualified_name_parser.rb

Overview

A fully resolved, validated, searchable collection of XSD schemas Provides namespace-aware type resolution across multiple schemas

Defined Under Namespace

Classes: NamespaceRegistry, QualifiedNameParser, TypeIndex

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**attributes) ⇒ SchemaRepository

Returns a new instance of SchemaRepository.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/lutaml/xsd/schema_repository.rb', line 41

def initialize(**attributes)
  # Initialize internal state first
  @parsed_schemas = {}
  @namespace_registry = NamespaceRegistry.new
  @type_index = TypeIndex.new
  @lazy_load = true
  @resolved = false
  @validated = false
  @verbose = false

  # Call super to set attributes from Lutaml::Model::Serializable
  super

  # Register namespace mappings AFTER super sets the attributes
  # This ensures they're available immediately when loading from packages
  return unless namespace_mappings && !namespace_mappings.empty?

  namespace_mappings.each do |mapping|
    @namespace_registry.register(mapping.prefix, mapping.uri)
  end
end

Instance Attribute Details

#lazy_loadObject (readonly)

Internal state (not serialized)



39
40
41
# File 'lib/lutaml/xsd/schema_repository.rb', line 39

def lazy_load
  @lazy_load
end

Class Method Details

.from_file(path) ⇒ SchemaRepository

Auto-detect and load from XSD, LXR, or YAML

Parameters:

  • path (String)

    Path to file (.xsd, .lxr, .yml, or .yaml)

Returns:



751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
# File 'lib/lutaml/xsd/schema_repository.rb', line 751

def self.from_file(path)
  # Check file exists first
  unless File.exist?(path)
    raise Errno::ENOENT,
          "No such file or directory - #{path}"
  end

  case File.extname(path).downcase
  when ".lxr"
    repo = from_package(path)
    # Ensure loaded repository is resolved
    repo.resolve unless repo.instance_variable_get(:@resolved)
    repo
  when ".xsd"
    repo = new
    repo.instance_variable_set(:@files, [File.expand_path(path)])
    repo.parse.resolve
    repo
  when ".yml", ".yaml"
    repo = from_yaml_file(path)
    # Parse and resolve if needed
    repo.parse.resolve	if repo.needs_parsing?
    repo
  else
    raise ConfigurationError,
          "Unsupported file type: #{path}. Expected .xsd, .lxr, .yml, or .yaml"
  end
end

.from_file_cached(source_path, lxr_path: nil) ⇒ SchemaRepository

Smart caching: only rebuild when source is newer than cache

Parameters:

  • source_path (String)

    Path to source file (.xsd or .yml/.yaml)

  • lxr_path (String, nil) (defaults to: nil)

    Optional path to cache file (default: source with .lxr extension)

Returns:



784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
# File 'lib/lutaml/xsd/schema_repository.rb', line 784

def self.from_file_cached(source_path, lxr_path: nil)
  lxr_path ||= source_path.sub(/\.(xsd|ya?ml)$/, ".lxr")

  # Check if cache exists and is fresh
  if File.exist?(lxr_path) &&
      File.mtime(lxr_path) >= File.mtime(source_path)
    # Use from_file to ensure proper resolution
    from_file(lxr_path)
  else
    # Cache missing or stale, rebuild
    repo = from_file(source_path)

    # Create cache package
    repo.to_package(
      lxr_path,
      xsd_mode: :include_all,
      resolution_mode: :resolved,
      serialization_format: :marshal,
    )

    repo
  end
end

.from_package(zip_path) ⇒ SchemaRepository

Load repository from a ZIP package

Parameters:

  • zip_path (String)

    Path to ZIP package file

Returns:



693
694
695
696
# File 'lib/lutaml/xsd/schema_repository.rb', line 693

def self.from_package(zip_path)
  package = SchemaRepositoryPackage.new(zip_path)
  package.load_repository
end

.from_yaml_file(yaml_path) ⇒ SchemaRepository

Load repository configuration from a YAML file

Parameters:

  • yaml_path (String)

    Path to YAML configuration file

Returns:



701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
# File 'lib/lutaml/xsd/schema_repository.rb', line 701

def self.from_yaml_file(yaml_path)
  yaml_content = File.read(yaml_path)
  base_dir = File.dirname(yaml_path)

  # Use Lutaml::Model's from_yaml to deserialize
  repository = from_yaml(yaml_content)

  # Resolve relative paths in files attribute
  if repository.files
    repository.instance_variable_set(
      :@files,
      repository.files.map do |file|
        if File.absolute_path?(file)
          file
        else
          File.expand_path(file,
                           base_dir)
        end
      end,
    )
  end

  # Resolve relative paths in base_packages attribute
  if repository.base_packages
    repository.instance_variable_set(
      :@base_packages,
      repository.base_packages.map do |pkg|
        pkg_path = pkg.package
        unless File.absolute_path?(pkg_path)
          expanded_path = File.expand_path(pkg_path, base_dir)
          pkg.package = expanded_path
        end
        pkg
      end,
    )
  end

  # Resolve relative paths in schema_location_mappings
  repository.schema_location_mappings&.each do |mapping|
    unless File.absolute_path?(mapping.to)
      mapping.to = File.expand_path(mapping.to, base_dir)
    end
  end

  repository
end

.validate_package(zip_path) ⇒ SchemaRepositoryPackage::ValidationResult

Validate a schema repository package

Parameters:

  • zip_path (String)

    Path to ZIP package file

Returns:



685
686
687
688
# File 'lib/lutaml/xsd/schema_repository.rb', line 685

def self.validate_package(zip_path)
  package = SchemaRepositoryPackage.new(zip_path)
  package.validate
end

Instance Method Details

#add_schema_file(file_path) ⇒ void

This method returns an undefined value.

Add a schema file to the repository

Parameters:

  • file_path (String)

    Path to the schema file



644
645
646
647
# File 'lib/lutaml/xsd/schema_repository.rb', line 644

def add_schema_file(file_path)
  @files ||= []
  @files << file_path unless @files.include?(file_path)
end

#add_schema_files(file_paths) ⇒ void

This method returns an undefined value.

Add multiple schema files to the repository

Parameters:

  • file_paths (Array<String>)

    Paths to the schema files



652
653
654
# File 'lib/lutaml/xsd/schema_repository.rb', line 652

def add_schema_files(file_paths)
  file_paths.each { |fp| add_schema_file(fp) }
end

#add_schema_location_mapping(mapping) ⇒ void

This method returns an undefined value.

Add a schema location mapping for resolving import/include paths

Parameters:



659
660
661
662
663
664
665
666
667
668
669
670
671
672
# File 'lib/lutaml/xsd/schema_repository.rb', line 659

def add_schema_location_mapping(mapping)
  @schema_location_mappings ||= []
  mapping_obj = if mapping.is_a?(SchemaLocationMapping)
                  mapping
                elsif mapping.is_a?(Hash)
                  SchemaLocationMapping.from_hash(mapping)
                else
                  raise ArgumentError,
                        "Expected SchemaLocationMapping or Hash, got #{mapping.class}"
                end
  @schema_location_mappings << mapping_obj unless @schema_location_mappings.any? do |m|
    m.from == mapping_obj.from
  end
end

#all_namespacesArray<String>

Get all registered namespace URIs

Returns:

  • (Array<String>)


442
443
444
# File 'lib/lutaml/xsd/schema_repository.rb', line 442

def all_namespaces
  @namespace_registry.all_uris
end

#all_schemasHash

Get all processed schemas (public accessor for validators/analyzers)

Returns:

  • (Hash)

    All schemas from the global processed_schemas cache



566
567
568
# File 'lib/lutaml/xsd/schema_repository.rb', line 566

def all_schemas
  get_all_processed_schemas
end

#all_type_names(namespace: nil, category: nil) ⇒ Array<String>

List all type names

Parameters:

  • namespace (String, nil) (defaults to: nil)

    Filter by namespace URI (optional)

  • category (Symbol, nil) (defaults to: nil)

    Filter by category (optional)

Returns:

  • (Array<String>)

    List of qualified type names



457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/lutaml/xsd/schema_repository.rb', line 457

def all_type_names(namespace: nil, category: nil)
  types = []

  @type_index.all.each_value do |type_info|
    # Filter by namespace if specified
    next if namespace && type_info[:namespace] != namespace

    # Filter by category if specified
    next if category && type_info[:type] != category

    # Build qualified name
    ns = type_info[:namespace]
    name = type_info[:definition]&.name
    next unless name

    prefix = namespace_to_prefix(ns)
    qualified_name = prefix ? "#{prefix}:#{name}" : name
    types << qualified_name
  end

  types.sort
end

#analyze_coverage(entry_types: []) ⇒ CoverageReport

Analyze coverage based on entry point types

Parameters:

  • entry_types (Array<String>) (defaults to: [])

    Entry point type names

Returns:



549
550
551
552
553
# File 'lib/lutaml/xsd/schema_repository.rb', line 549

def analyze_coverage(entry_types: [])
  require_relative "coverage_analyzer"
  analyzer = CoverageAnalyzer.new(self)
  analyzer.analyze(entry_types: entry_types)
end

#analyze_type_hierarchy(qualified_name, depth: 10) ⇒ Hash?

Analyze type inheritance hierarchy

Parameters:

  • qualified_name (String)

    The qualified type name (e.g., “gml:AbstractFeatureType”)

  • depth (Integer) (defaults to: 10)

    Maximum depth to traverse (default: 10)

Returns:

  • (Hash, nil)

    Hierarchy analysis result or nil if type not found



540
541
542
543
544
# File 'lib/lutaml/xsd/schema_repository.rb', line 540

def analyze_type_hierarchy(qualified_name, depth: 10)
  require_relative "type_hierarchy_analyzer"
  analyzer = TypeHierarchyAnalyzer.new(self)
  analyzer.analyze(qualified_name, depth: depth)
end

#apply_namespace_remapping_to_schemas(schemas, remappings) ⇒ Hash

Apply namespace remapping to schemas

Parameters:

  • schemas (Hash)

    Schema hash to transform

  • remappings (Array<NamespaceUriRemapping>)

    Remapping rules

Returns:

  • (Hash)

    Transformed schemas



940
941
942
943
944
945
946
947
948
949
# File 'lib/lutaml/xsd/schema_repository.rb', line 940

def apply_namespace_remapping_to_schemas(schemas, remappings)
  uri_mappings = {}
  remappings.each { |remap| uri_mappings[remap.from_uri] = remap.to_uri }

  # This is a simplified version - in practice, you'd need to
  # deeply transform all namespace references in the schema objects
  # For now, just return the schemas as-is since remapping is
  # handled during conflict detection
  schemas
end

#base_packagesObject

Override base_packages getter to ensure array



34
35
36
# File 'lib/lutaml/xsd/schema_repository.rb', line 34

def base_packages
  @base_packages || []
end

#base_packages=(value) ⇒ Object

Override base_packages setter to handle mixed types



29
30
31
# File 'lib/lutaml/xsd/schema_repository.rb', line 29

def base_packages=(value)
  @base_packages = value
end

#classify_schemasHash

Classify schemas by role and resolution status

Returns:

  • (Hash)

    Classification results



434
435
436
437
438
# File 'lib/lutaml/xsd/schema_repository.rb', line 434

def classify_schemas
  require_relative "schema_classifier"
  classifier = SchemaClassifier.new(self)
  classifier.classify
end

#configure_namespace(prefix:, uri:) ⇒ self

Configure a single namespace prefix mapping

Parameters:

  • prefix (String)

    The namespace prefix (e.g., “gml”)

  • uri (String)

    The namespace URI

Returns:

  • (self)


213
214
215
216
217
218
# File 'lib/lutaml/xsd/schema_repository.rb', line 213

def configure_namespace(prefix:, uri:)
  @namespace_mappings ||= []
  @namespace_mappings << NamespaceMapping.new(prefix: prefix, uri: uri)
  @namespace_registry.register(prefix, uri)
  self
end

#configure_namespaces(mappings) ⇒ self

Configure multiple namespace prefix mappings

Parameters:

  • mappings (Hash, Array)

    Prefix-to-URI mappings

Returns:

  • (self)


223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/lutaml/xsd/schema_repository.rb', line 223

def configure_namespaces(mappings)
  case mappings
  when Hash
    mappings.each do |prefix, uri|
      configure_namespace(prefix: prefix, uri: uri)
    end
  when Array
    mappings.each do |mapping|
      if mapping.is_a?(NamespaceMapping)
        configure_namespace(prefix: mapping.prefix, uri: mapping.uri)
      elsif mapping.is_a?(Hash)
        prefix = mapping[:prefix] || mapping["prefix"]
        uri = mapping[:uri] || mapping["uri"]
        configure_namespace(prefix: prefix, uri: uri)
      end
    end
  end
  self
end

#configure_schema_location_mappings(mappings) ⇒ self

Configure schema location mappings for imports/includes

Parameters:

Returns:

  • (self)


677
678
679
680
# File 'lib/lutaml/xsd/schema_repository.rb', line 677

def configure_schema_location_mappings(mappings)
  mappings.each { |m| add_schema_location_mapping(m) }
  self
end

#elements_by_namespace(namespace_uri: nil) ⇒ Hash{String => Array<Hash>}

Get all elements organized by namespace Returns hash: { namespace_uri => [type, minOccurs, maxOccurs, documentation] }

Parameters:

  • namespace_uri (String, nil) (defaults to: nil)

    Filter by specific namespace URI (optional)

Returns:

  • (Hash{String => Array<Hash>})

    Elements grouped by namespace



580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/lutaml/xsd/schema_repository.rb', line 580

def elements_by_namespace(namespace_uri: nil)
  results = {}

  get_all_processed_schemas.each_value do |schema|
    ns = schema.target_namespace
    next if namespace_uri && ns != namespace_uri

    results[ns] ||= []

    (schema.element || []).each do |elem|
      results[ns] << {
        name: elem.name,
        qualified_name: "#{namespace_to_prefix(ns)}:#{elem.name}",
        type: elem.type || "(inline complex type)",
        min_occurs: elem.min_occurs || "1",
        max_occurs: elem.max_occurs || "1",
        documentation: extract_element_documentation(elem),
      }
    end
  end

  results
end

#export_statistics(format: :yaml) ⇒ String

Export statistics in different formats

Parameters:

  • format (Symbol) (defaults to: :yaml)

    Output format (:yaml, :json, or :text)

Returns:

  • (String)

    Formatted statistics



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'lib/lutaml/xsd/schema_repository.rb', line 483

def export_statistics(format: :yaml)
  stats = statistics

  case format
  when :yaml
    require "yaml"
    stats.to_yaml
  when :json
    require "json"
    JSON.pretty_generate(stats)
  when :text
    format_statistics_as_text(stats)
  else
    raise ArgumentError, "Unsupported format: #{format}"
  end
end

#find_attribute(qualified_name) ⇒ Attribute?

Find an attribute definition by qualified name Searches across all schemas in the repository

Parameters:

  • qualified_name (String)

    Qualified attribute name (e.g., “xml:id”)

Returns:

  • (Attribute, nil)

    The attribute definition or nil if not found



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/lutaml/xsd/schema_repository.rb', line 311

def find_attribute(qualified_name)
  # Parse the qualified name
  parsed = QualifiedNameParser.parse(qualified_name, @namespace_registry)
  return nil unless parsed

  namespace_uri = parsed[:namespace]
  local_name = parsed[:local_name]

  # Look up attribute in the type index
  attr_info = @type_index.find_by_namespace_and_name(namespace_uri,
                                                     local_name)

  # Return the definition if it's an attribute
  return unless attr_info && attr_info[:type] == :attribute

  attr_info[:definition]
end

#find_attribute_group(qualified_name) ⇒ AttributeGroup?

Find an attribute group definition by qualified name Searches across all schemas in the repository

Parameters:

  • qualified_name (String)

    Qualified attribute group name

Returns:

  • (AttributeGroup, nil)

    The attribute group definition or nil if not found



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/lutaml/xsd/schema_repository.rb', line 388

def find_attribute_group(qualified_name)
  parsed = parse_qualified_name(qualified_name)
  return nil unless parsed

  namespace_uri = parsed[:namespace]
  local_name = parsed[:local_name]

  # Get all processed schemas (including those from loaded packages)
  all_schemas = get_all_processed_schemas

  all_schemas.each_value do |schema|
    next unless schema.target_namespace == namespace_uri

    ag = schema.attribute_group.find { |g| g.name == local_name }
    return ag if ag
  end

  nil
end

#find_element(qualified_name) ⇒ Element?

Find an element definition by qualified name Searches across all schemas in the repository

Parameters:

  • qualified_name (String)

    Qualified element name (e.g., “gml:FeatureCollection”)

Returns:

  • (Element, nil)

    The element definition or nil if not found



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/lutaml/xsd/schema_repository.rb', line 333

def find_element(qualified_name)
  # Parse the qualified name
  parsed = parse_qualified_name(qualified_name)
  return nil unless parsed

  namespace_uri = parsed[:namespace]
  local_name = parsed[:local_name]

  # Get all processed schemas (including those from loaded packages)
  all_schemas = get_all_processed_schemas

  # Search all schemas
  all_schemas.each_value do |schema|
    # For unprefixed names (namespace_uri is nil), search in all namespaces
    # For prefixed names, only search in matching namespace
    next if namespace_uri && schema.target_namespace != namespace_uri

    # Search in top-level elements
    elements = schema.element
    elements = [elements] unless elements.is_a?(Array)
    elem = elements.compact.find { |e| e.name == local_name }
    return elem if elem
  end

  nil
end

#find_group(qualified_name) ⇒ Group?

Find a group definition by qualified name Searches across all schemas in the repository

Parameters:

  • qualified_name (String)

    Qualified group name

Returns:

  • (Group, nil)

    The group definition or nil if not found



364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/lutaml/xsd/schema_repository.rb', line 364

def find_group(qualified_name)
  parsed = parse_qualified_name(qualified_name)
  return nil unless parsed

  namespace_uri = parsed[:namespace]
  local_name = parsed[:local_name]

  # Get all processed schemas (including those from loaded packages)
  all_schemas = get_all_processed_schemas

  all_schemas.each_value do |schema|
    next unless schema.target_namespace == namespace_uri

    grp = schema.group.find { |g| g.name == local_name }
    return grp if grp
  end

  nil
end

#find_type(qname) ⇒ TypeResolutionResult

Resolve a qualified type name to its definition

Parameters:

  • qname (String)

    Qualified name (e.g., “gml:CodeType”, “http://…CodeType”)

Returns:



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/lutaml/xsd/schema_repository.rb', line 246

def find_type(qname)
  resolution_path = [qname]

  # Parse the qualified name
  parsed = QualifiedNameParser.parse(qname, @namespace_registry)
  unless parsed
    return TypeResolutionResult.failure(
      qname: qname,
      error_message: "Failed to parse qualified name: #{qname}",
      resolution_path: resolution_path,
    )
  end

  namespace = parsed[:namespace]
  local_name = parsed[:local_name]

  # Add Clark notation to resolution path
  clark_notation = QualifiedNameParser.to_clark_notation(parsed)
  resolution_path << clark_notation if clark_notation != qname

  # Check if namespace was resolved for prefixed names
  # For unprefixed names, namespace can be nil and that's valid
  if parsed[:prefix] && !namespace
    return TypeResolutionResult.failure(
      qname: qname,
      local_name: local_name,
      error_message: "Namespace prefix '#{parsed[:prefix]}' not registered",
      resolution_path: resolution_path,
    )
  end

  # Look up type in index
  type_info = @type_index.find_by_namespace_and_name(namespace,
                                                     local_name)

  if type_info
    resolution_path << "#{type_info[:schema_file]}##{local_name}"

    TypeResolutionResult.success(
      qname: qname,
      namespace: namespace,
      local_name: local_name,
      definition: type_info[:definition],
      schema_file: type_info[:schema_file],
      resolution_path: resolution_path,
    )
  else
    # Provide suggestions for similar types
    suggestions = @type_index.suggest_similar(namespace, local_name)
    suggestion_text = suggestions.empty? ? "" : " Did you mean: #{suggestions.join(', ')}?"

    TypeResolutionResult.failure(
      qname: qname,
      namespace: namespace,
      local_name: local_name,
      error_message: "Type '#{local_name}' not found in namespace '#{namespace}'.#{suggestion_text}",
      resolution_path: resolution_path,
    )
  end
end

#load_base_packages_with_conflict_detection(glob_mappings) ⇒ Object

Load packages with conflict detection and resolution

Parameters:

  • glob_mappings (Array<Hash>)

    Schema location mappings

Raises:



834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
# File 'lib/lutaml/xsd/schema_repository.rb', line 834

def load_base_packages_with_conflict_detection(glob_mappings)
  configs = normalize_base_packages_to_configs

  # Validate all configs
  configs.each do |config|
    result = config.validate
    raise ValidationFailedError, result if result.invalid?
  end

  if @verbose
    puts "Detecting conflicts in #{configs.size} package(s)..."
  end

  # Detect conflicts
  detector = PackageConflictDetector.new(configs)
  report = detector.detect_conflicts

  if @verbose && report.has_conflicts?
    puts "⚠️  #{report.total_conflicts} conflict(s) detected"
  elsif @verbose
    puts "✓ No conflicts detected"
  end

  # Resolve conflicts (may raise PackageMergeError)
  resolver = PackageConflictResolver.new(report, report.package_sources)
  ordered_sources = resolver.resolve

  if @verbose
    puts "Loading #{ordered_sources.size} package(s) in priority order..."
  end

  # Load packages in resolved order
  ordered_sources.each_with_index do |source, idx|
    if @verbose
      print "\r[#{idx + 1}/#{ordered_sources.size}] #{File.basename(source.package_path)}"
      $stdout.flush
    end

    load_package_with_filtering(source, glob_mappings)
  end

  puts "\n✓ All packages merged successfully" if @verbose
end

#load_package_with_filtering(package_source, _glob_mappings) ⇒ Object

Load a single package with schema filtering

Parameters:

  • package_source (PackageSource)

    The package to load

  • glob_mappings (Array<Hash>)

    Schema location mappings



881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
# File 'lib/lutaml/xsd/schema_repository.rb', line 881

def load_package_with_filtering(package_source, _glob_mappings)
  repo = package_source.repository

  # Get all schemas from the package
  all_schemas = repo.instance_variable_get(:@parsed_schemas) || {}

  # Apply schema filtering from config
  filtered_schemas = all_schemas.select do |path, _schema|
    package_source.include_schema?(path)
  end

  # Apply namespace remapping if configured
  if package_source.namespace_remapping.any?
    filtered_schemas = apply_namespace_remapping_to_schemas(
      filtered_schemas,
      package_source.namespace_remapping,
    )
  end

  # Merge filtered schemas into current repository
  @parsed_schemas.merge!(filtered_schemas)

  # Merge files list
  @files ||= []
  filtered_files = (repo.files || []).select do |file|
    package_source.include_schema?(file)
  end
  @files.concat(filtered_files)

  # Merge namespace mappings
  repo.namespace_mappings&.each do |mapping|
    configure_namespace(prefix: mapping.prefix, uri: mapping.uri)
  end

  # Merge schema location mappings
  repo.schema_location_mappings&.each do |mapping|
    @schema_location_mappings ||= []
    unless @schema_location_mappings.any? { |m| m.from == mapping.from }
      @schema_location_mappings << mapping
    end
  end
end

#namespace_prefix_detailsArray<NamespacePrefixInfo>

Get detailed namespace prefix information

Returns:



523
524
525
526
# File 'lib/lutaml/xsd/schema_repository.rb', line 523

def namespace_prefix_details
  manager = NamespacePrefixManager.new(self)
  manager.detailed_prefix_info
end

#namespace_summaryArray<Hash>

Namespace summary

Returns:

  • (Array<Hash>)

    Summary of each namespace



502
503
504
505
506
507
508
509
510
# File 'lib/lutaml/xsd/schema_repository.rb', line 502

def namespace_summary
  all_namespaces.map do |ns|
    {
      uri: ns,
      prefix: namespace_to_prefix(ns),
      types: types_in_namespace(ns).size,
    }
  end
end

#namespace_to_prefix(namespace_uri) ⇒ String?

Get the namespace prefix for a URI

Parameters:

  • namespace_uri (String, nil)

    The namespace URI

Returns:

  • (String, nil)

    The prefix or nil



515
516
517
518
519
# File 'lib/lutaml/xsd/schema_repository.rb', line 515

def namespace_to_prefix(namespace_uri)
  return nil if namespace_uri.nil? || namespace_uri.empty?

  @namespace_registry.get_primary_prefix(namespace_uri)
end

#needs_parsing?Boolean

Check if repository needs parsing Used by demo scripts to determine if parse() should be called

Returns:

  • (Boolean)

    True if schemas need to be parsed from XSD files



635
636
637
638
639
# File 'lib/lutaml/xsd/schema_repository.rb', line 635

def needs_parsing?
  # Check if schemas are already in the global cache
  # (either from package loading or previous parse)
  get_all_processed_schemas.empty?
end

#normalize_base_packages_to_configsArray<BasePackageConfig>

Normalize base_packages to BasePackageConfig objects Handles both legacy string format and new hash/config format

Returns:



811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
# File 'lib/lutaml/xsd/schema_repository.rb', line 811

def normalize_base_packages_to_configs
  (base_packages || []).map do |pkg|
    case pkg
    when String
      BasePackageConfig.new(package: pkg)
    when Hash
      # Convert string keys to symbols for BasePackageConfig
      symbolized = pkg.transform_keys do |k|
        k.is_a?(String) ? k.to_sym : k
      end
      BasePackageConfig.new(**symbolized)
    when BasePackageConfig
      pkg
    else
      # Fallback for any other type
      BasePackageConfig.new(package: pkg.to_s)
    end
  end
end

#parse(schema_locations: {}, lazy_load: true, verbose: false) ⇒ self

Parse XSD schemas from configured files and base packages

Parameters:

  • schema_locations (Hash) (defaults to: {})

    Additional schema location mappings

  • lazy_load (Boolean) (defaults to: true)

    Whether to lazy load imported schemas

  • verbose (Boolean) (defaults to: false)

    Whether to show progress indicators

Returns:

  • (self)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/lutaml/xsd/schema_repository.rb', line 68

def parse(schema_locations: {}, lazy_load: true, verbose: false)
  @lazy_load = lazy_load
  @verbose = verbose

  # Register namespace mappings loaded from YAML with the namespace registry
  if namespace_mappings && !namespace_mappings.empty?
    namespace_mappings.each do |mapping|
      @namespace_registry.register(mapping.prefix, mapping.uri)
    end
  end

  # Convert schema_location_mappings to Glob format
  glob_mappings = (schema_location_mappings || []).map(&:to_glob_format)

  # Add any additional schema locations
  if schema_locations && !schema_locations.empty?
    schema_locations.each do |from, to|
      glob_mappings << { from: from, to: to }
    end
  end

  # Load base packages first - auto-detect which method to use
  if base_packages&.any?
    if supports_conflict_detection?
      load_base_packages_with_conflict_detection(glob_mappings)
    else
      load_base_packages(glob_mappings)
    end
  end

  # Parse each schema file with progress indicators
  if @verbose
    puts "Parsing #{(files || []).size} schema files..."
    (files || []).each_with_index do |file_path, idx|
      print "\r[#{idx + 1}/#{(files || []).size}] #{File.basename(file_path)}"
      $stdout.flush
      parse_schema_file(file_path, glob_mappings)
    end
    puts "\n✓ All schemas parsed"
  else
    (files || []).each do |file_path|
      parse_schema_file(file_path, glob_mappings)
    end
  end

  self
end

#parse_qualified_name(qualified_name) ⇒ Hash?

Parse a qualified name into its components

Parameters:

  • qualified_name (String)

    The qualified name to parse

Returns:

  • (Hash, nil)

    Parsed components with :prefix, :namespace, :local_name



412
413
414
# File 'lib/lutaml/xsd/schema_repository.rb', line 412

def parse_qualified_name(qualified_name)
  QualifiedNameParser.parse(qualified_name, @namespace_registry)
end

#remap_namespace_prefixes(changes) ⇒ SchemaRepository

Remap namespace prefixes

Parameters:

  • changes (Hash)

    Mapping of old_prefix => new_prefix

Returns:



531
532
533
534
# File 'lib/lutaml/xsd/schema_repository.rb', line 531

def remap_namespace_prefixes(changes)
  remapper = NamespaceRemapper.new(self)
  remapper.remap(changes)
end

#resolve(verbose: false) ⇒ self

Force full resolution of all imports/includes and build indexes

Parameters:

  • verbose (Boolean) (defaults to: false)

    Whether to show progress indicators

Returns:

  • (self)


119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/lutaml/xsd/schema_repository.rb', line 119

def resolve(verbose: false)
  return self if @resolved

  @verbose = verbose

  # Get all processed schemas including imports/includes
  all_schemas = get_all_processed_schemas

  if @verbose
    total_imports = count_total_imports(all_schemas)
    if total_imports.positive?
      puts "Resolving #{total_imports} schema dependencies..."

      processed = 0
      all_schemas.each_value do |schema|
        imports = schema.respond_to?(:import) ? schema.import : []
        (imports || []).each do |import|
          processed += 1
          namespace_info = import.respond_to?(:namespace) ? (import.namespace || "no namespace") : "unknown"
          print "\r[#{processed}/#{total_imports}] #{namespace_info}"
          $stdout.flush
        end
      end
      puts "\n✓ All dependencies resolved"
    else
      puts "✓ No schema dependencies to resolve"
    end
  end

  # Extract namespaces from parsed schemas if not configured
  if namespace_mappings.nil? || namespace_mappings.empty?
    @namespace_registry.extract_from_schemas(all_schemas.values)
  else
    # Register namespace mappings from configuration
    namespace_mappings.each do |mapping|
      @namespace_registry.register(mapping.prefix, mapping.uri)
    end
  end

  # Build type index from all parsed schemas (including imported/included)
  @type_index.build_from_schemas(all_schemas)

  @resolved = true
  self
end

#schemasHash

Get all schemas (alias for compatibility)

Returns:

  • (Hash)

    All schemas from the global processed_schemas cache



572
573
574
# File 'lib/lutaml/xsd/schema_repository.rb', line 572

def schemas
  get_all_processed_schemas
end

#statisticsHash

Get repository statistics

Returns:

  • (Hash)

    Statistics about the repository



418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/lutaml/xsd/schema_repository.rb', line 418

def statistics
  type_stats = @type_index.statistics

  {
    total_schemas: @parsed_schemas.size,
    total_types: type_stats[:total_types],
    types_by_category: type_stats[:by_type],
    total_namespaces: type_stats[:namespaces],
    namespace_prefixes: @namespace_registry.all_prefixes.size,
    resolved: @resolved,
    validated: @validated,
  }
end

#supports_conflict_detection?Boolean

Check if base_packages contains configuration objects

Returns:

  • (Boolean)


926
927
928
929
930
931
932
933
934
# File 'lib/lutaml/xsd/schema_repository.rb', line 926

def supports_conflict_detection?
  return false unless base_packages&.any?

  # If any element is a Hash or BasePackageConfig, use conflict detection
  base_packages.any? do |pkg|
    pkg.is_a?(Hash) || pkg.is_a?(BasePackageConfig) ||
      (pkg.is_a?(String) && pkg.start_with?("{"))
  end
end

#to_package(output_path, xsd_mode: :include_all, resolution_mode: :resolved, serialization_format: :marshal, metadata: {}) ⇒ SchemaRepositoryPackage

Export repository as a ZIP package with schemas and metadata

Parameters:

  • output_path (String)

    Path to output ZIP file

  • xsd_mode (Symbol) (defaults to: :include_all)

    :include_all or :allow_external

  • resolution_mode (Symbol) (defaults to: :resolved)

    :bare or :resolved

  • serialization_format (Symbol) (defaults to: :marshal)

    :marshal, :json, :yaml, or :parse

  • metadata (Hash) (defaults to: {})

    Additional metadata to include

Returns:



611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'lib/lutaml/xsd/schema_repository.rb', line 611

def to_package(output_path, xsd_mode: :include_all, resolution_mode: :resolved, serialization_format: :marshal,
               metadata: {})
  # Ensure repository is resolved if creating resolved package
  resolve unless @resolved || resolution_mode == :bare

  # Create package configuration
  config = PackageConfiguration.new(
    xsd_mode: xsd_mode,
    resolution_mode: resolution_mode,
    serialization_format: serialization_format,
  )

  # Delegate to SchemaRepositoryPackage
  SchemaRepositoryPackage.create(
    repository: self,
    output_path: output_path,
    config: config,
    metadata: ,
  )
end

#type_exists?(qualified_name) ⇒ Boolean

Quick type existence check

Parameters:

  • qualified_name (String)

    Qualified name (e.g., “gml:CodeType”)

Returns:

  • (Boolean)

    True if type exists and is resolved



449
450
451
# File 'lib/lutaml/xsd/schema_repository.rb', line 449

def type_exists?(qualified_name)
  find_type(qualified_name).resolved?
end

#validate(strict: false) ⇒ Array<String>

Validate the repository

Parameters:

  • strict (Boolean) (defaults to: false)

    Whether to fail on first error or collect all

Returns:

  • (Array<String>)

    List of validation errors (empty if valid)



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/lutaml/xsd/schema_repository.rb', line 168

def validate(strict: false)
  errors = []

  # Check that all files exist and are accessible
  (files || []).each do |file_path|
    next if File.exist?(file_path)

    error = "Schema file not found: #{file_path}"
    errors << error
    raise Error, error if strict
  end

  # Check that all schemas were parsed successfully
  missing_schemas = (files || []).reject { |f| @parsed_schemas.key?(f) }
  unless missing_schemas.empty?
    error = "Failed to parse schemas: #{missing_schemas.join(', ')}"
    errors << error
    raise Error, error if strict
  end

  # Check for circular imports (simple check)
  check_circular_imports(errors, strict)

  # Check that namespace mappings are valid
  (namespace_mappings || []).each do |mapping|
    if mapping.prefix.nil? || mapping.prefix.empty?
      error = "Invalid namespace mapping: prefix cannot be empty"
      errors << error
      raise Error, error if strict
    end
    next unless mapping.uri.nil? || mapping.uri.empty?

    error = "Invalid namespace mapping for prefix '#{mapping.prefix}': URI cannot be empty"
    errors << error
    raise Error, error if strict
  end

  @validated = errors.empty?
  errors
end

#validate_xsd_spec(version: "1.0") ⇒ SpecComplianceReport

Validate XSD specification compliance

Parameters:

  • version (String) (defaults to: "1.0")

    XSD version to validate against (‘1.0’ or ‘1.1’)

Returns:



558
559
560
561
562
# File 'lib/lutaml/xsd/schema_repository.rb', line 558

def validate_xsd_spec(version: "1.0")
  require_relative "xsd_spec_validator"
  validator = XsdSpecValidator.new(self, version: version)
  validator.validate
end