DEV: Allow `with_service` in jobs

This patch introduces a new `ServiceJob` class allowing the use of
`with_service` in jobs.

This way, it’s easier to use the chat service objects in jobs and
provides the same level of functionality than the one we have in
controllers.
This commit is contained in:
Loïc Guitaut 2023-02-21 16:02:10 +01:00 committed by Loïc Guitaut
parent 925a7bd48b
commit b8762172e4
6 changed files with 84 additions and 50 deletions

View File

@ -12,4 +12,18 @@ class Chat::Api < Chat::ChatBaseController
raise Discourse::NotFound unless SiteSetting.chat_enabled raise Discourse::NotFound unless SiteSetting.chat_enabled
guardian.ensure_can_chat! guardian.ensure_can_chat!
end end
def default_actions_for_service
proc do
on_success { render(json: success_json) }
on_failure { render(json: failed_json, status: 422) }
on_failed_policy(:invalid_access) { raise Discourse::InvalidAccess }
on_failed_contract do
render(
json: failed_json.merge(errors: result[:"result.contract.default"].errors.full_messages),
status: 400,
)
end
end
end
end end

View File

@ -6,32 +6,21 @@ module Chat
end end
def with_service(service, default_actions: true, **dependencies, &block) def with_service(service, default_actions: true, **dependencies, &block)
controller = self object = self
merged_block = merged_block =
proc do proc do
instance_eval(&controller.default_actions_for_service) if default_actions instance_exec(&object.method(:default_actions_for_service).call) if default_actions
instance_eval(&(block || proc {})) instance_exec(&(block || proc {}))
end end
Chat::Endpoint.call(service, controller, **dependencies, &merged_block) Chat::ServiceRunner.call(service, object, **dependencies, &merged_block)
end end
def run_service(service, dependencies) def run_service(service, dependencies)
@_result = service.call(params.to_unsafe_h.merge(guardian: guardian, **dependencies.to_h)) @_result = service.call(params.to_unsafe_h.merge(guardian: guardian, **dependencies))
end end
def default_actions_for_service def default_actions_for_service
proc do proc {}
on_success { render(json: success_json) }
on_failure { render(json: failed_json, status: 422) }
on_failed_policy(:invalid_access) { raise Discourse::InvalidAccess }
on_failed_contract do
render(
json:
failed_json.merge(errors: result[:"result.contract.default"].errors.full_messages),
status: 400,
)
end
end
end end
end end
end end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class ServiceJob < ::Jobs::Base
include Chat::WithServiceHelper
def run_service(service, dependencies)
@_result = service.call(dependencies)
end
end

View File

@ -1,12 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
# #
# = Chat::Endpoint # = Chat::ServiceRunner
# #
# This class is to be used via its helper +with_service+ in a controller. Its # This class is to be used via its helper +with_service+ in any class. Its
# main purpose is to ease how actions can be run upon a service completion. # main purpose is to ease how actions can be run upon a service completion.
# Since a service will likely return the same kind of things over and over, # Since a service will likely return the same kind of things over and over,
# this allows us to not have to repeat the same boilerplate code in every # this allows us to not have to repeat the same boilerplate code in every
# controller. # object.
# #
# There are several available actions and we can add new ones very easily: # There are several available actions and we can add new ones very easily:
# #
@ -19,8 +19,7 @@
# * +on_model_not_found(name)+: will execute the provided block if the service # * +on_model_not_found(name)+: will execute the provided block if the service
# fails and its model is not present # fails and its model is not present
# #
# @example # @example In a controller
# # in a controller
# def create # def create
# with_service MyService do # with_service MyService do
# on_success do # on_success do
@ -32,13 +31,21 @@
# end # end
# end # end
# #
# @example In a job (inheriting from +ServiceJob+)
# def execute(args = {})
# with_service(MyService, **args) do
# on_success { Rails.logger.info "SUCCESS" }
# on_failure { Rails.logger.error "FAILURE" }
# end
# end
#
# The actions will be evaluated in the order they appear. So even if the # The actions will be evaluated in the order they appear. So even if the
# service will ultimately fail with a failed policy, in this example only the # service will ultimately fail with a failed policy, in this example only the
# +on_failed_policy+ action will be executed and not the +on_failure+ one. # +on_failed_policy+ action will be executed and not the +on_failure+ one.
# The only exception to this being +on_failure+ as it will always be executed # The only exception to this being +on_failure+ as it will always be executed
# last. # last.
# #
class Chat::Endpoint class Chat::ServiceRunner
# @!visibility private # @!visibility private
NULL_RESULT = OpenStruct.new(failure?: false) NULL_RESULT = OpenStruct.new(failure?: false)
# @!visibility private # @!visibility private
@ -51,14 +58,14 @@ class Chat::Endpoint
}.with_indifferent_access.freeze }.with_indifferent_access.freeze
# @!visibility private # @!visibility private
attr_reader :service, :controller, :dependencies attr_reader :service, :object, :dependencies
delegate :result, to: :controller delegate :result, to: :object
# @!visibility private # @!visibility private
def initialize(service, controller, **dependencies) def initialize(service, object, **dependencies)
@service = service @service = service
@controller = controller @object = object
@dependencies = dependencies @dependencies = dependencies
@actions = {} @actions = {}
end end
@ -66,14 +73,14 @@ class Chat::Endpoint
# @param service [Class] a class including {Chat::Service::Base} # @param service [Class] a class including {Chat::Service::Base}
# @param block [Proc] a block containing the steps to match on # @param block [Proc] a block containing the steps to match on
# @return [void] # @return [void]
def self.call(service, controller, **dependencies, &block) def self.call(service, object, **dependencies, &block)
new(service, controller, **dependencies).call(&block) new(service, object, **dependencies).call(&block)
end end
# @!visibility private # @!visibility private
def call(&block) def call(&block)
instance_eval(&block) instance_eval(&block)
controller.run_service(service, dependencies) object.run_service(service, dependencies)
# Always have `on_failure` as the last action # Always have `on_failure` as the last action
( (
actions actions
@ -88,13 +95,13 @@ class Chat::Endpoint
attr_reader :actions attr_reader :actions
def failure_for?(key) def failure_for?(key)
(controller.result[key] || NULL_RESULT).failure? (object.result[key] || NULL_RESULT).failure?
end end
def add_action(name, *args, &block) def add_action(name, *args, &block)
actions[[name, *args].join("_").to_sym] = [ actions[[name, *args].join("_").to_sym] = [
-> { instance_exec(*args, &AVAILABLE_ACTIONS[name]) }, -> { instance_exec(*args, &AVAILABLE_ACTIONS[name]) },
-> { controller.instance_eval(&block) }, -> { object.instance_eval(&block) },
] ]
end end

View File

@ -200,7 +200,7 @@ after_initialize do
load File.expand_path("../lib/slack_compatibility.rb", __FILE__) load File.expand_path("../lib/slack_compatibility.rb", __FILE__)
load File.expand_path("../lib/post_notification_handler.rb", __FILE__) load File.expand_path("../lib/post_notification_handler.rb", __FILE__)
load File.expand_path("../lib/secure_uploads_compatibility.rb", __FILE__) load File.expand_path("../lib/secure_uploads_compatibility.rb", __FILE__)
load File.expand_path("../lib/endpoint.rb", __FILE__) load File.expand_path("../lib/service_runner.rb", __FILE__)
load File.expand_path("../lib/steps_inspector.rb", __FILE__) load File.expand_path("../lib/steps_inspector.rb", __FILE__)
load File.expand_path("../app/jobs/regular/auto_manage_channel_memberships.rb", __FILE__) load File.expand_path("../app/jobs/regular/auto_manage_channel_memberships.rb", __FILE__)
load File.expand_path("../app/jobs/regular/auto_join_channel_batch.rb", __FILE__) load File.expand_path("../app/jobs/regular/auto_join_channel_batch.rb", __FILE__)
@ -217,6 +217,7 @@ after_initialize do
load File.expand_path("../app/jobs/scheduled/email_chat_notifications.rb", __FILE__) load File.expand_path("../app/jobs/scheduled/email_chat_notifications.rb", __FILE__)
load File.expand_path("../app/jobs/scheduled/auto_join_users.rb", __FILE__) load File.expand_path("../app/jobs/scheduled/auto_join_users.rb", __FILE__)
load File.expand_path("../app/jobs/scheduled/chat_periodical_updates.rb", __FILE__) load File.expand_path("../app/jobs/scheduled/chat_periodical_updates.rb", __FILE__)
load File.expand_path("../app/jobs/service_job.rb", __FILE__)
load File.expand_path("../app/services/chat_publisher.rb", __FILE__) load File.expand_path("../app/services/chat_publisher.rb", __FILE__)
load File.expand_path("../app/services/trash_channel.rb", __FILE__) load File.expand_path("../app/services/trash_channel.rb", __FILE__)
load File.expand_path("../app/services/update_channel.rb", __FILE__) load File.expand_path("../app/services/update_channel.rb", __FILE__)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe Chat::Endpoint do RSpec.describe Chat::ServiceRunner do
class SuccessService class SuccessService
include Chat::Service::Base include Chat::Service::Base
end end
@ -77,13 +77,13 @@ RSpec.describe Chat::Endpoint do
end end
describe ".call(service, &block)" do describe ".call(service, &block)" do
subject(:endpoint) { described_class.call(service, controller, &actions_block) } subject(:runner) { described_class.call(service, object, &actions_block) }
let(:result) { controller.result } let(:result) { object.result }
let(:actions_block) { controller.instance_eval(actions) } let(:actions_block) { object.instance_eval(actions) }
let(:service) { SuccessService } let(:service) { SuccessService }
let(:actions) { "proc {}" } let(:actions) { "proc {}" }
let(:controller) do let(:object) do
Class Class
.new(Chat::Api) do .new(Chat::Api) do
def request def request
@ -101,7 +101,7 @@ RSpec.describe Chat::Endpoint do
end end
it "runs the provided service in the context of a controller" do it "runs the provided service in the context of a controller" do
endpoint runner
expect(result).to be_a Chat::Service::Base::Context expect(result).to be_a Chat::Service::Base::Context
expect(result).to be_a_success expect(result).to be_a_success
end end
@ -115,7 +115,7 @@ RSpec.describe Chat::Endpoint do
context "when the service succeeds" do context "when the service succeeds" do
it "runs the provided block" do it "runs the provided block" do
expect(endpoint).to eq :success expect(runner).to eq :success
end end
end end
@ -123,7 +123,7 @@ RSpec.describe Chat::Endpoint do
let(:service) { FailureService } let(:service) { FailureService }
it "does not run the provided block" do it "does not run the provided block" do
expect(endpoint).not_to eq :success expect(runner).not_to eq :success
end end
end end
end end
@ -139,7 +139,7 @@ RSpec.describe Chat::Endpoint do
let(:service) { FailureService } let(:service) { FailureService }
it "runs the provided block" do it "runs the provided block" do
expect(endpoint).to eq :fail expect(runner).to eq :fail
end end
end end
@ -147,7 +147,7 @@ RSpec.describe Chat::Endpoint do
let(:service) { SuccessService } let(:service) { SuccessService }
it "does not run the provided block" do it "does not run the provided block" do
expect(endpoint).not_to eq :fail expect(runner).not_to eq :fail
end end
end end
end end
@ -163,7 +163,7 @@ RSpec.describe Chat::Endpoint do
let(:service) { FailedPolicyService } let(:service) { FailedPolicyService }
it "runs the provided block" do it "runs the provided block" do
expect(endpoint).to eq :policy_failure expect(runner).to eq :policy_failure
end end
end end
@ -171,7 +171,7 @@ RSpec.describe Chat::Endpoint do
let(:service) { SuccessPolicyService } let(:service) { SuccessPolicyService }
it "does not run the provided block" do it "does not run the provided block" do
expect(endpoint).not_to eq :policy_failure expect(runner).not_to eq :policy_failure
end end
end end
end end
@ -187,7 +187,7 @@ RSpec.describe Chat::Endpoint do
let(:service) { FailedContractService } let(:service) { FailedContractService }
it "runs the provided block" do it "runs the provided block" do
expect(endpoint).to eq :contract_failure expect(runner).to eq :contract_failure
end end
end end
@ -195,7 +195,7 @@ RSpec.describe Chat::Endpoint do
let(:service) { SuccessContractService } let(:service) { SuccessContractService }
it "does not run the provided block" do it "does not run the provided block" do
expect(endpoint).not_to eq :contract_failure expect(runner).not_to eq :contract_failure
end end
end end
end end
@ -211,7 +211,7 @@ RSpec.describe Chat::Endpoint do
let(:service) { FailureWithModelService } let(:service) { FailureWithModelService }
it "runs the provided block" do it "runs the provided block" do
expect(endpoint).to eq :no_model expect(runner).to eq :no_model
end end
end end
@ -219,7 +219,7 @@ RSpec.describe Chat::Endpoint do
let(:service) { SuccessWithModelService } let(:service) { SuccessWithModelService }
it "does not run the provided block" do it "does not run the provided block" do
expect(endpoint).not_to eq :no_model expect(runner).not_to eq :no_model
end end
end end
end end
@ -235,7 +235,21 @@ RSpec.describe Chat::Endpoint do
BLOCK BLOCK
it "runs the first matching action" do it "runs the first matching action" do
expect(endpoint).to eq :failure expect(runner).to eq :failure
end
end
context "when running in the context of a job" do
let(:object) { Class.new(ServiceJob).new }
let(:actions) { <<-BLOCK }
proc do
on_success { :success }
on_failure { :failure }
end
BLOCK
it "runs properly" do
expect(runner).to eq :success
end end
end end
end end