Skip to content

Commit

Permalink
Generator for ActiveJob::Base descendants
Browse files Browse the repository at this point in the history
Need this to properly type `perform_later` and `perform_now` methods, as their arguments change based on the signature of the `perform` method.
  • Loading branch information
kddnewton committed Apr 30, 2021
1 parent bf4617d commit bb0fb63
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 0 deletions.
71 changes: 71 additions & 0 deletions lib/tapioca/compilers/dsl/active_job.rb
@@ -0,0 +1,71 @@
# typed: strict
# frozen_string_literal: true

require "parlour"

begin
require "active_job"
rescue LoadError
return
end

module Tapioca
module Compilers
module Dsl
# `Tapioca::Compilers::Dsl::ActiveJob` generates RBI files for subclasses of
# [`ActiveJob::Base`](https://api.rubyonrails.org/classes/ActiveJob/Base.html).
#
# For example, with the following `ActiveJob` subclass:
#
# ~~~rb
# class NotifyUserJob < ActiveJob::Base
# def perform(user)
# # ...
# end
# end
# ~~~
#
# this generator will produce the RBI file `notify_user_job.rbi` with the following content:
#
# ~~~rbi
# # notify_user_job.rbi
# # typed: true
# class NotifyUserJob
# sig { params(user: T.untyped).returns(T.attached_class) }
# def self.perform_later(user); end
#
# sig { params(user: T.untyped).returns(T.attached_class) }
# def self.perform_now(user); end
# end
# ~~~
class ActiveJob < Base
extend T::Sig

sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActiveJob::Base)).void }
def decorate(root, constant)
root.path(constant) do |job|
next unless constant.instance_methods(false).include?(:perform)

method = constant.instance_method(:perform)
parameters = compile_method_parameters_to_parlour(method)

%w[perform_later perform_now].each do |name|
create_method(
job,
name,
parameters: parameters,
return_type: "T.attached_class",
class_method: true
)
end
end
end

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
::ActiveJob::Base.descendants
end
end
end
end
end
28 changes: 28 additions & 0 deletions manual/generator_activejob.md
@@ -0,0 +1,28 @@
## ActiveJob

`Tapioca::Compilers::Dsl::ActiveJob` generates RBI files for subclasses of
[`ActiveJob::Base`](https://api.rubyonrails.org/classes/ActiveJob/Base.html).

For example, with the following `ActiveJob` subclass:

~~~rb
class NotifyUserJob < ActiveJob::Base
def perform(user)
# ...
end
end
~~~

this generator will produce the RBI file `notify_user_job.rbi` with the following content:

~~~rbi
# notify_user_job.rbi
# typed: true
class NotifyUserJob
sig { params(user: T.untyped).returns(T.attached_class) }
def self.perform_later(user); end

sig { params(user: T.untyped).returns(T.attached_class) }
def self.perform_now(user); end
end
~~~
1 change: 1 addition & 0 deletions manual/generators.md
Expand Up @@ -5,6 +5,7 @@ In the following section you will find all available DSL generators:
<!-- START_GENERATOR_LIST -->
* [ActionControllerHelpers](generator_actioncontrollerhelpers.md)
* [ActionMailer](generator_actionmailer.md)
* [ActiveJob](generator_activejob.md)
* [ActiveRecordAssociations](generator_activerecordassociations.md)
* [ActiveRecordColumns](generator_activerecordcolumns.md)
* [ActiveRecordEnum](generator_activerecordenum.md)
Expand Down
101 changes: 101 additions & 0 deletions spec/tapioca/compilers/dsl/active_job_spec.rb
@@ -0,0 +1,101 @@
# typed: strict
# frozen_string_literal: true

require "spec_helper"

class Tapioca::Compilers::Dsl::ActiveJobSpec < DslSpec
describe("#initialize") do
it("gathers no constants if there are no ActiveJob subclasses") do
assert_empty(gathered_constants)
end

it("gathers only ActiveJob subclasses") do
add_ruby_file("content.rb", <<~RUBY)
class NotifyJob < ActiveJob::Base
end
class User
end
RUBY

assert_equal(["NotifyJob"], gathered_constants)
end

it("gathers subclasses of ActiveJob subclasses") do
add_ruby_file("content.rb", <<~RUBY)
class NotifyJob < ActiveJob::Base
end
class SecondaryNotifyJob < NotifyJob
end
RUBY

assert_equal(["NotifyJob", "SecondaryNotifyJob"], gathered_constants)
end
end

describe("#decorate") do
it("generates empty RBI file if there is no perform method") do
add_ruby_file("job.rb", <<~RUBY)
class NotifyJob < ActiveJob::Base
end
RUBY

expected = <<~RBI
# typed: strong
class NotifyJob
end
RBI

assert_equal(expected, rbi_for(:NotifyJob))
end

it("generates correct RBI file for subclass with methods") do
add_ruby_file("job.rb", <<~RUBY)
class NotifyJob < ActiveJob::Base
def perform(user_id)
# ...
end
end
RUBY

expected = <<~RBI
# typed: strong
class NotifyJob
sig { params(user_id: T.untyped).returns(T.attached_class) }
def self.perform_later(user_id); end
sig { params(user_id: T.untyped).returns(T.attached_class) }
def self.perform_now(user_id); end
end
RBI

assert_equal(expected, rbi_for(:NotifyJob))
end

it("generates correct RBI file for subclass with method signatures") do
add_ruby_file("job.rb", <<~RUBY)
class NotifyJob < ActiveJob::Base
extend T::Sig
sig { params(user_id: Integer).void }
def perform(user_id)
# ...
end
end
RUBY

expected = <<~RBI
# typed: strong
class NotifyJob
sig { params(user_id: Integer).returns(T.attached_class) }
def self.perform_later(user_id); end
sig { params(user_id: Integer).returns(T.attached_class) }
def self.perform_now(user_id); end
end
RBI

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

0 comments on commit bb0fb63

Please sign in to comment.