Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ group :development do
gem "flag_shih_tzu"
gem "kaminari-activerecord"
gem "money-rails"
gem "noticed", require: false
gem "paperclip"
gem "rails", "~> 7.2"
gem "rubocop-rspec"
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ GEM
nokogiri (1.16.7)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
noticed (3.0.0)
rails (>= 6.1.0)
paperclip (6.1.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
Expand Down Expand Up @@ -313,6 +315,7 @@ DEPENDENCIES
minitest-hooks
minitest-reporters
money-rails
noticed
paperclip
rails (~> 7.2)
rubocop-rspec
Expand Down
93 changes: 93 additions & 0 deletions lib/tapioca/dsl/compilers/noticed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# typed: strict
# frozen_string_literal: true

return unless defined?(Noticed::Event) && defined?(Noticed::Ephemeral)

module Tapioca
module Dsl
module Compilers
# `Tapioca::Dsl::Compilers::Noticed` decorates RBI files for subclasses
# of `Noticed::Event` and `Noticed::Ephemeral`.
#
# For example, with the following notifier class:
#
# ~~~rb
# class NewCommentNotifier < Noticed::Event
# required_params :comment
# deliver_by :email
# end
# ~~~
#
# This compiler will produce the RBI file `new_comment_notifier.rbi` with the following content:
#
# ~~~rbi
# # new_comment_notifier.rbi
# # typed: true
# class NewCommentNotifier
# class << self
# sig { params(params: T::Hash[Symbol, T.untyped]).returns(NewCommentNotifier) }
# def with(params); end
#
# sig { params(recipients: T.untyped, enqueue_job: T.nilable(T::Boolean), options: T.untyped).returns(NewCommentNotifier) }
# def deliver(recipients = T.unsafe(nil), enqueue_job: T.unsafe(nil), **options); end
#
# sig { params(recipients: T.untyped, enqueue_job: T.nilable(T::Boolean), options: T.untyped).returns(NewCommentNotifier) }
# def deliver_later(recipients = T.unsafe(nil), enqueue_job: T.unsafe(nil), **options); end
# end
# end
# ~~~
class Noticed < Tapioca::Dsl::Compiler
extend T::Sig

ConstantType = type_member do
{ fixed: T.any(T.class_of(::Noticed::Event), T.class_of(::Noticed::Ephemeral)) }
end

class << self
extend T::Sig

sig { override.returns(T::Enumerable[T::Module[T.anything]]) }
def gather_constants
all_classes.select do |klass|
klass < ::Noticed::Event || klass < ::Noticed::Ephemeral
end
end
end

sig { override.void }
def decorate
root.create_path(constant) do |klass|
singleton = RBI::SingletonClass.new
klass << singleton

singleton.create_method(
"with",
parameters: [create_param("params", type: "T::Hash[Symbol, T.untyped]")],
return_type: "::#{constant}",
)

singleton.create_method(
"deliver",
parameters: [
create_opt_param("recipients", type: "T.untyped", default: "T.unsafe(nil)"),
create_kw_opt_param("enqueue_job", type: "T.nilable(T::Boolean)", default: "T.unsafe(nil)"),
create_kw_rest_param("options", type: "T.untyped"),
],
return_type: "::#{constant}",
)

singleton.create_method(
"deliver_later",
parameters: [
create_opt_param("recipients", type: "T.untyped", default: "T.unsafe(nil)"),
create_kw_opt_param("enqueue_job", type: "T.nilable(T::Boolean)", default: "T.unsafe(nil)"),
create_kw_rest_param("options", type: "T.untyped"),
],
return_type: "::#{constant}",
)
Comment on lines 69 to 87
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking, but can we use reflection to make any of these types more precise?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upgraded to within possible options and appropriate class type - but recipients can basically be anything. very adhoc Railsy approach to things that doesn't translate well to any sort of structured programming

end
end
end
end
end
end
32 changes: 32 additions & 0 deletions manual/compiler_noticed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Noticed

`Tapioca::Dsl::Compilers::Noticed` decorates RBI files for subclasses
of `Noticed::Event` and `Noticed::Ephemeral`.

For example, with the following notifier class:

~~~rb
class NewCommentNotifier < Noticed::Event
required_params :comment
deliver_by :email
end
~~~

This compiler will produce the RBI file `new_comment_notifier.rbi` with the following content:

~~~rbi
# new_comment_notifier.rbi
# typed: true
class NewCommentNotifier
class << self
sig { params(params: T::Hash[Symbol, T.untyped]).returns(NewCommentNotifier) }
def with(params); end

sig { params(recipients: T.untyped, enqueue_job: T.nilable(T::Boolean), options: T.untyped).returns(NewCommentNotifier) }
def deliver(recipients = T.unsafe(nil), enqueue_job: T.unsafe(nil), **options); end

sig { params(recipients: T.untyped, enqueue_job: T.nilable(T::Boolean), options: T.untyped).returns(NewCommentNotifier) }
def deliver_later(recipients = T.unsafe(nil), enqueue_job: T.unsafe(nil), **options); end
end
end
~~~
1 change: 1 addition & 0 deletions manual/compilers.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ This list is an evergeen list of currently available compilers.
* [FlagShihTzu](compiler_flagshihtzu.md)
* [Kaminari](compiler_kaminari.md)
* [MoneyRails](compiler_moneyrails.md)
* [Noticed](compiler_noticed.md)
* [Paperclip](compiler_paperclip.md)
<!-- END_COMPILER_LIST -->
4 changes: 2 additions & 2 deletions sorbet/tapioca/config.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
gem:
# Add your `gem` command parameters here:
#
# exclude:
# - gem_name
# doc: true
# workers: 5
exclude:
- noticed
dsl:
# Add your `dsl` command parameters here:
#
Expand Down
142 changes: 142 additions & 0 deletions spec/tapioca/dsl/compilers/noticed_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# typed: strict
# frozen_string_literal: true

require "spec_helper"

# Stub classes for the Noticed gem.
#
# Unlike gems like FlagShihTzu or MoneyRails which just extend ActiveRecord,
# Noticed is a Rails Engine that requires a full Rails boot. The real
# Noticed::Event and Noticed::Ephemeral classes:
# - Are in `app/models/` (Rails autoloading)
# - Include `Rails.application.routes.url_helpers`
# - Require `ApplicationRecord` to exist
#
# For testing the compiler, we define stub base classes that notifiers can
# inherit from. Test notifier classes are created using `add_ruby_file` for
# proper isolation (same pattern as the AR service specs).
module Noticed
class Event; end
class Ephemeral; end
end

module Tapioca
module Dsl
module Compilers
class NoticedSpec < ::DslSpec
describe "Tapioca::Dsl::Compilers::Noticed" do
describe "initialize" do
it "gathers no constants if there are no Noticed classes" do
add_ruby_file("post.rb", <<~RUBY)
class Post
end
RUBY

assert_equal(gathered_constants, [])
end

it "gathers Noticed::Event subclasses" do
add_ruby_file("new_comment_notifier.rb", <<~RUBY)
class NewCommentNotifier < Noticed::Event
end
RUBY

assert_equal(["NewCommentNotifier"], gathered_constants)
end

it "gathers Noticed::Ephemeral subclasses" do
add_ruby_file("welcome_notifier.rb", <<~RUBY)
class WelcomeNotifier < Noticed::Ephemeral
end
RUBY

assert_equal(["WelcomeNotifier"], gathered_constants)
end
end

describe "decorate" do
it "generates RBI for Noticed::Event subclass" do
add_ruby_file("new_comment_notifier.rb", <<~RUBY)
class NewCommentNotifier < Noticed::Event
end
RUBY

expected = <<~RBI
# typed: strong

class NewCommentNotifier
class << self
sig { params(recipients: T.untyped, enqueue_job: T.nilable(T::Boolean), options: T.untyped).returns(::NewCommentNotifier) }
def deliver(recipients = T.unsafe(nil), enqueue_job: T.unsafe(nil), **options); end

sig { params(recipients: T.untyped, enqueue_job: T.nilable(T::Boolean), options: T.untyped).returns(::NewCommentNotifier) }
def deliver_later(recipients = T.unsafe(nil), enqueue_job: T.unsafe(nil), **options); end

sig { params(params: T::Hash[Symbol, T.untyped]).returns(::NewCommentNotifier) }
def with(params); end
end
end
RBI

assert_equal(expected, rbi_for(:NewCommentNotifier))
end

it "generates RBI for Noticed::Ephemeral subclass" do
add_ruby_file("welcome_notifier.rb", <<~RUBY)
class WelcomeNotifier < Noticed::Ephemeral
end
RUBY

expected = <<~RBI
# typed: strong

class WelcomeNotifier
class << self
sig { params(recipients: T.untyped, enqueue_job: T.nilable(T::Boolean), options: T.untyped).returns(::WelcomeNotifier) }
def deliver(recipients = T.unsafe(nil), enqueue_job: T.unsafe(nil), **options); end

sig { params(recipients: T.untyped, enqueue_job: T.nilable(T::Boolean), options: T.untyped).returns(::WelcomeNotifier) }
def deliver_later(recipients = T.unsafe(nil), enqueue_job: T.unsafe(nil), **options); end

sig { params(params: T::Hash[Symbol, T.untyped]).returns(::WelcomeNotifier) }
def with(params); end
end
end
RBI

assert_equal(expected, rbi_for(:WelcomeNotifier))
end

it "generates RBI for nested notifier classes" do
add_ruby_file("admin/alert_notifier.rb", <<~RUBY)
module Admin
class AlertNotifier < Noticed::Event
end
end
RUBY

expected = <<~RBI
# typed: strong

class Admin::AlertNotifier
class << self
sig { params(recipients: T.untyped, enqueue_job: T.nilable(T::Boolean), options: T.untyped).returns(::Admin::AlertNotifier) }
def deliver(recipients = T.unsafe(nil), enqueue_job: T.unsafe(nil), **options); end

sig { params(recipients: T.untyped, enqueue_job: T.nilable(T::Boolean), options: T.untyped).returns(::Admin::AlertNotifier) }
def deliver_later(recipients = T.unsafe(nil), enqueue_job: T.unsafe(nil), **options); end

sig { params(params: T::Hash[Symbol, T.untyped]).returns(::Admin::AlertNotifier) }
def with(params); end
end
end
RBI

assert_equal(expected, rbi_for("Admin::AlertNotifier"))
end
end
end
end
end
end
end