From d7d9b3004098c850d6b9d3d310f6b2d72a8a5b1a Mon Sep 17 00:00:00 2001 From: Jeremy Woertink Date: Mon, 15 Aug 2022 07:54:08 -0700 Subject: [PATCH] Callbacks and deliver short circuiting (#72) * allows the emails to have before/after callbacks. Fixes #65 * adds in functionality to stop an email from being delivered. Fixes #61 --- spec/callbacks_spec.cr | 105 ++++++++++++++++++++++++++++++++++++++++ spec/email_spec.cr | 14 ++++++ src/carbon/callbacks.cr | 81 +++++++++++++++++++++++++++++++ src/carbon/email.cr | 16 +++++- 4 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 spec/callbacks_spec.cr create mode 100644 src/carbon/callbacks.cr diff --git a/spec/callbacks_spec.cr b/spec/callbacks_spec.cr new file mode 100644 index 0000000..b02bbf1 --- /dev/null +++ b/spec/callbacks_spec.cr @@ -0,0 +1,105 @@ +require "./spec_helper" + +abstract class BaseTestEmail < Carbon::Email + subject "My great subject" + from Carbon::Address.new("from@example.com") + to Carbon::Address.new("to@example.com") +end + +BaseTestEmail.configure do |setting| + setting.adapter = Carbon::DevAdapter.new +end + +private class EmailWithBeforeCallbacks < BaseTestEmail + property ran_before_callback : Bool = false + + before_send do + self.ran_before_callback = true + end +end + +private class EmailWithAfterCallbacks < BaseTestEmail + property ran_after_callback : Bool = false + + after_send do |_response| + self.ran_after_callback = true + end +end + +private class EmailWithBothBeforeAndAfterCallbacks < BaseTestEmail + property ran_before_callback : Bool = false + property ran_after_callback : Bool = false + + before_send :mark_before_send + after_send :mark_after_send + + private def mark_before_send + self.ran_before_callback = true + end + + private def mark_after_send(_response) + self.ran_after_callback = true + end +end + +private class EmailUsingBeforeToStopSending < BaseTestEmail + before_send :dont_actually_send + after_send :never_actually_ran + + property ran_after_callback : Bool = false + + private def dont_actually_send + @deliverable = false + end + + private def never_actually_ran(_response) + self.ran_after_callback = true + end +end + +describe "before/after callbacks" do + context "before an email is sent" do + it "runs the before_send callback" do + email = EmailWithBeforeCallbacks.new + email.ran_before_callback.should eq(false) + email.deliver + Carbon.should have_delivered_emails + + email.ran_before_callback.should eq(true) + end + end + + context "after an email is sent" do + it "runs the after_send callback" do + email = EmailWithAfterCallbacks.new + email.ran_after_callback.should eq(false) + email.deliver + Carbon.should have_delivered_emails + + email.ran_after_callback.should eq(true) + end + end + + context "running both callbacks" do + it "runs both callbacks" do + email = EmailWithBothBeforeAndAfterCallbacks.new + email.ran_before_callback.should eq(false) + email.ran_after_callback.should eq(false) + email.deliver + Carbon.should have_delivered_emails + + email.ran_before_callback.should eq(true) + email.ran_after_callback.should eq(true) + end + end + + context "Halting the deliver before it's sent" do + it "never sends" do + email = EmailUsingBeforeToStopSending.new + email.deliver + Carbon.should_not have_delivered_emails + email.deliverable?.should eq(false) + email.ran_after_callback.should eq(false) + end + end +end diff --git a/spec/email_spec.cr b/spec/email_spec.cr index b995b78..1968aaf 100644 --- a/spec/email_spec.cr +++ b/spec/email_spec.cr @@ -78,6 +78,12 @@ private class EmailWithLayout < BareMinimumEmail layout custom_layout end +private class UndeliverableEmail < Carbon::Email + subject "My great subject" + from Carbon::Address.new("from@example.com") + to Carbon::Address.new("to@example.com") +end + describe Carbon::Email do it "can build a bare minimum email" do email = BareMinimumEmail.new @@ -146,4 +152,12 @@ describe Carbon::Email do email.html_body.should contain "Email Layout" email.html_body.should contain "Email body" end + + context "deliverable?" do + it "is not delivery it is digiorno" do + email = UndeliverableEmail.new + email.deliverable = false + email.deliverable?.should eq(false) + end + end end diff --git a/src/carbon/callbacks.cr b/src/carbon/callbacks.cr new file mode 100644 index 0000000..cfd4649 --- /dev/null +++ b/src/carbon/callbacks.cr @@ -0,0 +1,81 @@ +module Carbon::Callbacks + # Runs the given method before the adapter calls `deliver_now` + # + # ``` + # before_send :attach_metadata + # + # private def attach_metadata + # # ... + # end + # ``` + macro before_send(method_name) + before_send do + {{ method_name.id }} + end + end + + # Runs the block before the adapter calls `deliver_now` + # + # ``` + # before_send do + # # ... + # end + # ``` + macro before_send + def before_send + {% if @type.methods.map(&.name).includes?(:before_send.id) %} + previous_def + {% else %} + super + {% end %} + + {{ yield }} + end + end + + # Runs the given method after the adapter calls `deliver_now`. + # Passes in the return value of the adapter's `deliver_now` method. + # + # ``` + # after_send :mark_email_as_sent + # + # private def mark_email_as_sent(response) + # # ... + # end + # ``` + macro after_send(method_name) + after_send do |object| + {{ method_name.id }}(object) + end + end + + # Runs the block after the adapter calls `deliver_now`, and passes the + # return value of the adapter's `deliver_now` method to the block. + # + # ``` + # after_send do |response| + # # ... + # end + # ``` + macro after_send(&block) + {% + if block.args.size != 1 + raise <<-ERR + The 'after_send' callback requires exactly 1 block arg to be passed. + Example: + after_send { |value| some_method(value) } + ERR + end + %} + def after_send(%object) + {% if @type.methods.map(&.name).includes?(:after_send.id) %} + previous_def + {% else %} + super + {% end %} + + {{ block.args.first }} = %object + {{ block.body }} + end + end +end diff --git a/src/carbon/email.cr b/src/carbon/email.cr index ec84ced..45c3c63 100644 --- a/src/carbon/email.cr +++ b/src/carbon/email.cr @@ -1,6 +1,7 @@ require "ecr" abstract class Carbon::Email + include Carbon::Callbacks alias Recipients = Carbon::Emailable | Array(Carbon::Emailable) abstract def subject : String @@ -9,6 +10,10 @@ abstract class Carbon::Email def_equals subject, from, to, cc, bcc, headers, text_body, html_body + # Set this value to `false` to prevent the email from + # being delivered + property? deliverable : Bool = true + def cc [] of Carbon::Address end @@ -25,6 +30,10 @@ abstract class Carbon::Email def html_layout(content_io : IO); end + def before_send; end + + def after_send(result); end + getter headers macro inherited @@ -122,7 +131,12 @@ abstract class Carbon::Email end def deliver - settings.adapter.deliver_now(self) + before_send + + if deliverable? + response = settings.adapter.deliver_now(self) + after_send(response) + end end def deliver_later