tapioca icon indicating copy to clipboard operation
tapioca copied to clipboard

Suggestion: Add an ability to enforce a type on ActiveModel::Attribute

Open lavoiesl opened this issue 9 months ago • 1 comments

The idea would be to add a way to express the Sorbet type for attributes.

Example model:

class Foo
  include ActiveModel::Model

  attribute :model
  validates :model, presence: true, type: T.class_of(ApplicationRecord)
end

I have some code that works on my project, but the extension is a bit hacky:

Validator:

module ActiveModel
  module Validations
    class TypeValidator < EachValidator
      sig { params(options: T::Hash[Symbol, T.untyped]).void }
      def initialize(options)
        with = options.delete(:with)
        raise ArgumentError, ":with cannot be blank" if with.nil?

        @with = T.let(with, T.any(T::Types::Base, String, Module, Proc))
        super
      end

      sig { params(record: ActiveModel::Validations, attribute: Symbol, value: T.untyped).void }
      def validate_each(record, attribute, value)
        unless (message = type.error_message_for_obj(value)).nil?
          record.errors.add(attribute, :class, message:)
        end
      end

      sig { returns(T::Types::Base) }
      def type
        @type ||= T.let(
          begin
            with = @with
            with = with.call if with.is_a?(Proc)
            with = with.safe_constantize if with.is_a?(String)
            with = T::Utils.coerce(with) if with.is_a?(Module)
            with
          end,
          T.nilable(T::Types::Base),
        )
      end
    end
  end
end

Extension:

# typed: ignore
# frozen_string_literal: true

require "active_model/attributes"
require "tapioca/dsl/compilers/active_model_attributes"

module Tapioca
  module Compilers
    module ActiveModelTypeValidator
      def attribute_methods_for_constant
        return super unless constant.is_a?(ActiveModel::Validations::ClassMethods)

        attribute_methods = super.to_h # Convert to hash for easier manipulation

        attribute_methods.each do |method, _|
          constant
            .validators_on(method)
            .grep(ActiveModel::Validations::TypeValidator)
            .each do |validator|
              attribute_methods[method] = validator.type.to_s
              attribute_methods["#{method}="] = validator.type.to_s
              break
            end
        end

        attribute_methods.to_a
      end
    end

    Dsl::Compilers::ActiveModelAttributes.prepend(ActiveModelTypeValidator)
  end
end

lavoiesl avatar Apr 09 '25 19:04 lavoiesl

See also known-type signatures: https://github.com/Shopify/tapioca/issues/2250

lavoiesl avatar Apr 09 '25 19:04 lavoiesl