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]`.
This commit is contained in:
Loïc Guitaut
2024-10-01 17:17:14 +02:00
committed by Loïc Guitaut
parent 76ad581f67
commit fc1c5f6a8d
45 changed files with 369 additions and 498 deletions

View File

@ -78,10 +78,12 @@ module Service
steps << ModelStep.new(name, step_name, optional: optional)
end
def contract(name = :default, class_name: self::Contract, default_values_from: nil)
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)
steps << ContractStep.new(
name,
class_name: class_name,
class_name: contract_class,
default_values_from: default_values_from,
)
end
@ -214,20 +216,6 @@ module Service
included do
# The global context which is available from any step.
attr_reader :context
# @!visibility private
# Internal class used to setup the base contract of the service.
self::Contract =
Class.new do
include ActiveModel::API
include ActiveModel::Attributes
include ActiveModel::AttributeMethods
include ActiveModel::Validations::Callbacks
def raw_attributes
@attributes.values_before_type_cast
end
end
end
class_methods do
@ -306,10 +294,10 @@ module Service
# end
# @!scope class
# @!method contract(name = :default, class_name: self::Contract, default_values_from: nil)
# @!method contract(name = :default, default_values_from: nil, &block)
# @param name [Symbol] name for this contract
# @param class_name [Class] a class defining the contract
# @param default_values_from [Symbol] name of the model to get default values from
# @param block [Proc] a block containing validations
# Checks the validity of the input parameters.
# Implements ActiveModel::Validations and ActiveModel::Attributes.
#
@ -317,9 +305,7 @@ module Service
# (can be customized by providing the +name+ argument).
#
# @example
# contract
#
# class Contract
# contract do
# attribute :name
# validates :name, presence: true
# end