require_relative "../mapping_hash" require_relative "xml_element" require_relative "xml_attribute" require_relative "xml_namespace" module Lutaml module Model module XmlAdapter class XmlDocument attr_reader :root def initialize(root) @root = root end def self.parse(xml) raise NotImplementedError, "Subclasses must implement `parse`." end def children @root.children 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 = "\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[:namespace_set] = set_namespace?(rule) options end def parse_element(element, klass = nil, format = nil) result = Lutaml::Model::MappingHash.new result.node = element result.item_order = element.order element.children.each_with_object(result) do |child, hash| attr = klass.attribute_for_child(child.name, format) if klass&.<= Serialize value = if child.text? child.text elsif attr&.raw? child.children.map(&:to_xml).join else parse_element(child, attr&.type || klass, format) end hash[child.namespaced_name] = if hash[child.namespaced_name] [hash[child.namespaced_name], value].flatten else value end end result.merge(attributes_hash(element)) 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, } else result[attr.namespaced_name] = attr.value end 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 = {}) if value.is_a?(Array) value.each do |item| add_to_xml(xml, element, prefix, item, options) end return end 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 return if !render_element?(rule, element, value) if value && (attribute&.type&.<= Lutaml::Model::Serialize) handle_nested_elements( xml, value, options.merge({ rule: rule, attribute: attribute }), ) elsif rule.prefix_set? xml.create_and_add_element(rule.name, prefix: prefix) do add_value(xml, value, attribute) end else xml.create_and_add_element(rule.name) do add_value(xml, value, attribute) end end end def add_value(xml, value, attribute) if !value.nil? serialized_value = attribute.type.serialize(value) if 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) end end end def build_unordered_element(xml, element, options = {}) mapper_class = options[:mapper_class] || element.class xml_mapping = mapper_class.mappings_for(:xml) return xml unless xml_mapping attributes = options[:xml_attributes] ||= {} attributes = build_attributes(element, xml_mapping, options).merge(attributes)&.compact if element.respond_to?(:schema_location) && element.schema_location attributes.merge!(element.schema_location.to_xml_attributes) end prefix = if options.key?(:namespace_prefix) options[:namespace_prefix] elsif xml_mapping.namespace_prefix xml_mapping.namespace_prefix end 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 xml_mapping.elements.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 value.nil? && !element_rule.render_nil? 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 if (content_rule = xml_mapping.content_mapping) if content_rule.custom_methods[:to] @root.send(content_rule.custom_methods[:to], element, prefixed_xml.parent, prefixed_xml) else text = content_rule.serialize(element) text = text.join if text.is_a?(Array) prefixed_xml.add_text(xml, text) end end 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? || !rule.namespace.nil? end def render_element?(rule, element, value) render_default?(rule, element) && render_value?(rule, value) end def render_value?(rule, value) rule.attribute? || rule.render_nil? || !value.nil? 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(:namespace_set, true) namespace_attributes(xml_mapping) else {} end if element.respond_to?(:schema_location) && element.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] if mapping_rule.namespace && mapping_rule.prefix && mapping_rule.name != "lang" hash["xmlns:#{mapping_rule.prefix}"] = mapping_rule.namespace end if render_element?(mapping_rule, element, mapping_rule.to_value_for(element)) hash[mapping_rule.prefixed_name] = mapping_rule.to_value_for(element) 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 end end end end