Class: Lutaml::Xml::FormatChooser

Inherits:
Object
  • Object
show all
Defined in:
lib/lutaml/xml/format_chooser.rb

Overview

Handles namespace format decision logic (default vs prefix)

Implements multi-tier priority system: Tier 1: Stored plan from parsed XML (format preservation) Tier 2: Explicit user preference (options) Tier 3: W3C rules (attributes require prefix) Tier 4: Default preference (cleaner)

Examples:

chooser = FormatChooser.new
format = chooser.choose(mapping, ns_class, needs, options)

Instance Method Summary collapse

Constructor Details

#initialize(register = nil) ⇒ FormatChooser

Initialize format chooser with register for type resolution

Parameters:

  • register (Symbol) (defaults to: nil)

    the register ID for type resolution



21
22
23
# File 'lib/lutaml/xml/format_chooser.rb', line 21

def initialize(register = nil)
  @register = register || Lutaml::Model::Config.default_register
end

Instance Method Details

#build_declaration(ns_class, format, options = {}, prefix_override: nil) ⇒ String

Build xmlns declaration string from XmlNamespace class

Parameters:

  • ns_class (Class)

    the XmlNamespace class

  • format (Symbol)

    :default or :prefix

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

    serialization options (may contain custom prefix)

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

    explicit custom prefix override

Returns:

  • (String)

    the xmlns declaration attribute



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/lutaml/xml/format_chooser.rb', line 189

def build_declaration(ns_class, format, options = {},
  prefix_override: nil)
  # CRITICAL: If namespace has no prefix, MUST use default format
  # Using prefix format without a prefix creates invalid xmlns:=""
  if format == :prefix && !ns_class.prefix_default && !prefix_override
    format = :default
  end

  if format == :default
    "xmlns=\"#{ns_class.uri}\""
  else
    # PRIORITY ORDER: explicit override > options > class default
    prefix = prefix_override ||
      (options[:prefix].is_a?(String) ? options[:prefix] : nil) ||
      (options[:use_prefix].is_a?(String) ? options[:use_prefix] : nil) ||
      ns_class.prefix_default
    "xmlns:#{prefix}=\"#{ns_class.uri}\""
  end
end

#choose(mapping, needs, options) ⇒ Symbol

Choose format for namespace declaration

Implements the decision logic:

  1. Explicit user preference (options or options)

  2. W3C rule: prefix required if attributes in same namespace

  3. Check if we’re declaring in child context with qualified elements

  4. Default: prefer default namespace (cleaner)

Parameters:

  • mapping (Xml::Mapping)

    the element mapping

  • needs (Hash)

    namespace needs (with string keys)

  • options (Hash)

    serialization options

Returns:

  • (Symbol)

    :default or :prefix



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/lutaml/xml/format_chooser.rb', line 37

def choose(mapping, needs, options)
  return :default unless mapping.namespace_class

  # 1. Explicit user preference via prefix or use_prefix option
  # Check options[:prefix] first for backward compatibility
  if options[:prefix].is_a?(String)
    return :prefix
  elsif options[:use_prefix].is_a?(String)
    return :prefix
  end

  # 2. W3C rule: attributes in own namespace REQUIRE prefix
  # Check if this namespace is used for attributes (by key lookup)
  key = mapping.namespace_class.to_key
  if needs[:namespaces][key]
    ns_entry = needs[:namespaces][key]
    # Own namespace used in attributes → MUST use prefix
    return :prefix if ns_entry[:used_in].include?(:attributes)

    # Cascading prefix: If children need this namespace with prefix, provide it
    return :prefix if ns_entry[:children_need_prefix]
  end

  # 3. Check if any child elements use :inherit
  # If they do and we have a prefix, use prefixed format
  # so children can properly reference the namespace
  if mapping.namespace_class.prefix_default && mapping.respond_to?(:elements)
    has_inherit_children = mapping.elements.any? do |elem_rule|
      elem_rule.namespace_param == :inherit
    end
    return :prefix if has_inherit_children

    # Also check if any children have form: :qualified
    # They need prefixed format to reference parent namespace
    has_qualified_children = mapping.elements.any?(&:qualified?)
    return :prefix if has_qualified_children
  end

  # 4. Default: prefer default namespace (cleaner, no prefix needed)
  :default
end

#choose_with_override(mapping, effective_ns_class, needs, options, plan: nil) ⇒ Symbol

Choose format for namespace declaration with override support

Supports custom prefix overrides and stored plan format preservation. This is the main entry point that includes Tier 1 priority check.

Parameters:

  • mapping (Xml::Mapping)

    the element mapping

  • effective_ns_class (Class)

    the effective namespace class (may be override)

  • needs (Hash)

    namespace needs (with string keys)

  • options (Hash)

    serialization options

  • plan (DeclarationPlan, nil) (defaults to: nil)

    current plan for tier 1 check

Returns:

  • (Symbol)

    :default or :prefix



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
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/lutaml/xml/format_chooser.rb', line 90

def choose_with_override(mapping, effective_ns_class, needs, options,
  plan: nil)
  return :default unless effective_ns_class

  # Tier 1: Check for stored plan format (preserve from parsed XML)
  # Tier 1a: Check input_prefix_formats for doubly-defined prefix variants
  # When same URI appears with multiple prefixes (e.g., xmlns:a="..." and xmlns:b="..."),
  # we need to preserve the specific prefix format used at each element.
  # Only check if no explicit user preference is set (prefix: true/false overrides this).
  has_explicit_pref = options.key?(:prefix) || options.key?(:use_prefix)
  if !has_explicit_pref && options[:stored_xml_declaration_plan]
    stored_plan = options[:stored_xml_declaration_plan]
    if stored_plan.respond_to?(:input_prefix_formats)
      # Check canonical URI and all aliases
      uris_to_check = [effective_ns_class.uri] + effective_ns_class.uri_aliases
      stored_plan.input_prefix_formats.each do |key, format|
        matching_uri = uris_to_check.find { |u| key.end_with?(":#{u}") }
        next unless matching_uri

        prefix_in_key = key.split(":").first
        next if prefix_in_key.empty? # skip default namespace

        return format
      end
    end
  end

  # Tier 1b: Check stored plan format (legacy namespace-level format)
  # CRITICAL: This is the format preservation fix from Session 167
  # Only check if no explicit user preference is set (prefix: true/false overrides this).
  unless has_explicit_pref
    if plan && options[:stored_xml_declaration_plan]
      stored_plan = options[:stored_xml_declaration_plan]
      input_ns_decl = stored_plan.namespaces.values.find do |decl|
        decl.from_input? && decl.uri == effective_ns_class.uri
      end
      return input_ns_decl.format if input_ns_decl
    elsif plan
      # Fallback: check current plan for input namespaces
      input_ns_decl = plan.namespaces.values.find do |decl|
        decl.from_input? && decl.uri == effective_ns_class.uri
      end
      return input_ns_decl.format if input_ns_decl
    end
  end

  # Tier 2: Explicit user preference via prefix or use_prefix option
  # Check both options[:prefix] (direct call) and options[:use_prefix] (from serialize.rb)
  if options.key?(:prefix)
    case options[:prefix]
    when true, String
      return :prefix
    when false, nil
      return :default
    end
  elsif options.key?(:use_prefix)
    # options[:use_prefix] can be a string (custom prefix) or boolean
    case options[:use_prefix]
    when true, String
      return :prefix
    when false, nil
      return :default
    end
  end

  # Tier 3: W3C rule: attributes in same namespace require prefix
  # Cascading prefix requirement from children
  key = effective_ns_class.to_key
  if needs[:namespaces][key]
    ns_entry = needs[:namespaces][key]
    # Own namespace used in attributes → MUST use prefix
    return :prefix if ns_entry[:used_in].include?(:attributes)

    # Cascading prefix: If children need this namespace with prefix, provide it
    return :prefix if ns_entry[:children_need_prefix]
  end

  # 3. Check if any child elements use :inherit or form: :qualified
  if effective_ns_class.prefix_default && mapping.respond_to?(:elements)
    has_inherit_children = mapping.elements.any? do |elem_rule|
      elem_rule.namespace_param == :inherit
    end
    return :prefix if has_inherit_children

    has_qualified_children = mapping.elements.any?(&:qualified?)
    return :prefix if has_qualified_children
  end

  # Tier 4: Default: prefer default namespace (cleaner, no prefix needed)
  :default
end