diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c07380f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,46 @@
+name: CI
+
+on:
+ push:
+ schedule:
+ - cron: '0 0 * * *'
+
+jobs:
+ php81:
+ name: PHP 8.1
+ runs-on: ubuntu-22.04
+ steps:
+ - name: checkout
+ uses: actions/checkout@v3
+ - name: composer test
+ uses: docker://chubbyphp/ci-php81:latest
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
+ php82:
+ name: PHP 8.2
+ runs-on: ubuntu-22.04
+ steps:
+ - name: checkout
+ uses: actions/checkout@v3
+ - name: composer test
+ uses: docker://chubbyphp/ci-php82:latest
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
+ php83:
+ name: PHP 8.3
+ runs-on: ubuntu-22.04
+ steps:
+ - name: checkout
+ uses: actions/checkout@v3
+ - name: composer test
+ uses: docker://chubbyphp/ci-php83:latest
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
+ - name: sonarcloud.io
+ uses: sonarsource/sonarcloud-github-action@master
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..91ca7d8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+.idea/
+.phpunit.cache
+.vscode/
+build/
+composer.lock
+vendor/
diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
new file mode 100644
index 0000000..1a57ecd
--- /dev/null
+++ b/.php-cs-fixer.php
@@ -0,0 +1,24 @@
+files()
+ ->name('*.php')
+ ->in(__DIR__ . '/src')
+ ->in(__DIR__ . '/tests')
+;
+
+/** @var array $config */
+$config = require __DIR__ . '/vendor/chubbyphp/chubbyphp-dev-helper/phpcs.php';
+
+// rules is buggy
+unset($config['rules']['simplified_null_return']);
+
+return (new PhpCsFixer\Config)
+ ->setIndent($config['indent'])
+ ->setLineEnding($config['lineEnding'])
+ ->setRules($config['rules'])
+ ->setRiskyAllowed($config['riskyAllowed'])
+ ->setFinder($finder)
+;
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f71a527
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2024 Dominik Zogg
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3f86f16
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+# chubbyphp-parsing
+
+[![CI](https://github.com/chubbyphp/chubbyphp-parsing/workflows/CI/badge.svg?branch=master)](https://github.com/chubbyphp/chubbyphp-parsing/actions?query=workflow%3ACI)
+[![Coverage Status](https://coveralls.io/repos/github/chubbyphp/chubbyphp-parsing/badge.svg?branch=master)](https://coveralls.io/github/chubbyphp/chubbyphp-parsing?branch=master)
+[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fchubbyphp%2Fchubbyphp-parsing%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/chubbyphp/chubbyphp-parsing/master)
+[![Latest Stable Version](https://poser.pugx.org/chubbyphp/chubbyphp-parsing/v/stable.png)](https://packagist.org/packages/chubbyphp/chubbyphp-parsing)
+[![Total Downloads](https://poser.pugx.org/chubbyphp/chubbyphp-parsing/downloads.png)](https://packagist.org/packages/chubbyphp/chubbyphp-parsing)
+[![Monthly Downloads](https://poser.pugx.org/chubbyphp/chubbyphp-parsing/d/monthly)](https://packagist.org/packages/chubbyphp/chubbyphp-parsing)
+
+[![bugs](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=bugs)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+[![code_smells](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=code_smells)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+[![coverage](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=coverage)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+[![duplicated_lines_density](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=duplicated_lines_density)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+[![ncloc](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=ncloc)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+[![sqale_rating](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+[![alert_status](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=alert_status)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+[![reliability_rating](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+[![security_rating](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=security_rating)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+[![sqale_index](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=sqale_index)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+[![vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=chubbyphp_chubbyphp-parsing&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=chubbyphp_chubbyphp-parsing)
+
+
+## Description
+
+
+
+## Requirements
+
+ * php: ^8.1
+
+## Installation
+
+Through [Composer](http://getcomposer.org) as [chubbyphp/chubbyphp-parsing][1].
+
+```sh
+composer require chubbyphp/chubbyphp-parsing "^1.0"
+```
+
+## Usage
+
+## Copyright
+
+2024 Dominik Zogg
+
+[1]: https://packagist.org/packages/chubbyphp/chubbyphp-parsing
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..303d79c
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,65 @@
+{
+ "name": "chubbyphp/chubbyphp-parsing",
+ "description": "Chubbyphp Parse",
+ "keywords": [
+ "chubbyphp"
+ ],
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Dominik Zogg",
+ "email": "dominik.zogg@gmail.com"
+ }
+ ],
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "chubbyphp/chubbyphp-dev-helper": "dev-master",
+ "chubbyphp/chubbyphp-mock": "^1.7",
+ "infection/infection": "^0.27.8",
+ "php-coveralls/php-coveralls": "^2.7.0",
+ "phpstan/extension-installer": "^1.3.1",
+ "phpstan/phpstan": "^1.10.45",
+ "phpunit/phpunit": "^10.4.2"
+ },
+ "autoload": {
+ "psr-4": {
+ "Chubbyphp\\Parsing\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Chubbyphp\\Tests\\Parsing\\": "tests/"
+ }
+ },
+ "config": {
+ "sort-packages": true,
+ "allow-plugins": {
+ "infection/extension-installer": true,
+ "phpstan/extension-installer": true
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "scripts": {
+ "fix:cs": "mkdir -p build && PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --cache-file=build/phpcs.cache",
+ "test": [
+ "@test:lint",
+ "@test:unit",
+ "@test:integration",
+ "@test:infection",
+ "@test:static-analysis",
+ "@test:cs"
+ ],
+ "test:cs": "mkdir -p build && PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --dry-run --stop-on-violation --cache-file=build/phpcs.cache",
+ "test:infection": "vendor/bin/infection --threads=$(nproc) --min-msi=97 --verbose --coverage=build/phpunit",
+ "test:integration": "vendor/bin/phpunit --testsuite=Integration --cache-result-file=build/phpunit/result.cache",
+ "test:lint": "mkdir -p build && find src tests -name '*.php' -print0 | xargs -0 -n1 -P$(nproc) php -l | tee build/phplint.log",
+ "test:static-analysis": "mkdir -p build && bash -c 'vendor/bin/phpstan analyse src --no-progress --level=8 --error-format=junit | tee build/phpstan.junit.xml; if [ ${PIPESTATUS[0]} -ne \"0\" ]; then exit 1; fi'",
+ "test:unit": "vendor/bin/phpunit --testsuite=Unit --coverage-text --coverage-clover=build/phpunit/clover.xml --coverage-html=build/phpunit/coverage-html --coverage-xml=build/phpunit/coverage-xml --log-junit=build/phpunit/junit.xml --cache-result-file=build/phpunit/result.cache"
+ }
+}
diff --git a/infection.json b/infection.json
new file mode 100644
index 0000000..96e37a6
--- /dev/null
+++ b/infection.json
@@ -0,0 +1,20 @@
+{
+ "source": {
+ "directories": [
+ "src"
+ ]
+ },
+ "timeout": 10,
+ "logs": {
+ "text": "build/phpinfection/infection.log",
+ "html": "build/phpinfection/infection.html",
+ "json": "build/phpinfection/infection.json",
+ "summary": "build/phpinfection/summary.log",
+ "stryker": {
+ "report": "master"
+ }
+ },
+ "mutators": {
+ "@default": true
+ }
+}
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..f51e71c
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,2 @@
+parameters:
+ ignoreErrors: []
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..8d051b4
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ ./tests/Integration
+
+
+ ./tests/Unit
+
+
+
+
+ ./src
+
+
+
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..6dadf1c
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,10 @@
+sonar.organization=chubbyphp
+sonar.projectKey=chubbyphp_chubbyphp-parsing
+sonar.projectName=chubbyphp-parsing
+
+sonar.sources=src
+sonar.tests=tests
+sonar.language=php
+sonar.sourceEncoding=UTF-8
+sonar.php.coverage.reportPaths=build/phpunit/clover.xml
+sonar.php.tests.reportPath=build/phpunit/junit.xml
diff --git a/src/ParseError.php b/src/ParseError.php
new file mode 100644
index 0000000..da9f76b
--- /dev/null
+++ b/src/ParseError.php
@@ -0,0 +1,20 @@
+getData();
+ }
+
+ public function getData(): array|string
+ {
+ return $this->data;
+ }
+}
diff --git a/src/ParseErrorInterface.php b/src/ParseErrorInterface.php
new file mode 100644
index 0000000..d0ec6c4
--- /dev/null
+++ b/src/ParseErrorInterface.php
@@ -0,0 +1,12 @@
+
+ */
+ private array $parseErrors;
+
+ /**
+ * @param array $parseErrors
+ */
+ public function __construct(array $parseErrors)
+ {
+ foreach ($parseErrors as $key => $parseError) {
+ if (!$parseError instanceof ParseErrorInterface) {
+ $type = \is_object($parseError) ? $parseError::class : \gettype($parseError);
+
+ throw new \InvalidArgumentException(sprintf('Argument #1 value of #%s ($parseErrors) must be of type ParseError, %s given', (string) $key, $type));
+ }
+ }
+
+ $this->parseErrors = $parseErrors;
+ }
+
+ public function __toString(): string
+ {
+ return json_encode($this->getData());
+ }
+
+ public function getData(): array|string
+ {
+ $data = [];
+ foreach ($this->parseErrors as $key => $parseError) {
+ $data[$key] = $parseError->getData();
+ }
+
+ return $data;
+ }
+}
diff --git a/src/Parser.php b/src/Parser.php
new file mode 100644
index 0000000..c763da5
--- /dev/null
+++ b/src/Parser.php
@@ -0,0 +1,32 @@
+ $objectSchema
+ * @param class-string $classname
+ */
+ public function object(array $objectSchema, string $classname = \stdClass::class): ObjectSchema
+ {
+ return new ObjectSchema($objectSchema, $classname);
+ }
+
+ public function string(): StringSchema
+ {
+ return new StringSchema();
+ }
+}
diff --git a/src/Result.php b/src/Result.php
new file mode 100644
index 0000000..102ae67
--- /dev/null
+++ b/src/Result.php
@@ -0,0 +1,15 @@
+success = null === $error;
+ }
+}
diff --git a/src/Schema/ArraySchema.php b/src/Schema/ArraySchema.php
new file mode 100644
index 0000000..e33dd4a
--- /dev/null
+++ b/src/Schema/ArraySchema.php
@@ -0,0 +1,124 @@
+ &$parseError): mixed>
+ */
+ private array $transform = [];
+
+ private mixed $default = null;
+
+ /**
+ * @var \Closure(mixed, ParseErrorInterface): mixed
+ */
+ private mixed $catch = null;
+
+ private bool $nullable = false;
+
+ public function __construct(private SchemaInterface $itemSchema) {}
+
+ public function parse(mixed $input): null|array
+ {
+ $input ??= $this->default;
+
+ if (null === $input && $this->nullable) {
+ return null;
+ }
+
+ try {
+ if (!\is_array($input)) {
+ throw new ParseError(sprintf("Input needs to be array, '%s'", \gettype($input)));
+ }
+
+ $output = [];
+ $parseErrors = [];
+
+ foreach ($input as $i => $item) {
+ try {
+ $output[$i] = $this->itemSchema->parse($item);
+ } catch (ParseErrorInterface $parseError) {
+ $parseErrors[$i] = $parseError;
+ }
+ }
+
+ foreach ($this->transform as $transform) {
+ $output = $transform($output, $parseErrors);
+ }
+
+ if (\count($parseErrors)) {
+ throw new ParseErrors($parseErrors);
+ }
+
+ return $output;
+ } catch (ParseErrorInterface $parseError) {
+ if ($this->catch) {
+ return ($this->catch)($input, $parseError);
+ }
+
+ throw $parseError;
+ }
+ }
+
+ public function safeParse(mixed $input): Result
+ {
+ try {
+ return new Result($this->parse($input), null);
+ } catch (ParseErrorInterface $parseError) {
+ return new Result(null, $parseError);
+ }
+ }
+
+ /**
+ * @param \Closure(mixed $input, array &$parseError): mixed $transform
+ */
+ public function transform(\Closure $transform): static
+ {
+ $this->transform[] = $transform;
+
+ return $this;
+ }
+
+ public function default(mixed $default): static
+ {
+ $this->default = $default;
+
+ return $this;
+ }
+
+ /**
+ * @param \Closure(mixed $input, ParseErrorInterface $parseError): mixed $catch
+ */
+ public function catch(\Closure $catch): static
+ {
+ $this->catch = $catch;
+
+ return $this;
+ }
+
+ public function nullable(): static
+ {
+ $this->nullable = true;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function array(): array
+ {
+ return [
+ clone $this,
+ ];
+ }
+}
diff --git a/src/Schema/ObjectSchema.php b/src/Schema/ObjectSchema.php
new file mode 100644
index 0000000..b89502e
--- /dev/null
+++ b/src/Schema/ObjectSchema.php
@@ -0,0 +1,155 @@
+
+ */
+ private array $objectSchema;
+
+ /**
+ * @var array<\Closure(mixed, array &$parseError): mixed>
+ */
+ private array $transform = [];
+ private mixed $default = null;
+
+ /**
+ * @var \Closure(mixed, ParseErrorInterface): mixed
+ */
+ private mixed $catch = null;
+
+ private bool $nullable = false;
+
+ /**
+ * @param array $objectSchema
+ * @param class-string $classname
+ */
+ public function __construct(array $objectSchema, private string $classname)
+ {
+ foreach ($objectSchema as $name => $fieldSchema) {
+ if (!\is_string($name)) {
+ $type = \is_object($name) ? $name::class : \gettype($name);
+
+ throw new \InvalidArgumentException(sprintf('Argument #1 name #%s ($parseErrors) must be of type string, %s given', (string) $name, $type));
+ }
+
+ if (!$fieldSchema instanceof SchemaInterface) {
+ $type = \is_object($fieldSchema) ? $fieldSchema::class : \gettype($fieldSchema);
+
+ throw new \InvalidArgumentException(sprintf('Argument #1 value of #%s ($parseErrors) must be of type ParseError, %s given', (string) $name, $type));
+ }
+ }
+
+ $this->objectSchema = $objectSchema;
+ }
+
+ public function parse(mixed $input): null|object
+ {
+ $input ??= $this->default;
+
+ if (null === $input && $this->nullable) {
+ return null;
+ }
+
+ try {
+ if (!\is_array($input)) {
+ throw new ParseError(sprintf("Input needs to be array, '%s'", \gettype($input)));
+ }
+
+ $output = new $this->classname();
+ $parseErrors = [];
+
+ foreach (array_keys($input) as $property) {
+ if (!isset($this->objectSchema[$property])) {
+ $parseErrors[$property] = new ParseError(sprintf("Additional property '%s'", $property));
+ }
+ }
+
+ foreach ($this->objectSchema as $property => $fieldSchema) {
+ try {
+ $output->{$property} = $fieldSchema->parse($input[$property] ?? null);
+ } catch (ParseErrorInterface $parseError) {
+ $parseErrors[$property] = $parseError;
+ }
+ }
+
+ foreach ($this->transform as $transform) {
+ $output = $transform($output, $parseErrors);
+ }
+
+ if (\count($parseErrors)) {
+ throw new ParseErrors($parseErrors);
+ }
+
+ return $output;
+ } catch (ParseErrorInterface $parseError) {
+ if ($this->catch) {
+ return ($this->catch)($input, $parseError);
+ }
+
+ throw $parseError;
+ }
+ }
+
+ public function safeParse(mixed $input): Result
+ {
+ try {
+ return new Result($this->parse($input), null);
+ } catch (ParseErrorInterface $parseError) {
+ return new Result(null, $parseError);
+ }
+ }
+
+ /**
+ * @param \Closure(mixed $input, array &$parseError): mixed $transform
+ */
+ public function transform(\Closure $transform): static
+ {
+ $this->transform[] = $transform;
+
+ return $this;
+ }
+
+ public function default(mixed $default): static
+ {
+ $this->default = $default;
+
+ return $this;
+ }
+
+ /**
+ * @param \Closure(mixed $input, ParseErrorInterface $parseError): mixed $catch
+ */
+ public function catch(\Closure $catch): static
+ {
+ $this->catch = $catch;
+
+ return $this;
+ }
+
+ public function nullable(): static
+ {
+ $this->nullable = true;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function array(): array
+ {
+ return [
+ clone $this,
+ ];
+ }
+}
diff --git a/src/Schema/SchemaInterface.php b/src/Schema/SchemaInterface.php
new file mode 100644
index 0000000..782eab7
--- /dev/null
+++ b/src/Schema/SchemaInterface.php
@@ -0,0 +1,34 @@
+ &$parseError): mixed $transform
+ */
+ public function transform(\Closure $transform): static;
+
+ public function default(mixed $default): static;
+
+ /**
+ * @param \Closure(mixed $input, ParseErrorInterface $parseError): mixed $catch
+ */
+ public function catch(\Closure $catch): static;
+
+ public function nullable(): static;
+
+ /**
+ * @return array
+ */
+ public function array(): array;
+}
diff --git a/src/Schema/StringSchema.php b/src/Schema/StringSchema.php
new file mode 100644
index 0000000..9b71368
--- /dev/null
+++ b/src/Schema/StringSchema.php
@@ -0,0 +1,114 @@
+ &$parseError): mixed>
+ */
+ private array $transform = [];
+
+ private mixed $default = null;
+
+ /**
+ * @var \Closure(mixed, ParseErrorInterface): mixed
+ */
+ private mixed $catch = null;
+
+ private bool $nullable = false;
+
+ public function parse(mixed $input): null|string
+ {
+ $input ??= $this->default;
+
+ if (null === $input && $this->nullable) {
+ return null;
+ }
+
+ try {
+ if (!\is_string($input)) {
+ throw new ParseError(sprintf("Type should be 'string' '%s' given", \gettype($input)));
+ }
+
+ $output = $input;
+ $parseErrors = [];
+
+ foreach ($this->transform as $transform) {
+ $output = $transform($output, $parseErrors);
+ }
+
+ if (\count($parseErrors)) {
+ throw new ParseErrors($parseErrors);
+ }
+
+ return $output;
+ } catch (ParseErrorInterface $parseError) {
+ if ($this->catch) {
+ return ($this->catch)($input, $parseError);
+ }
+
+ throw $parseError;
+ }
+ }
+
+ public function safeParse(mixed $input): Result
+ {
+ try {
+ return new Result($this->parse($input), null);
+ } catch (ParseErrorInterface $parseError) {
+ return new Result(null, $parseError);
+ }
+ }
+
+ /**
+ * @param \Closure(mixed $input, array &$parseError): mixed $transform
+ */
+ public function transform(\Closure $transform): static
+ {
+ $this->transform[] = $transform;
+
+ return $this;
+ }
+
+ public function default(mixed $default): static
+ {
+ $this->default = $default;
+
+ return $this;
+ }
+
+ /**
+ * @param \Closure(mixed $input, ParseErrorInterface $parseError): mixed $catch
+ */
+ public function catch(\Closure $catch): static
+ {
+ $this->catch = $catch;
+
+ return $this;
+ }
+
+ public function nullable(): static
+ {
+ $this->nullable = true;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function array(): array
+ {
+ return [
+ clone $this,
+ ];
+ }
+}
diff --git a/tests/Integration/.gitkeep b/tests/Integration/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/Integration/ParserTest.php b/tests/Integration/ParserTest.php
new file mode 100644
index 0000000..6835262
--- /dev/null
+++ b/tests/Integration/ParserTest.php
@@ -0,0 +1,42 @@
+array(
+ $p->object(['firstname' => $p->string()->nullable(),
+ 'lastname' => $p->string()->default('Doe'),
+ ], $person::class)
+ );
+
+ $result = $schema->safeParse([
+ ['firstname' => 'Jane', 'lastname' => 'Doe'],
+ ['firstname' => 'John', 'lastname' => 'Doe'],
+ [],
+ ]);
+
+ var_dump($result);
+
+ self::assertCount(3, $result->data);
+ }
+}