Class: Lutaml::Xml::DeclarationPlan

Inherits:
Object
  • Object
show all
Defined in:
lib/lutaml/xml/declaration_plan.rb,
lib/lutaml/xml/declaration_plan/element_node.rb,
lib/lutaml/xml/declaration_plan/attribute_node.rb

Overview

Represents the complete namespace declaration plan for an XML element

DeclarationPlan uses a TREE STRUCTURE that is isomorphic to XmlDataModel, enabling index-based parallel traversal for W3C-compliant attribute prefix handling.

The tree consists of ElementNode objects (containing AttributeNode arrays), structured identically to the XmlDataModel tree to enable position-based matching.

Examples:

Creating a tree-mode declaration plan

root_node = DeclarationPlan::ElementNode.new(...)
plan = DeclarationPlan.new(root_node: root_node, global_prefix_registry: {...})

Defined Under Namespace

Classes: AttributeNode, ElementNode

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root_node:, global_prefix_registry: {}, input_formats: {}, namespace_classes: {}, children_plans: {}, input_prefix_formats: {}, original_namespace_uris: {}) ⇒ DeclarationPlan

Initialize a declaration plan with tree structure

Parameters:

  • root_node (ElementNode)

    Root element node

  • global_prefix_registry (Hash<String, String>) (defaults to: {})

    Global prefix registry

  • input_formats (Hash<String, Symbol>) (defaults to: {})

    Input format tracking (URI => format)

  • namespace_classes (Hash<String, Class>) (defaults to: {})

    Namespace classes by URI

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

    Children plans for collections

  • original_namespace_uris (Hash<String, String>) (defaults to: {})

    Original alias URI mapping



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/lutaml/xml/declaration_plan.rb', line 59

def initialize(root_node:, global_prefix_registry: {},
               input_formats: {}, namespace_classes: {}, children_plans: {},
               input_prefix_formats: {}, original_namespace_uris: {})
  @root_node = root_node
  @global_prefix_registry = global_prefix_registry
  @input_formats = input_formats
  @namespace_classes = namespace_classes
  @children_plans = children_plans
  @input_prefix_formats = input_prefix_formats
  @original_namespace_uris = original_namespace_uris

  # Performance: Cached lookups
  @namespaces_cache = nil
  @uri_to_info_cache = nil
end

Instance Attribute Details

#children_plansHash (readonly)

Returns Children plans (for collection items).

Returns:

  • (Hash)

    Children plans (for collection items)



40
41
42
# File 'lib/lutaml/xml/declaration_plan.rb', line 40

def children_plans
  @children_plans
end

#global_prefix_registryHash<String, String> (readonly)

Returns Global prefix registry (URI => prefix).

Returns:

  • (Hash<String, String>)

    Global prefix registry (URI => prefix)



26
27
28
# File 'lib/lutaml/xml/declaration_plan.rb', line 26

def global_prefix_registry
  @global_prefix_registry
end

#input_formatsHash<String, Symbol> (readonly)

Returns Input format tracking (URI => :default or :prefix).

Returns:

  • (Hash<String, Symbol>)

    Input format tracking (URI => :default or :prefix)



29
30
31
# File 'lib/lutaml/xml/declaration_plan.rb', line 29

def input_formats
  @input_formats
end

#input_prefix_formatsHash<String, Symbol> (readonly)

Key: “#prefix:#uri” for prefixed, “:#uri” for default Value: :default or :prefix

Returns:

  • (Hash<String, Symbol>)

    Per-(prefix, URI) format tracking for doubly-defined namespaces



34
35
36
# File 'lib/lutaml/xml/declaration_plan.rb', line 34

def input_prefix_formats
  @input_prefix_formats
end

#namespace_classesHash<String, Class> (readonly)

Returns Namespace classes by URI (for namespace lookup).

Returns:

  • (Hash<String, Class>)

    Namespace classes by URI (for namespace lookup)



37
38
39
# File 'lib/lutaml/xml/declaration_plan.rb', line 37

def namespace_classes
  @namespace_classes
end

#namespace_locationsHash<String, Hash>

Format: { “elementName” => { nil => “uri” } or { “prefix” => “uri” } } Used for preserving original namespace URIs during serialization.

Returns:

  • (Hash<String, Hash>)

    Namespace declarations by element path



49
50
51
# File 'lib/lutaml/xml/declaration_plan.rb', line 49

def namespace_locations
  @namespace_locations
end

#original_namespace_urisHash<String, String> (readonly)

Used for round-trip fidelity when namespace has uri_aliases.

Returns:

  • (Hash<String, String>)

    Original alias URI mapping (canonical URI => original alias URI)



44
45
46
# File 'lib/lutaml/xml/declaration_plan.rb', line 44

def original_namespace_uris
  @original_namespace_uris
end

#root_nodeElementNode (readonly)

Returns Root of the element tree containing all decisions.

Returns:

  • (ElementNode)

    Root of the element tree containing all decisions



23
24
25
# File 'lib/lutaml/xml/declaration_plan.rb', line 23

def root_node
  @root_node
end

Class Method Details

.emptyDeclarationPlan

Create an empty plan (for compatibility)

Returns:



132
133
134
135
136
137
138
139
# File 'lib/lutaml/xml/declaration_plan.rb', line 132

def self.empty
  empty_node = ElementNode.new(
    qualified_name: "",
    use_prefix: nil,
    hoisted_declarations: {},
  )
  new(root_node: empty_node, global_prefix_registry: {})
end

.from_input(input_namespaces, mapping) ⇒ DeclarationPlan

Create DeclarationPlan from parsed XML input namespaces Used during deserialization to capture input format for round-trip preservation

Parameters:

  • input_namespaces (Hash)

    Namespace declarations from parsed XML

  • mapping (Xml::Mapping)

    XML mapping

Returns:



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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
# File 'lib/lutaml/xml/declaration_plan.rb', line 147

def self.from_input(input_namespaces, mapping)
  # Create minimal tree with just root node capturing input xmlns
  hoisted = {}

  # Track input formats
  input_formats = {}
  input_prefix_formats = {} # NEW: per-(prefix, URI) format

  input_namespaces.each_value do |ns_config|
    prefix = ns_config[:prefix]
    uri = ns_config[:uri]
    format = ns_config[:format] || (prefix ? :prefix : :default)

    # CRITICAL: Hash key based on FORMAT
    # nil = default namespace (xmlns="...")
    # "prefix" = prefixed namespace (xmlns:prefix="...")
    xmlns_key = if format == :default
                  nil
                else
                  prefix
                end
    hoisted[xmlns_key] = uri

    # Track format used in input for this URI
    # ONLY for default namespace declarations (xmlns="...").
    # For prefixed namespaces (xmlns:prefix="..."), the format is tracked
    # in input_prefix_formats, not input_formats. input_formats tracks
    # namespace-level format for elements WITHOUT their own prefix.
    input_formats[uri] = format if format == :default

    # NEW: Build per-prefix-URI format
    key = prefix ? "#{prefix}:#{uri}" : ":#{uri}"
    input_prefix_formats[key] = format
  end

  # Build global prefix registry (only for prefixed namespaces)
  registry = {}
  input_namespaces.each_value do |ns_config|
    if ns_config[:format] == :prefix && ns_config[:prefix]
      registry[ns_config[:uri]] = ns_config[:prefix]
    end
  end

  root_node = ElementNode.new(
    qualified_name: mapping.root_element || "",
    use_prefix: nil, # Will be determined from input_formats
    hoisted_declarations: hoisted,
  )

  new(root_node: root_node, global_prefix_registry: registry,
      input_formats: input_formats, input_prefix_formats: input_prefix_formats)
end

.from_input_with_locations(namespaces_with_locations, mapping) ⇒ DeclarationPlan

Create DeclarationPlan from parsed XML input namespaces WITH location tracking

This method preserves WHERE each namespace was declared in the original XML, enabling proper round-trip fidelity for namespace declarations.

Parameters:

  • namespaces_with_locations (Hash)

    Location-aware namespace info Format: { path_array => namespace_hash } Where path_array is [] for root, [“child”] for child, etc. And namespace_hash is { prefix_key => { uri:, format:, prefix: } }

  • mapping (Xml::Mapping)

    XML mapping

Returns:



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
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
# File 'lib/lutaml/xml/declaration_plan.rb', line 211

def self.from_input_with_locations(namespaces_with_locations, mapping)
  # Build hoisted declarations for root (empty path)
  root_namespaces = namespaces_with_locations[[]] || {}

  # Track input formats for ALL namespaces (for format preservation)
  input_formats = {}
  input_prefix_formats = {} # NEW: per-(prefix, URI) format
  root_hoisted = {}

  # Process root namespaces
  root_namespaces.each_value do |ns_config|
    prefix = ns_config[:prefix]
    uri = ns_config[:uri]
    format = ns_config[:format] || (prefix ? :prefix : :default)

    xmlns_key = format == :default ? nil : prefix
    root_hoisted[xmlns_key] = uri
    # Only track default namespace format in input_formats
    # Prefixed namespaces are tracked in input_prefix_formats
    input_formats[uri] = format if format == :default

    # NEW: Build per-prefix-URI format
    key = prefix ? "#{prefix}:#{uri}" : ":#{uri}"
    input_prefix_formats[key] = format
  end

  # Build global prefix registry from ALL locations
  registry = {}
  namespaces_with_locations.each_value do |ns_hash|
    ns_hash.each_value do |ns_config|
      if ns_config[:format] == :prefix && ns_config[:prefix]
        registry[ns_config[:uri]] = ns_config[:prefix]
      end
      # Track format for default namespaces only
      # Prefixed namespaces are tracked in input_prefix_formats
      format = ns_config[:format] || (prefix ? :prefix : :default)
      input_formats[ns_config[:uri]] = format if format == :default

      # NEW: Build per-prefix-URI format for all locations
      prefix = ns_config[:prefix]
      uri = ns_config[:uri]
      key = prefix ? "#{prefix}:#{uri}" : ":#{uri}"
      input_prefix_formats[key] =
        ns_config[:format] || (prefix ? :prefix : :default)
    end
  end

  # Build element node tree with location info
  root_node = ElementNode.new(
    qualified_name: mapping.root_element || "",
    use_prefix: nil,
    hoisted_declarations: root_hoisted,
  )

  # Store location data for use during serialization
  # This is the KEY addition - tracking WHERE namespaces were declared
  location_data = {}
  namespaces_with_locations.each do |path, ns_hash|
    next if path.empty? # Root already handled

    # Convert path array to string key for storage
    path_key = path.join("/")
    hoisted = {}
    ns_hash.each_value do |ns_config|
      prefix = ns_config[:prefix]
      uri = ns_config[:uri]
      format = ns_config[:format] || (prefix ? :prefix : :default)
      xmlns_key = format == :default ? nil : prefix
      hoisted[xmlns_key] = uri
    end
    location_data[path_key] = hoisted
  end

  plan = new(root_node: root_node, global_prefix_registry: registry,
             input_formats: input_formats,
             input_prefix_formats: input_prefix_formats)
  plan.namespace_locations = location_data
  plan
end

Instance Method Details

#[](key) ⇒ Hash?

Backward-compatible Hash-like access for older adapters

Parameters:

  • key (Symbol)

    Key to access (:namespaces, :children_plans, :type_namespaces)

Returns:

  • (Hash, nil)

    The requested data as a Hash, or nil for unknown keys



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
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/lutaml/xml/declaration_plan.rb', line 79

def [](key)
  case key
  when :namespaces
    # Convert namespaces to the old Hash format expected by REXML adapter
    ns_hash = {}
    return nil unless @namespace_classes

    @namespace_classes.each do |uri, ns_class|
      key_str = ns_class.to_key

      # Determine format and build ns_config hash
      format = @input_formats[uri] || (
        if @root_node.hoisted_declarations.value?(uri)
          hoisted_key = @root_node.hoisted_declarations.key(uri)
          hoisted_key.nil? ? :default : :prefix
        else
          ns_class.prefix_default ? :prefix : :default
        end
      )

      prefix_override = nil
      if format == :prefix
        hoisted_key = @root_node.hoisted_declarations.key(uri)
        prefix_override = hoisted_key if hoisted_key
      end

      # Build xmlns_declaration string
      xmlns_decl = if format == :prefix
                     "xmlns:#{prefix_override || ns_class.prefix_default}=\"#{uri}\""
                   else
                     "xmlns=\"#{uri}\""
                   end

      ns_hash[key_str] = {
        ns_object: ns_class,
        format: format,
        declared_at: :here,
        xmlns_declaration: xmlns_decl,
        prefix_override: prefix_override,
      }
    end
    ns_hash
  when :children_plans
    @children_plans
  when :type_namespaces
    # type_namespaces is no longer used in the same way, return empty hash
    {}
  end
end

#child_plan(name) ⇒ DeclarationPlan?

Get child element plan by attribute name

Parameters:

  • name (Symbol)

    Child attribute name

Returns:



431
432
433
# File 'lib/lutaml/xml/declaration_plan.rb', line 431

def child_plan(name)
  @children_plans&.dig(name)
end

#collect_ns_classes_recursive(element_node, ns_classes) ⇒ void

This method returns an undefined value.

Recursively collect namespace classes from element nodes

Parameters:

  • element_node (ElementNode)

    Current element node

  • ns_classes (Array<Class>)

    Accumulator for namespace classes



453
454
455
456
457
458
459
460
461
462
463
464
465
# File 'lib/lutaml/xml/declaration_plan.rb', line 453

def collect_ns_classes_recursive(element_node, ns_classes)
  # Add namespace from element's own namespace_class
  if element_node.respond_to?(:qualified_name) && @namespace_classes
    # Try to find namespace class from namespace_classes by matching URI
    # The ElementNode itself doesn't store ns_class, but we can check if
    # any of our namespace_classes match the element's context
  end

  # Recursively collect from children
  element_node.element_nodes.each do |child_node|
    collect_ns_classes_recursive(child_node, ns_classes)
  end
end

#find_namespace_by_uri(uri) ⇒ Hash?

Performance: O(1) namespace lookup by URI

Parameters:

  • uri (String)

    Namespace URI to search for

Returns:

  • (Hash, nil)

    Namespace info hash or nil



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/lutaml/xml/declaration_plan.rb', line 408

def find_namespace_by_uri(uri)
  return nil unless uri

  # Build cache on first access
  unless @uri_to_info_cache
    @uri_to_info_cache = {}
    @root_node.hoisted_declarations.each do |xmlns_key, xmlns_uri|
      @uri_to_info_cache[xmlns_uri] = {
        prefix: xmlns_key,
        format: xmlns_key ? :prefix : :default,
        declared_at: :here,
        uri: xmlns_uri,
      }
    end
  end

  @uri_to_info_cache[uri]
end

#namespace(key) ⇒ NamespaceDeclaration?

Get a specific namespace declaration by key

Parameters:

  • key (String)

    Namespace key (e.g., namespace class to_key)

Returns:



385
386
387
# File 'lib/lutaml/xml/declaration_plan.rb', line 385

def namespace(key)
  namespaces[key]
end

#namespace_declared_at_path?(uri, path) ⇒ Boolean

Check if a namespace was declared at a specific path in the input

Parameters:

  • uri (String)

    Namespace URI to check

  • path (Array<String>)

    Element path

Returns:

  • (Boolean)

    True if namespace was declared at this path



314
315
316
317
318
319
# File 'lib/lutaml/xml/declaration_plan.rb', line 314

def namespace_declared_at_path?(uri, path)
  ns_at_path = namespaces_at_path(path)
  return false unless ns_at_path

  ns_at_path.value?(uri)
end

#namespace_for_class(ns_class) ⇒ NamespaceDeclaration?

Get namespace declaration by namespace class

Parameters:

  • ns_class (Class)

    Namespace class to look up

Returns:



393
394
395
396
397
398
399
400
401
402
# File 'lib/lutaml/xml/declaration_plan.rb', line 393

def namespace_for_class(ns_class)
  return nil unless ns_class && @namespace_classes

  # Find the URI for this namespace class
  uri = @namespace_classes.key(ns_class)
  return nil unless uri

  # Get the namespace using the class's to_key method
  namespace(ns_class.to_key)
end

#namespacesHash<String, NamespaceDeclaration>

Get namespace declarations as a Hash

Converts hoisted_declarations into NamespaceDeclaration objects for querying and inspection.

Returns:



327
328
329
330
331
332
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
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/lutaml/xml/declaration_plan.rb', line 327

def namespaces
  # Performance: Return cached result if available
  return @namespaces_cache if @namespaces_cache

  return {} unless @namespace_classes

  result = {}
  @namespace_classes.each do |uri, ns_class|
    key = ns_class.to_key

    # Determine format by checking hoisted_declarations
    # Priority: input_formats (for preservation) > hoisted_declarations (actual format) > default
    format = @input_formats[uri]
    prefix_override = nil
    unless format
      # Check hoisted_declarations to see what format is actually being used
      # hoisted_declarations keys: nil = default format, "prefix" = prefix format
      hoisted_key = @root_node.hoisted_declarations.key(uri)

      if @root_node.hoisted_declarations.value?(uri)
        # Namespace IS in hoisted_declarations - check what key it has
        hoisted_key = @root_node.hoisted_declarations.key(uri)
        if hoisted_key.nil?
          # Namespace found with nil key = default format
          format = :default
        else
          # Namespace found with prefix key = prefix format
          format = :prefix
          # Set prefix_override to the actual prefix being used
          prefix_override = hoisted_key
        end
      else
        # Namespace NOT found in hoisted_declarations, use default logic
        # For root elements, prefer default format unless namespace has prefix_default and no hoisted
        format = ns_class.prefix_default ? :prefix : :default
      end
    end

    # Check if this format came from input (for from_input? method)
    from_input = @input_formats.key?(uri)

    data = NamespaceDeclarationData.new(
      namespace_class: ns_class,
      format: format,
      declared_at: :here,
      source: from_input ? :input : nil,
      prefix_override: prefix_override,
    )
    result[key] = NamespaceDeclaration.new(data)
  end
  # Performance: Cache the result
  @namespaces_cache = result
end

#namespaces_at_path(path) ⇒ Hash?

Get namespace declarations at a specific element path

Parameters:

  • path (Array<String>)

    Element path (e.g., [“child”, “grandchild”])

Returns:

  • (Hash, nil)

    Namespace declarations at that path, or nil if none



295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/lutaml/xml/declaration_plan.rb', line 295

def namespaces_at_path(path)
  return nil unless @namespace_locations

  # For root path (empty array), return root_node's hoisted declarations
  # since root namespaces are stored there (not in @namespace_locations
  # which only has child paths)
  if path.empty?
    return root_node.hoisted_declarations
  end

  path_key = path.join("/")
  @namespace_locations[path_key]
end

#namespaces_with_schema_locationArray<Class>

Collect all namespace classes in the tree that have schema_location

Returns:

  • (Array<Class>)

    Array of namespace classes with schema_location



438
439
440
441
442
443
444
445
446
# File 'lib/lutaml/xml/declaration_plan.rb', line 438

def namespaces_with_schema_location
  return [] unless @namespace_classes

  ns_classes = []
  collect_ns_classes_recursive(@root_node, ns_classes)
  ns_classes.uniq.select do |ns_class|
    ns_class.respond_to?(:schema_location) && ns_class.schema_location
  end
end

#xsi_prefixString?

Get the XSI prefix from hoisted_declarations

Returns:

  • (String, nil)

    The prefix for XSI namespace if found



470
471
472
473
474
475
476
477
# File 'lib/lutaml/xml/declaration_plan.rb', line 470

def xsi_prefix
  return nil unless @root_node&.hoisted_declarations

  @root_node.hoisted_declarations.each do |prefix, uri|
    return prefix if uri == W3c::XsiNamespace.uri
  end
  nil
end