DEV: Move chat service objects into core (#26506)

This commit is contained in:
Jan Cernik 2024-04-04 08:57:41 -05:00 committed by GitHub
parent 9af957014e
commit cab178a405
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 313 additions and 321 deletions

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module WithServiceHelper
def result
@_result
end
# @param service [Class] A class including {Service::Base}
# @param dependencies [kwargs] Any additional params to load into the service context,
# in addition to controller @params.
def with_service(service, **dependencies, &block)
object = self
ServiceRunner.call(
service,
object,
**dependencies,
&proc { instance_exec(&(block || proc {})) }
)
end
def run_service(service, dependencies)
params = self.try(:params) || ActionController::Parameters.new
@_result =
service.call(params.to_unsafe_h.merge(guardian: self.try(:guardian) || nil, **dependencies))
end
end

View File

@ -61,7 +61,7 @@ module Service
end
def inspect_steps
Chat::StepsInspector.new(self)
StepsInspector.new(self)
end
private

124
lib/steps_inspector.rb Normal file
View File

@ -0,0 +1,124 @@
# frozen_string_literal: true
# = StepsInspector
#
# This class takes a {Service::Base::Context} object and inspects it.
# It will output a list of steps and what is their known state.
class StepsInspector
# @!visibility private
class Step
attr_reader :step, :result, :nesting_level
delegate :name, to: :step
delegate :failure?, :success?, :error, to: :step_result, allow_nil: true
def self.for(step, result, nesting_level: 0)
class_name =
"#{module_parent_name}::#{step.class.name.split("::").last.sub(/^(\w+)Step$/, "\\1")}"
class_name.constantize.new(step, result, nesting_level: nesting_level)
end
def initialize(step, result, nesting_level: 0)
@step = step
@result = result
@nesting_level = nesting_level
end
def type
self.class.name.split("::").last.downcase
end
def emoji
"#{result_emoji}#{unexpected_result_emoji}"
end
def steps
[self]
end
def inspect
"#{" " * nesting_level}[#{type}] '#{name}' #{emoji}".rstrip
end
private
def step_result
result["result.#{type}.#{name}"]
end
def result_emoji
return "" if failure?
return "" if success?
""
end
def unexpected_result_emoji
" ⚠️#{unexpected_result_text}" if step_result.try(:[], "spec.unexpected_result")
end
def unexpected_result_text
return " <= expected to return true but got false instead" if failure?
" <= expected to return false but got true instead"
end
end
# @!visibility private
class Model < Step
def error
return result[name].errors.inspect if step_result.invalid
step_result.exception.full_message
end
end
# @!visibility private
class Contract < Step
def error
step_result.errors.inspect
end
end
# @!visibility private
class Policy < Step
def error
step_result.reason
end
end
# @!visibility private
class Transaction < Step
def steps
[self, *step.steps.map { Step.for(_1, result, nesting_level: nesting_level + 1).steps }]
end
def inspect
"#{" " * nesting_level}[#{type}]"
end
def step_result
nil
end
end
attr_reader :steps, :result
def initialize(result)
@steps = result.__steps__.map { Step.for(_1, result).steps }.flatten
@result = result
end
# Inspect the provided result object.
# Example output:
# [1/4] [model] 'channel' ✅
# [2/4] [contract] 'default' ✅
# [3/4] [policy] 'check_channel_permission' ❌
# [4/4] [step] 'change_status'
# @return [String] the steps of the result object with their state
def inspect
steps.map.with_index { |step, index| "[#{index + 1}/#{steps.size}] #{step.inspect}" }.join("\n")
end
# @return [String, nil] the first available error, if any.
def error
steps.detect(&:failure?)&.error
end
end

View File

@ -2,6 +2,6 @@
module Chat
class ApiController < ::Chat::BaseController
include Chat::WithServiceHelper
include WithServiceHelper
end
end

View File

@ -2,7 +2,7 @@
module Chat
class IncomingWebhooksController < ::ApplicationController
include Chat::WithServiceHelper
include WithServiceHelper
requires_plugin Chat::PLUGIN_NAME

View File

@ -1,28 +0,0 @@
# frozen_string_literal: true
module Chat
module WithServiceHelper
def result
@_result
end
# @param service [Class] A class including {Chat::Service::Base}
# @param dependencies [kwargs] Any additional params to load into the service context,
# in addition to controller @params.
def with_service(service, **dependencies, &block)
object = self
ServiceRunner.call(
service,
object,
**dependencies,
&proc { instance_exec(&(block || proc {})) }
)
end
def run_service(service, dependencies)
params = self.try(:params) || ActionController::Parameters.new
@_result =
service.call(params.to_unsafe_h.merge(guardian: self.try(:guardian) || nil, **dependencies))
end
end
end

View File

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

View File

@ -1,129 +0,0 @@
# frozen_string_literal: true
module Chat
# = Chat::StepsInspector
#
# This class takes a {Service::Base::Context} object and inspects it.
# It will output a list of steps and what is their known state.
class StepsInspector
# @!visibility private
class Step
attr_reader :step, :result, :nesting_level
delegate :name, to: :step
delegate :failure?, :success?, :error, to: :step_result, allow_nil: true
def self.for(step, result, nesting_level: 0)
class_name =
"#{module_parent_name}::#{step.class.name.split("::").last.sub(/^(\w+)Step$/, "\\1")}"
class_name.constantize.new(step, result, nesting_level: nesting_level)
end
def initialize(step, result, nesting_level: 0)
@step = step
@result = result
@nesting_level = nesting_level
end
def type
self.class.name.split("::").last.downcase
end
def emoji
"#{result_emoji}#{unexpected_result_emoji}"
end
def steps
[self]
end
def inspect
"#{" " * nesting_level}[#{type}] '#{name}' #{emoji}".rstrip
end
private
def step_result
result["result.#{type}.#{name}"]
end
def result_emoji
return "" if failure?
return "" if success?
""
end
def unexpected_result_emoji
" ⚠️#{unexpected_result_text}" if step_result.try(:[], "spec.unexpected_result")
end
def unexpected_result_text
return " <= expected to return true but got false instead" if failure?
" <= expected to return false but got true instead"
end
end
# @!visibility private
class Model < Step
def error
return result[name].errors.inspect if step_result.invalid
step_result.exception.full_message
end
end
# @!visibility private
class Contract < Step
def error
step_result.errors.inspect
end
end
# @!visibility private
class Policy < Step
def error
step_result.reason
end
end
# @!visibility private
class Transaction < Step
def steps
[self, *step.steps.map { Step.for(_1, result, nesting_level: nesting_level + 1).steps }]
end
def inspect
"#{" " * nesting_level}[#{type}]"
end
def step_result
nil
end
end
attr_reader :steps, :result
def initialize(result)
@steps = result.__steps__.map { Step.for(_1, result).steps }.flatten
@result = result
end
# Inspect the provided result object.
# Example output:
# [1/4] [model] 'channel' ✅
# [2/4] [contract] 'default' ✅
# [3/4] [policy] 'check_channel_permission' ❌
# [4/4] [step] 'change_status'
# @return [String] the steps of the result object with their state
def inspect
steps
.map
.with_index { |step, index| "[#{index + 1}/#{steps.size}] #{step.inspect}" }
.join("\n")
end
# @return [String, nil] the first available error, if any.
def error
steps.detect(&:failure?)&.error
end
end
end

View File

@ -2,7 +2,7 @@
module ChatSDK
class Channel
include Chat::WithServiceHelper
include WithServiceHelper
# Retrieves messages from a specified channel.
#

View File

@ -2,7 +2,7 @@
module ChatSDK
class Message
include Chat::WithServiceHelper
include WithServiceHelper
# Creates a new message in a chat channel.
#
@ -159,7 +159,7 @@ module ChatSDK
end
class StreamHelper
include Chat::WithServiceHelper
include WithServiceHelper
attr_reader :message
attr_reader :guardian

View File

@ -2,7 +2,7 @@
module ChatSDK
class Thread
include Chat::WithServiceHelper
include WithServiceHelper
# Updates the title of a specified chat thread.
#

View File

@ -130,8 +130,8 @@ end
RSpec.configure do |config|
config.include ChatSystemHelpers, type: :system
config.include ChatSpecHelpers
config.include Chat::WithServiceHelper
config.include Chat::ServiceMatchers
config.include WithServiceHelper
config.include ServiceMatchers
config.expect_with :rspec do |c|
# Or a very large value, if you do want to truncate at some point

View File

@ -1,153 +0,0 @@
# frozen_string_literal: true
module Chat
module ServiceMatchers
class RunServiceSuccessfully
attr_reader :result
def matches?(result)
@result = result
result.success?
end
def failure_message
inspector = StepsInspector.new(result)
"Expected to run the service sucessfully but failed:\n\n#{inspector.inspect}\n\n#{inspector.error}"
end
end
class FailStep
attr_reader :name, :result
def initialize(name)
@name = name
end
def matches?(result)
@result = result
step_exists? && step_failed? && service_failed?
end
def failure_message
set_unexpected_result
message =
if !step_exists?
"Expected #{type} '#{name}' (key: '#{step}') was not found in the result object."
elsif !step_failed?
"Expected #{type} '#{name}' (key: '#{step}') to fail but it succeeded."
else
"expected the service to fail but it succeeded."
end
error_message_with_inspection(message)
end
def failure_message_when_negated
set_unexpected_result
message = "Expected #{type} '#{name}' (key: '#{step}') to succeed but it failed."
error_message_with_inspection(message)
end
def description
"fail a #{type} named '#{name}'"
end
private
def step_exists?
result[step].present?
end
def step_failed?
result[step].failure?
end
def service_failed?
result.failure?
end
def type
self.class.name.split("::").last.sub("Fail", "").downcase
end
def step
"result.#{type}.#{name}"
end
def error_message_with_inspection(message)
inspector = StepsInspector.new(result)
"#{message}\n\n#{inspector.inspect}\n\n#{inspector.error}"
end
def set_unexpected_result
return unless result[step]
result[step]["spec.unexpected_result"] = true
end
end
class FailContract < FailStep
end
class FailPolicy < FailStep
end
class FailToFindModel < FailStep
def type
"model"
end
def description
"fail to find a model named '#{name}'"
end
def step_failed?
super && result[name].blank?
end
end
class FailWithInvalidModel < FailStep
def type
"model"
end
def description
"fail to have a valid model named '#{name}'"
end
def step_failed?
super && result[step].invalid
end
end
def fail_a_policy(name)
FailPolicy.new(name)
end
def fail_a_contract(name = "default")
FailContract.new(name)
end
def fail_to_find_a_model(name = "model")
FailToFindModel.new(name)
end
def fail_with_an_invalid_model(name = "model")
FailWithInvalidModel.new(name)
end
def fail_a_step(name = "model")
FailStep.new(name)
end
def run_service_successfully
RunServiceSuccessfully.new
end
def inspect_steps(result)
inspector = Chat::StepsInspector.new(result)
puts "Steps:"
puts inspector.inspect
puts "\nFirst error:"
puts inspector.error
end
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe Chat::StepsInspector do
RSpec.describe StepsInspector do
class DummyService
include Service::Base

View File

@ -0,0 +1,151 @@
# frozen_string_literal: true
module ServiceMatchers
class RunServiceSuccessfully
attr_reader :result
def matches?(result)
@result = result
result.success?
end
def failure_message
inspector = StepsInspector.new(result)
"Expected to run the service sucessfully but failed:\n\n#{inspector.inspect}\n\n#{inspector.error}"
end
end
class FailStep
attr_reader :name, :result
def initialize(name)
@name = name
end
def matches?(result)
@result = result
step_exists? && step_failed? && service_failed?
end
def failure_message
set_unexpected_result
message =
if !step_exists?
"Expected #{type} '#{name}' (key: '#{step}') was not found in the result object."
elsif !step_failed?
"Expected #{type} '#{name}' (key: '#{step}') to fail but it succeeded."
else
"expected the service to fail but it succeeded."
end
error_message_with_inspection(message)
end
def failure_message_when_negated
set_unexpected_result
message = "Expected #{type} '#{name}' (key: '#{step}') to succeed but it failed."
error_message_with_inspection(message)
end
def description
"fail a #{type} named '#{name}'"
end
private
def step_exists?
result[step].present?
end
def step_failed?
result[step].failure?
end
def service_failed?
result.failure?
end
def type
self.class.name.split("::").last.sub("Fail", "").downcase
end
def step
"result.#{type}.#{name}"
end
def error_message_with_inspection(message)
inspector = StepsInspector.new(result)
"#{message}\n\n#{inspector.inspect}\n\n#{inspector.error}"
end
def set_unexpected_result
return unless result[step]
result[step]["spec.unexpected_result"] = true
end
end
class FailContract < FailStep
end
class FailPolicy < FailStep
end
class FailToFindModel < FailStep
def type
"model"
end
def description
"fail to find a model named '#{name}'"
end
def step_failed?
super && result[name].blank?
end
end
class FailWithInvalidModel < FailStep
def type
"model"
end
def description
"fail to have a valid model named '#{name}'"
end
def step_failed?
super && result[step].invalid
end
end
def fail_a_policy(name)
FailPolicy.new(name)
end
def fail_a_contract(name = "default")
FailContract.new(name)
end
def fail_to_find_a_model(name = "model")
FailToFindModel.new(name)
end
def fail_with_an_invalid_model(name = "model")
FailWithInvalidModel.new(name)
end
def fail_a_step(name = "model")
FailStep.new(name)
end
def run_service_successfully
RunServiceSuccessfully.new
end
def inspect_steps(result)
inspector = StepsInspector.new(result)
puts "Steps:"
puts inspector.inspect
puts "\nFirst error:"
puts inspector.error
end
end