src/derails/passive_model/attribute_registration.rb
# frozen_string_literal: true

require "active_support/core_ext/class/subclasses"
require "passive_model/attribute_set"
require "passive_model/attribute/user_provided_default"

module PassiveModel
  module AttributeRegistration # :nodoc:
    extend ActiveSupport::Concern

    module ClassMethods # :nodoc:
      def attribute(name, type = nil, default: (no_default = true), **options)
        name = resolve_attribute_name(name)
        type = resolve_type_name(type, **options) if type.is_a?(Symbol)
        type = hook_attribute_type(name, type) if type

        pending_attribute_modifications << PendingType.new(name, type) if type || no_default
        pending_attribute_modifications << PendingDefault.new(name, default) unless no_default

        reset_default_attributes
      end

      def decorate_attributes(names = nil, &decorator) # :nodoc:
        names = names&.map { |name| resolve_attribute_name(name) }

        pending_attribute_modifications << PendingDecorator.new(names, decorator)

        reset_default_attributes
      end

      def _default_attributes # :nodoc:
        @default_attributes ||= AttributeSet.new({}).tap do |attribute_set|
          apply_pending_attribute_modifications(attribute_set)
        end
      end

      def attribute_types # :nodoc:
        @attribute_types ||= _default_attributes.cast_types.tap do |hash|
          hash.default = Type.default_value
        end
      end

      def type_for_attribute(attribute_name, &block)
        attribute_name = resolve_attribute_name(attribute_name)

        if block
          attribute_types.fetch(attribute_name, &block)
        else
          attribute_types[attribute_name]
        end
      end

      private
        PendingType = Struct.new(:name, :type) do # :nodoc:
          def apply_to(attribute_set)
            attribute = attribute_set[name]
            attribute_set[name] = attribute.with_type(type || attribute.type)
          end
        end

        PendingDefault = Struct.new(:name, :default) do # :nodoc:
          def apply_to(attribute_set)
            attribute_set[name] = attribute_set[name].with_user_default(default)
          end
        end

        PendingDecorator = Struct.new(:names, :decorator) do # :nodoc:
          def apply_to(attribute_set)
            (names || attribute_set.keys).each do |name|
              attribute = attribute_set[name]
              type = decorator.call(name, attribute.type)
              attribute_set[name] = attribute.with_type(type) if type
            end
          end
        end

        def pending_attribute_modifications
          @pending_attribute_modifications ||= []
        end

        def apply_pending_attribute_modifications(attribute_set)
          if superclass.respond_to?(:apply_pending_attribute_modifications, true)
            superclass.send(:apply_pending_attribute_modifications, attribute_set)
          end

          pending_attribute_modifications.each do |modification|
            modification.apply_to(attribute_set)
          end
        end

        def reset_default_attributes
          reset_default_attributes!
          subclasses.each { |subclass| subclass.send(:reset_default_attributes) }
        end

        def reset_default_attributes!
          @default_attributes = nil
          @attribute_types = nil
        end

        def resolve_attribute_name(name)
          name.to_s
        end

        def resolve_type_name(name, **options)
          Type.lookup(name, **options)
        end

        # Hook for other modules to override. The attribute type is passed
        # through this method immediately after it is resolved, before any type
        # decorations are applied.
        def hook_attribute_type(attribute, type)
          type
        end
    end
  end
end
