2023-03-17 09:24:38 -04:00
|
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
|
|
module Service
|
|
|
|
|
module Base
|
|
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
|
|
|
|
|
# The only exception that can be raised by a service.
|
|
|
|
|
class Failure < StandardError
|
|
|
|
|
# @return [Context]
|
|
|
|
|
attr_reader :context
|
|
|
|
|
|
|
|
|
|
# @!visibility private
|
|
|
|
|
def initialize(context = nil)
|
|
|
|
|
@context = context
|
|
|
|
|
super
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Simple structure to hold the context of the service during its whole lifecycle.
|
2024-10-03 12:05:45 -04:00
|
|
|
|
class Context
|
|
|
|
|
delegate :slice, to: :store
|
|
|
|
|
|
|
|
|
|
def initialize(context = {})
|
|
|
|
|
@store = context.symbolize_keys
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def [](key)
|
|
|
|
|
store[key.to_sym]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def []=(key, value)
|
|
|
|
|
store[key.to_sym] = value
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def to_h
|
|
|
|
|
store.dup
|
|
|
|
|
end
|
2023-07-05 12:18:27 -04:00
|
|
|
|
|
2023-04-03 13:27:32 -04:00
|
|
|
|
# @return [Boolean] returns +true+ if the context is set as successful (default)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
def success?
|
|
|
|
|
!failure?
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# @return [Boolean] returns +true+ if the context is set as failed
|
|
|
|
|
# @see #fail!
|
|
|
|
|
# @see #fail
|
|
|
|
|
def failure?
|
|
|
|
|
@failure || false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Marks the context as failed.
|
|
|
|
|
# @param context [Hash, Context] the context to merge into the current one
|
|
|
|
|
# @example
|
|
|
|
|
# context.fail!("failure": "something went wrong")
|
|
|
|
|
# @return [Context]
|
|
|
|
|
def fail!(context = {})
|
2023-12-06 17:25:00 -05:00
|
|
|
|
self.fail(context)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
raise Failure, self
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Marks the context as failed without raising an exception.
|
|
|
|
|
# @param context [Hash, Context] the context to merge into the current one
|
|
|
|
|
# @example
|
|
|
|
|
# context.fail("failure": "something went wrong")
|
|
|
|
|
# @return [Context]
|
|
|
|
|
def fail(context = {})
|
2024-10-03 12:05:45 -04:00
|
|
|
|
store.merge!(context.symbolize_keys)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
@failure = true
|
|
|
|
|
self
|
|
|
|
|
end
|
|
|
|
|
|
2023-05-16 08:51:13 -04:00
|
|
|
|
def inspect_steps
|
2024-10-03 12:05:45 -04:00
|
|
|
|
Service::StepsInspector.new(self)
|
2023-05-16 08:51:13 -04:00
|
|
|
|
end
|
|
|
|
|
|
2023-03-17 09:24:38 -04:00
|
|
|
|
private
|
|
|
|
|
|
2024-10-03 12:05:45 -04:00
|
|
|
|
attr_reader :store
|
|
|
|
|
|
2023-03-17 09:24:38 -04:00
|
|
|
|
def self.build(context = {})
|
|
|
|
|
self === context ? context : new(context)
|
|
|
|
|
end
|
2024-10-03 12:05:45 -04:00
|
|
|
|
|
|
|
|
|
def method_missing(method_name, *args, &block)
|
|
|
|
|
return super if args.present?
|
|
|
|
|
store[method_name]
|
|
|
|
|
end
|
2023-03-17 09:24:38 -04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Internal module to define available steps as DSL
|
|
|
|
|
# @!visibility private
|
|
|
|
|
module StepsHelpers
|
2023-07-06 05:33:41 -04:00
|
|
|
|
def model(name = :model, step_name = :"fetch_#{name}", optional: false)
|
|
|
|
|
steps << ModelStep.new(name, step_name, optional: optional)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
end
|
|
|
|
|
|
DEV: Have `contract` take a block in services
Currently in services, the `contract` step is only used to define where
the contract will be called in the execution flow. Then, a `Contract`
class has to be defined with validations in it.
This patch allows the `contract` step to take a block containing
validations, attributes, etc. directly. No need to then open a
`Contract` class later in the service.
It also has a nice side effect, as it’s now easy to define multiples
contracts inside the same service. Before, we had the `class_name:`
option, but it wasn’t really useful as you had to redefine a complete
new contract class.
Now, when using a name for the contract other than `default`, a new
contract will be created automatically using the provided name.
Example:
```ruby
contract(:user) do
attribute :user_id, :integer
validates :user_id, presence: true
end
```
This will create a `UserContract` class and use it, also putting the
resulting contract in `context[:user_contract]`.
2024-10-01 11:17:14 -04:00
|
|
|
|
def contract(name = :default, default_values_from: nil, &block)
|
|
|
|
|
contract_class = Class.new(Service::ContractBase).tap { _1.class_eval(&block) }
|
|
|
|
|
const_set("#{name.to_s.classify.sub("Default", "")}Contract", contract_class)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
steps << ContractStep.new(
|
|
|
|
|
name,
|
DEV: Have `contract` take a block in services
Currently in services, the `contract` step is only used to define where
the contract will be called in the execution flow. Then, a `Contract`
class has to be defined with validations in it.
This patch allows the `contract` step to take a block containing
validations, attributes, etc. directly. No need to then open a
`Contract` class later in the service.
It also has a nice side effect, as it’s now easy to define multiples
contracts inside the same service. Before, we had the `class_name:`
option, but it wasn’t really useful as you had to redefine a complete
new contract class.
Now, when using a name for the contract other than `default`, a new
contract will be created automatically using the provided name.
Example:
```ruby
contract(:user) do
attribute :user_id, :integer
validates :user_id, presence: true
end
```
This will create a `UserContract` class and use it, also putting the
resulting contract in `context[:user_contract]`.
2024-10-01 11:17:14 -04:00
|
|
|
|
class_name: contract_class,
|
2023-03-17 09:24:38 -04:00
|
|
|
|
default_values_from: default_values_from,
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
2023-05-24 10:32:49 -04:00
|
|
|
|
def policy(name = :default, class_name: nil)
|
|
|
|
|
steps << PolicyStep.new(name, class_name: class_name)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def step(name)
|
|
|
|
|
steps << Step.new(name)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def transaction(&block)
|
|
|
|
|
steps << TransactionStep.new(&block)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# @!visibility private
|
|
|
|
|
class Step
|
|
|
|
|
attr_reader :name, :method_name, :class_name
|
|
|
|
|
|
|
|
|
|
def initialize(name, method_name = name, class_name: nil)
|
|
|
|
|
@name = name
|
|
|
|
|
@method_name = method_name
|
|
|
|
|
@class_name = class_name
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def call(instance, context)
|
2023-05-24 10:32:49 -04:00
|
|
|
|
object = class_name&.new(context)
|
|
|
|
|
method = object&.method(:call) || instance.method(method_name)
|
2024-09-19 06:37:47 -04:00
|
|
|
|
if method.parameters.any? { _1[0] != :keyreq }
|
|
|
|
|
raise "In #{type} '#{name}': default values in step implementations are not allowed. Maybe they could be defined in a contract?"
|
|
|
|
|
end
|
2024-10-03 12:05:45 -04:00
|
|
|
|
args = context.slice(*method.parameters.select { _1[0] == :keyreq }.map(&:last))
|
2023-05-24 10:32:49 -04:00
|
|
|
|
context[result_key] = Context.build(object: object)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
instance.instance_exec(**args, &method)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def type
|
|
|
|
|
self.class.name.split("::").last.downcase.sub(/^(\w+)step$/, "\\1")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def result_key
|
|
|
|
|
"result.#{type}.#{name}"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# @!visibility private
|
|
|
|
|
class ModelStep < Step
|
2023-07-06 05:33:41 -04:00
|
|
|
|
attr_reader :optional
|
|
|
|
|
|
|
|
|
|
def initialize(name, method_name = name, class_name: nil, optional: nil)
|
|
|
|
|
super(name, method_name, class_name: class_name)
|
|
|
|
|
@optional = optional.present?
|
|
|
|
|
end
|
|
|
|
|
|
2023-03-17 09:24:38 -04:00
|
|
|
|
def call(instance, context)
|
|
|
|
|
context[name] = super
|
2024-08-20 06:05:41 -04:00
|
|
|
|
if !optional && (!context[name] || context[name].try(:empty?))
|
|
|
|
|
raise ArgumentError, "Model not found"
|
|
|
|
|
end
|
2023-04-23 19:15:16 -04:00
|
|
|
|
if context[name].try(:invalid?)
|
|
|
|
|
context[result_key].fail(invalid: true)
|
|
|
|
|
context.fail!
|
|
|
|
|
end
|
2023-03-17 09:24:38 -04:00
|
|
|
|
rescue ArgumentError => exception
|
2024-08-20 06:05:41 -04:00
|
|
|
|
context[result_key].fail(exception: exception, not_found: true)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
context.fail!
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# @!visibility private
|
|
|
|
|
class PolicyStep < Step
|
|
|
|
|
def call(instance, context)
|
|
|
|
|
if !super
|
2023-05-24 10:32:49 -04:00
|
|
|
|
context[result_key].fail(reason: context[result_key].object&.reason)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
context.fail!
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# @!visibility private
|
|
|
|
|
class ContractStep < Step
|
|
|
|
|
attr_reader :default_values_from
|
|
|
|
|
|
|
|
|
|
def initialize(name, method_name = name, class_name: nil, default_values_from: nil)
|
|
|
|
|
super(name, method_name, class_name: class_name)
|
|
|
|
|
@default_values_from = default_values_from
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def call(instance, context)
|
|
|
|
|
attributes = class_name.attribute_names.map(&:to_sym)
|
|
|
|
|
default_values = {}
|
|
|
|
|
default_values = context[default_values_from].slice(*attributes) if default_values_from
|
2024-10-03 12:05:45 -04:00
|
|
|
|
contract = class_name.new(default_values.merge(context.slice(*attributes)))
|
2023-03-17 09:24:38 -04:00
|
|
|
|
context[contract_name] = contract
|
|
|
|
|
context[result_key] = Context.build
|
|
|
|
|
if contract.invalid?
|
2024-06-13 05:55:57 -04:00
|
|
|
|
context[result_key].fail(errors: contract.errors, parameters: contract.raw_attributes)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
context.fail!
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def contract_name
|
|
|
|
|
return :contract if name.to_sym == :default
|
|
|
|
|
:"#{name}_contract"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# @!visibility private
|
|
|
|
|
class TransactionStep < Step
|
|
|
|
|
include StepsHelpers
|
|
|
|
|
|
|
|
|
|
attr_reader :steps
|
|
|
|
|
|
|
|
|
|
def initialize(&block)
|
|
|
|
|
@steps = []
|
|
|
|
|
instance_exec(&block)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def call(instance, context)
|
|
|
|
|
ActiveRecord::Base.transaction { steps.each { |step| step.call(instance, context) } }
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
included do
|
|
|
|
|
# The global context which is available from any step.
|
|
|
|
|
attr_reader :context
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
class_methods do
|
|
|
|
|
include StepsHelpers
|
|
|
|
|
|
2024-09-03 12:30:22 -04:00
|
|
|
|
def call(context = {}, &actions)
|
|
|
|
|
return new(context).tap(&:run).context unless block_given?
|
2024-09-19 11:52:44 -04:00
|
|
|
|
Service::Runner.call(self, context, &actions)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def call!(context = {})
|
|
|
|
|
new(context).tap(&:run!).context
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def steps
|
|
|
|
|
@steps ||= []
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# @!scope class
|
2023-07-06 05:33:41 -04:00
|
|
|
|
# @!method model(name = :model, step_name = :"fetch_#{name}", optional: false)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# @param name [Symbol] name of the model
|
|
|
|
|
# @param step_name [Symbol] name of the method to call for this step
|
2023-07-06 05:33:41 -04:00
|
|
|
|
# @param optional [Boolean] if +true+, then the step won’t fail if its return value is falsy.
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# Evaluates arbitrary code to build or fetch a model (typically from the
|
|
|
|
|
# DB). If the step returns a falsy value, then the step will fail.
|
|
|
|
|
#
|
|
|
|
|
# It stores the resulting model in +context[:model]+ by default (can be
|
|
|
|
|
# customized by providing the +name+ argument).
|
|
|
|
|
#
|
|
|
|
|
# @example
|
|
|
|
|
# model :channel, :fetch_channel
|
|
|
|
|
#
|
|
|
|
|
# private
|
|
|
|
|
#
|
2024-03-07 09:44:12 -05:00
|
|
|
|
# def fetch_channel(channel_id:)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# Chat::Channel.find_by(id: channel_id)
|
|
|
|
|
# end
|
|
|
|
|
|
|
|
|
|
# @!scope class
|
2023-05-24 10:32:49 -04:00
|
|
|
|
# @!method policy(name = :default, class_name: nil)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# @param name [Symbol] name for this policy
|
2023-05-24 10:32:49 -04:00
|
|
|
|
# @param class_name [Class] a policy object (should inherit from +PolicyBase+)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# Performs checks related to the state of the system. If the
|
|
|
|
|
# step doesn’t return a truthy value, then the policy will fail.
|
|
|
|
|
#
|
2023-05-24 10:32:49 -04:00
|
|
|
|
# When using a policy object, there is no need to define a method on the
|
|
|
|
|
# service for the policy step. The policy object `#call` method will be
|
|
|
|
|
# called and if the result isn’t truthy, a `#reason` method is expected to
|
|
|
|
|
# be implemented to explain the failure.
|
|
|
|
|
#
|
|
|
|
|
# Policy objects are usually useful for more complex logic.
|
|
|
|
|
#
|
|
|
|
|
# @example Without a policy object
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# policy :no_direct_message_channel
|
|
|
|
|
#
|
|
|
|
|
# private
|
|
|
|
|
#
|
2024-03-07 09:44:12 -05:00
|
|
|
|
# def no_direct_message_channel(channel:)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# !channel.direct_message_channel?
|
|
|
|
|
# end
|
2023-05-24 10:32:49 -04:00
|
|
|
|
#
|
|
|
|
|
# @example With a policy object
|
|
|
|
|
# # in the service object
|
|
|
|
|
# policy :no_direct_message_channel, class_name: NoDirectMessageChannelPolicy
|
|
|
|
|
#
|
|
|
|
|
# # in the policy object File
|
|
|
|
|
# class NoDirectMessageChannelPolicy < PolicyBase
|
|
|
|
|
# def call
|
|
|
|
|
# !context.channel.direct_message_channel?
|
|
|
|
|
# end
|
|
|
|
|
#
|
|
|
|
|
# def reason
|
|
|
|
|
# "Direct message channels aren’t supported"
|
|
|
|
|
# end
|
|
|
|
|
# end
|
2023-03-17 09:24:38 -04:00
|
|
|
|
|
|
|
|
|
# @!scope class
|
DEV: Have `contract` take a block in services
Currently in services, the `contract` step is only used to define where
the contract will be called in the execution flow. Then, a `Contract`
class has to be defined with validations in it.
This patch allows the `contract` step to take a block containing
validations, attributes, etc. directly. No need to then open a
`Contract` class later in the service.
It also has a nice side effect, as it’s now easy to define multiples
contracts inside the same service. Before, we had the `class_name:`
option, but it wasn’t really useful as you had to redefine a complete
new contract class.
Now, when using a name for the contract other than `default`, a new
contract will be created automatically using the provided name.
Example:
```ruby
contract(:user) do
attribute :user_id, :integer
validates :user_id, presence: true
end
```
This will create a `UserContract` class and use it, also putting the
resulting contract in `context[:user_contract]`.
2024-10-01 11:17:14 -04:00
|
|
|
|
# @!method contract(name = :default, default_values_from: nil, &block)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# @param name [Symbol] name for this contract
|
|
|
|
|
# @param default_values_from [Symbol] name of the model to get default values from
|
DEV: Have `contract` take a block in services
Currently in services, the `contract` step is only used to define where
the contract will be called in the execution flow. Then, a `Contract`
class has to be defined with validations in it.
This patch allows the `contract` step to take a block containing
validations, attributes, etc. directly. No need to then open a
`Contract` class later in the service.
It also has a nice side effect, as it’s now easy to define multiples
contracts inside the same service. Before, we had the `class_name:`
option, but it wasn’t really useful as you had to redefine a complete
new contract class.
Now, when using a name for the contract other than `default`, a new
contract will be created automatically using the provided name.
Example:
```ruby
contract(:user) do
attribute :user_id, :integer
validates :user_id, presence: true
end
```
This will create a `UserContract` class and use it, also putting the
resulting contract in `context[:user_contract]`.
2024-10-01 11:17:14 -04:00
|
|
|
|
# @param block [Proc] a block containing validations
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# Checks the validity of the input parameters.
|
|
|
|
|
# Implements ActiveModel::Validations and ActiveModel::Attributes.
|
|
|
|
|
#
|
|
|
|
|
# It stores the resulting contract in +context[:contract]+ by default
|
|
|
|
|
# (can be customized by providing the +name+ argument).
|
|
|
|
|
#
|
|
|
|
|
# @example
|
DEV: Have `contract` take a block in services
Currently in services, the `contract` step is only used to define where
the contract will be called in the execution flow. Then, a `Contract`
class has to be defined with validations in it.
This patch allows the `contract` step to take a block containing
validations, attributes, etc. directly. No need to then open a
`Contract` class later in the service.
It also has a nice side effect, as it’s now easy to define multiples
contracts inside the same service. Before, we had the `class_name:`
option, but it wasn’t really useful as you had to redefine a complete
new contract class.
Now, when using a name for the contract other than `default`, a new
contract will be created automatically using the provided name.
Example:
```ruby
contract(:user) do
attribute :user_id, :integer
validates :user_id, presence: true
end
```
This will create a `UserContract` class and use it, also putting the
resulting contract in `context[:user_contract]`.
2024-10-01 11:17:14 -04:00
|
|
|
|
# contract do
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# attribute :name
|
|
|
|
|
# validates :name, presence: true
|
|
|
|
|
# end
|
|
|
|
|
|
|
|
|
|
# @!scope class
|
|
|
|
|
# @!method step(name)
|
|
|
|
|
# @param name [Symbol] the name of this step
|
|
|
|
|
# Runs arbitrary code. To mark a step as failed, a call to {#fail!} needs
|
|
|
|
|
# to be made explicitly.
|
|
|
|
|
#
|
|
|
|
|
# @example
|
|
|
|
|
# step :update_channel
|
|
|
|
|
#
|
|
|
|
|
# private
|
|
|
|
|
#
|
2024-03-07 09:44:12 -05:00
|
|
|
|
# def update_channel(channel:, params_to_edit:)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# channel.update!(params_to_edit)
|
|
|
|
|
# end
|
|
|
|
|
# @example using {#fail!} in a step
|
|
|
|
|
# step :save_channel
|
|
|
|
|
#
|
|
|
|
|
# private
|
|
|
|
|
#
|
2024-03-07 09:44:12 -05:00
|
|
|
|
# def save_channel(channel:)
|
2023-03-17 09:24:38 -04:00
|
|
|
|
# fail!("something went wrong") if !channel.save
|
|
|
|
|
# end
|
|
|
|
|
|
|
|
|
|
# @!scope class
|
|
|
|
|
# @!method transaction(&block)
|
|
|
|
|
# @param block [Proc] a block containing steps to be run inside a transaction
|
|
|
|
|
# Runs steps inside a DB transaction.
|
|
|
|
|
#
|
|
|
|
|
# @example
|
|
|
|
|
# transaction do
|
|
|
|
|
# step :prevents_slug_collision
|
|
|
|
|
# step :soft_delete_channel
|
|
|
|
|
# step :log_channel_deletion
|
|
|
|
|
# end
|
|
|
|
|
|
|
|
|
|
# @!visibility private
|
|
|
|
|
def initialize(initial_context = {})
|
|
|
|
|
@context = Context.build(initial_context.merge(__steps__: self.class.steps))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# @!visibility private
|
|
|
|
|
def run
|
|
|
|
|
run!
|
|
|
|
|
rescue Failure => exception
|
|
|
|
|
raise if context.object_id != exception.context.object_id
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# @!visibility private
|
|
|
|
|
def run!
|
|
|
|
|
self.class.steps.each { |step| step.call(self, context) }
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# @!visibility private
|
|
|
|
|
def fail!(message)
|
2023-09-07 02:57:29 -04:00
|
|
|
|
step_name = caller_locations(1, 1)[0].base_label
|
2023-03-17 09:24:38 -04:00
|
|
|
|
context["result.step.#{step_name}"].fail(error: message)
|
|
|
|
|
context.fail!
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|