lib/lutaml/model/xml/mapping.rb



require_relative "../mapping/mapping"
require_relative "mapping_rule"

module Lutaml
  module Model
    module Xml
      class Mapping < Mapping
        TYPES = {
          attribute: :map_attribute,
          element: :map_element,
          content: :map_content,
          all_content: :map_all,
        }.freeze

        attr_reader :root_element,
                    :namespace_uri,
                    :namespace_prefix,
                    :mixed_content,
                    :ordered,
                    :element_sequence

        def initialize
          super

          @elements = {}
          @attributes = {}
          @element_sequence = []
          @content_mapping = nil
          @raw_mapping = nil
          @mixed_content = false
          @format = :xml
        end

        def finalize(mapper_class)
          if !root_element && !no_root?
            root(mapper_class.model.to_s)
          end
        end

        alias mixed_content? mixed_content
        alias ordered? ordered

        def root(name, mixed: false, ordered: false)
          @root_element = name
          @mixed_content = mixed
          @ordered = ordered || mixed # mixed contenet will always be ordered
        end

        def root?
          !!root_element
        end

        def no_root
          @no_root = true
        end

        def no_root?
          !!@no_root
        end

        def prefixed_root
          if namespace_uri && namespace_prefix
            "#{namespace_prefix}:#{root_element}"
          else
            root_element
          end
        end

        def namespace(uri, prefix = nil)
          raise Lutaml::Model::NoRootNamespaceError if no_root?

          @namespace_uri = uri
          @namespace_prefix = prefix
        end

        # rubocop:disable Metrics/ParameterLists
        def map_element(
          name,
          to: nil,
          render_nil: false,
          render_default: false,
          render_empty: false,
          treat_nil: :nil,
          treat_empty: :empty,
          treat_omitted: :nil,
          with: {},
          delegate: nil,
          cdata: false,
          polymorphic: {},
          namespace: (namespace_set = false
                      nil),
          prefix: (prefix_set = false
                   nil),
          transform: {},
          value_map: {}
        )
          validate!(
            name, to, with, render_nil, render_empty, type: TYPES[:element]
          )

          rule = MappingRule.new(
            name,
            to: to,
            render_nil: render_nil,
            render_default: render_default,
            render_empty: render_empty,
            treat_nil: treat_nil,
            treat_empty: treat_empty,
            treat_omitted: treat_omitted,
            with: with,
            delegate: delegate,
            cdata: cdata,
            namespace: namespace,
            default_namespace: namespace_uri,
            prefix: prefix,
            polymorphic: polymorphic,
            namespace_set: namespace_set != false,
            prefix_set: prefix_set != false,
            transform: transform,
            value_map: value_map,
          )
          @elements[rule.namespaced_name] = rule
        end

        def map_attribute(
          name,
          to: nil,
          render_nil: false,
          render_default: false,
          render_empty: false,
          with: {},
          delegate: nil,
          polymorphic_map: {},
          namespace: (namespace_set = false
                      nil),
          prefix: (prefix_set = false
                   nil),
          transform: {},
          value_map: {}
        )
          validate!(
            name, to, with, render_nil, render_empty, type: TYPES[:attribute]
          )

          if name == "schemaLocation"
            Logger.warn_auto_handling(
              name: name,
              caller_file: File.basename(caller_locations(1, 1)[0].path),
              caller_line: caller_locations(1, 1)[0].lineno,
            )
          end

          rule = MappingRule.new(
            name,
            to: to,
            render_nil: render_nil,
            render_default: render_default,
            with: with,
            delegate: delegate,
            namespace: namespace,
            prefix: prefix,
            attribute: true,
            polymorphic_map: polymorphic_map,
            default_namespace: namespace_uri,
            namespace_set: namespace_set != false,
            prefix_set: prefix_set != false,
            transform: transform,
            value_map: value_map,
          )
          @attributes[rule.namespaced_name] = rule
        end

        # rubocop:enable Metrics/ParameterLists

        def map_content(
          to: nil,
          render_nil: false,
          render_default: false,
          render_empty: false,
          with: {},
          delegate: nil,
          mixed: false,
          cdata: false,
          transform: {},
          value_map: {}
        )
          validate!(
            "content", to, with, render_nil, render_empty, type: TYPES[:content]
          )

          @content_mapping = MappingRule.new(
            nil,
            to: to,
            render_nil: render_nil,
            render_default: render_default,
            render_empty: render_empty,
            with: with,
            delegate: delegate,
            mixed_content: mixed,
            cdata: cdata,
            transform: transform,
            value_map: value_map,
          )
        end

        def map_all(
          to:,
          render_nil: false,
          render_default: false,
          delegate: nil,
          with: {},
          namespace: (namespace_set = false
                      nil),
          prefix: (prefix_set = false
                   nil),
          render_empty: false
        )
          validate!(
            Constants::RAW_MAPPING_KEY,
            to,
            with,
            render_nil,
            render_empty,
            type: TYPES[:all_content],
          )

          rule = MappingRule.new(
            Constants::RAW_MAPPING_KEY,
            to: to,
            render_nil: render_nil,
            render_default: render_default,
            with: with,
            delegate: delegate,
            namespace: namespace,
            prefix: prefix,
            default_namespace: namespace_uri,
            namespace_set: namespace_set != false,
            prefix_set: prefix_set != false,
          )

          @raw_mapping = rule
        end

        alias map_all_content map_all

        def sequence(&block)
          @element_sequence << Sequence.new(self).tap do |s|
            s.instance_eval(&block)
          end
        end

        def import_model_mappings(model)
          raise Lutaml::Model::ImportModelWithRootError.new(model) if model.root?

          mappings = model.mappings_for(:xml)
          @elements.merge!(mappings.instance_variable_get(:@elements))
          @attributes.merge!(mappings.instance_variable_get(:@attributes))
          (@element_sequence << mappings.element_sequence).flatten!
        end

        def validate!(key, to, with, render_nil, render_empty, type: nil)
          validate_raw_mappings!(type)
          validate_to_and_with_arguments!(key, to, with)

          if render_nil == :as_empty || render_empty == :as_empty
            raise IncorrectMappingArgumentsError.new(
              ":as_empty is not supported for XML mappings",
            )
          end
        end

        def validate_to_and_with_arguments!(key, to, with)
          if to.nil? && with.empty?
            raise IncorrectMappingArgumentsError.new(
              ":to or :with argument is required for mapping '#{key}'",
            )
          end

          validate_with_options!(key, to, with)
        end

        def validate_with_options!(key, to, with)
          return true if to

          if !with.empty? && (with[:from].nil? || with[:to].nil?)
            raise IncorrectMappingArgumentsError.new(
              ":with argument for mapping '#{key}' requires :to and :from keys",
            )
          end
        end

        def validate_raw_mappings!(type)
          if !@raw_mapping.nil? && type != TYPES[:attribute]
            raise StandardError, "#{type} is not allowed, only #{TYPES[:attribute]} " \
                                 "is allowed with #{TYPES[:all_content]}"
          end

          if !(elements.empty? && content_mapping.nil?) && type == TYPES[:all_content]
            raise StandardError, "#{TYPES[:all_content]} is not allowed with other mappings"
          end
        end

        def elements
          @elements.values
        end

        def attributes
          @attributes.values
        end

        def content_mapping
          @content_mapping
        end

        def raw_mapping
          @raw_mapping
        end

        def mappings
          elements + attributes + [content_mapping, raw_mapping].compact
        end

        def element(name)
          elements.detect do |rule|
            name == rule.to
          end
        end

        def attribute(name)
          attributes.detect do |rule|
            name == rule.to
          end
        end

        def find_by_name(name)
          if ["text", "#cdata-section"].include?(name.to_s)
            content_mapping
          else
            mappings.detect do |rule|
              rule.name == name.to_s || rule.name == name.to_sym
            end
          end
        end

        def mapping_attributes_hash
          @attributes
        end

        def mapping_elements_hash
          @elements
        end

        def merge_mapping_attributes(mapping)
          mapping_attributes_hash.merge!(mapping.mapping_attributes_hash)
        end

        def merge_mapping_elements(mapping)
          mapping_elements_hash.merge!(mapping.mapping_elements_hash)
        end

        def merge_elements_sequence(mapping)
          mapping.element_sequence.each do |sequence|
            element_sequence << Lutaml::Model::Sequence.new(self).tap do |instance|
              sequence.attributes.each do |attr|
                instance.attributes << attr.deep_dup
              end
            end
          end
        end

        def deep_dup
          self.class.new.tap do |xml_mapping|
            xml_mapping.root(@root_element.dup, mixed: @mixed_content,
                                                ordered: @ordered)
            xml_mapping.namespace(@namespace_uri.dup, @namespace_prefix.dup)

            attributes_to_dup.each do |var_name|
              value = instance_variable_get(var_name)
              xml_mapping.instance_variable_set(var_name, Utils.deep_dup(value))
            end
          end
        end

        def polymorphic_mapping
          mappings.find(&:polymorphic_mapping?)
        end

        def attributes_to_dup
          @attributes_to_dup ||= %i[
            @content_mapping
            @raw_mapping
            @element_sequence
            @attributes
            @elements
          ]
        end

        def dup_mappings(mappings)
          new_mappings = {}

          mappings.each do |key, mapping_rule|
            new_mappings[key] = mapping_rule.deep_dup
          end

          new_mappings
        end
      end
    end
  end
end