lib/lutaml/model/xml_adapter/nokogiri_adapter.rb



require "nokogiri"
require_relative "xml_document"

module Lutaml
  module Model
    module XmlAdapter
      class NokogiriAdapter < XmlDocument
        def self.parse(xml)
          parsed = Nokogiri::XML(xml)
          root = NokogiriElement.new(parsed.root)
          new(root)
        end

        def to_xml(options = {})
          builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
            if root.is_a?(Lutaml::Model::XmlAdapter::NokogiriElement)
              root.to_xml(xml)
            else
              mapper_class = options[:mapper_class] || @root.class
              options[:xml_attributes] = build_namespace_attributes(mapper_class)
              build_element(xml, @root, options)
            end
          end

          xml_options = {}
          xml_options[:indent] = 2 if options[:pretty]

          xml_data = builder.doc.root.to_xml(xml_options)
          options[:declaration] ? declaration(options) + xml_data : xml_data
        end

        private

        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).merge(attributes)&.compact

          prefixed_xml = if options.key?(:namespace_prefix)
                           options[:namespace_prefix] ? xml[options[:namespace_prefix]] : xml
                         elsif xml_mapping.namespace_prefix
                           xml[xml_mapping.namespace_prefix]
                         else
                           xml
                         end

          tag_name = options[:tag_name] || xml_mapping.root_element
          prefixed_xml.public_send(tag_name, attributes) do
            if options.key?(:namespace_prefix) && !options[:namespace_prefix]
              xml.parent.namespace = nil
            end

            xml_mapping.elements.each do |element_rule|
              attribute_def = attribute_definition_for(element, element_rule, mapper_class: mapper_class)
              value = attribute_value_for(element, element_rule)

              next if value.nil? && !element_rule.render_nil?

              nsp_xml = element_rule.prefix ? xml[element_rule.prefix] : xml

              if attribute_def.collection?
                value.each do |v|
                  add_to_xml(nsp_xml, v, attribute_def, element_rule)
                end
              elsif !value.nil? || element_rule.render_nil?
                add_to_xml(nsp_xml, value, attribute_def, element_rule)
              end
            end

            if xml_mapping.content_mapping
              text = element.send(xml_mapping.content_mapping.to)
              text = text.join if text.is_a?(Array)

              prefixed_xml.text text
            end
          end
        end

        def build_ordered_element(xml, element, options = {})
          mapper_class = options[:mapper_class] || element.class
          xml_mapping = mapper_class.mappings_for(:xml)
          return xml unless xml_mapping

          attributes = build_attributes(element, xml_mapping)&.compact

          prefixed_xml = if options.key?(:namespace_prefix)
                           options[:namespace_prefix] ? xml[options[:namespace_prefix]] : xml
                         elsif xml_mapping.namespace_prefix
                           xml[xml_mapping.namespace_prefix]
                         else
                           xml
                         end

          tag_name = options[:tag_name] || xml_mapping.root_element
          prefixed_xml.public_send(tag_name, attributes) do
            if options.key?(:namespace_prefix) && !options[:namespace_prefix]
              xml.parent.namespace = nil
            end

            index_hash = {}

            element.element_order.each do |name|
              index_hash[name] ||= -1
              curr_index = index_hash[name] += 1

              element_rule = xml_mapping.find_by_name(name)
              next if element_rule.nil?

              attribute_def = attribute_definition_for(element, element_rule, mapper_class: mapper_class)
              value = attribute_value_for(element, element_rule)
              nsp_xml = element_rule.prefix ? xml[element_rule.prefix] : xml

              if element_rule == xml_mapping.content_mapping
                text = element.send(xml_mapping.content_mapping.to)
                text = text[curr_index] if text.is_a?(Array)

                prefixed_xml.text text
              elsif attribute_def.collection?
                add_to_xml(nsp_xml, value[curr_index], attribute_def,
                           element_rule)
              elsif !value.nil? || element_rule.render_nil?
                add_to_xml(nsp_xml, value, attribute_def, element_rule)
              end
            end
          end
        end

        def add_to_xml(xml, value, attribute, rule)
          if value && (attribute&.type&.<= Lutaml::Model::Serialize)
            handle_nested_elements(
              xml,
              value,
              rule: rule,
              attribute: attribute,
            )
          else
            xml.public_send(rule.name) do
              if !value.nil?
                serialized_value = attribute.type.serialize(value)

                if attribute.type == Lutaml::Model::Type::Hash
                  serialized_value.each do |key, val|
                    xml.public_send(key) { xml.text val }
                  end
                else
                  xml.text(serialized_value)
                end
              end
            end
          end
        end
      end

      class NokogiriElement < XmlElement
        def initialize(node, root_node: nil)
          if root_node
            node.namespaces.each do |prefix, name|
              namespace = XmlNamespace.new(name, prefix)

              root_node.add_namespace(namespace)
            end
          end

          attributes = {}
          node.attributes.transform_values do |attr|
            name = if attr.namespace
                     "#{attr.namespace.prefix}:#{attr.name}"
                   else
                     attr.name
                   end

            attributes[name] = XmlAttribute.new(
              name,
              attr.value,
              namespace: attr.namespace&.href,
              namespace_prefix: attr.namespace&.prefix,
            )
          end

          super(
            node.name,
            attributes,
            parse_all_children(node, root_node: root_node || self),
            node.text,
            parent_document: root_node,
            namespace_prefix: node.namespace&.prefix,
          )
        end

        def text?
          # false
          children.empty? && text.length.positive?
        end

        def to_xml(builder = nil)
          builder ||= Nokogiri::XML::Builder.new

          if name == "text"
            builder.text(text)
          else
            builder.send(name, build_attributes(self)) do |xml|
              children.each { |child| child.to_xml(xml) }
            end
          end

          builder
        end

        private

        def parse_children(node, root_node: nil)
          node.children.select(&:element?).map do |child|
            NokogiriElement.new(child, root_node: root_node)
          end
        end

        def parse_all_children(node, root_node: nil)
          node.children.map do |child|
            NokogiriElement.new(child, root_node: root_node)
          end
        end

        def build_attributes(node)
          attrs = node.attributes.transform_values(&:value)

          attrs.merge(build_namespace_attributes(node))
        end

        def build_namespace_attributes(node)
          namespace_attrs = {}

          node.own_namespaces.each_value do |namespace|
            namespace_attrs[namespace.attr_name] = namespace.uri
          end

          node.children.each do |child|
            namespace_attrs = namespace_attrs.merge(
              build_namespace_attributes(child),
            )
          end

          namespace_attrs
        end
      end
    end
  end
end