diff --git a/lib/tapioca/dsl/compilers/active_model_secure_password.rb b/lib/tapioca/dsl/compilers/active_model_secure_password.rb index 6ff34016a..d9fa6c053 100644 --- a/lib/tapioca/dsl/compilers/active_model_secure_password.rb +++ b/lib/tapioca/dsl/compilers/active_model_secure_password.rb @@ -83,6 +83,8 @@ def decorate else create_method_from_def(klass, constant.instance_method(method)) end + + create_reset_token_finders(klass, method) end end end @@ -104,6 +106,32 @@ def gather_constants all_classes.grep(::ActiveModel::SecurePassword::ClassMethods) end end + + private + + # Active Record models get class-level finders for each reset token, defined by `has_secure_password` + # directly on the model rather than on the `InstanceMethodsOnActivation` module. + #: (RBI::Scope klass, Symbol method) -> void + def create_reset_token_finders(klass, method) + method_name = method.to_s + return unless method_name.end_with?("_reset_token") + + finder = "find_by_#{method_name}" + return unless constant.respond_to?(finder) + + klass.create_method( + finder, + parameters: [create_param("token", type: "String")], + return_type: "T.nilable(#{constant})", + class_method: true, + ) + klass.create_method( + "#{finder}!", + parameters: [create_param("token", type: "String")], + return_type: constant.to_s, + class_method: true, + ) + end end end end diff --git a/spec/tapioca/dsl/compilers/active_model_secure_password_spec.rb b/spec/tapioca/dsl/compilers/active_model_secure_password_spec.rb index d73621a31..38bb17c3d 100644 --- a/spec/tapioca/dsl/compilers/active_model_secure_password_spec.rb +++ b/spec/tapioca/dsl/compilers/active_model_secure_password_spec.rb @@ -222,6 +222,62 @@ def token_salt; end assert_equal(expected, rbi_for(:User)) end + + it "generates reset token finders for Active Record models" do + add_ruby_file("user.rb", <<~RUBY) + require "active_record" + + class User < ActiveRecord::Base + has_secure_password + end + RUBY + + expected = template(<<~RBI) + # typed: strong + + class User + sig { params(unencrypted_password: T.untyped).returns(T.any(User, FalseClass)) } + def authenticate(unencrypted_password); end + + sig { params(unencrypted_password: T.untyped).returns(T.any(User, FalseClass)) } + def authenticate_password(unencrypted_password); end + + sig { returns(T.untyped) } + def password; end + + sig { params(unencrypted_password: T.untyped).returns(T.untyped) } + def password=(unencrypted_password); end + + sig { returns(T.untyped) } + def password_challenge; end + + sig { params(_arg0: T.untyped).returns(T.untyped) } + def password_challenge=(_arg0); end + + sig { returns(T.untyped) } + def password_confirmation; end + + sig { params(_arg0: T.untyped).returns(T.untyped) } + def password_confirmation=(_arg0); end + + sig { returns(T.untyped) } + def password_reset_token; end + + sig { returns(T.untyped) } + def password_salt; end + + class << self + sig { params(token: String).returns(T.nilable(User)) } + def find_by_password_reset_token(token); end + + sig { params(token: String).returns(User) } + def find_by_password_reset_token!(token); end + end + end + RBI + + assert_equal(expected, rbi_for(:User)) + end end end end