Class: Lutaml::Xml::DeclarationPlanner

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

Overview

Phase 2: Declaration Planning

Builds ElementNode tree with W3C-compliant attribute prefix decisions. The tree is isomorphic to XmlDataModel for index-based parallel traversal.

CRITICAL: This planner ONLY builds trees. NO flat mode.

Instance Method Summary collapse

Constructor Details

#initialize(register = nil) ⇒ DeclarationPlanner

Initialize planner with register for type resolution

Parameters:

  • register (Symbol) (defaults to: nil)

    the register ID for type resolution



16
17
18
# File 'lib/lutaml/xml/declaration_planner.rb', line 16

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

Instance Method Details

#plan(root_element, mapping, needs, parent_plan: nil, options: {}, visited_types: Set.new) ⇒ DeclarationPlan

Create declaration plan tree for XmlElement

Parameters:

  • root_element (XmlDataModel::XmlElement, Model, nil, Class)

    root element, model instance, or nil/Class for unit testing

  • mapping (Xml::Mapping)

    the XML mapping

  • needs (NamespaceNeeds)

    namespace needs from collector

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

    serialization options (may contain :stored_xml_declaration_plan with input_formats)

Returns:



27
28
29
30
31
32
33
34
35
36
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
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
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
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
208
209
210
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
# File 'lib/lutaml/xml/declaration_planner.rb', line 27

def plan(root_element, mapping, needs, parent_plan: nil, options: {},
  visited_types: Set.new)
  # Normalize root_element: transform Model instances to XmlElement
  root_element = normalize_root_element(root_element, mapping, options)

  # Allow nil and Class for unit testing (type analysis without element instance)
  if root_element && !root_element.is_a?(Lutaml::Xml::DataModel::XmlElement) && !root_element.is_a?(Class)
    raise ArgumentError,
          "DeclarationPlanner ONLY works with XmlElement trees. Got: #{root_element.class}"
  end

  # Handle nil or Class root_element for unit testing
  if root_element.nil? || root_element.is_a?(Class)
    # CRITICAL: Resolve type namespace refs BEFORE using type_attribute_namespaces
    TypeNamespaceResolver.new(@register).resolve(needs)

    # Build namespace_classes hash from needs for unit testing
    namespace_classes = {}
    needs.all_namespace_classes.each do |ns_class|
      namespace_classes[ns_class.uri] = ns_class
    end

    # CRITICAL: Add namespace_scope namespaces to namespace_classes
    # (even if not used, :always mode requires them to be declared)
    needs.namespace_scope_configs.each do |scope_config|
      ns_class = scope_config.namespace_class
      namespace_classes[ns_class.uri] ||= ns_class
    end

    # Get element's own namespace (from mapping)
    element_namespace = mapping&.namespace_class

    # Create minimal root node with namespace hoisting info
    # W3C rule: Namespaces used in attributes MUST use prefix format
    hoisted = {}

    # FIRST: Process namespace_scope configurations (root-only)
    needs.namespace_scope_configs.each do |scope_config|
      ns_class = scope_config.namespace_class
      next if ns_class == element_namespace # Don't add root's own namespace here

      # Check :always mode or :auto mode with usage
      ns_usage = needs.namespace(ns_class.to_key)
      should_declare = scope_config.always_mode? ||
        (scope_config.auto_mode? && ns_usage&.used_in&.any?)

      if should_declare
        prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}"
        hoisted[prefix] = ns_class.uri
      end
    end

    # SECOND: Add element's own namespace (if not already added)
    if element_namespace && !hoisted.value?(element_namespace.uri)
      # Check if element's namespace is used in type attributes
      # If so, use prefix format (W3C rule: namespaces in attributes MUST use prefix)
      element_ns_in_attributes = needs.type_attribute_namespaces.any? do |ns|
        ns.uri == element_namespace.uri
      end

      # Check use_prefix option (Tier 1 priority)
      use_prefix_option = options[:use_prefix]

      if element_ns_in_attributes
        # Namespace used in attributes - MUST use prefix format
        prefix = element_namespace.prefix_default || "ns#{hoisted.keys.length}"
        hoisted[prefix] = element_namespace.uri
      elsif use_prefix_option == true
        # Force prefix format when use_prefix: true
        prefix = element_namespace.prefix_default || "ns#{hoisted.keys.length}"
        hoisted[prefix] = element_namespace.uri
      elsif use_prefix_option.is_a?(String)
        # Use custom prefix string
        hoisted[use_prefix_option] = element_namespace.uri
      elsif use_prefix_option == false
        # Force default format when use_prefix: false
        hoisted[nil] = element_namespace.uri
      elsif element_namespace.element_form_default_set? &&
          element_namespace.element_form_default == :unqualified
        # W3C elementFormDefault="unqualified": local elements should be unqualified.
        # When parent uses prefix format, children can simply omit xmlns (blank namespace).
        # When parent uses default format, children need xmlns="" to opt out.
        # Prefer prefix format so children can be truly blank (no xmlns attribute).
        # CRITICAL: Only applies when explicitly set, not when defaulted to :unqualified.
        prefix = element_namespace.prefix_default || "ns#{hoisted.keys.length}"
        hoisted[prefix] = element_namespace.uri
      else
        # Default: prefer default format (cleaner)
        hoisted[nil] = element_namespace.uri
      end
    end

    # THIRD: Add type namespaces (W3C: type namespaces MUST use prefix)
    # CRITICAL: Type namespaces respect namespace_scope directive.
    # When namespace_scope is configured, only hoist type namespaces in scope.
    #
    # Check if namespace_scope is configured
    has_namespace_scope = !needs.namespace_scope_configs.empty?

    # Type attribute namespaces
    needs.type_attribute_namespaces.each do |ns_class|
      ns_uri = ns_class.uri
      next if hoisted.value?(ns_uri) # Skip if already added

      # If namespace_scope is configured, only hoist if in scope
      if has_namespace_scope
        scope_config = needs.scope_config_for(ns_class)
        next unless scope_config
      end

      # Namespaces in attributes MUST use prefix format (W3C rule)
      prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}"
      hoisted[prefix] = ns_class.uri
    end

    # Type element namespaces
    needs.type_element_namespaces.each do |ns_class|
      ns_uri = ns_class.uri
      next if hoisted.value?(ns_uri) # Skip if already added
      next if ns_class == element_namespace # Skip element's own namespace

      # NOTE: Type namespaces are different from child element namespaces.
      # Type namespaces are declared on Type::Value subclasses and used as
      # prefixes on child elements. They MUST be hoisted to root with prefix
      # format (W3C constraint: only one default namespace per element).
      #
      # The condition below only applies to child element namespaces, NOT Type
      # namespaces. Type namespaces should ALWAYS be hoisted.
      #
      # Examples of child element namespaces (should NOT be hoisted if different):
      # - Root has XMI namespace, child has XMI_NEW namespace → child declares
      # - Root has NO namespace, child has XMI namespace → child declares
      #
      # Examples of Type namespaces (should ALWAYS be hoisted):
      # - Root has any namespace, attribute type has XMI namespace → hoist XMI
      # - Root has NO namespace, attribute type has XMI namespace → hoist XMI
      # Type namespaces are NOT about element structure, they're about type identity.

      # If namespace_scope is configured, only hoist if in scope
      if has_namespace_scope
        scope_config = needs.scope_config_for(ns_class)
        next unless scope_config
      end

      # Type element namespaces MUST use prefix format
      prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}"
      hoisted[prefix] = ns_class.uri
    end

    # FOURTH: Add remaining namespaces not in namespace_scope
    needs.all_namespace_classes.each do |ns_class|
      ns_uri = ns_class.uri
      next if hoisted.value?(ns_uri) # Skip if already added

      # Check if this namespace is in namespace_scope (skip if yes)
      scope_config = needs.scope_config_for(ns_class)
      next if scope_config

      # Add remaining namespace (default format preferred)
      hoisted[nil] = ns_class.uri
    end

    root_node = DeclarationPlan::ElementNode.new(
      qualified_name: "",
      use_prefix: nil,
      hoisted_declarations: hoisted,
    )

    # Build children_plans for attributes with Serializable types
    children_plans = (mapping, needs,
                                                        options)

    return DeclarationPlan.new(
      root_node: root_node,
      global_prefix_registry: build_prefix_registry(needs),
      input_formats: {},
      namespace_classes: namespace_classes,
      children_plans: children_plans,
      original_namespace_uris: options[:__original_namespace_uris] || {},
    )
  end

  # TREE PATH: XmlElement tree path
  # CRITICAL: Resolve type namespace refs BEFORE using type_attribute_namespaces
  TypeNamespaceResolver.new(@register).resolve(needs)

  # Extract input_formats from stored plan if present (format preservation)
  input_formats = options[:stored_xml_declaration_plan]&.input_formats || {}
  build_options = options.merge(input_formats: input_formats)

  # Build namespace_classes hash for unit testing compatibility
  namespace_classes = {}
  needs.all_namespace_classes.each do |ns_class|
    namespace_classes[ns_class.uri] = ns_class
  end

  # Build the element node tree recursively (mark root, no parent context)
  root_node = build_element_node(
    root_element, mapping, needs, build_options,
    is_root: true,
    parent_format: nil,
    parent_namespace_class: nil,
    parent_namespace_prefix: nil,
    parent_hoisted: {}
  )

  # Build children_plans for each child element (for child_plan() method)
  children_plans = build_children_plans(root_element, mapping, needs,
                                        build_options)

  # Create DeclarationPlan with tree and input_formats
  DeclarationPlan.new(
    root_node: root_node,
    global_prefix_registry: build_prefix_registry(needs),
    input_formats: input_formats,
    namespace_classes: namespace_classes,
    children_plans: children_plans,
    original_namespace_uris: options[:__original_namespace_uris] || {},
  )
end

#plan_collection(collection, mapping, needs) ⇒ DeclarationPlan

Create declaration plan for a collection

Parameters:

  • collection (Collection)

    the collection object

  • mapping (Xml::Mapping)

    the XML mapping

  • needs (NamespaceNeeds)

    namespace needs from collector

Returns:



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
# File 'lib/lutaml/xml/declaration_planner.rb', line 254

def plan_collection(collection, mapping, needs)
  # For collections, create a plan with children_plans for each item
  root_node = DeclarationPlan::ElementNode.new(
    qualified_name: mapping.root_element || "",
    use_prefix: nil,
    hoisted_declarations: {},
  )

  # Build namespace_classes from needs
  namespace_classes = {}
  needs.all_namespace_classes.each do |ns_class|
    namespace_classes[ns_class.uri] = ns_class
  end

  # Build individual child plans for collection items
  children_plans = build_collection_item_plans(collection, mapping, needs)

  DeclarationPlan.new(
    root_node: root_node,
    global_prefix_registry: build_prefix_registry(needs),
    input_formats: {},
    namespace_classes: namespace_classes,
    children_plans: children_plans,
  )
end