Shopify / Shopify/tapioca

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

Open
#2,251 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
873
Forks
164
Avg merge
4d 27m
Merged PRs (30d)
9

Description

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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading active_model/attributes and the Tapioca ActiveModel attributes compiler referenced in the issue. Compare the proposed TypeValidator and compiler extension with the existing attribute RBI generation, then define how the shown type-enforced attribute should be represented for both reading and writing; done means the behavior is covered by the project's relevant tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
developer-experience, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.