diff --git a/docs/README.md b/docs/README.md index 2dc967f7..4e025bdd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,7 @@ * [Psl\Async](../src/Psl/Async/README.md) * [Psl\Default](../src/Psl/Default/README.md) * [Psl\Range](../src/Psl/Range/README.md) + * [Psl\Type](../src/Psl/Type/README.md) --- @@ -53,7 +54,6 @@ - [Psl\Str\Grapheme](./component/str-grapheme.md) - [Psl\TCP](./component/tcp.md) - [Psl\Trait](./component/trait.md) -- [Psl\Type](./component/type.md) - [Psl\Unix](./component/unix.md) - [Psl\Vec](./component/vec.md) diff --git a/docs/component/type.md b/docs/component/type.md deleted file mode 100644 index 7656de12..00000000 --- a/docs/component/type.md +++ /dev/null @@ -1,72 +0,0 @@ - - -[*index](./../README.md) - ---- - -### `Psl\Type` Component - -#### `Functions` - -- [array_key](./../../src/Psl/Type/array_key.php#L14) -- [backed_enum](./../../src/Psl/Type/backed_enum.php#L18) -- [bool](./../../src/Psl/Type/bool.php#L14) -- [class_string](./../../src/Psl/Type/class_string.php#L16) -- [converted](./../../src/Psl/Type/converted.php#L23) -- [dict](./../../src/Psl/Type/dict.php#L18) -- [f32](./../../src/Psl/Type/f32.php#L16) -- [f64](./../../src/Psl/Type/f64.php#L16) -- [float](./../../src/Psl/Type/float.php#L14) -- [i16](./../../src/Psl/Type/i16.php#L16) -- [i32](./../../src/Psl/Type/i32.php#L16) -- [i64](./../../src/Psl/Type/i64.php#L16) -- [i8](./../../src/Psl/Type/i8.php#L16) -- [instance_of](./../../src/Psl/Type/instance_of.php#L16) -- [int](./../../src/Psl/Type/int.php#L14) -- [intersection](./../../src/Psl/Type/intersection.php#L20) -- [is_nan](./../../src/Psl/Type/is_nan.php#L14) -- [iterable](./../../src/Psl/Type/iterable.php#L18) -- [literal_scalar](./../../src/Psl/Type/literal_scalar.php#L16) -- [map](./../../src/Psl/Type/map.php#L20) -- [mixed](./../../src/Psl/Type/mixed.php#L14) -- [mixed_dict](./../../src/Psl/Type/mixed_dict.php#L12) -- [mixed_vec](./../../src/Psl/Type/mixed_vec.php#L12) -- [mutable_map](./../../src/Psl/Type/mutable_map.php#L20) -- [mutable_vector](./../../src/Psl/Type/mutable_vector.php#L18) -- [non_empty_dict](./../../src/Psl/Type/non_empty_dict.php#L18) -- [non_empty_string](./../../src/Psl/Type/non_empty_string.php#L14) -- [non_empty_vec](./../../src/Psl/Type/non_empty_vec.php#L16) -- [nonnull](./../../src/Psl/Type/nonnull.php#L16) -- [null](./../../src/Psl/Type/null.php#L14) -- [nullable](./../../src/Psl/Type/nullable.php#L16) -- [num](./../../src/Psl/Type/num.php#L14) -- [numeric_string](./../../src/Psl/Type/numeric_string.php#L14) -- [object](./../../src/Psl/Type/object.php#L14) -- [optional](./../../src/Psl/Type/optional.php#L16) -- [positive_int](./../../src/Psl/Type/positive_int.php#L16) -- [resource](./../../src/Psl/Type/resource.php#L14) -- [scalar](./../../src/Psl/Type/scalar.php#L14) -- [shape](./../../src/Psl/Type/shape.php#L17) -- [string](./../../src/Psl/Type/string.php#L14) -- [u16](./../../src/Psl/Type/u16.php#L16) -- [u32](./../../src/Psl/Type/u32.php#L16) -- [u8](./../../src/Psl/Type/u8.php#L16) -- [uint](./../../src/Psl/Type/uint.php#L16) -- [union](./../../src/Psl/Type/union.php#L18) -- [unit_enum](./../../src/Psl/Type/unit_enum.php#L18) -- [vec](./../../src/Psl/Type/vec.php#L16) -- [vector](./../../src/Psl/Type/vector.php#L18) - -#### `Interfaces` - -- [TypeInterface](./../../src/Psl/Type/TypeInterface.php#L13) - -#### `Classes` - -- [Type](./../../src/Psl/Type/Type.php#L14) - - diff --git a/docs/documenter.php b/docs/documenter.php index 1fd92dd4..54931a03 100644 --- a/docs/documenter.php +++ b/docs/documenter.php @@ -223,7 +223,6 @@ function get_all_components(): array 'Psl\\Str\\Grapheme', 'Psl\\TCP', 'Psl\\Trait', - 'Psl\\Type', 'Psl\\Unix', 'Psl\\Locale', 'Psl\\Vec', diff --git a/docs/templates/README.template.md b/docs/templates/README.template.md index 278bb739..13224712 100644 --- a/docs/templates/README.template.md +++ b/docs/templates/README.template.md @@ -9,6 +9,7 @@ * [Psl\Async](../src/Psl/Async/README.md) * [Psl\Default](../src/Psl/Default/README.md) * [Psl\Range](../src/Psl/Range/README.md) + * [Psl\Type](../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..8d18e048 --- /dev/null +++ b/src/Psl/Type/README.md @@ -0,0 +1,200 @@ +# 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 + use Psl\Type; + + $dateTimeType = Type\converted( + Type\string(), + Type\instance_of(DateTimeImmutable::class), + static function (string $value): DateTimeImmutable { + $date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $value); + if (!$date) { + // Exceptions will be transformed to CoerceException + throw new \RuntimeException('Invalid format given. Expected date to be of format {format}'); + } + + return $date; + } + ); + + $emailType = Type\converted( + Type\string(), + Type\instance_of(EmailValueObject::class), + static function (string $value): EmailValueObject { + // Exceptions will be transformed to CoerceException + return EmailValueObject::tryParse($value); + } + ); + + $shape = Type\shape([ + 'email' => $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 "int" **at path "articles.0.comments.0.user"**. + +