Commit Graph

8 Commits

Author SHA1 Message Date
Loïc Guitaut c78211cf8d DEV: Make service contracts immutable
We decided to make contracts immutable once their validations have run.
Indeed, it doesn’t make a lot of sense to modify a contract value
outside the contract itself.

If processing is needed, then it should happen inside the contract
itself.
2024-10-29 12:23:35 +01:00
Loïc Guitaut 584424594e DEV: Replace `params` by the contract object in services
This patch replaces the parameters provided to a service through
`params` by the contract object.

That way, it allows better consistency when accessing input params. For
example, if you have a service without a contract, to access a
parameter, you need to use `params[:my_parameter]`. But with a contract,
you do this through `contract.my_parameter`. Now, with this patch,
you’ll be able to access it through `params.my_parameter` or
`params[:my_parameter]`.

Some methods have been added to the contract object to better mimic a
Hash. That way, when accessing/using `params`, you don’t have to think
too much about it:
- `params.my_key` is also accessible through `params[:my_key]`.
- `params.my_key = value` can also be done through `params[:my_key] =
  value`.
- `#slice` and `#merge` are available.
- `#to_hash` has been implemented, so the contract object will be
  automatically cast as a hash by Ruby depending on the context. For
  example, with an AR model, you can do this: `user.update(**params)`.
2024-10-25 14:48:34 +02:00
Loïc Guitaut 41584ab40c DEV: Provide user input to services using `params` key
Currently in services, we don’t make a distinction between input
parameters, options and dependencies.

This can lead to user input modifying the service behavior, whereas it
was not the developer intention.

This patch addresses the issue by changing how data is provided to
services:
- `params` is now used to hold all data coming from outside (typically
  user input from a controller) and a contract will take its values from
  `params`.
- `options` is a new key to provide options to a service. This typically
  allows changing a service behavior at runtime. It is, of course,
  totally optional.
- `dependencies` is actually anything else provided to the service (like
  `guardian`) and available directly from the context object.

The `service_params` helper in controllers has been updated to reflect
those changes, so most of the existing services didn’t need specific
changes.

The options block has the same DSL as contracts, as it’s also based on
`ActiveModel`. There aren’t any validations, though. Here’s an example:
```ruby
options do
  attribute :allow_changing_hidden, :boolean, default: false
end
```
And here’s an example of how to call a service with the new keys:
```ruby
MyService.call(params: { key1: value1, … }, options: { my_option: true }, guardian:, …)
```
2024-10-25 09:57:59 +02:00
Loïc Guitaut 229773e7a8 DEV: Drop `OpenStruct` for the context object in services
While using `OpenStruct` is nice, it’s generally not a very good idea as
it usually leads to performance problems.

The `OpenStruct` source code even says basically to avoid it.

Since the context object is crucial in our services, this patch replaces
`OpenStruct` with a custom implementation instead.
2024-10-08 10:34:55 +02:00
Loïc Guitaut fc1c5f6a8d 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-02 17:00:01 +09:00
Loïc Guitaut 78d9f9fa40 DEV: Rename `ServiceRunner` to `Service::Runner` for consistency 2024-09-20 10:04:42 +02:00
Loïc Guitaut afdb1ac0a0 DEV: Disallow default params in service steps
With the current implementation, a service step can be written as:
```ruby
def my_step(a_default_value: 2)
  …
end
```
That’s a pattern we want to avoid as default values (if needed) should
be probably defined in a contract.

This patch makes a service raise an exception if a default value is
encountered.
2024-09-19 14:47:55 +02:00
Loïc Guitaut 05b8ff436c DEV: Introduce a `Service::ActionBase` class for service actions
This will help to enforce a consistent pattern for creating service
actions.

This patch also namespaces actions and policies, making everything
related to a service available directly in
`app/services/<concept-name>`, making things more consistent at that
level too.
2024-09-18 17:02:46 +02:00