tapioca
tapioca copied to clipboard
Suggestion: Generate T.any signatures when an ActiveModel::Attribute has a known type
We use a lot of class restrictions like:
class ProcessingJob < ApplicationJob
PROCESSORS = [
ProcessorFoo,
ProcessorBar,
]
attribute :processor, :class
validates :processor, presence: true, inclusion: { in: PROCESSORS }
end
And this extension:
# typed: ignore
# frozen_string_literal: true
require "active_model/attributes"
require "tapioca/dsl/compilers/active_model_attributes"
require_relative "active_model_presence_validator"
module Tapioca
module Compilers
module ActiveModelInClassValidator
prepend ActiveModelPresenceValidator
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::InclusionValidator)
.each do |validator|
type = inclusion_validator_type(validator)
next unless type
attribute_methods[method] = type.to_s
attribute_methods["#{method}="] = type.to_s
break
end
end
attribute_methods.to_a
end
private
def inclusion_validator_type(validator)
delimiter = validator.options[:in] || validator.options[:within]
if delimiter.is_a?(Proc) && delimiter.arity == 0
delimiter = delimiter.call
end
if delimiter.is_a?(Array) && delimiter.all?(Module)
return if delimiter.empty? # Absurd, but don't raise
types = delimiter.map do |klass|
klass = klass.safe_constantize if klass.is_a?(String) # rubocop:disable Sorbet/ConstantsFromStrings
T.class_of(klass)
end
type = if types.size == 1
types.first
else
T.any(*types)
end
T.nilable(type)
end
end
end
Dsl::Compilers::ActiveModelAttributes.prepend(ActiveModelInClassValidator)
end
end
Which generates this signature:
sig { returns(T.any(T.class_of(ProcessorFoo), T.class_of(ProcessorBar)) }
def processor
end
FWIW, the class type is also in-house:
module ActiveModel
module Type
class ClassType < ::ActiveModel::Type::Value
sig { override.returns(Symbol) }
def type
:class
end
sig { override.params(value: T.nilable(T::Class[T.anything])).returns(T.nilable(String)) }
def serialize(value)
value&.name
end
sig do
override
.params(input: T.nilable(T.any(String, T::Class[T.anything])))
.returns(T.nilable(T::Class[T.anything]))
end
def cast(input)
return input if input.is_a?(::Class)
return if input.blank?
input.safe_constantize
end
end
end
end
See also non-nilable signatures: https://github.com/Shopify/tapioca/issues/2249 And explicit type signatures: https://github.com/Shopify/tapioca/issues/2251
Highly related: https://github.com/Shopify/tapioca/issues/2272