Skip to content

Commit 1f74e40

Browse files
committed
Support signature generation for ActionMailer
ActionMailer automatically generates class methods from the instance methods user defined via method_missing. For example, `AccountMailer.welcome_mail` will be generated from `AccountMail#welcome_mail` that is defined by users. This starts to support signature generation for auto-generated class methods for mailer classes dynamically.
1 parent beafce2 commit 1f74e40

File tree

15 files changed

+206
-12
lines changed

15 files changed

+206
-12
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ $ bin/rails g rbs_rails:install
2828

2929
Then, the following three tasks are available.
3030

31+
* `rbs_rails:generate_rbs_for_mailers`: Generate RBS files for Action Mailer classes
3132
* `rbs_rails:generate_rbs_for_models`: Generate RBS files for Active Record models
3233
* `rbs_rails:generate_rbs_for_path_helpers`: Generate RBS files for path helpers
3334
* `rbs_rails:all`: Execute all tasks of RBS Rails
@@ -38,6 +39,9 @@ You can also run rbs_rails from command line:
3839
# Generate all RBS files
3940
$ bundle exec rbs_rails all
4041

42+
# Generate RBS files for mailers
43+
$ bundle exec rbs_rails mailers
44+
4145
# Generate RBS files for models
4246
$ bundle exec rbs_rails models
4347

lib/rbs_rails.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
require_relative "rbs_rails/version"
66
require_relative "rbs_rails/util"
7+
require_relative 'rbs_rails/action_mailer'
78
require_relative 'rbs_rails/active_record'
89
require_relative 'rbs_rails/active_record/enum'
910
require_relative 'rbs_rails/path_helpers'

lib/rbs_rails/action_mailer.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
module RbsRails
2+
module ActionMailer
3+
# @rbs klass: singleton(ActionMailer::Base)
4+
def self.class_to_rbs(klass) #: String
5+
Generator.new(klass).generate
6+
end
7+
8+
class Generator
9+
# @rbs @klass_name: String
10+
11+
# @rbs klass: singleton(ActionMailer::Base)
12+
def initialize(klass) #: void
13+
@klass = klass
14+
@klass_name = Util.module_name(klass, abs: false)
15+
end
16+
17+
def generate #: String
18+
Util.format_rbs klass_decl
19+
end
20+
21+
private def klass_decl #: String
22+
<<~RBS
23+
# resolve-type-names: false
24+
25+
#{header}
26+
#{methods}
27+
#{footer}
28+
RBS
29+
end
30+
31+
private def header #: String
32+
namespace = +''
33+
klass_name(abs: false).split('::').map do |mod_name|
34+
namespace += "::#{mod_name}"
35+
mod_object = Object.const_get(namespace)
36+
case mod_object
37+
when Class
38+
# @type var superclass: Class
39+
superclass = _ = mod_object.superclass
40+
superclass_name = Util.module_name(superclass, abs: false)
41+
42+
"class #{namespace} < ::#{superclass_name}"
43+
when Module
44+
"module #{namespace}"
45+
else
46+
raise 'unreachable'
47+
end
48+
end.join("\n")
49+
end
50+
51+
private def methods #: String
52+
klass.action_methods.map do |method_name|
53+
"def self.#{method_name}: (*untyped) -> ::ActionMailer::MessageDelivery"
54+
end.join("\n")
55+
end
56+
57+
private def footer #: String
58+
"end\n" * klass_name(abs: false).split('::').size
59+
end
60+
61+
# @rbs abs: bool
62+
private def klass_name(abs: true) #: String
63+
abs ? "::#{@klass_name}" : @klass_name
64+
end
65+
66+
private
67+
attr_reader :klass #: singleton(ActionMailer::Base)
68+
end
69+
end
70+
end

lib/rbs_rails/cli.rb

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,15 @@ def run(argv) #: Integer
2828
when "all"
2929
load_application
3030
load_config
31+
generate_mailers
3132
generate_models
3233
generate_path_helpers
3334
0
35+
when "mailers"
36+
load_application
37+
load_config
38+
generate_mailers
39+
0
3440
when "models"
3541
load_application
3642
load_config
@@ -89,6 +95,18 @@ def install_hooks #: void
8995
require 'rbs_rails/active_record/enum'
9096
end
9197

98+
def generate_mailers #: void
99+
Rails.application.eager_load!
100+
101+
::ActionMailer::Base.descendants.each do |klass|
102+
path = config.signature_root_dir / rbs_path_for(klass)
103+
path.dirname.mkpath
104+
105+
sig = RbsRails::ActionMailer.class_to_rbs(klass)
106+
path.write sig
107+
end
108+
end
109+
92110
def generate_models #: void
93111
Rails.application.eager_load!
94112

@@ -112,17 +130,7 @@ def generate_single_model(klass, dep_builder) #: bool
112130
return false if config.ignored_model?(klass)
113131
return false unless RbsRails::ActiveRecord.generatable?(klass)
114132

115-
original_path, _line = Object.const_source_location(klass.name) rescue nil
116-
117-
rbs_relative_path = if original_path && Pathname.new(original_path).fnmatch?("#{Rails.root}/**")
118-
Pathname.new(original_path)
119-
.relative_path_from(Rails.root)
120-
.sub_ext('.rbs')
121-
else
122-
"app/models/#{klass.name.underscore}.rbs"
123-
end
124-
125-
path = config.signature_root_dir / rbs_relative_path
133+
path = config.signature_root_dir / rbs_path_for(klass)
126134
path.dirname.mkpath
127135

128136
sig = RbsRails::ActiveRecord.class_to_rbs(klass, dependencies: dep_builder.deps)
@@ -140,6 +148,20 @@ def generate_path_helpers #: void
140148
path.write sig
141149
end
142150

151+
# @rbs klass: singleton(Object)
152+
def rbs_path_for(klass) #: String
153+
original_path, _line = Object.const_source_location(klass.name) rescue nil
154+
155+
if original_path && Pathname.new(original_path).fnmatch?("#{Rails.root}/**")
156+
Pathname.new(original_path)
157+
.relative_path_from(Rails.root)
158+
.sub_ext('.rbs')
159+
.to_s
160+
else
161+
"app/models/#{klass.name.underscore}.rbs"
162+
end
163+
end
164+
143165
def create_option_parser #: OptionParser
144166
OptionParser.new do |opts|
145167
opts.banner = <<~BANNER

lib/rbs_rails/rake_task.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def initialize(name = :rbs_rails, &block) #: void
2323
block.call(self) if block
2424

2525
def_generate_rbs_for_models
26+
def_generate_rbs_for_mailers
2627
def_generate_rbs_for_path_helpers
2728
def_all
2829
end
@@ -51,6 +52,17 @@ def def_generate_rbs_for_models #: void
5152
end
5253
end
5354

55+
def def_generate_rbs_for_mailers #: void
56+
desc 'Generate RBS files for Action Mailer mailers'
57+
task :"#{name}:generate_rbs_for_mailers" do
58+
if signature_root_dir
59+
sh "rbs_rails", "mailers", "--signature-root-dir=#{signature_root_dir}"
60+
else
61+
sh "rbs_rails", "mailers"
62+
end
63+
end
64+
end
65+
5466
def def_generate_rbs_for_path_helpers #: void
5567
desc 'Generate RBS files for path helpers'
5668
task :"#{name}:generate_rbs_for_path_helpers" do

sig/rbs_rails/action_mailer.rbs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Generated from lib/rbs_rails/action_mailer.rb with RBS::Inline
2+
3+
module RbsRails
4+
module ActionMailer
5+
# @rbs klass: singleton(ActionMailer::Base)
6+
def self.class_to_rbs: (singleton(ActionMailer::Base) klass) -> String
7+
8+
class Generator
9+
@klass_name: String
10+
11+
# @rbs klass: singleton(ActionMailer::Base)
12+
def initialize: (singleton(ActionMailer::Base) klass) -> void
13+
14+
def generate: () -> String
15+
16+
private def klass_decl: () -> String
17+
18+
private def header: () -> String
19+
20+
private def methods: () -> String
21+
22+
private def footer: () -> String
23+
24+
# @rbs abs: bool
25+
private def klass_name: (?abs: bool) -> String
26+
27+
private
28+
29+
attr_reader klass: singleton(ActionMailer::Base)
30+
end
31+
end
32+
end

sig/rbs_rails/cli.rbs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ module RbsRails
2020

2121
def install_hooks: () -> void
2222

23+
def generate_mailers: () -> void
24+
2325
def generate_models: () -> void
2426

2527
# @rbs klass: singleton(ActiveRecord::Base)
@@ -28,6 +30,9 @@ module RbsRails
2830

2931
def generate_path_helpers: () -> void
3032

33+
# @rbs klass: singleton(Object)
34+
def rbs_path_for: (singleton(Object) klass) -> String
35+
3136
def create_option_parser: () -> OptionParser
3237
end
3338
end

sig/rbs_rails/rake_task.rbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ module RbsRails
2020

2121
def def_generate_rbs_for_models: () -> void
2222

23+
def def_generate_rbs_for_mailers: () -> void
24+
2325
def def_generate_rbs_for_path_helpers: () -> void
2426

2527
private def signature_root_dir: () -> Pathname?
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class ApplicationMailer < ActionMailer::Base
2+
default from: '[email protected]'
3+
layout 'mailer'
4+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class UserMailer < ApplicationMailer
2+
default from: '[email protected]'
3+
4+
def welcome_email
5+
@user = params[:user]
6+
@url = 'http://example.com/login'
7+
mail(to: @user.email, subject: 'Welcome to My Awesome Site')
8+
end
9+
end

0 commit comments

Comments
 (0)