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
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ Heavily inspired by the well-known TypeScript library [zod](https://github.com/c
Through [Composer](http://getcomposer.org) as [chubbyphp/chubbyphp-parsing][1].

```sh
composer require chubbyphp/chubbyphp-parsing "^1.4"
composer require chubbyphp/chubbyphp-parsing "^2.0"
```

## Usage

```php
use Chubbyphp\Parsing\ErrorsException;
use Chubbyphp\Parsing\Schema\SchemaInterface;

/** @var SchemaInterface $schema */
Expand All @@ -51,7 +52,13 @@ $schema->preParse(static fn ($input) => $input);
$schema->postParse(static fn (string $output) => $output);
$schema->parse('test');
$schema->safeParse('test');
$schema->catch(static fn (string $output, ParserErrorException $e) => $output);
$schema->catch(static fn (string $output, ErrorsException $e) => $output);

try {
$schema->parse('test');
} catch (ErrorsException $e) {
var_dump($e->errors->toApiProblems());
}
```

### array
Expand Down Expand Up @@ -391,8 +398,15 @@ $schema = $p->union([$p->string(), $p->int()]);
$data = $schema->parse('42');
```

## Migration

* [1.x to 2.x][10]

## Copyright

2025 Dominik Zogg

[1]: https://packagist.org/packages/chubbyphp/chubbyphp-parsing


[10]: doc/Migration/1.x-2.x.md
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
"dev-master": "2.0-dev"
}
},
"scripts": {
Expand Down
60 changes: 60 additions & 0 deletions doc/Migration/1.x-2.x.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 1.x to 2.x

Adapted error handling.

Added:

- `Chubbyphp\Parsing\Errors`
- `Chubbyphp\Parsing\ErrorsException`

Removed:

- `Chubbyphp\Parsing\ParserErrorException`
- `Chubbyphp\Parsing\ParserErrorExceptionToString`


old:

```php
<?php

declare(strict_types=1);

namespace App;

use Chubbyphp\Parsing\Parser;
use Chubbyphp\Parsing\ParserErrorException;

$p = new Parser();

$schema = $p->string();

try {
$schema->parse('test');
} catch (ParserErrorException $e) {
var_dump($e->getApiProblemErrorMessages());
}
```

new

```php
<?php

declare(strict_types=1);

namespace App;

use Chubbyphp\Parsing\ErrorsException;
use Chubbyphp\Parsing\Parser;

$p = new Parser();

$schema = $p->string();

try {
$schema->parse('test');
} catch (ErrorsException $e) {
var_dump($e->errors->toApiProblems());
}
```
3 changes: 0 additions & 3 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
parameters:
ignoreErrors:
-
message: '/type specified in iterable type array/'
path: %currentWorkingDirectory%/src/ParserErrorException.php
-
message: '/Instanceof between Chubbyphp\\Parsing\\Schema\\ObjectSchemaInterface and Chubbyphp\\Parsing\\Schema\\ObjectSchemaInterface will always evaluate to true./'
path: %currentWorkingDirectory%/src/Schema/DiscriminatedUnionSchema.php
Expand Down
19 changes: 17 additions & 2 deletions src/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

namespace Chubbyphp\Parsing;

final class Error
/**
* @phpstan-type ErrorAsJson array{code: string, template: string, variables: array<string, mixed>}
*/
final class Error implements \JsonSerializable
{
/**
* @param array<string, mixed> $variables
*/
public function __construct(public string $code, public string $template, public array $variables) {}
public function __construct(public readonly string $code, public readonly string $template, public readonly array $variables) {}

public function __toString()
{
Expand All @@ -25,4 +28,16 @@ public function __toString()

return $message;
}

/**
* @return ErrorAsJson
*/
public function jsonSerialize(): array
{
return [
'code' => $this->code,
'template' => $this->template,
'variables' => json_decode(json_encode($this->variables, JSON_THROW_ON_ERROR), true),
];
}
}
122 changes: 122 additions & 0 deletions src/Errors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

declare(strict_types=1);

namespace Chubbyphp\Parsing;

/**
* @phpstan-type ErrorWithPath array{path: string, error: Error}
* @phpstan-type ErrorsWithPath array<ErrorWithPath>
* @phpstan-type ErrorAsJson array{code: string, template: string, variables: array<string, mixed>}
* @phpstan-type ErrorWithPathJson array{path: string, error: ErrorAsJson}
* @phpstan-type ErrorsWithPathJson array<ErrorWithPathJson>
* @phpstan-type ApiProblem array{name: string, reason: string, details: non-empty-array<string, mixed>}
*/
final class Errors implements \JsonSerializable
{
/**
* @var ErrorsWithPath
*/
private array $errorsWithPath = [];

public function __toString()
{
return implode(PHP_EOL, array_map(static fn ($error) => ('' !== $error['path'] ? $error['path'].': ' : '').$error['error'], $this->errorsWithPath));
}

public function add(Error|self $errors, string $path = ''): self
{
if ($errors instanceof self) {
foreach ($errors->errorsWithPath as $errorWithPath) {
$this->errorsWithPath[] = ['path' => $this->mergePath($path, $errorWithPath['path']), 'error' => $errorWithPath['error']];
}

return $this;
}

$this->errorsWithPath[] = ['path' => $path, 'error' => $errors];

return $this;
}

public function has(): bool
{
return 0 !== \count($this->errorsWithPath);
}

/**
* @return ErrorsWithPathJson
*/
public function jsonSerialize(): array
{
return array_map(static fn ($errorWithPath) => [
'path' => $errorWithPath['path'],
'error' => $errorWithPath['error']->jsonSerialize(),
], $this->errorsWithPath);
}

/**
* @return array<ApiProblem>
*/
public function toApiProblems(): array
{
return array_map(
fn ($errorWithPath) => [
'name' => $this->pathToName($errorWithPath['path']),
'reason' => (string) $errorWithPath['error'],
'details' => [
'_template' => $errorWithPath['error']->template,
...$errorWithPath['error']->variables,
],
],
$this->errorsWithPath
);
}

/**
* @return array<string, mixed>
*/
public function toTree(): array
{
// @var array<string, mixed> $tree
return array_reduce(
$this->errorsWithPath,
static function (array $tree, $errorWithPath): array {
$pathParts = explode('.', $errorWithPath['path']);

$current = &$tree;
$lastIndex = \count($pathParts) - 1;

foreach ($pathParts as $i => $pathPart) {
if ($i < $lastIndex) {
$current[$pathPart] ??= [];
$current = &$current[$pathPart];

continue;
}

$current[$pathPart] = array_merge($current[$pathPart] ?? [], [(string) $errorWithPath['error']]);
}

return $tree;
},
[]
);
}

private function mergePath(string $path, string $existingPath): string
{
return implode('.', array_filter([$path, $existingPath], static fn ($part) => '' !== $part));
}

private function pathToName(string $path): string
{
$pathParts = explode('.', $path);

return implode('', array_map(
static fn (string $pathPart, $i) => 0 === $i ? $pathPart : '['.$pathPart.']',
$pathParts,
array_keys($pathParts)
));
}
}
18 changes: 18 additions & 0 deletions src/ErrorsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Chubbyphp\Parsing;

final class ErrorsException extends \RuntimeException
{
public readonly Errors $errors;

public function __construct(Error|Errors $errorsOrError)
{
$errors = $errorsOrError instanceof Errors ? $errorsOrError : (new Errors())->add($errorsOrError);

$this->errors = $errors;
parent::__construct((string) $errors);
}
}
Loading