DEV: Move chat service objects into core (#26506)
This commit is contained in:
parent
9af957014e
commit
cab178a405
|
@ -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
|
|
@ -61,7 +61,7 @@ module Service
|
|||
end
|
||||
|
||||
def inspect_steps
|
||||
Chat::StepsInspector.new(self)
|
||||
StepsInspector.new(self)
|
||||
end
|
||||
|
||||
private
|
|
@ -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
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
module Chat
|
||||
class ApiController < ::Chat::BaseController
|
||||
include Chat::WithServiceHelper
|
||||
include WithServiceHelper
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Chat
|
||||
class IncomingWebhooksController < ::ApplicationController
|
||||
include Chat::WithServiceHelper
|
||||
include WithServiceHelper
|
||||
|
||||
requires_plugin Chat::PLUGIN_NAME
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module ChatSDK
|
||||
class Channel
|
||||
include Chat::WithServiceHelper
|
||||
include WithServiceHelper
|
||||
|
||||
# Retrieves messages from a specified channel.
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module ChatSDK
|
||||
class Thread
|
||||
include Chat::WithServiceHelper
|
||||
include WithServiceHelper
|
||||
|
||||
# Updates the title of a specified chat thread.
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::StepsInspector do
|
||||
RSpec.describe StepsInspector do
|
||||
class DummyService
|
||||
include Service::Base
|
||||
|
|
@ -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
|
Loading…
Reference in New Issue