Skip to content

Commit c32bd05

Browse files
Simple path reporter: a simpler way to provide errors (#45)
1 parent 5d53141 commit c32bd05

16 files changed

+879
-193
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- `SimplePathReporter` error reporter.
10+
### Changed
11+
- `Facile\PhpCodec\PathReporter` moved to `Facile\PhpCodec\Reporters\PathReporter`.
812

913
## [0.0.2] - 2021-08-13
1014
### Added

README.md

+46-8
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ Install it now. It only requires PHP > 7.2.
1111

1212
composer require facile-it/php-codec
1313

14-
## Disclaimer
14+
# Disclaimer
1515

1616
This project is under active development: it's unstable and still poorly documented.
1717
The API is likely to change several times, and it won't be ready for production soon.
1818

1919
The project follows [semantic versioning](https://semver.org/).
2020

21-
## Introduction
21+
# Introduction
2222

2323
This project is a partial porting of the fantastic [io-ts](https://github.com/gcanti/io-ts) library for Typescript.
2424
Everything rounds about the concept of decoder, encoder and codec.
@@ -66,18 +66,18 @@ I recommend reading the [The Idea](https://github.com/gcanti/io-ts/blob/master/i
6666
documentation of io-ts. It starts with a beautiful description of what codecs are.
6767
> A value of type `Type<A, O, I>` (called "codec") is the run time representation of the static type `A`.
6868
69-
## Getting started
69+
# Getting started
7070

7171
composer require facile-it/php-codec
7272

73-
### Decoders
73+
## Decoders
7474

7575
Decoders are objects with decoding capabilities.
7676
A decoder of type `Decoder<I, A>` takes an input of type `I` and builds a result of type `Validation<A>`.
7777

7878
The class `Facile\PhpCodec\Decoders` provides factory methods for built-in decoders and combinators.
7979

80-
#### How to use decoders
80+
### How to use decoders
8181

8282
```php
8383
use Facile\PhpCodec\Decoders;
@@ -109,7 +109,7 @@ if($v2 instanceof ValidationFailures) {
109109
}
110110
```
111111

112-
#### Dealing with the validation result
112+
### Dealing with the validation result
113113

114114
We can use `Validation::fold` to destruct the validation result while providing
115115
a valid result in any case.
@@ -142,7 +142,7 @@ use Facile\PhpCodec\Decoders;
142142

143143
$decoder = Decoders::intFromString();
144144
$v = $decoder->decode('hello');
145-
$msgs = \Facile\PhpCodec\PathReporter::create()->report($v);
145+
$msgs = \Facile\PhpCodec\Reporters::path()->report($v);
146146

147147
var_dump($msgs);
148148
/* This will print
@@ -153,6 +153,44 @@ array(1) {
153153
*/
154154
```
155155

156-
### Examples
156+
## Examples
157157

158158
Take a look to the [examples](https://github.com/facile-it/php-codec/tree/master/tests/examples) folder.
159+
160+
161+
# Reporters
162+
163+
Reporters do create reports from `Validation` objects.
164+
Generally speaking, reporters are objects that implement the `Reporter<T>` interface, given `T` the type of the report that the generate.
165+
166+
One interesting group of reporters is the validation error reporters group.
167+
They implements `Reporter<list<string>>`.
168+
Thus, given a `Validation` object, they generate a list of error messages for each validation error.
169+
170+
PHP-Codec comes with two error reporters:
171+
172+
- PathReporter, which is a pretty straightforward porting of io-ts' [PathReporter](https://github.com/gcanti/io-ts/blob/master/index.md#error-reporters).
173+
- SimplePathReporter, which is a simplified (read: shorter messages) version of the PathReporter.
174+
175+
```php
176+
$d = Decoders::arrayProps([
177+
'a' => Decoders::arrayProps([
178+
'a1' => Decoders::int(),
179+
'a2' => Decoders::string(),
180+
]),
181+
'b' => Decoders::arrayProps(['b1' => Decoders::bool()])
182+
]);
183+
$v = $d->decode(['a'=> ['a1' => 'str', 'a2' => 1], 'b' => 2]);
184+
185+
$x = \Facile\PhpCodec\Reporters::path()->report($v);
186+
// $x will be
187+
// ['Invalid value "str" supplied to : {a: {a1: int, a2: string}, b: {b1: bool}}/a: {a1: int, a2: string}/a1: int',
188+
// 'Invalid value 1 supplied to : {a: {a1: int, a2: string}, b: {b1: bool}}/a: {a1: int, a2: string}/a2: string',
189+
// 'Invalid value undefined supplied to : {a: {a1: int, a2: string}, b: {b1: bool}}/b: {b1: bool}/b1: bool']
190+
191+
$y = \Facile\PhpCodec\Reporters::simplePath()->report($v);
192+
// $y will be
193+
// ['/a/a1: Invalid value "str" supplied to decoder "int"',
194+
// '/a/a2: Invalid value 1 supplied to decoder "string"',
195+
// '/b/b1: Invalid value undefined supplied to decoder "bool"']
196+
```

src/Decoders.php

+23-8
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,27 @@ public static function union(Decoder $a, Decoder $b, ?Decoder $c = null, ?Decode
122122
{
123123
// Order is important, this is not commutative
124124

125-
return new UnionDecoder(
126-
$a,
127-
$c instanceof Decoder
128-
? self::union($b, $c, $d, $e)
129-
: $b
125+
$args = array_values(
126+
array_filter(
127+
func_get_args(),
128+
static function ($x): bool {
129+
return $x instanceof Decoder;
130+
}
131+
)
130132
);
133+
$argc = count($args);
134+
135+
$res = new UnionDecoder($args[$argc - 2], $args[$argc - 1], $argc - 2);
136+
137+
for ($i = $argc - 3; $i >= 0; --$i) {
138+
$res = new UnionDecoder(
139+
$args[$i],
140+
$res,
141+
$i
142+
);
143+
}
144+
145+
return $res;
131146
}
132147

133148
/**
@@ -224,21 +239,21 @@ public static function arrayProps(array $props): Decoder
224239
* @psalm-template CF of callable(...mixed):T
225240
* @psalm-param Decoder<mixed, PD> $propsDecoder
226241
* @psalm-param CF $factory
227-
* @psalm-param class-string<T> $fqcn
242+
* @psalm-param string $decoderName
228243
* @psalm-return Decoder<mixed, T>
229244
*/
230245
public static function classFromArrayPropsDecoder(
231246
Decoder $propsDecoder,
232247
callable $factory,
233-
string $fqcn
248+
string $decoderName
234249
): Decoder {
235250
return self::pipe(
236251
$propsDecoder,
237252
new MapDecoder(
238253
function (array $props) use ($factory) {
239254
return destructureIn($factory)(\array_values($props));
240255
},
241-
\sprintf('%s(%s)', $fqcn, $propsDecoder->getName())
256+
\sprintf('%s(%s)', $decoderName, $propsDecoder->getName())
242257
)
243258
);
244259
}

src/Internal/Arrays/ListOfDecoder.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,17 @@ public function validate($i, Context $context): Validation
5050
$validation = [];
5151

5252
/** @var IT $item */
53-
foreach ($i as $item) {
54-
$validation[] = $this->elementDecoder->validate($item, $context);
53+
foreach ($i as $index => $item) {
54+
$validation[] = $this->elementDecoder->validate(
55+
$item,
56+
$context->appendEntries(
57+
new ContextEntry(
58+
(string) $index,
59+
$this->elementDecoder,
60+
$item
61+
)
62+
)
63+
);
5564
}
5665

5766
return ListOfValidation::sequence($validation);

src/Internal/Combinators/IntersectionDecoder.php

+64-42
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
namespace Facile\PhpCodec\Internal\Combinators;
66

77
use Facile\PhpCodec\Decoder;
8+
use function Facile\PhpCodec\destructureIn;
89
use function Facile\PhpCodec\Internal\standardDecode;
910
use Facile\PhpCodec\Validation\Context;
11+
use Facile\PhpCodec\Validation\ContextEntry;
12+
use Facile\PhpCodec\Validation\ListOfValidation;
1013
use Facile\PhpCodec\Validation\Validation;
14+
use Facile\PhpCodec\Validation\ValidationFailures;
15+
use Facile\PhpCodec\Validation\ValidationSuccess;
1116

1217
/**
1318
* @psalm-template I
@@ -35,23 +40,45 @@ public function __construct(Decoder $a, Decoder $b)
3540

3641
public function validate($i, Context $context): Validation
3742
{
38-
return Validation::bind(
39-
/**
40-
* @psalm-param A $a
41-
* @psalm-return Validation<A & B>
42-
*
43-
* @param mixed $a
44-
*/
45-
function ($a) use ($i): Validation {
46-
/** @psalm-var Closure(B):A&B $f */
47-
$f = $this->curryIntersect($a);
43+
/** @var Validation<A> $va */
44+
$va = $this->a->validate($i, $context->appendEntries(new ContextEntry('0', $this->a, $i)));
45+
/** @var Validation<B> $vb */
46+
$vb = $this->b->validate($i, $context->appendEntries(new ContextEntry('1', $this->b, $i)));
4847

49-
return Validation::map(
50-
$f,
51-
$this->b->decode($i)
52-
);
53-
},
54-
$this->a->decode($i)
48+
if ($va instanceof ValidationFailures && $vb instanceof ValidationFailures) {
49+
return ValidationFailures::failures(
50+
array_merge(
51+
$va->getErrors(),
52+
$vb->getErrors()
53+
)
54+
);
55+
}
56+
57+
if ($va instanceof ValidationFailures && $vb instanceof ValidationSuccess) {
58+
/** @psalm-var Validation<A&B> $va */
59+
return $va;
60+
}
61+
62+
if ($va instanceof ValidationSuccess && $vb instanceof ValidationFailures) {
63+
/** @psalm-var Validation<A&B> $vb */
64+
return $vb;
65+
}
66+
67+
return Validation::map(
68+
destructureIn(
69+
/**
70+
* @psalm-param A $a
71+
* @psalm-param B $b
72+
* @psalm-return A&B
73+
*
74+
* @param mixed $a
75+
* @param mixed $b
76+
*/
77+
function ($a, $b) {
78+
return self::intersectResults($a, $b);
79+
}
80+
),
81+
ListOfValidation::sequence([$va, $vb])
5582
);
5683
}
5784

@@ -67,37 +94,32 @@ public function getName(): string
6794
}
6895

6996
/**
70-
* @psalm-param A $a
71-
* @psalm-return Closure(B):A&B
97+
* @template T1
98+
* @template T2
99+
* @psalm-param T1 $a
100+
* @psalm-param T2 $b
101+
* @psalm-return T1&T2
72102
*
73103
* @param mixed $a
104+
* @param mixed $b
105+
*
106+
* @return array|object
74107
*/
75-
private function curryIntersect($a): \Closure
108+
private static function intersectResults($a, $b)
76109
{
77-
$f =
78-
/**
79-
* @psalm-param B $b
80-
* @psalm-return A&B
81-
*
82-
* @param mixed $b
83-
*/
84-
static function ($b) use ($a) {
85-
if (\is_array($a) && \is_array($b)) {
86-
/** @var A&B */
87-
return \array_merge($a, $b);
88-
}
89-
90-
if ($a instanceof \stdClass && $b instanceof \stdClass) {
91-
/** @var A&B */
92-
return (object) \array_merge((array) $a, (array) $b);
93-
}
110+
if (\is_array($a) && \is_array($b)) {
111+
/** @var T1&T2 */
112+
return \array_merge($a, $b);
113+
}
94114

95-
/**
96-
* @psalm-var A&B $b
97-
*/
98-
return $b;
99-
};
115+
if ($a instanceof \stdClass && $b instanceof \stdClass) {
116+
/** @var T1&T2 */
117+
return (object) \array_merge((array) $a, (array) $b);
118+
}
100119

101-
return $f;
120+
/**
121+
* @psalm-var T1&T2 $b
122+
*/
123+
return $b;
102124
}
103125
}

src/Internal/Combinators/UnionDecoder.php

+39-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Facile\PhpCodec\Decoder;
88
use function Facile\PhpCodec\Internal\standardDecode;
99
use Facile\PhpCodec\Validation\Context;
10+
use Facile\PhpCodec\Validation\ContextEntry;
1011
use Facile\PhpCodec\Validation\Validation;
1112
use Facile\PhpCodec\Validation\ValidationFailures;
1213

@@ -24,25 +25,60 @@ final class UnionDecoder implements Decoder
2425
private $a;
2526
/** @var Decoder<IB, B> */
2627
private $b;
28+
/** @var int */
29+
private $indexBegin;
2730

2831
/**
2932
* @psalm-param Decoder<IA, A> $a
3033
* @psalm-param Decoder<IB, B> $b
3134
*/
3235
public function __construct(
3336
Decoder $a,
34-
Decoder $b
37+
Decoder $b,
38+
int $indexBegin = 0
3539
) {
3640
$this->a = $a;
3741
$this->b = $b;
42+
$this->indexBegin = $indexBegin;
3843
}
3944

4045
public function validate($i, Context $context): Validation
4146
{
42-
$va = $this->a->validate($i, $context);
47+
$va = $this->a->validate(
48+
$i,
49+
$context->appendEntries(
50+
new ContextEntry(
51+
(string) $this->indexBegin,
52+
$this->a,
53+
$i
54+
)
55+
)
56+
);
4357

4458
if ($va instanceof ValidationFailures) {
45-
return $this->b->validate($i, $context);
59+
$vb = $this->b->validate(
60+
$i,
61+
$this->b instanceof self
62+
? $context
63+
: $context->appendEntries(
64+
new ContextEntry(
65+
(string) ($this->indexBegin + 1),
66+
$this->b,
67+
$i
68+
)
69+
)
70+
);
71+
72+
if ($vb instanceof ValidationFailures) {
73+
return Validation::failures(
74+
array_merge(
75+
$va->getErrors(),
76+
$vb->getErrors()
77+
)
78+
);
79+
}
80+
81+
return $vb;
4682
}
4783

4884
return $va;

0 commit comments

Comments
 (0)