Skip to content

Commit

Permalink
Add ActiveModel::SecurePassword DSL generator
Browse files Browse the repository at this point in the history
  • Loading branch information
paracycle committed Oct 21, 2021
1 parent 6f742de commit 05cb401
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 0 deletions.
101 changes: 101 additions & 0 deletions lib/tapioca/compilers/dsl/active_model_secure_password.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# typed: strict
# frozen_string_literal: true

begin
require "active_model"
rescue LoadError
return
end

module Tapioca
module Compilers
module Dsl
# `Tapioca::Compilers::Dsl::ActiveModelSecurePassword` decorates RBI files for all
# classes that use [`ActiveModel::SecurePassword`](http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html).
#
# For example, with the following class:
#
# ~~~rb
# class User
# include ActiveModel::SecurePassword
#
# has_secure_password
# has_secure_password :token
# end
# ~~~
#
# this generator will produce an RBI file with the following content:
# ~~~rbi
# # typed: true
#
# class User
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
# def authenticate(unencrypted_password); end
#
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
# def authenticate_password(unencrypted_password); end
#
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
# def authenticate_token(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 { params(unencrypted_password: T.untyped).returns(T.untyped) }
# def password_confirmation=(unencrypted_password); end
#
# sig { returns(T.untyped) }
# def token; end
#
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
# def token=(unencrypted_password); end
#
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
# def token_confirmation=(unencrypted_password); end
# end
# ~~~
class ActiveModelSecurePassword < Base
extend T::Sig

sig do
override
.params(root: RBI::Tree, constant: T.all(Class, ::ActiveModel::SecurePassword::ClassMethods))
.void
end
def decorate(root, constant)
instance_methods_modules = if constant < ActiveModel::SecurePassword::InstanceMethodsOnActivation
# pre Rails 6.0, this used to be a single static module
[ActiveModel::SecurePassword::InstanceMethodsOnActivation]
else
# post Rails 6.0, this is now using a dynmaic module builder pattern
# and we can have multiple different ones included into the model
constant.ancestors.grep(ActiveModel::SecurePassword::InstanceMethodsOnActivation)
end

return if instance_methods_modules.empty?

methods = instance_methods_modules.flat_map { |mod| mod.instance_methods(false) }
return if methods.empty?

root.create_path(constant) do |klass|
methods.each do |method|
create_method_from_def(klass, constant.instance_method(method))
end
end
end

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
# This selects all classes that are `ActiveModel::SecurePassword::ClassMethods === klass`.
# In other words, we select all classes that have `ActiveModel::SecurePassword::ClassMethods`
# as an ancestor of its singleton class, i.e. all classes that have extended the
# `ActiveModel::SecurePassword::ClassMethods` module.
all_classes.grep(::ActiveModel::SecurePassword::ClassMethods)
end
end
end
end
end
49 changes: 49 additions & 0 deletions manual/generator_activemodelsecurepassword.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## ActiveModelSecurePassword

`Tapioca::Compilers::Dsl::ActiveModelSecurePassword` decorates RBI files for all
classes that use [`ActiveModel::SecurePassword`](http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html).

For example, with the following class:

~~~rb
class User
include ActiveModel::SecurePassword

has_secure_password
has_secure_password :token
end
~~~

this generator will produce an RBI file with the following content:
~~~rbi
# typed: true

class User
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def authenticate(unencrypted_password); end

sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def authenticate_password(unencrypted_password); end

sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def authenticate_token(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 { params(unencrypted_password: T.untyped).returns(T.untyped) }
def password_confirmation=(unencrypted_password); end

sig { returns(T.untyped) }
def token; end

sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def token=(unencrypted_password); end

sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def token_confirmation=(unencrypted_password); end
end
~~~
1 change: 1 addition & 0 deletions manual/generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ In the following section you will find all available DSL generators:
* [ActionMailer](generator_actionmailer.md)
* [ActiveJob](generator_activejob.md)
* [ActiveModelAttributes](generator_activemodelattributes.md)
* [ActiveModelSecurePassword](generator_activemodelsecurepassword.md)
* [ActiveRecordAssociations](generator_activerecordassociations.md)
* [ActiveRecordColumns](generator_activerecordcolumns.md)
* [ActiveRecordEnum](generator_activerecordenum.md)
Expand Down
156 changes: 156 additions & 0 deletions spec/tapioca/compilers/dsl/active_model_secure_password_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# typed: strict
# frozen_string_literal: true

require "spec_helper"

class Tapioca::Compilers::Dsl::ActiveModelSecurePasswordSpec < DslSpec
describe("#initialize") do
it("gathers no constants if there are no classes using ActiveModel::SecurePassword") do
assert_empty(gathered_constants)
end

it("gathers only classes including ActiveModel::SecurePassword") do
add_ruby_file("user.rb", <<~RUBY)
class User
end
class UserWithSecurePasswordModule
include ActiveModel::SecurePassword
end
class UserWithSecurePassword
include ActiveModel::SecurePassword
has_secure_password
end
RUBY

assert_equal(["UserWithSecurePassword", "UserWithSecurePasswordModule"], gathered_constants)
end
end

describe("#decorate") do
it("generates empty RBI file if there are no calls to has_secure_password") do
add_ruby_file("user.rb", <<~RUBY)
class User
include ActiveModel::SecurePassword
end
RUBY

expected = <<~RBI
# typed: strong
RBI

assert_equal(expected, rbi_for(:User))
end

it("generates default secure password methods") do
add_ruby_file("user.rb", <<~RUBY)
class User
include ActiveModel::SecurePassword
has_secure_password
end
RUBY

expected = <<~RBI
# typed: strong
class User
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def authenticate(unencrypted_password); end
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
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 { params(unencrypted_password: T.untyped).returns(T.untyped) }
def password_confirmation=(unencrypted_password); end
end
RBI

assert_equal(expected, rbi_for(:User))
end

it("generates custom secure password methods") do
add_ruby_file("user.rb", <<~RUBY)
class User
include ActiveModel::SecurePassword
has_secure_password :token
end
RUBY

expected = <<~RBI
# typed: strong
class User
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def authenticate_token(unencrypted_password); end
sig { returns(T.untyped) }
def token; end
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def token=(unencrypted_password); end
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def token_confirmation=(unencrypted_password); end
end
RBI

assert_equal(expected, rbi_for(:User))
end

it("generates multiple secure password methods") do
add_ruby_file("user.rb", <<~RUBY)
class User
include ActiveModel::SecurePassword
has_secure_password :token
has_secure_password
end
RUBY

expected = <<~RBI
# typed: strong
class User
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def authenticate(unencrypted_password); end
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def authenticate_password(unencrypted_password); end
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def authenticate_token(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 { params(unencrypted_password: T.untyped).returns(T.untyped) }
def password_confirmation=(unencrypted_password); end
sig { returns(T.untyped) }
def token; end
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def token=(unencrypted_password); end
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
def token_confirmation=(unencrypted_password); end
end
RBI

assert_equal(expected, rbi_for(:User))
end
end
end

0 comments on commit 05cb401

Please sign in to comment.