Class: Lutaml::Xml::Adapter::BaseAdapter Abstract

Inherits:
Document
  • Object
show all
Includes:
DeclarationHandler, PolymorphicValueHandler
Defined in:
lib/lutaml/xml/adapter/base_adapter.rb

Overview

This class is abstract.

Subclass and implement required methods

Base class for XML adapters providing shared functionality.

This class extracts common code from NokogiriAdapter, OxAdapter, OgaAdapter, and RexmlAdapter to reduce duplication and ensure consistent behavior across adapters.

Subclasses must implement:

  • self.parse(xml, options) - Parse XML string to document

  • to_xml(options) - Serialize document to XML string

Direct Known Subclasses

NokogiriAdapter, OgaAdapter, OxAdapter, RexmlAdapter

Instance Attribute Summary

Attributes inherited from Document

#doctype, #encoding, #parsed_doc, #register, #root, #xml_declaration

Class Method Summary collapse

Instance Method Summary collapse

Methods included from PolymorphicValueHandler

#polymorphic_value?

Methods included from DeclarationHandler

extract_attribute, extract_xml_declaration, #generate_declaration, #generate_doctype_declaration, #should_include_declaration?

Methods inherited from Document

#attributes, #cdata, #children, #declaration, #doctype_declaration, #element_children, #element_children_index, encoding, #initialize, #order, parse, #parse_element, #text, #to_h, type

Constructor Details

This class inherits a constructor from Lutaml::Xml::Document

Class Method Details

.extract_document_processing_instructions(moxml_doc) ⇒ Array<Lutaml::Xml::DataModel::XmlProcessingInstruction>

Extract processing instructions from a moxml document that appear before the root element.

Parameters:

  • moxml_doc (Moxml::Document)

    the parsed document

Returns:



94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 94

def self.extract_document_processing_instructions(moxml_doc)
  pis = []
  root = moxml_doc.root
  moxml_doc.children.each do |child|
    break if child == root
    next unless child.is_a?(Moxml::ProcessingInstruction)

    pis << Lutaml::Xml::DataModel::XmlProcessingInstruction.new(
      child.target, child.content.to_s.strip
    )
  end
  pis
end

.fpi?(uri) ⇒ Boolean

Detect if a string is an FPI (Formal Public Identifier), not a valid namespace URI. FPIs start with -// or +// (SGML-style, not a URI scheme).

Returns:

  • (Boolean)


85
86
87
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 85

def self.fpi?(uri)
  uri.is_a?(String) && uri.start_with?("-//", "+//")
end

.fpi_to_urn(fpi) ⇒ Object

Convert a Formal Public Identifier (FPI) to a URN per RFC 3151. FPI examples: “-//OASIS//DTD XML Exchange Table Model 19990315//EN” Returns nil if the string is not an FPI.

RFC 3151 format: urn:publicid:prefix:+/-//registrant//description//language// Conversion: replace spaces with +, prepend “urn:publicid:”



74
75
76
77
78
79
80
81
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 74

def self.fpi_to_urn(fpi)
  return nil unless fpi.is_a?(String) && fpi.start_with?("-//", "+//")

  # Replace spaces with + per RFC 3151
  normalized = fpi.gsub(" ", "+")

  "urn:publicid:#{normalized}"
end

.name_of(element) ⇒ String

Get the local name of an element

Parameters:

  • element (Object)

    the element to inspect

Returns:

  • (String)

    the element’s local name



32
33
34
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 32

def self.name_of(element)
  element.name
end

.namespaced_attr_name(prefix, name) ⇒ String

Build a namespaced attribute name

Parameters:

  • prefix (String, nil)

    the namespace prefix

  • name (String)

    the attribute name

Returns:

  • (String)

    the qualified attribute name



113
114
115
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 113

def self.namespaced_attr_name(prefix, name)
  prefix ? "#{prefix}:#{name}" : name
end

.namespaced_name(namespace_uri, prefix, name) ⇒ String

Build a namespaced element name

Parameters:

  • namespace_uri (String, nil)

    the namespace URI

  • prefix (String, nil)

    the namespace prefix

  • name (String)

    the element name

Returns:

  • (String)

    the qualified element name



123
124
125
126
127
128
129
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 123

def self.namespaced_name(namespace_uri, prefix, name)
  if namespace_uri
    prefix ? "#{prefix}:#{name}" : name
  else
    name
  end
end

.namespaced_name_of(element) ⇒ String

Get the namespaced name of an element

Parameters:

  • element (Object)

    the element to inspect

Returns:

  • (String)

    the namespaced name



56
57
58
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 56

def self.namespaced_name_of(element)
  element.namespaced_name
end

.order_of(element) ⇒ Array

Get the order of child elements

Parameters:

  • element (Object)

    the parent element

Returns:

  • (Array)

    ordered list of children



64
65
66
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 64

def self.order_of(element)
  element.order
end

.prefixed_name_of(node) ⇒ String

Get the prefixed name of an element

Parameters:

  • node (Object)

    the element node

Returns:

  • (String)

    the prefixed name (prefix:localname)



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

def self.prefixed_name_of(node)
  node.prefixed_name
end

.text_of(element) ⇒ String

Get the text content of an element

Parameters:

  • element (Object)

    the element to get text from

Returns:

  • (String)

    the text content



48
49
50
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 48

def self.text_of(element)
  element.text
end

Instance Method Details

#add_value(xml, value, attribute, cdata: false) ⇒ Object

Add text content to XML builder

Parameters:

  • xml (Builder)

    the XML builder

  • value (Object)

    the value to add

  • attribute (Attribute, nil)

    the attribute definition

  • cdata (Boolean) (defaults to: false)

    whether to use CDATA



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
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 264

def add_value(xml, value, attribute, cdata: false)
  if !value.nil?
    if attribute.nil?
      # For delegated attributes where attribute is nil, just use the raw value
      xml.add_text(xml, value.to_s, cdata: cdata)
    elsif attribute.transform.is_a?(Class) && attribute.transform < Lutaml::Model::ValueTransformer
      # Value has already been transformed, use it directly
      xml.add_text(xml, value.to_s, cdata: cdata)
    else
      # Normal serialization through attribute type system
      serialized_value = attribute.serialize(value, :xml, register)
      if attribute.raw?
        xml.add_xml_fragment(xml, value)
      elsif serialized_value.is_a?(Hash)
        serialized_value.each do |key, val|
          xml.create_and_add_element(key) do |element|
            element.text(val)
          end
        end
      else
        xml.add_text(xml, serialized_value, cdata: cdata)
      end
    end
  end
end

#attribute_definition_for(element, rule, mapper_class: nil) ⇒ Attribute?

Get attribute definition for an element and rule

Parameters:

  • element (Object)

    the model instance

  • rule (MappingRule)

    the mapping rule

  • mapper_class (Class, nil) (defaults to: nil)

    optional mapper class

Returns:

  • (Attribute, nil)

    the attribute definition



188
189
190
191
192
193
194
195
196
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 188

def attribute_definition_for(element, rule, mapper_class: nil)
  klass = mapper_class || element.class
  return klass.attributes[rule.to] unless rule.delegate

  delegated_obj = element.send(rule.delegate)
  return nil if delegated_obj.nil?

  delegated_obj.class.attributes[rule.to]
end

#attribute_value_for(element, rule) ⇒ Object?

Get attribute value for an element and rule

Parameters:

  • element (Object)

    the model instance

  • rule (MappingRule)

    the mapping rule

Returns:

  • (Object, nil)

    the attribute value or nil if delegate is nil



203
204
205
206
207
208
209
210
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 203

def attribute_value_for(element, rule)
  return element.send(rule.to) unless rule.delegate

  delegate_obj = element.send(rule.delegate)
  return nil if delegate_obj.nil?

  delegate_obj.send(rule.to)
end

#attributes_hash(element) ⇒ Hash

Build attributes hash from element attributes

Parameters:

  • element (Object)

    the element with attributes

Returns:

  • (Hash)

    hash of attribute names to values



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 240

def attributes_hash(element)
  result = Lutaml::Model::MappingHash.new

  element.attributes.each_value do |attr|
    if attr.unprefixed_name == "schemaLocation"
      result["__schema_location"] = {
        namespace: attr.namespace,
        prefix: attr.namespace_prefix,
        schema_location: attr.value,
      }
    else
      result[attr.namespaced_name] = attr.value
    end
  end

  result
end

#build_ordered_element_with_plan(xml, element, plan, options) ⇒ Object

Build ordered child elements using prepared namespace declaration plan

This is the shared implementation for all adapters. Adapters may override if they need custom behavior.

Parameters:

  • xml (Builder)

    the XML builder

  • element (Object)

    the model instance

  • plan (DeclarationPlan, Hash)

    the declaration plan

  • options (Hash)

    serialization options



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 381

def build_ordered_element_with_plan(xml, element, plan, options)
  mapper_class = options[:mapper_class] || element.class
  xml_mapping = mapper_class.mappings_for(:xml)

  index_hash = {}
  content = []

  element.element_order.each do |object|
    object_key = "#{object.name}-#{object.type}"
    index_hash[object_key] ||= -1
    curr_index = index_hash[object_key] += 1

    element_rule = xml_mapping.find_by_name(object.name,
                                            type: object.type,
                                            node_type: object.node_type,
                                            namespace_uri: object.namespace_uri)
    next if element_rule.nil? || options[:except]&.include?(element_rule.to)

    # Handle custom methods
    if element_rule.custom_methods[:to]
      mapper_class.new.send(element_rule.custom_methods[:to], element,
                            xml.parent, xml)
      next
    end

    # Get attribute definition and value (handle delegation)
    attribute_def, value = fetch_attribute_and_value(element,
                                                     element_rule, mapper_class)

    next if element_rule == xml_mapping.content_mapping && element_rule.cdata && object.text?

    if element_rule == xml_mapping.content_mapping
      process_ordered_content(element, xml_mapping, xml, curr_index,
                              content)
    elsif !value.nil? || element_rule.render_nil?
      process_ordered_element(xml, element, element_rule, attribute_def,
                              value, curr_index, plan, xml_mapping, options)
    end
  end

  add_ordered_content(xml, content) unless content.empty?
end

#build_unordered_children_with_plan(xml, element, plan, options) ⇒ Object

Build unordered child elements using prepared namespace declaration plan

This is the shared implementation for all adapters. Adapters may override if they need custom behavior.

Parameters:

  • xml (Builder)

    the XML builder

  • element (Object)

    the model instance

  • plan (DeclarationPlan, Hash)

    the declaration plan

  • options (Hash)

    serialization options



316
317
318
319
320
321
322
323
324
325
326
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
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 316

def build_unordered_children_with_plan(xml, element, plan, options)
  mapper_class = options[:mapper_class] || element.class
  xml_mapping = mapper_class.mappings_for(:xml)

  # Process child elements with their plans (INCLUDING raw_mapping for map all)
  mappings = xml_mapping.elements + [xml_mapping.raw_mapping].compact
  mappings.each do |element_rule|
    next if options[:except]&.include?(element_rule.to)

    # Handle custom methods
    if element_rule.custom_methods[:to]
      mapper_class.new.send(element_rule.custom_methods[:to], element,
                            xml.parent, xml)
      next
    end

    attribute_def = attribute_definition_for(element, element_rule,
                                             mapper_class: mapper_class)

    # For delegated attributes, attribute_def might be nil
    next unless attribute_def || element_rule.delegate

    value = attribute_value_for(element, element_rule)
    next unless element_rule.render?(value, element)

    # Get child's plan if available
    child_plan = child_plan_for(plan, element_rule.to)

    # Check if value is a Collection instance
    is_collection_instance = value.is_a?(Lutaml::Model::Collection)

    if value && (attribute_def&.type(register)&.<=(Lutaml::Model::Serialize) || is_collection_instance)
      handle_nested_elements_with_plan(
        xml,
        value,
        element_rule,
        attribute_def,
        child_plan,
        options,
        parent_plan: plan,
      )
    elsif element_rule.delegate && attribute_def.nil?
      # Handle non-model values (strings, etc.) for delegated attributes
      add_simple_value(xml, element_rule, value, nil, plan: plan,
                                                      mapping: xml_mapping, options: options)
    else
      add_simple_value(xml, element_rule, value, attribute_def,
                       plan: plan, mapping: xml_mapping, options: options)
    end
  end

  # Process content mapping
  process_content_mapping(element, xml_mapping.content_mapping,
                          xml, mapper_class)
end

#child_plan_for(plan, attr_name) ⇒ DeclarationPlan, ...

Get child plan from parent plan (unified access for both object and hash plans)

Parameters:

  • plan (DeclarationPlan, Hash, nil)

    the parent plan

  • attr_name (Symbol)

    the attribute name

Returns:



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

def child_plan_for(plan, attr_name)
  return nil unless plan

  if plan.respond_to?(:child_plan)
    # DeclarationPlan object (Nokogiri/Oga)
    plan.child_plan(attr_name)
  elsif plan.respond_to?(:[])
    # Hash-based plan (Ox/REXML)
    plan[:children_plans]&.[](attr_name)
  end
end

#determine_encoding(options) ⇒ String?

Determine encoding for XML output Returns nil when encoding is explicitly set to nil (to not set encoding at all)

Parameters:

  • options (Hash)

    serialization options

Returns:

  • (String, nil)

    the encoding to use, or nil to skip setting encoding



138
139
140
141
142
143
144
145
146
147
148
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 138

def determine_encoding(options)
  if options.key?(:encoding)
    # Return nil if encoding is explicitly nil (don't set encoding)
    # Return the value otherwise
    options[:encoding]
  elsif options.key?(:parse_encoding)
    options[:parse_encoding]
  else
    "UTF-8"
  end
end

#ordered?(element, options = {}) ⇒ Boolean

Check if element has ordered content

Parameters:

  • element (Object)

    the model instance

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

    serialization options

Returns:

  • (Boolean)

    true if element has ordered content



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 165

def ordered?(element, options = {})
  return false unless element.respond_to?(:element_order)

  mapper_class = options[:mapper_class]
  xml_mapping = mapper_class&.mappings_for(:xml)

  # Class mapping is the authoritative source for ordered/mixed.
  # Instance @ordered/@mixed are stale after class definition changes.
  if xml_mapping&.mixed_content? || xml_mapping&.ordered?
    return !element.element_order.nil? && !element.element_order.empty?
  end

  return options[:mixed_content] if options.key?(:mixed_content)

  false
end

#process_content_mapping(element, content_rule, xml, mapper_class) ⇒ Object

Process content mapping for an element

Parameters:

  • element (Object)

    the model instance

  • content_rule (MappingRule)

    the content mapping rule

  • xml (Builder)

    the XML builder

  • mapper_class (Class)

    the mapper class



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 218

def process_content_mapping(element, content_rule, xml, mapper_class)
  return unless content_rule

  if content_rule.custom_methods[:to]
    mapper_class.new.send(
      content_rule.custom_methods[:to],
      element,
      xml.parent,
      xml,
    )
  else
    text = content_rule.serialize(element)
    text = text.join if text.is_a?(Array)

    xml.add_text(xml, text, cdata: content_rule.cdata)
  end
end

#render_element?(rule, element, value) ⇒ Boolean

Check if an element should be rendered

Parameters:

  • rule (MappingRule)

    the mapping rule

  • element (Object)

    the model instance

  • value (Object)

    the value to check

Returns:

  • (Boolean)

    true if the element should be rendered



156
157
158
# File 'lib/lutaml/xml/adapter/base_adapter.rb', line 156

def render_element?(rule, element, value)
  rule.render?(value, element)
end