From a10263e2d89acf62b917da330f6c44fa5a1b2852 Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Fri, 5 Apr 2024 15:40:18 +0200 Subject: [PATCH] docs(type) Provides an initial documentation document for the Type component --- src/Psl/Type/README.md | 201 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/Psl/Type/README.md diff --git a/src/Psl/Type/README.md b/src/Psl/Type/README.md new file mode 100644 index 00000000..a14a5900 --- /dev/null +++ b/src/Psl/Type/README.md @@ -0,0 +1,201 @@ +# Type + +## Introduction + +The type component provides a set functions to ensure that a given value is of a specific type. +It aims to provide a solution for the [Parse, Don't Validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) problem. + +## Usage + +```php +use Psl; +use Psl\Type; + +$untrustedInput = $request->get('input'); + +// Turns a string-like value into a non-empty-string +$trustedInput = Type\non_empty_string()->coerce($untrustedInput); + +// Or assert that its already a non-empty-string +$trustedInput = Type\non_empty_string()->assert($untrustedInput); + +// Or check if its a non-empty-string +$isTrustworthy = Type\non_empty_string()->matches($untrustedInput); +``` + +Every type provided by this component is an instance of `Type\TypeInterface`. +This interface provides the following methods: + +- `matches(mixed $value): $value is Tv` - Checks if the provided value is of the type. +- `assert(mixed $value): Tv` - Asserts that the provided value is of the type or throws an `AssertException` on failure. +- `coerce(mixed $value): Tv` - Coerces the provided value into the type or throws a `CoercionException` on failure. + + +## API + +### Functions + +
+ +* [`Type\converted(TypeInterface $from, TypeInteface, (Closure(I): O)): TypeInterface` php] + + Provides a type `O` that can be converted from a type `I` using a converter function. + + ```php + $emailType, + 'dateTime' => $dateTimeType + ]); + + // Coerce will convert from -> into, If the provided value is already into - it will skip conversion. + $coerced = $shape->coerce($data); + + // Assert will check if the value is of the type it converts into! + $shape->assert($coerced); + ``` + + This type can also be used to transform array-shaped values into custom Data-Transfer-Objects: + + ```php + use Psl\Type; + use Psl\Type\TypeInterface; + + /** + * @psalm-immutable + */ + final class Person { + + public function __construct( + public readonly string $firstName, + public readonly string $lastName, + ) { + } + + /** + * @pure + * + * @return TypeInterface + */ + public static function type(): TypeInterface { + return Type\converted( + Type\shape([ + 'firstName' => Type\string(), + 'lastName' => Type\string(), + ]), + Type\instance_of(Person::class), + fn (array $data): Person => new Person( + $data['firstName'], + $data['lastName'] + ) + ); + } + + /** + * @pure + */ + public static function parse(mixed $data): self + { + return self::type()->coerce($data); + } + } + + // The Person::type() function can now be used as its own type so that it can easily be reused throughout your application. + + $nested = Type\shape([ + 'person' => Person::type(), + ]); + ``` + +* [`Type\shape(dict $elements, bool $allow_unknown_fields = false): TypeInterface>` php] + + Provides a type that can parse (deeply nested) arrays. A shape can consist out of multiple child-shapes and structures: + + ```php + use Psl\Type; + + $shape = Type\shape([ + 'name' => Type\string(), + 'articles' => Type\vec(Type\shape([ + 'title' => Type\string(), + 'content' => Type\string(), + 'likes' => Type\int(), + 'comments' => Type\optional( + Type\vec(Type\shape([ + 'user' => Type\string(), + 'comment' => Type\string() + ])) + ), + ])), + 'dictionary' => Type\dict(Type\string(), Type\vec(Type\shape([ + 'title' => Type\string(), + 'content' => Type\string(), + ]))), + 'pagination' => Type\optional(Type\shape([ + 'currentPage' => Type\uint(), + 'totalPages' => Type\uint(), + 'perPage' => Type\uint(), + 'totalRows' => Type\uint(), + ])) + ]); + + $validData = $shape->coerce([ + 'name' => 'ok', + 'articles' => [ + [ + 'title' => 'ok', + 'content' => 'ok', + 'likes' => 1, + 'comments' => [ + [ + 'user' => 'ok', + 'comment' => 'ok' + ], + [ + 'user' => 'ok', + 'comment' => 'ok', + ] + ] + ] + ], + 'dictionary' => [ + 'key' => [ + [ + 'title' => 'ok', + 'content' => 'ok', + ] + ] + ] + ]); + ``` + + When the data structure does not match the specified shape, you will get detailed information about what went wrong exactly: + + > Expected "array{'name': string, 'articles': vec}>, 'dictionary': dict>, 'pagination'?: array{'currentPage': uint, 'totalPages': uint, 'perPage': uint, 'totalRows': uint}}", got "array" at path **"articles.0.comments.0"**. + +