Skip to content

Commit 05cb401

Browse files
committed
Add ActiveModel::SecurePassword DSL generator
1 parent 6f742de commit 05cb401

File tree

4 files changed

+307
-0
lines changed

4 files changed

+307
-0
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
begin
5+
require "active_model"
6+
rescue LoadError
7+
return
8+
end
9+
10+
module Tapioca
11+
module Compilers
12+
module Dsl
13+
# `Tapioca::Compilers::Dsl::ActiveModelSecurePassword` decorates RBI files for all
14+
# classes that use [`ActiveModel::SecurePassword`](http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html).
15+
#
16+
# For example, with the following class:
17+
#
18+
# ~~~rb
19+
# class User
20+
# include ActiveModel::SecurePassword
21+
#
22+
# has_secure_password
23+
# has_secure_password :token
24+
# end
25+
# ~~~
26+
#
27+
# this generator will produce an RBI file with the following content:
28+
# ~~~rbi
29+
# # typed: true
30+
#
31+
# class User
32+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
33+
# def authenticate(unencrypted_password); end
34+
#
35+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
36+
# def authenticate_password(unencrypted_password); end
37+
#
38+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
39+
# def authenticate_token(unencrypted_password); end
40+
#
41+
# sig { returns(T.untyped) }
42+
# def password; end
43+
#
44+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
45+
# def password=(unencrypted_password); end
46+
#
47+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
48+
# def password_confirmation=(unencrypted_password); end
49+
#
50+
# sig { returns(T.untyped) }
51+
# def token; end
52+
#
53+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
54+
# def token=(unencrypted_password); end
55+
#
56+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
57+
# def token_confirmation=(unencrypted_password); end
58+
# end
59+
# ~~~
60+
class ActiveModelSecurePassword < Base
61+
extend T::Sig
62+
63+
sig do
64+
override
65+
.params(root: RBI::Tree, constant: T.all(Class, ::ActiveModel::SecurePassword::ClassMethods))
66+
.void
67+
end
68+
def decorate(root, constant)
69+
instance_methods_modules = if constant < ActiveModel::SecurePassword::InstanceMethodsOnActivation
70+
# pre Rails 6.0, this used to be a single static module
71+
[ActiveModel::SecurePassword::InstanceMethodsOnActivation]
72+
else
73+
# post Rails 6.0, this is now using a dynmaic module builder pattern
74+
# and we can have multiple different ones included into the model
75+
constant.ancestors.grep(ActiveModel::SecurePassword::InstanceMethodsOnActivation)
76+
end
77+
78+
return if instance_methods_modules.empty?
79+
80+
methods = instance_methods_modules.flat_map { |mod| mod.instance_methods(false) }
81+
return if methods.empty?
82+
83+
root.create_path(constant) do |klass|
84+
methods.each do |method|
85+
create_method_from_def(klass, constant.instance_method(method))
86+
end
87+
end
88+
end
89+
90+
sig { override.returns(T::Enumerable[Module]) }
91+
def gather_constants
92+
# This selects all classes that are `ActiveModel::SecurePassword::ClassMethods === klass`.
93+
# In other words, we select all classes that have `ActiveModel::SecurePassword::ClassMethods`
94+
# as an ancestor of its singleton class, i.e. all classes that have extended the
95+
# `ActiveModel::SecurePassword::ClassMethods` module.
96+
all_classes.grep(::ActiveModel::SecurePassword::ClassMethods)
97+
end
98+
end
99+
end
100+
end
101+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
## ActiveModelSecurePassword
2+
3+
`Tapioca::Compilers::Dsl::ActiveModelSecurePassword` decorates RBI files for all
4+
classes that use [`ActiveModel::SecurePassword`](http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html).
5+
6+
For example, with the following class:
7+
8+
~~~rb
9+
class User
10+
include ActiveModel::SecurePassword
11+
12+
has_secure_password
13+
has_secure_password :token
14+
end
15+
~~~
16+
17+
this generator will produce an RBI file with the following content:
18+
~~~rbi
19+
# typed: true
20+
21+
class User
22+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
23+
def authenticate(unencrypted_password); end
24+
25+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
26+
def authenticate_password(unencrypted_password); end
27+
28+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
29+
def authenticate_token(unencrypted_password); end
30+
31+
sig { returns(T.untyped) }
32+
def password; end
33+
34+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
35+
def password=(unencrypted_password); end
36+
37+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
38+
def password_confirmation=(unencrypted_password); end
39+
40+
sig { returns(T.untyped) }
41+
def token; end
42+
43+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
44+
def token=(unencrypted_password); end
45+
46+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
47+
def token_confirmation=(unencrypted_password); end
48+
end
49+
~~~

manual/generators.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ In the following section you will find all available DSL generators:
88
* [ActionMailer](generator_actionmailer.md)
99
* [ActiveJob](generator_activejob.md)
1010
* [ActiveModelAttributes](generator_activemodelattributes.md)
11+
* [ActiveModelSecurePassword](generator_activemodelsecurepassword.md)
1112
* [ActiveRecordAssociations](generator_activerecordassociations.md)
1213
* [ActiveRecordColumns](generator_activerecordcolumns.md)
1314
* [ActiveRecordEnum](generator_activerecordenum.md)
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "spec_helper"
5+
6+
class Tapioca::Compilers::Dsl::ActiveModelSecurePasswordSpec < DslSpec
7+
describe("#initialize") do
8+
it("gathers no constants if there are no classes using ActiveModel::SecurePassword") do
9+
assert_empty(gathered_constants)
10+
end
11+
12+
it("gathers only classes including ActiveModel::SecurePassword") do
13+
add_ruby_file("user.rb", <<~RUBY)
14+
class User
15+
end
16+
17+
class UserWithSecurePasswordModule
18+
include ActiveModel::SecurePassword
19+
end
20+
21+
class UserWithSecurePassword
22+
include ActiveModel::SecurePassword
23+
24+
has_secure_password
25+
end
26+
RUBY
27+
28+
assert_equal(["UserWithSecurePassword", "UserWithSecurePasswordModule"], gathered_constants)
29+
end
30+
end
31+
32+
describe("#decorate") do
33+
it("generates empty RBI file if there are no calls to has_secure_password") do
34+
add_ruby_file("user.rb", <<~RUBY)
35+
class User
36+
include ActiveModel::SecurePassword
37+
end
38+
RUBY
39+
40+
expected = <<~RBI
41+
# typed: strong
42+
RBI
43+
44+
assert_equal(expected, rbi_for(:User))
45+
end
46+
47+
it("generates default secure password methods") do
48+
add_ruby_file("user.rb", <<~RUBY)
49+
class User
50+
include ActiveModel::SecurePassword
51+
52+
has_secure_password
53+
end
54+
RUBY
55+
56+
expected = <<~RBI
57+
# typed: strong
58+
59+
class User
60+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
61+
def authenticate(unencrypted_password); end
62+
63+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
64+
def authenticate_password(unencrypted_password); end
65+
66+
sig { returns(T.untyped) }
67+
def password; end
68+
69+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
70+
def password=(unencrypted_password); end
71+
72+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
73+
def password_confirmation=(unencrypted_password); end
74+
end
75+
RBI
76+
77+
assert_equal(expected, rbi_for(:User))
78+
end
79+
80+
it("generates custom secure password methods") do
81+
add_ruby_file("user.rb", <<~RUBY)
82+
class User
83+
include ActiveModel::SecurePassword
84+
85+
has_secure_password :token
86+
end
87+
RUBY
88+
89+
expected = <<~RBI
90+
# typed: strong
91+
92+
class User
93+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
94+
def authenticate_token(unencrypted_password); end
95+
96+
sig { returns(T.untyped) }
97+
def token; end
98+
99+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
100+
def token=(unencrypted_password); end
101+
102+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
103+
def token_confirmation=(unencrypted_password); end
104+
end
105+
RBI
106+
107+
assert_equal(expected, rbi_for(:User))
108+
end
109+
110+
it("generates multiple secure password methods") do
111+
add_ruby_file("user.rb", <<~RUBY)
112+
class User
113+
include ActiveModel::SecurePassword
114+
115+
has_secure_password :token
116+
has_secure_password
117+
end
118+
RUBY
119+
120+
expected = <<~RBI
121+
# typed: strong
122+
123+
class User
124+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
125+
def authenticate(unencrypted_password); end
126+
127+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
128+
def authenticate_password(unencrypted_password); end
129+
130+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
131+
def authenticate_token(unencrypted_password); end
132+
133+
sig { returns(T.untyped) }
134+
def password; end
135+
136+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
137+
def password=(unencrypted_password); end
138+
139+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
140+
def password_confirmation=(unencrypted_password); end
141+
142+
sig { returns(T.untyped) }
143+
def token; end
144+
145+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
146+
def token=(unencrypted_password); end
147+
148+
sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
149+
def token_confirmation=(unencrypted_password); end
150+
end
151+
RBI
152+
153+
assert_equal(expected, rbi_for(:User))
154+
end
155+
end
156+
end

0 commit comments

Comments
 (0)