Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,23 @@ All notable changes to this project will be documented in this file. This projec

## Next Major Release

### Added

- **BREAKING**: Added `code()` method to the `ListOfErrors` interface. This returns the first error code in the list, or
`null` if there are no error codes.

### Changed

- **BREAKING**: The constructor argument order for the `Error` class has changed. The new order is `code`, `message`,
`key`. Previously it was `key`, `message`, `code` because code was added later. However, in most instances `code` is
the most important property as it is used to programmatically detect specific error scenarios. This hopefully will not
be breaking, as the docs specified that named arguments should be used when constructing error objects.
- **BREAKING** The error and error list interfaces now accept `UnitEnum` instead of `BackedEnum` for error codes.
Although technically breaking, this will only affect your implementation if you have implemented these interfaces. All
concrete classes provided by this package have been updated.
- **BREAKING**: The key of an error can now be a enum - previously only strings were accepted. This is only breaking if
you have implemented the interface yourself.
- Updated `KeyedSetOfErrors` to handle error keys now being strings or enums.
- **BREAKING**: The `Guid::make()` method will now convert a string that is a UUID to a UUID GUID. Previously it would
use a string id.
- **BREAKING**: Updated the `GuidTypeMap` class so that it now supports enum aliases, enum types, and UUID identifiers.
Expand Down
22 changes: 16 additions & 6 deletions docs/guide/toolkit/results.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,12 @@ Alternatively, you can provide the `failed()` method with an error object or a l

The error object provided by this package has the following properties:

- `code`: an enum that identifies the error and can be used to programmatically detect and handle specific errors. It
is intentionally an enum, because to programmatically detect specific errors, the codes need to be a defined list of
values.
- `message`: a string message that describes the error.
- `code`: a backed enum that identifies error and can be used to programmatically detect and handle specific errors. It
is intentionally an enum, because if you need to detect specific errors, the codes need to be a defined list of
values - which is an enum.
- `key`: optionally, a key for the error. This can be used to group errors by keys, for example to group errors by
properties that exist on a command message.
- `key`: optionally, a key for the error. Use this to group errors by keys, for example to group errors by
properties that exist on a command message. The key can be a string or enum.

Error objects _must_ be instantiated with either a code or a message. They are immutable, so you cannot change their
properties after they have been created.
Expand Down Expand Up @@ -181,7 +181,7 @@ The error list class is iterable, countable and has `isEmpty()` and `isNotEmpty(
### Keyed Error Sets

If you are using the `key` property on error objects, we provide a keyed set of errors class that groups errors by their
key.
key. Keys can be strings or enums.

To create one, provide error objects to its constructor:

Expand Down Expand Up @@ -243,6 +243,16 @@ if ($errors->contains(CancelAttendeeTicketError::AlreadyCancelled)) {
}
```

If you only want to handle one error code at once, use the `code()` method on the errors list. This will return the first error code, or `null` if there are no error codes.

```php
$explanation = match ($errors->code()) {
CancelAttendeeTicketError::AlreadyCancelled => 'The ticket has already been cancelled.',
CancelAttendeeTicketError::NotFound => 'The ticket was not found.',
default => null,
};
```

## Exception

We provide a `FailedResultException` that you can use to throw a result object:
Expand Down
14 changes: 7 additions & 7 deletions src/Contracts/Toolkit/Result/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@

namespace CloudCreativity\Modules\Contracts\Toolkit\Result;

use BackedEnum;
use UnitEnum;

interface Error
{
/**
* Get the error key.
*
* @return string|null
* @return UnitEnum|string|null
*/
public function key(): ?string;
public function key(): UnitEnum|string|null;

/**
* Get the error detail.
Expand All @@ -33,15 +33,15 @@ public function message(): string;
/**
* Get the error code.
*
* @return BackedEnum|null
* @return UnitEnum|null
*/
public function code(): ?BackedEnum;
public function code(): ?UnitEnum;

/**
* Is the error the specified error code?
*
* @param BackedEnum $code
* @param UnitEnum $code
* @return bool
*/
public function is(BackedEnum $code): bool;
public function is(UnitEnum $code): bool;
}
19 changes: 13 additions & 6 deletions src/Contracts/Toolkit/Result/ListOfErrors.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

namespace CloudCreativity\Modules\Contracts\Toolkit\Result;

use BackedEnum;
use Closure;
use CloudCreativity\Modules\Contracts\Toolkit\Iterables\ListIterator;
use UnitEnum;

/**
* @extends ListIterator<Error>
Expand All @@ -24,26 +24,33 @@ interface ListOfErrors extends ListIterator
/**
* Get the first error in the list, or the first matching error.
*
* @param Closure(Error): bool|BackedEnum|null $matcher
* @param Closure(Error): bool|UnitEnum|null $matcher
* @return Error|null
*/
public function first(Closure|BackedEnum|null $matcher = null): ?Error;
public function first(Closure|UnitEnum|null $matcher = null): ?Error;

/**
* Does the list contain a matching error?
*
* @param Closure(Error): bool|BackedEnum $matcher
* @param Closure(Error): bool|UnitEnum $matcher
* @return bool
*/
public function contains(Closure|BackedEnum $matcher): bool;
public function contains(Closure|UnitEnum $matcher): bool;

/**
* Get all the unique error codes in the list.
*
* @return array<BackedEnum>
* @return array<UnitEnum>
*/
public function codes(): array;

/**
* Get the first error code in the list.
*
* @return UnitEnum|null
*/
public function code(): ?UnitEnum;

/**
* Return a new instance with the provided error pushed on to the end of the list.
*
Expand Down
6 changes: 5 additions & 1 deletion src/Toolkit/Loggable/ResultDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use CloudCreativity\Modules\Contracts\Toolkit\Result\Error;
use CloudCreativity\Modules\Contracts\Toolkit\Result\Result;

use function CloudCreativity\Modules\Toolkit\enum_string;

final readonly class ResultDecorator implements ContextProvider
{
/**
Expand Down Expand Up @@ -85,8 +87,10 @@ private function error(Error $error): array
return $error->context();
}

$code = $error->code();

return array_filter([
'code' => $error->code()?->value,
'code' => $code ? enum_string($code) : null,
'key' => $error->key(),
'message' => $error->message(),
]);
Expand Down
20 changes: 10 additions & 10 deletions src/Toolkit/Result/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,28 @@

namespace CloudCreativity\Modules\Toolkit\Result;

use BackedEnum;
use CloudCreativity\Modules\Contracts\Toolkit\Result\Error as IError;
use CloudCreativity\Modules\Toolkit\Contracts;
use UnitEnum;

final readonly class Error implements IError
{
/**
* @var string|null
* @var UnitEnum|string|null
*/
private ?string $key;
private UnitEnum|string|null $key;

/**
* Error constructor.
*
* @param string|null $key
* @param UnitEnum|null $code
* @param string $message
* @param BackedEnum|null $code
* @param UnitEnum|string|null $key
*/
public function __construct(
?string $key = null,
private ?UnitEnum $code = null,
private string $message = '',
private ?BackedEnum $code = null,
UnitEnum|string|null $key = null,
) {
Contracts::assert(!empty($message) || $code !== null, 'Error must have a message or a code.');
$this->key = $key ?: null;
Expand All @@ -42,7 +42,7 @@ public function __construct(
/**
* @inheritDoc
*/
public function key(): ?string
public function key(): UnitEnum|string|null
{
return $this->key;
}
Expand All @@ -58,15 +58,15 @@ public function message(): string
/**
* @inheritDoc
*/
public function code(): ?BackedEnum
public function code(): ?UnitEnum
{
return $this->code;
}

/**
* @inheritDoc
*/
public function is(BackedEnum $code): bool
public function is(UnitEnum $code): bool
{
return $this->code === $code;
}
Expand Down
15 changes: 11 additions & 4 deletions src/Toolkit/Result/KeyedSetOfErrors.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
use CloudCreativity\Modules\Contracts\Toolkit\Result\Error as IError;
use CloudCreativity\Modules\Contracts\Toolkit\Result\ListOfErrors as IListOfErrors;
use CloudCreativity\Modules\Toolkit\Iterables\IsKeyedSet;
use UnitEnum;

use function CloudCreativity\Modules\Toolkit\enum_string;

/**
* @implements KeyedSet<IListOfErrors>
Expand Down Expand Up @@ -55,7 +58,7 @@ public function __construct(IError ...$errors)
$this->stack[$key] = $this->get($key)->push($error);
}

ksort($this->stack);
ksort($this->stack, SORT_STRING | SORT_FLAG_CASE);
}

/**
Expand Down Expand Up @@ -105,11 +108,13 @@ public function keys(): array
/**
* Get errors by key.
*
* @param string $key
* @param UnitEnum|string $key
* @return IListOfErrors
*/
public function get(string $key): IListOfErrors
public function get(UnitEnum|string $key): IListOfErrors
{
$key = enum_string($key);

return $this->stack[$key] ?? new ListOfErrors();
}

Expand Down Expand Up @@ -151,6 +156,8 @@ public function count(): int
*/
private function keyFor(IError $error): string
{
return $error->key() ?? self::DEFAULT_KEY;
$key = $error->key();

return $key === null ? self::DEFAULT_KEY : enum_string($key);
}
}
30 changes: 22 additions & 8 deletions src/Toolkit/Result/ListOfErrors.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,29 @@

namespace CloudCreativity\Modules\Toolkit\Result;

use BackedEnum;
use Closure;
use CloudCreativity\Modules\Contracts\Toolkit\Result\Error as IError;
use CloudCreativity\Modules\Contracts\Toolkit\Result\ListOfErrors as IListOfErrors;
use CloudCreativity\Modules\Toolkit\Iterables\IsList;
use UnitEnum;

final class ListOfErrors implements IListOfErrors
{
/** @use IsList<IError> */
use IsList;

/**
* @param IListOfErrors|IError|BackedEnum|array<IError>|string $value
* @param IListOfErrors|IError|UnitEnum|array<IError>|string $value
* @return self
*/
public static function from(IListOfErrors|IError|BackedEnum|array|string $value): self
public static function from(IListOfErrors|IError|UnitEnum|array|string $value): self
{
return match(true) {
$value instanceof self => $value,
$value instanceof IListOfErrors, is_array($value) => new self(...$value),
$value instanceof IError => new self($value),
is_string($value) => new self(new Error(message: $value)),
$value instanceof BackedEnum => new self(new Error(code: $value)),
$value instanceof UnitEnum => new self(new Error(code: $value)),
};
}

Expand All @@ -49,13 +49,13 @@ public function __construct(IError ...$errors)
/**
* @inheritDoc
*/
public function first(Closure|BackedEnum|null $matcher = null): ?IError
public function first(Closure|UnitEnum|null $matcher = null): ?IError
{
if ($matcher === null) {
return $this->stack[0] ?? null;
}

if ($matcher instanceof BackedEnum) {
if ($matcher instanceof UnitEnum) {
$matcher = static fn (IError $error): bool => $error->is($matcher);
}

Expand All @@ -71,9 +71,9 @@ public function first(Closure|BackedEnum|null $matcher = null): ?IError
/**
* @inheritDoc
*/
public function contains(Closure|BackedEnum $matcher): bool
public function contains(Closure|UnitEnum $matcher): bool
{
if ($matcher instanceof BackedEnum) {
if ($matcher instanceof UnitEnum) {
$matcher = static fn (IError $error): bool => $error->is($matcher);
}

Expand Down Expand Up @@ -104,6 +104,20 @@ public function codes(): array
return $codes;
}

/**
* @inheritDoc
*/
public function code(): ?UnitEnum
{
foreach ($this->stack as $error) {
if ($code = $error->code()) {
return $code;
}
}

return null;
}

/**
* @inheritDoc
*/
Expand Down
Loading