-
Notifications
You must be signed in to change notification settings - Fork 136
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ActiveModel::SecurePassword DSL generator
- Loading branch information
Showing
4 changed files
with
307 additions
and
0 deletions.
There are no files selected for viewing
101 changes: 101 additions & 0 deletions
101
lib/tapioca/compilers/dsl/active_model_secure_password.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
~~~ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
spec/tapioca/compilers/dsl/active_model_secure_password_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |