require_relative "../mapping_hash"
require_relative "xml_element"
require_relative "xml_attribute"
require_relative "xml_namespace"
require_relative "element"
module Lutaml
module Model
module Xml
class Document
attr_reader :root, :encoding
def initialize(root, encoding = nil)
@root = root
@encoding = encoding
end
def self.parse(xml, _options = {})
raise NotImplementedError, "Subclasses must implement `parse`."
end
def children
@root.children
end
def attributes
root.attributes
end
def self.encoding(xml, options)
if options.key?(:encoding)
options[:encoding]
else
xml.encoding.to_s
end
end
def declaration(options)
version = "1.0"
version = options[:declaration] if options[:declaration].is_a?(String)
encoding = options[:encoding] ? "UTF-8" : nil
encoding = options[:encoding] if options[:encoding].is_a?(String)
declaration = "<?xml version=\"#{version}\""
declaration += " encoding=\"#{encoding}\"" if encoding
declaration += "?>\n"
declaration
end
def to_h
parse_element(@root)
end
def order
@root.order
end
def handle_nested_elements(builder, value, options = {})
element_options = build_options_for_nested_elements(options)
case value
when Array
value.each { |val| build_element(builder, val, element_options) }
else
build_element(builder, value, element_options)
end
end
def build_options_for_nested_elements(options = {})
attribute = options.delete(:attribute)
rule = options.delete(:rule)
return {} unless rule
# options = {}
options[:namespace_prefix] = rule.prefix if rule&.namespace_set?
options[:mixed_content] = rule.mixed_content
options[:tag_name] = rule.name
options[:mapper_class] = attribute&.type if attribute
options[:set_namespace] = set_namespace?(rule)
options
end
def parse_element(element, klass = nil, format = nil)
result = Lutaml::Model::MappingHash.new
result.node = element
result.item_order = self.class.order_of(element)
element.children.each do |child|
if klass&.<= Serialize
attr = klass.attribute_for_child(self.class.name_of(child),
format)
end
if child.respond_to?(:text?) && child.text?
result.assign_or_append_value(
self.class.name_of(child),
self.class.text_of(child),
)
next
end
result["elements"] ||= Lutaml::Model::MappingHash.new
result["elements"].assign_or_append_value(
self.class.namespaced_name_of(child),
parse_element(child, attr&.type || klass, format),
)
end
result["attributes"] = attributes_hash(element) if element.attributes&.any?
result.merge(attributes_hash(element))
result
end
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,
}
end
result[attr.namespaced_name] = attr.value
end
result
end
def build_element(xml, element, options = {})
if ordered?(element, options)
build_ordered_element(xml, element, options)
else
build_unordered_element(xml, element, options)
end
end
def add_to_xml(xml, element, prefix, value, options = {})
attribute = options[:attribute]
rule = options[:rule]
if rule.custom_methods[:to]
options[:mapper_class].new.send(rule.custom_methods[:to], element,
xml.parent, xml)
return
end
# Only transform when recursion is not called
if !attribute.collection? || value.is_a?(Array)
value = ExportTransformer.call(value, rule, attribute)
end
if value.is_a?(Array) && !Utils.empty_collection?(value)
value.each do |item|
add_to_xml(xml, element, prefix, item, options)
end
return
end
return if !render_element?(rule, element, value)
value = rule.render_value_for(value)
if value && (attribute&.type&.<= Lutaml::Model::Serialize)
handle_nested_elements(
xml,
value,
options.merge({ rule: rule, attribute: attribute }),
)
elsif value.nil?
xml.create_and_add_element(rule.name, attributes: { "xsi:nil" => true })
elsif Utils.empty?(value)
xml.create_and_add_element(rule.name)
elsif rule.raw_mapping?
xml.add_xml_fragment(xml, value)
elsif rule.prefix_set?
xml.create_and_add_element(rule.name, prefix: prefix) do
add_value(xml, value, attribute, cdata: rule.cdata)
end
else
xml.create_and_add_element(rule.name) do
add_value(xml, value, attribute, cdata: rule.cdata)
end
end
end
def add_value(xml, value, attribute, cdata: false)
if !value.nil?
serialized_value = attribute.serialize(value, :xml)
if attribute.raw?
xml.add_xml_fragment(xml, value)
elsif attribute.type == Lutaml::Model::Type::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
def build_unordered_element(xml, element, options = {})
mapper_class = determine_mapper_class(element, options)
xml_mapping = mapper_class.mappings_for(:xml)
return xml unless xml_mapping
attributes = build_element_attributes(element, xml_mapping, options)
prefix = determine_namespace_prefix(options, xml_mapping)
prefixed_xml = xml.add_namespace_prefix(prefix)
tag_name = options[:tag_name] || xml_mapping.root_element
prefixed_xml.create_and_add_element(tag_name, prefix: prefix,
attributes: attributes) do
if options.key?(:namespace_prefix) && !options[:namespace_prefix]
prefixed_xml.add_namespace_prefix(nil)
end
xml_mapping.attributes.each do |attribute_rule|
attribute_rule.serialize_attribute(element, prefixed_xml.parent,
xml)
end
mappings = xml_mapping.elements + [xml_mapping.raw_mapping].compact
mappings.each do |element_rule|
attribute_def = attribute_definition_for(element, element_rule,
mapper_class: mapper_class)
if attribute_def
value = attribute_value_for(element, element_rule)
next if !element_rule.render?(value, element)
value = [value] if attribute_def.collection? && !value.is_a?(Array)
end
add_to_xml(
prefixed_xml,
element,
element_rule.prefix,
value,
options.merge({ attribute: attribute_def, rule: element_rule,
mapper_class: mapper_class }),
)
end
process_content_mapping(element, xml_mapping.content_mapping,
prefixed_xml, mapper_class)
end
end
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
def ordered?(element, options = {})
return false unless element.respond_to?(:element_order)
return element.ordered? if element.respond_to?(:ordered?)
return options[:mixed_content] if options.key?(:mixed_content)
mapper_class = options[:mapper_class]
mapper_class ? mapper_class.mappings_for(:xml).mixed_content? : false
end
def set_namespace?(rule)
rule.nil? || !rule.namespace_set?
end
def render_element?(rule, element, value)
rule.render?(value, element)
end
def render_default?(rule, element)
!element.respond_to?(:using_default?) ||
rule.render_default? ||
!element.using_default?(rule.to)
end
def build_namespace_attributes(klass, processed = {}, options = {})
xml_mappings = klass.mappings_for(:xml)
attributes = klass.attributes
attrs = {}
if xml_mappings.namespace_uri && set_namespace?(options[:caller_rule])
prefixed_name = [
"xmlns",
xml_mappings.namespace_prefix,
].compact.join(":")
attrs[prefixed_name] = xml_mappings.namespace_uri
end
xml_mappings.mappings.each do |mapping_rule|
processed[klass] ||= {}
next if processed[klass][mapping_rule.name]
processed[klass][mapping_rule.name] = true
type = if mapping_rule.delegate
attributes[mapping_rule.delegate].type.attributes[mapping_rule.to].type
else
attributes[mapping_rule.to]&.type
end
next unless type
if type <= Lutaml::Model::Serialize
attrs = attrs.merge(build_namespace_attributes(type, processed,
{ caller_rule: mapping_rule }))
end
if mapping_rule.namespace && mapping_rule.prefix && mapping_rule.name != "lang"
attrs["xmlns:#{mapping_rule.prefix}"] = mapping_rule.namespace
end
end
attrs
end
def build_attributes(element, xml_mapping, options = {})
attrs = if options.fetch(:set_namespace, true)
namespace_attributes(xml_mapping)
else
{}
end
if element.respond_to?(:schema_location) && element.schema_location.is_a?(Lutaml::Model::SchemaLocation) && !options[:except]&.include?(:schema_location)
attrs.merge!(element.schema_location.to_xml_attributes)
end
xml_mapping.attributes.each_with_object(attrs) do |mapping_rule, hash|
next if options[:except]&.include?(mapping_rule.to)
next if mapping_rule.custom_methods[:to]
mapping_rule_name = mapping_rule.multiple_mappings? ? mapping_rule.name.first : mapping_rule.name
if mapping_rule.namespace && mapping_rule.prefix && mapping_rule_name != "lang"
hash["xmlns:#{mapping_rule.prefix}"] = mapping_rule.namespace
end
value = mapping_rule.to_value_for(element)
attr = attribute_definition_for(element, mapping_rule, mapper_class: options[:mapper_class])
value = attr.serialize(value, :xml) if attr
value = ExportTransformer.call(value, mapping_rule, attr)
if render_element?(mapping_rule, element, value)
hash[mapping_rule.prefixed_name] = value ? value.to_s : value
end
end
xml_mapping.elements.each_with_object(attrs) do |mapping_rule, hash|
next if options[:except]&.include?(mapping_rule.to)
if mapping_rule.namespace && mapping_rule.prefix
hash["xmlns:#{mapping_rule.prefix}"] = mapping_rule.namespace
end
end
end
def attribute_definition_for(element, rule, mapper_class: nil)
klass = mapper_class || element.class
return klass.attributes[rule.to] unless rule.delegate
element.send(rule.delegate).class.attributes[rule.to]
end
def attribute_value_for(element, rule)
return element.send(rule.to) unless rule.delegate
element.send(rule.delegate).send(rule.to)
end
def namespace_attributes(xml_mapping)
return {} unless xml_mapping.namespace_uri
key = ["xmlns", xml_mapping.namespace_prefix].compact.join(":")
{ key => xml_mapping.namespace_uri }
end
def self.type
Utils.snake_case(self).split("/").last.split("_").first
end
def self.order_of(element)
element.order
end
def self.name_of(element)
element.name
end
def self.text_of(element)
element.text
end
def self.namespaced_name_of(element)
element.namespaced_name
end
def text
return @root.text_children.map(&:text) if @root.children.count > 1
@root.text
end
def cdata
@root.cdata
end
private
def determine_mapper_class(element, options)
if options[:mapper_class] && element.is_a?(options[:mapper_class])
element.class
else
options[:mapper_class] || element.class
end
end
def determine_namespace_prefix(options, mapping)
return options[:namespace_prefix] if options.key?(:namespace_prefix)
mapping.namespace_prefix
end
def build_element_attributes(element, mapping, options)
xml_attributes = options[:xml_attributes] ||= {}
attributes = build_attributes(element, mapping, options)
attributes.merge(xml_attributes)&.compact
end
end
end
end
end