Suggestion: Generate `T.any` signatures when an `ActiveModel::Attribute` has a known type
Open
Nobody has claimed this yet.
- Dominant language
- Ruby
- Stars
- 873
- Forks
- 164
- Avg merge
- 4d 27m
- Merged PRs (30d)
- 9
Description
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
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with tapioca/dsl/compilers/active_model_attributes, then compare its behavior with the ActiveModelInClassValidator extension shown in the issue. Verify the ProcessingJob example generates a processor signature using T.any(T.class_of(ProcessorFoo), T.class_of(ProcessorBar)); the change is done when this works without the extension.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- ruby
- Domain
- tooling
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100