tapioca
tapioca copied to clipboard
Suggestion: Treat ActiveModel::Attribute as non-nilable if a PresenceValidator is present
class Foo
include ActiveModel::Model
attribute :name, :string
validates :name, presence: true
end
Under normal circumstances, Foo#name should never be nil.
Yes, it's possible to get a nil like Foo.new.name, but I'd argue that returning a type error would make sense here.
I ended up implementing this tapioca extension in my codebase:
# sorbet/tapioca/compilers/active_model_presence_validator.rb
require "active_model/attributes"
require "tapioca/dsl/compilers/active_model_attributes"
module Tapioca
module Compilers
module 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, type|
has_unconditional_validator = constant
.validators_on(method)
.grep(ActiveModel::Validations::PresenceValidator)
# Skip if any options would mean that the attribute is not always non-nilable
.any? { |v| v.options.slice(:if, :unless, :on, :allow_nil, :allow_blank).empty? }
next unless has_unconditional_validator
type = as_non_nilable_type(type)
attribute_methods[method] = type
attribute_methods["#{method}="] = type
end
attribute_methods.to_a
end
end
Dsl::Compilers::ActiveModelAttributes.prepend(ActiveModelPresenceValidator)
end
end
It's hacky, but it works 😅
See also known-type signatures: https://github.com/Shopify/tapioca/issues/2250