diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 6ff8bff..31bd97d 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -15,14 +15,20 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none - name: Validate composer.json and composer.lock run: composer validate --strict - name: Cache Composer packages id: composer-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} @@ -36,7 +42,7 @@ jobs: run: vendor/bin/ecs check - name: Run static analysis via PHPStan - run: vendor/bin/phpstan --xdebug analyse src tests + run: vendor/bin/phpstan analyse src tests - name: Run test suite run: composer run-script test diff --git a/README.md b/README.md index b6256a1..e3e9027 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,100 @@ -# Serverless Workflow Specification - PHP SDK +# Open Workflow Specification - PHP SDK [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fopen-workflow-specification%2Fsdk-php.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fopen-workflow-specification%2Fsdk-php?ref=badge_shield) - -Provides the PHP API/SPI for the [Serverless Workflow Specification](https://github.com/serverlessworkflow/specification). +Provides the PHP API/SPI for the [Open Workflow Specification](https://github.com/open-workflow-specification/specification) +(formerly the Serverless Workflow Specification). With the SDK you can: -* Programmatically build workflow definitions +* Programmatically build workflow definitions * Parse workflow JSON and YAML definitions -* Validate workflow definitions +* Validate workflow definitions against the DSL schema ### Status -Current SDK version conforms to the [Serverless Workflow specification v0.8](https://github.com/serverlessworkflow/specification/tree/0.8.x). +The current SDK version conforms to the [Open Workflow Specification DSL v1.0.1](https://github.com/open-workflow-specification/specification/tree/v1.0.1). +Every example of the specification is parsed, serialized without loss and validated by the test suite. +Definitions written for the legacy 0.8 DSL are not supported by this version. Use the +[migration tool](https://github.com/open-workflow-specification/migration-tool) to convert them, or the +`1.0.0` release of this package for the legacy object model. ## Installation ```bash -composer install serverlessworkflow/sdk +composer require serverlessworkflow/sdk ``` -## Build +PHP 8.2 or later is required. + +## Build + +Every DSL object is a class with public properties named after the DSL properties. Objects can be built from +arrays, from instances, or from a mix of both; task lists keep the order of their named tasks. ```php -use Serverless\Workflow\Action; -use Serverless\Workflow\ActionDataFilter; -use Serverless\Workflow\FunctionDef; -use Serverless\Workflow\FunctionRef; -use Serverless\Workflow\OperationState; +use Serverless\Workflow\Document; +use Serverless\Workflow\Endpoint; +use Serverless\Workflow\Task\Call\HttpArguments; +use Serverless\Workflow\Task\CallHttp; +use Serverless\Workflow\Task\SetTask; +use Serverless\Workflow\TaskList; +use Serverless\Workflow\Timeout; use Serverless\Workflow\Workflow; $workflow = new Workflow([ - 'id' => 'greeting', - 'name' => 'Greeting Workflow', - 'description' => 'Greet Someone', - 'version' => '1.0', - 'specVersion' => '0.8', - 'start' => 'Greet', - 'states' => [ - new OperationState([ - 'name' => 'Greet', - 'type' => 'operation', - 'actions' => [ - new Action([ - 'functionRef' => new FunctionRef([ - 'refName' => 'greetingFunction', - 'arguments' => [ - 'name' => '${ .person.name }', + 'document' => new Document([ + 'dsl' => '1.0.1', + 'namespace' => 'examples', + 'name' => 'buy-pet', + 'version' => '0.1.0', + ]), + 'do' => new TaskList([ + 'getPet' => new CallHttp([ + 'with' => new HttpArguments([ + 'method' => 'get', + 'endpoint' => new Endpoint([ + 'uri' => 'https://petstore.swagger.io/v2/pet/{petId}', + 'authentication' => [ + 'bearer' => [ + 'token' => '${ .token }', ], - ]), - 'actionDataFilter' => new ActionDataFilter([ - 'results' => '${ .greeting }', - ]), + ], ]), + ]), + ]), + 'markAsSold' => new SetTask([ + 'set' => [ + 'status' => 'sold', ], - 'end' => true, ]), - ], - 'functions' => [ - new FunctionDef([ - 'name' => 'greetingFunction', - 'operation' => 'file://myapis/greetingapis.json#greeting', + 'buyPet' => new CallHttp([ + 'with' => [ + 'method' => 'put', + 'endpoint' => 'https://petstore.swagger.io/v2/pet/{petId}', + 'body' => '${ . + { status: .status } }', + ], + 'then' => 'end', ]), - ], + ]), + 'timeout' => new Timeout([ + 'after' => 'PT5M', + ]), ]); ``` +Tasks are resolved to their concrete type (`CallHttp`, `CallOpenApi`, `CallGrpc`, `CallAsyncApi`, `CallFunction`, +`DoTask`, `EmitTask`, `ForTask`, `ForkTask`, `ListenTask`, `RaiseTask`, `RunTask`, `SetTask`, `SwitchTask`, +`TryTask`, `WaitTask`) from their properties when they are created from arrays: + +```php +use Serverless\Workflow\Task\Task; + +$task = Task::fromArray(['wait' => 'PT30S']); // Serverless\Workflow\Task\WaitTask +``` + +Properties the model does not know are preserved as additional properties, so definitions using extensions or a +newer DSL can still be parsed, serialized and validated. + ## Parse ### Convert from JSON/YAML source @@ -76,24 +105,56 @@ $workflow = Workflow::fromJson(file_get_contents('workflow.json')); $workflow = Workflow::fromYaml(file_get_contents('workflow.yaml')); ``` +### Navigate + +```php +$getPet = $workflow->do->get('getPet'); // Serverless\Workflow\Task\CallHttp +$method = $getPet->with->method; // 'get' +$uri = $getPet->with->endpoint->uri; // 'https://petstore.swagger.io/v2/pet/{petId}' + +foreach ($workflow->do as $name => $task) { + // ... +} +``` + ### Convert to JSON/YAML ```php $json = $workflow->toJson(); $yaml = $workflow->toYaml(); + +$array = $workflow->toArray(); ``` ## Validate ```php +use Serverless\Workflow\WorkflowValidationException; use Serverless\Workflow\WorkflowValidator; -WorkflowValidator::validate($workflow); +try { + $workflow->validate(); // or WorkflowValidator::validate($workflow); +} catch (WorkflowValidationException $exception) { + foreach ($exception->getErrors() as $pointer => $messages) { + // '/document' => ['The required properties (version) are missing'] + } +} + +$errors = WorkflowValidator::errors($workflow); // [] when valid ``` -The `validate` method will raise an exception if the provided workflow does not comply with the specification. +Validation uses the DSL JSON schema bundled in [`schema/workflow.json`](schema/workflow.json), so it works +offline. Raw arrays can be validated too: `WorkflowValidator::errors(Yaml::parse($yaml))`. + +## Development +```bash +composer install +composer run-script ecs # fix coding style +composer run-script stan # static analysis +composer run-script test # test suite +``` ## License -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fopen-workflow-specification%2Fsdk-php.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fopen-workflow-specification%2Fsdk-php?ref=badge_large) \ No newline at end of file +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fopen-workflow-specification%2Fsdk-php.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fopen-workflow-specification%2Fsdk-php?ref=badge_large) diff --git a/composer.json b/composer.json index c1285d9..caf0fd9 100644 --- a/composer.json +++ b/composer.json @@ -1,28 +1,40 @@ { "name": "serverlessworkflow/sdk", - "description": "Provides the PHP API/SPI for the Serverless Workflow Specification.", + "description": "Provides the PHP API/SPI for the Open Workflow Specification (formerly Serverless Workflow Specification).", "type": "library", "license": "Apache-2.0", - "autoload": { - "psr-4": { - "Serverless\\Workflow\\": "src/" - } - }, + "keywords": [ + "workflow", + "serverless-workflow", + "open-workflow-specification", + "dsl" + ], + "homepage": "https://github.com/open-workflow-specification/sdk-php", "authors": [ { "name": "Serverless Workflow Specification Authors" } ], + "require": { + "php": "^8.2", + "opis/json-schema": "^2.4", + "symfony/yaml": "^6.4 || ^7.0" + }, "require-dev": { "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^10.4", "rector/rector": "^0.18.10", "symplify/easy-coding-standard": "^11.1" }, - "require": { - "symfony/yaml": "^6.3", - "phpdocumentor/reflection-docblock": "^5.3", - "swaggest/json-schema": "^0.12.42" + "autoload": { + "psr-4": { + "Serverless\\Workflow\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Serverless\\Workflow\\Tests\\": "tests/" + } }, "scripts": { "rector": "vendor/bin/rector", diff --git a/composer.lock b/composer.lock index 985e20b..e030b10 100644 --- a/composer.lock +++ b/composer.lock @@ -4,424 +4,210 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3d055e9325e7721177c29385dec6f957", + "content-hash": "dcd3b6671668b8544e30a07e018d9260", "packages": [ { - "name": "doctrine/deprecations", - "version": "1.1.2", + "name": "opis/json-schema", + "version": "2.6.0", "source": { "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931" + "url": "https://github.com/opis/json-schema.git", + "reference": "8458763e0dd0b6baa310e04f1829fc73da4e8c8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931", - "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "url": "https://api.github.com/repos/opis/json-schema/zipball/8458763e0dd0b6baa310e04f1829fc73da4e8c8a", + "reference": "8458763e0dd0b6baa310e04f1829fc73da4e8c8a", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "ext-json": "*", + "opis/string": "^2.1", + "opis/uri": "^1.0", + "php": "^7.4 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", - "phpstan/phpstan": "1.4.10 || 1.10.15", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psalm/plugin-phpunit": "0.18.4", - "psr/log": "^1 || ^2 || ^3", - "vimeo/psalm": "4.30.0 || 5.12.0" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", - "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.2" - }, - "time": "2023-09-27T20:04:15+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" + "ext-bcmath": "*", + "ext-intl": "*", + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-2.x": "2.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": "src/" + "Opis\\JsonSchema\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + }, + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "description": "Json Schema Validator for PHP", + "homepage": "https://opis.io/json-schema", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "json", + "json-schema", + "schema", + "validation", + "validator" ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + "issues": "https://github.com/opis/json-schema/issues", + "source": "https://github.com/opis/json-schema/tree/2.6.0" }, - "time": "2020-06-27T09:03:43+00:00" + "time": "2025-10-17T12:46:48+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", + "name": "opis/string", + "version": "2.1.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + "url": "https://github.com/opis/string.git", + "reference": "3e4d2aaff518ac518530b89bb26ed40f4503635e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "url": "https://api.github.com/repos/opis/string/zipball/3e4d2aaff518ac518530b89bb26ed40f4503635e", + "reference": "3e4d2aaff518ac518530b89bb26ed40f4503635e", "shasum": "" }, "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" + "ext-iconv": "*", + "ext-json": "*", + "php": "^7.4 || ^8.0" }, "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": "src" + "Opis\\String\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" }, { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" } ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "description": "Multibyte strings as objects", + "homepage": "https://opis.io/string", + "keywords": [ + "multi-byte", + "opis", + "string", + "string manipulation", + "utf-8" + ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + "issues": "https://github.com/opis/string/issues", + "source": "https://github.com/opis/string/tree/2.1.0" }, - "time": "2021-10-19T17:43:47+00:00" + "time": "2025-10-17T12:38:41+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.7.3", + "name": "opis/uri", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419" + "url": "https://github.com/opis/uri.git", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", - "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "url": "https://api.github.com/repos/opis/uri/zipball/0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.13" + "opis/string": "^2.0", + "php": "^7.4 || ^8.0" }, "require-dev": { - "ext-tokenizer": "*", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" + "phpunit/phpunit": "^9" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": "src" + "Opis\\Uri\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.3" - }, - "time": "2023-08-12T11:01:26+00:00" - }, - { - "name": "phplang/scope-exit", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/phplang/scope-exit.git", - "reference": "239b73abe89f9414aa85a7ca075ec9445629192b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phplang/scope-exit/zipball/239b73abe89f9414aa85a7ca075ec9445629192b", - "reference": "239b73abe89f9414aa85a7ca075ec9445629192b", - "shasum": "" - }, - "require-dev": { - "phpunit/phpunit": "*" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpLang\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD" - ], - "authors": [ + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, { - "name": "Sara Golemon", - "email": "pollita@php.net", - "homepage": "https://twitter.com/SaraMG", - "role": "Developer" + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" } ], - "description": "Emulation of SCOPE_EXIT construct from C++", - "homepage": "https://github.com/phplang/scope-exit", + "description": "Build, parse and validate URIs and URI-templates", + "homepage": "https://opis.io", "keywords": [ - "cleanup", - "exit", - "scope" + "URI Template", + "parse url", + "punycode", + "uri", + "uri components", + "url", + "validate uri" ], "support": { - "issues": "https://github.com/phplang/scope-exit/issues", - "source": "https://github.com/phplang/scope-exit/tree/master" + "issues": "https://github.com/opis/uri/issues", + "source": "https://github.com/opis/uri/tree/1.1.0" }, - "time": "2016-09-17T00:15:18+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "1.24.3", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "12f01d214f1c73b9c91fdb3b1c415e4c70652083" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/12f01d214f1c73b9c91fdb3b1c415e4c70652083", - "reference": "12f01d214f1c73b9c91fdb3b1c415e4c70652083", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^4.15", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.3" - }, - "time": "2023-11-18T20:15:32+00:00" - }, - { - "name": "swaggest/json-diff", - "version": "v3.10.5", - "source": { - "type": "git", - "url": "https://github.com/swaggest/json-diff.git", - "reference": "17bfc66b330f46e12a7e574133497a290cd79ba5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/swaggest/json-diff/zipball/17bfc66b330f46e12a7e574133497a290cd79ba5", - "reference": "17bfc66b330f46e12a7e574133497a290cd79ba5", - "shasum": "" - }, - "require": { - "ext-json": "*" - }, - "require-dev": { - "phperf/phpunit": "4.8.37" - }, - "type": "library", - "autoload": { - "psr-4": { - "Swaggest\\JsonDiff\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Viacheslav Poturaev", - "email": "vearutop@gmail.com" - } - ], - "description": "JSON diff/rearrange/patch/pointer library for PHP", - "support": { - "issues": "https://github.com/swaggest/json-diff/issues", - "source": "https://github.com/swaggest/json-diff/tree/v3.10.5" - }, - "time": "2023-11-17T11:12:46+00:00" - }, - { - "name": "swaggest/json-schema", - "version": "v0.12.42", - "source": { - "type": "git", - "url": "https://github.com/swaggest/php-json-schema.git", - "reference": "d23adb53808b8e2da36f75bc0188546e4cbe3b45" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/swaggest/php-json-schema/zipball/d23adb53808b8e2da36f75bc0188546e4cbe3b45", - "reference": "d23adb53808b8e2da36f75bc0188546e4cbe3b45", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=5.4", - "phplang/scope-exit": "^1.0", - "swaggest/json-diff": "^3.8.2", - "symfony/polyfill-mbstring": "^1.19" - }, - "require-dev": { - "phperf/phpunit": "4.8.37" - }, - "suggest": { - "ext-mbstring": "For better performance" - }, - "type": "library", - "autoload": { - "psr-4": { - "Swaggest\\JsonSchema\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Viacheslav Poturaev", - "email": "vearutop@gmail.com" - } - ], - "description": "High definition PHP structures with JSON-schema based validation", - "support": { - "email": "vearutop@gmail.com", - "issues": "https://github.com/swaggest/php-json-schema/issues", - "source": "https://github.com/swaggest/php-json-schema/tree/v0.12.42" - }, - "time": "2023-09-12T14:43:42+00:00" + "time": "2021-05-22T15:57:08+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -429,12 +215,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { @@ -459,7 +245,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -470,29 +256,33 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.28.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -502,12 +292,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -541,7 +328,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -553,86 +340,7 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-26T09:26:14+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/nicolas-grekas", "type": "github" }, { @@ -640,32 +348,32 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/yaml", - "version": "v6.3.8", + "version": "v7.4.21", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "3493af8a8dad7fa91c77fa473ba23ecd95334a92" + "reference": "044907929c7999814378be22da34722c7a86bad4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/3493af8a8dad7fa91c77fa473ba23ecd95334a92", - "reference": "3493af8a8dad7fa91c77fa473ba23ecd95334a92", + "url": "https://api.github.com/repos/symfony/yaml/zipball/044907929c7999814378be22da34722c7a86bad4", + "reference": "044907929c7999814378be22da34722c7a86bad4", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<5.4" + "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -696,7 +404,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.3.8" + "source": "https://github.com/symfony/yaml/tree/v7.4.21" }, "funding": [ { @@ -707,97 +415,44 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2023-11-06T10:58:05+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" - }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2026-10-12T12:34:30+00:00" } ], "packages-dev": [ { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.0" }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -823,37 +478,38 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://github.com/mnapoli", + "type": "github" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2026-08-11T10:17:44+00:00" }, { "name": "nikic/php-parser", - "version": "v4.17.1", + "version": "v5.9.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", "shasum": "" }, "require": { + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -861,7 +517,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -885,26 +541,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2026-09-13T18:51:52+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -945,9 +602,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -1002,16 +665,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.44", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "bf84367c53a23f759513985c54ffe0d0c249825b" - }, + "version": "1.12.34", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/bf84367c53a23f759513985c54ffe0d0c249825b", - "reference": "bf84367c53a23f759513985c54ffe0d0c249825b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4dd89ca7aa30fdc6760be21550d583bcc32e8476", + "reference": "4dd89ca7aa30fdc6760be21550d583bcc32e8476", "shasum": "" }, "require": { @@ -1054,42 +712,38 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2023-11-21T16:30:46+00:00" + "time": "2026-07-28T10:04:39+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.1.9", + "version": "10.1.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "a56a9ab2f680246adcf3db43f38ddf1765774735" + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/a56a9ab2f680246adcf3db43f38ddf1765774735", - "reference": "a56a9ab2f680246adcf3db43f38ddf1765774735", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=8.1", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-text-template": "^3.0", - "sebastian/code-unit-reverse-lookup": "^3.0", - "sebastian/complexity": "^3.0", - "sebastian/environment": "^6.0", - "sebastian/lines-of-code": "^2.0", - "sebastian/version": "^4.0", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { "phpunit/phpunit": "^10.1" @@ -1101,7 +755,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1-dev" + "dev-main": "10.1.x-dev" } }, "autoload": { @@ -1130,7 +784,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.9" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, "funding": [ { @@ -1138,7 +792,7 @@ "type": "github" } ], - "time": "2023-11-23T12:23:20+00:00" + "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1385,45 +1039,45 @@ }, { "name": "phpunit/phpunit", - "version": "10.4.2", + "version": "10.5.66", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "cacd8b9dd224efa8eb28beb69004126c7ca1a1a1" + "reference": "747c9889613d0b3ffa08b74272e50ed3b8fa6d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/cacd8b9dd224efa8eb28beb69004126c7ca1a1a1", - "reference": "cacd8b9dd224efa8eb28beb69004126c7ca1a1a1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/747c9889613d0b3ffa08b74272e50ed3b8fa6d72", + "reference": "747c9889613d0b3ffa08b74272e50ed3b8fa6d72", "shasum": "" }, "require": { "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.5", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-invoker": "^4.0", - "phpunit/php-text-template": "^3.0", - "phpunit/php-timer": "^6.0", - "sebastian/cli-parser": "^2.0", - "sebastian/code-unit": "^2.0", - "sebastian/comparator": "^5.0", - "sebastian/diff": "^5.0", - "sebastian/environment": "^6.0", - "sebastian/exporter": "^5.1", - "sebastian/global-state": "^6.0.1", - "sebastian/object-enumerator": "^5.0", - "sebastian/recursion-context": "^5.0", - "sebastian/type": "^4.0", - "sebastian/version": "^4.0" + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.2", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" @@ -1434,7 +1088,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.4-dev" + "dev-main": "10.5-dev" } }, "autoload": { @@ -1466,36 +1120,28 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.4.2" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.66" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2023-10-26T07:21:45+00:00" + "time": "2026-10-05T05:46:26+00:00" }, { "name": "rector/rector", - "version": "0.18.10", + "version": "0.18.13", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "f36bc0a707fd8af301df5108740ce41f9db8eded" + "reference": "f8011a76d36aa4f839f60f3b4f97707d97176618" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/f36bc0a707fd8af301df5108740ce41f9db8eded", - "reference": "f36bc0a707fd8af301df5108740ce41f9db8eded", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/f8011a76d36aa4f839f60f3b4f97707d97176618", + "reference": "f8011a76d36aa4f839f60f3b4f97707d97176618", "shasum": "" }, "require": { @@ -1530,7 +1176,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/0.18.10" + "source": "https://github.com/rectorphp/rector/tree/0.18.13" }, "funding": [ { @@ -1538,20 +1184,20 @@ "type": "github" } ], - "time": "2023-11-16T19:42:21+00:00" + "time": "2023-12-20T16:08:01+00:00" }, { "name": "sebastian/cli-parser", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae" + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae", - "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { @@ -1586,7 +1232,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.0" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" }, "funding": [ { @@ -1594,7 +1241,7 @@ "type": "github" } ], - "time": "2023-02-03T06:58:15+00:00" + "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", @@ -1709,16 +1356,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.1", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2db5010a484d53ebf536087a70b4a5423c102372" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", - "reference": "2db5010a484d53ebf536087a70b4a5423c102372", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { @@ -1729,7 +1376,7 @@ "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -1774,32 +1421,44 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2023-08-14T13:18:12+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", - "version": "3.1.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "68cfb347a44871f01e33ab0ef8215966432f6957" + "reference": "68ff824baeae169ec9f2137158ee529584553799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68cfb347a44871f01e33ab0ef8215966432f6957", - "reference": "68cfb347a44871f01e33ab0ef8215966432f6957", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", "shasum": "" }, "require": { - "nikic/php-parser": "^4.10", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1" }, "require-dev": { @@ -1808,7 +1467,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.1-dev" + "dev-main": "3.2-dev" } }, "autoload": { @@ -1832,7 +1491,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/3.1.0" + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" }, "funding": [ { @@ -1840,20 +1499,20 @@ "type": "github" } ], - "time": "2023-09-28T11:50:59+00:00" + "time": "2023-12-21T08:37:17+00:00" }, { "name": "sebastian/diff", - "version": "5.0.3", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b" + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/912dc2fbe3e3c1e7873313cc801b100b6c68c87b", - "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { @@ -1861,12 +1520,12 @@ }, "require-dev": { "phpunit/phpunit": "^10.0", - "symfony/process": "^4.2 || ^5" + "symfony/process": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1899,7 +1558,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" }, "funding": [ { @@ -1907,20 +1566,20 @@ "type": "github" } ], - "time": "2023-05-01T07:48:21+00:00" + "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", - "version": "6.0.1", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", - "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { @@ -1935,7 +1594,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -1963,7 +1622,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" }, "funding": [ { @@ -1971,20 +1630,20 @@ "type": "github" } ], - "time": "2023-04-11T05:39:26+00:00" + "time": "2024-03-23T08:47:14+00:00" }, { "name": "sebastian/exporter", - "version": "5.1.1", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc" + "reference": "0735b90f4da94969541dac1da743446e276defa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/64f51654862e0f5e318db7e9dcc2292c63cdbddc", - "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", "shasum": "" }, "require": { @@ -1993,7 +1652,7 @@ "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -2041,28 +1700,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.1" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2023-09-24T13:22:09+00:00" + "time": "2025-09-24T06:09:11+00:00" }, { "name": "sebastian/global-state", - "version": "6.0.1", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4" + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/7ea9ead78f6d380d2a667864c132c2f7b83055e4", - "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { @@ -2096,14 +1767,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { @@ -2111,24 +1782,24 @@ "type": "github" } ], - "time": "2023-07-19T07:19:23+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "649e40d279e243d985aa8fb6e74dd5bb28dc185d" + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/649e40d279e243d985aa8fb6e74dd5bb28dc185d", - "reference": "649e40d279e243d985aa8fb6e74dd5bb28dc185d", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.10", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1" }, "require-dev": { @@ -2161,7 +1832,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.1" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" }, "funding": [ { @@ -2169,7 +1840,7 @@ "type": "github" } ], - "time": "2023-08-31T09:25:50+00:00" + "time": "2023-12-21T08:38:20+00:00" }, { "name": "sebastian/object-enumerator", @@ -2285,23 +1956,23 @@ }, { "name": "sebastian/recursion-context", - "version": "5.0.0", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + "reference": "5d32fe257a9b39cb63146924d6b4e32a22d4502a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5d32fe257a9b39cb63146924d6b4e32a22d4502a", + "reference": "5d32fe257a9b39cb63146924d6b4e32a22d4502a", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -2336,15 +2007,28 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T07:05:40+00:00" + "time": "2026-08-11T05:27:39+00:00" }, { "name": "sebastian/type", @@ -2460,12 +2144,12 @@ "version": "11.5.0", "source": { "type": "git", - "url": "https://github.com/easy-coding-standard/easy-coding-standard.git", + "url": "https://github.com/ecsphp/ecs.git", "reference": "1d2400f7bfe92e3754ce71f0782f2c0521bade3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/1d2400f7bfe92e3754ce71f0782f2c0521bade3d", + "url": "https://api.github.com/repos/ecsphp/ecs/zipball/1d2400f7bfe92e3754ce71f0782f2c0521bade3d", "reference": "1d2400f7bfe92e3754ce71f0782f2c0521bade3d", "shasum": "" }, @@ -2498,8 +2182,8 @@ "static analysis" ], "support": { - "issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues", - "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/11.5.0" + "issues": "https://github.com/easy-coding-standard/ecs/issues", + "source": "https://github.com/easy-coding-standard/ecs/tree/11.5.0" }, "funding": [ { @@ -2515,16 +2199,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -2553,7 +2237,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -2561,15 +2245,17 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/schema/README.md b/schema/README.md new file mode 100644 index 0000000..ac64324 --- /dev/null +++ b/schema/README.md @@ -0,0 +1,10 @@ +# DSL schema + +`workflow.json` is the unmodified JSON schema of the Open Workflow Specification DSL, +copied from the [specification repository](https://github.com/open-workflow-specification/specification) +at tag `v1.0.1` (`schema/workflow.json`). It is bundled so that `WorkflowValidator` +works offline and deterministically. + +To upgrade the SDK to a newer DSL release, replace this file with the schema of that +release, update `Workflow::DSL_VERSION`, refresh `tests/Fixtures/examples` from the +release's `examples` directory and adjust the model where the schema changed. diff --git a/schema/workflow.json b/schema/workflow.json new file mode 100644 index 0000000..30a222b --- /dev/null +++ b/schema/workflow.json @@ -0,0 +1,2835 @@ +{ + "$id": "https://serverlessworkflow.io/schemas/1.0.1/workflow.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Serverless Workflow DSL - Workflow Schema.", + "type": "object", + "required": [ + "document", + "do" + ], + "properties": { + "document": { + "type": "object", + "title": "Document", + "description": "Documents the workflow.", + "unevaluatedProperties": false, + "properties": { + "dsl": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "title": "WorkflowDSL", + "description": "The version of the DSL used by the workflow." + }, + "namespace": { + "type": "string", + "pattern": "^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", + "title": "WorkflowNamespace", + "description": "The workflow's namespace." + }, + "name": { + "type": "string", + "pattern": "^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", + "title": "WorkflowName", + "description": "The workflow's name." + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "title": "WorkflowVersion", + "description": "The workflow's semantic version." + }, + "title": { + "type": "string", + "title": "WorkflowTitle", + "description": "The workflow's title." + }, + "summary": { + "type": "string", + "title": "WorkflowSummary", + "description": "The workflow's Markdown summary." + }, + "tags": { + "type": "object", + "title": "WorkflowTags", + "description": "A key/value mapping of the workflow's tags, if any.", + "additionalProperties": true + }, + "metadata": { + "type": "object", + "title": "WorkflowMetadata", + "description": "Holds additional information about the workflow.", + "additionalProperties": true + } + }, + "required": [ + "dsl", + "namespace", + "name", + "version" + ] + }, + "input": { + "$ref": "#/$defs/input", + "title": "Input", + "description": "Configures the workflow's input." + }, + "use": { + "type": "object", + "title": "Use", + "description": "Defines the workflow's reusable components.", + "unevaluatedProperties": false, + "properties": { + "authentications": { + "type": "object", + "title": "UseAuthentications", + "description": "The workflow's reusable authentication policies.", + "additionalProperties": { + "$ref": "#/$defs/authenticationPolicy" + } + }, + "errors": { + "type": "object", + "title": "UseErrors", + "description": "The workflow's reusable errors.", + "additionalProperties": { + "$ref": "#/$defs/error" + } + }, + "extensions": { + "type": "array", + "title": "UseExtensions", + "description": "The workflow's extensions.", + "items": { + "type": "object", + "title": "ExtensionItem", + "minProperties": 1, + "maxProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/extension" + } + } + }, + "functions": { + "type": "object", + "title": "UseFunctions", + "description": "The workflow's reusable functions.", + "additionalProperties": { + "$ref": "#/$defs/task" + } + }, + "retries": { + "type": "object", + "title": "UseRetries", + "description": "The workflow's reusable retry policies.", + "additionalProperties": { + "$ref": "#/$defs/retryPolicy" + } + }, + "secrets": { + "type": "array", + "title": "UseSecrets", + "description": "The workflow's reusable secrets.", + "items": { + "type": "string", + "description": "The workflow's secrets." + } + }, + "timeouts": { + "type": "object", + "title": "UseTimeouts", + "description": "The workflow's reusable timeouts.", + "additionalProperties": { + "$ref": "#/$defs/timeout" + } + }, + "catalogs": { + "type": "object", + "title": "UseCatalogs", + "description": "The workflow's reusable catalogs.", + "additionalProperties": { + "$ref": "#/$defs/catalog" + } + } + } + }, + "do": { + "$ref": "#/$defs/taskList", + "title": "Do", + "description": "Defines the task(s) the workflow must perform." + }, + "timeout": { + "title": "DoTimeout", + "oneOf": [ + { + "$ref": "#/$defs/timeout", + "title": "TimeoutDefinition", + "description": "The workflow's timeout configuration, if any." + }, + { + "type": "string", + "title": "TimeoutReference", + "description": "The name of the workflow's timeout, if any." + } + ] + }, + "output": { + "$ref": "#/$defs/output", + "title": "Output", + "description": "Configures the workflow's output." + }, + "schedule": { + "type": "object", + "title": "Schedule", + "description": "Schedules the workflow.", + "unevaluatedProperties": false, + "properties": { + "every": { + "$ref": "#/$defs/duration", + "title": "ScheduleEvery", + "description": "Specifies the duration of the interval at which the workflow should be executed." + }, + "cron": { + "type": "string", + "title": "ScheduleCron", + "description": "Specifies the schedule using a cron expression, e.g., '0 0 * * *' for daily at midnight." + }, + "after": { + "$ref": "#/$defs/duration", + "title": "ScheduleAfter", + "description": "Specifies a delay duration that the workflow must wait before starting again after it completes." + }, + "on": { + "$ref": "#/$defs/eventConsumptionStrategy", + "title": "ScheduleOn", + "description": "Specifies the events that trigger the workflow execution." + } + } + } + }, + "$defs": { + "taskList": { + "title": "TaskList", + "description": "List of named tasks to perform.", + "type": "array", + "items": { + "type": "object", + "title": "TaskItem", + "minProperties": 1, + "maxProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/task" + } + } + }, + "taskBase": { + "type": "object", + "title": "TaskBase", + "description": "An object inherited by all tasks.", + "properties": { + "if": { + "type": "string", + "title": "TaskBaseIf", + "description": "A runtime expression, if any, used to determine whether or not the task should be run." + }, + "input": { + "$ref": "#/$defs/input", + "title": "TaskBaseInput", + "description": "Configure the task's input." + }, + "output": { + "$ref": "#/$defs/output", + "title": "TaskBaseOutput", + "description": "Configure the task's output." + }, + "export": { + "$ref": "#/$defs/export", + "title": "TaskBaseExport", + "description": "Export task output to context." + }, + "timeout": { + "title": "TaskTimeout", + "oneOf": [ + { + "$ref": "#/$defs/timeout", + "title": "TaskTimeoutDefinition", + "description": "The task's timeout configuration, if any." + }, + { + "type": "string", + "title": "TaskTimeoutReference", + "description": "The name of the task's timeout, if any." + } + ] + }, + "then": { + "$ref": "#/$defs/flowDirective", + "title": "TaskBaseThen", + "description": "The flow directive to be performed upon completion of the task." + }, + "metadata": { + "type": "object", + "title": "TaskMetadata", + "description": "Holds additional information about the task.", + "additionalProperties": true + } + } + }, + "task": { + "title": "Task", + "description": "A discrete unit of work that contributes to achieving the overall objectives defined by the workflow.", + "unevaluatedProperties": false, + "oneOf": [ + { + "$ref": "#/$defs/callTask" + }, + { + "$ref": "#/$defs/doTask" + }, + { + "$ref": "#/$defs/forkTask" + }, + { + "$ref": "#/$defs/emitTask" + }, + { + "$ref": "#/$defs/forTask" + }, + { + "$ref": "#/$defs/listenTask" + }, + { + "$ref": "#/$defs/raiseTask" + }, + { + "$ref": "#/$defs/runTask" + }, + { + "$ref": "#/$defs/setTask" + }, + { + "$ref": "#/$defs/switchTask" + }, + { + "$ref": "#/$defs/tryTask" + }, + { + "$ref": "#/$defs/waitTask" + } + ] + }, + "callTask": { + "title": "CallTask", + "description": "Defines the call to perform.", + "oneOf": [ + { + "title": "CallAsyncAPI", + "description": "Defines the AsyncAPI call to perform.", + "type": "object", + "required": [ + "call", + "with" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "call": { + "type": "string", + "const": "asyncapi" + }, + "with": { + "type": "object", + "title": "AsyncApiArguments", + "description": "The Async API call arguments.", + "properties": { + "document": { + "$ref": "#/$defs/externalResource", + "title": "AsyncAPIDocument", + "description": "The document that defines the AsyncAPI operation to call." + }, + "channel": { + "type": "string", + "title": "With", + "description": "The name of the channel on which to perform the operation. Used only in case the referenced document uses AsyncAPI v2.6.0." + }, + "operation": { + "type": "string", + "title": "AsyncAPIOperation", + "description": "A reference to the AsyncAPI operation to call." + }, + "server": { + "$ref": "#/$defs/asyncApiServer", + "title": "AsyncAPIServer", + "description": "An object used to configure to the server to call the specified AsyncAPI operation on." + }, + "protocol": { + "type": "string", + "title": "AsyncApiProtocol", + "description": "The protocol to use to select the target server.", + "enum": [ + "amqp", + "amqp1", + "anypointmq", + "googlepubsub", + "http", + "ibmmq", + "jms", + "kafka", + "mercure", + "mqtt", + "mqtt5", + "nats", + "pulsar", + "redis", + "sns", + "solace", + "sqs", + "stomp", + "ws" + ] + }, + "message": { + "$ref": "#/$defs/asyncApiOutboundMessage", + "title": "AsyncApiMessage", + "description": "An object used to configure the message to publish using the target operation." + }, + "subscription": { + "$ref": "#/$defs/asyncApiSubscription", + "title": "AsyncApiSubscription", + "description": "An object used to configure the subscription to messages consumed using the target operation." + }, + "authentication": { + "$ref": "#/$defs/referenceableAuthenticationPolicy", + "title": "AsyncAPIAuthentication", + "description": "The authentication policy, if any, to use when calling the AsyncAPI operation." + } + }, + "oneOf": [ + { + "required": [ + "document", + "operation", + "message" + ] + }, + { + "required": [ + "document", + "operation", + "subscription" + ] + }, + { + "required": [ + "document", + "channel", + "message" + ] + }, + { + "required": [ + "document", + "channel", + "subscription" + ] + } + ], + "unevaluatedProperties": false + } + } + } + ] + }, + { + "title": "CallGRPC", + "description": "Defines the GRPC call to perform.", + "type": "object", + "unevaluatedProperties": false, + "required": [ + "call", + "with" + ], + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "call": { + "type": "string", + "const": "grpc" + }, + "with": { + "type": "object", + "title": "GRPCArguments", + "description": "The GRPC call arguments.", + "properties": { + "proto": { + "$ref": "#/$defs/externalResource", + "title": "WithGRPCProto", + "description": "The proto resource that describes the GRPC service to call." + }, + "service": { + "type": "object", + "title": "WithGRPCService", + "unevaluatedProperties": false, + "properties": { + "name": { + "type": "string", + "title": "WithGRPCServiceName", + "description": "The name of the GRPC service to call." + }, + "host": { + "type": "string", + "title": "WithGRPCServiceHost", + "description": "The hostname of the GRPC service to call.", + "pattern": "^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$" + }, + "port": { + "type": "integer", + "title": "WithGRPCServicePost", + "description": "The port number of the GRPC service to call.", + "minimum": 0, + "maximum": 65535 + }, + "authentication": { + "$ref": "#/$defs/referenceableAuthenticationPolicy", + "title": "WithGRPCServiceAuthentication", + "description": "The endpoint's authentication policy, if any." + } + }, + "required": [ + "name", + "host" + ] + }, + "method": { + "type": "string", + "title": "WithGRPCMethod", + "description": "The name of the method to call on the defined GRPC service." + }, + "arguments": { + "type": "object", + "title": "WithGRPCArguments", + "description": "The arguments, if any, to call the method with.", + "additionalProperties": true + } + }, + "required": [ + "proto", + "service", + "method" + ], + "unevaluatedProperties": false + } + } + } + ] + }, + { + "title": "CallHTTP", + "description": "Defines the HTTP call to perform.", + "type": "object", + "unevaluatedProperties": false, + "required": [ + "call", + "with" + ], + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "call": { + "type": "string", + "const": "http" + }, + "with": { + "type": "object", + "title": "HTTPArguments", + "description": "The HTTP call arguments.", + "properties": { + "method": { + "type": "string", + "title": "HTTPMethod", + "description": "The HTTP method of the HTTP request to perform." + }, + "endpoint": { + "title": "HTTPEndpoint", + "description": "The HTTP endpoint to send the request to.", + "$ref": "#/$defs/endpoint" + }, + "headers": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "$ref": "#/$defs/runtimeExpression" + } + ], + "title": "HTTPHeaders", + "description": "A name/value mapping of the headers, if any, of the HTTP request to perform." + }, + "body": { + "title": "HTTPBody", + "description": "The body, if any, of the HTTP request to perform." + }, + "query": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "$ref": "#/$defs/runtimeExpression" + } + ], + "title": "HTTPQuery", + "description": "A name/value mapping of the query parameters, if any, of the HTTP request to perform.", + "additionalProperties": true + }, + "output": { + "type": "string", + "title": "HTTPOutput", + "description": "The http call output format. Defaults to 'content'.", + "enum": [ + "raw", + "content", + "response" + ] + }, + "redirect": { + "type": "boolean", + "title": "HttpRedirect", + "description": "Specifies whether redirection status codes (`300–399`) should be treated as errors." + } + }, + "required": [ + "method", + "endpoint" + ], + "unevaluatedProperties": false + } + } + } + ] + }, + { + "title": "CallOpenAPI", + "description": "Defines the OpenAPI call to perform.", + "type": "object", + "unevaluatedProperties": false, + "required": [ + "call", + "with" + ], + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "call": { + "type": "string", + "const": "openapi" + }, + "with": { + "type": "object", + "title": "OpenAPIArguments", + "description": "The OpenAPI call arguments.", + "properties": { + "document": { + "$ref": "#/$defs/externalResource", + "title": "WithOpenAPIDocument", + "description": "The document that defines the OpenAPI operation to call." + }, + "operationId": { + "type": "string", + "title": "WithOpenAPIOperation", + "description": "The id of the OpenAPI operation to call." + }, + "parameters": { + "type": "object", + "title": "WithOpenAPIParameters", + "description": "A name/value mapping of the parameters of the OpenAPI operation to call.", + "additionalProperties": true + }, + "authentication": { + "$ref": "#/$defs/referenceableAuthenticationPolicy", + "title": "WithOpenAPIAuthentication", + "description": "The authentication policy, if any, to use when calling the OpenAPI operation." + }, + "output": { + "type": "string", + "enum": [ + "raw", + "content", + "response" + ], + "title": "WithOpenAPIOutput", + "description": "The http call output format. Defaults to 'content'." + }, + "redirect": { + "type": "boolean", + "title": "HttpRedirect", + "description": "Specifies whether redirection status codes (`300–399`) should be treated as errors." + } + }, + "required": [ + "document", + "operationId" + ], + "unevaluatedProperties": false + } + } + } + ] + }, + { + "title": "CallFunction", + "description": "Defines the function call to perform.", + "type": "object", + "unevaluatedProperties": false, + "required": [ + "call" + ], + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "call": { + "type": "string", + "not": { + "enum": [ + "asyncapi", + "grpc", + "http", + "openapi" + ] + }, + "description": "The name of the function to call." + }, + "with": { + "type": "object", + "title": "FunctionArguments", + "description": "A name/value mapping of the parameters, if any, to call the function with.", + "additionalProperties": true + } + } + } + ] + } + ] + }, + "forkTask": { + "type": "object", + "title": "ForkTask", + "description": "Allows workflows to execute multiple tasks concurrently and optionally race them against each other, with a single possible winner, which sets the task's output.", + "unevaluatedProperties": false, + "required": [ + "fork" + ], + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "fork": { + "type": "object", + "title": "ForkTaskConfiguration", + "description": "The configuration of the branches to perform concurrently.", + "unevaluatedProperties": false, + "required": [ + "branches" + ], + "properties": { + "branches": { + "$ref": "#/$defs/taskList", + "title": "ForkBranches" + }, + "compete": { + "type": "boolean", + "title": "ForkCompete", + "description": "Indicates whether or not the concurrent tasks are racing against each other, with a single possible winner, which sets the composite task's output.", + "default": false + } + } + } + } + } + ] + }, + "doTask": { + "type": "object", + "title": "DoTask", + "description": "Allows to execute a list of tasks in sequence.", + "unevaluatedProperties": false, + "required": [ + "do" + ], + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "do": { + "$ref": "#/$defs/taskList", + "title": "DoTaskConfiguration", + "description": "The configuration of the tasks to perform sequentially." + } + } + } + ] + }, + "emitTask": { + "type": "object", + "title": "EmitTask", + "description": "Allows workflows to publish events to event brokers or messaging systems, facilitating communication and coordination between different components and services.", + "required": [ + "emit" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "emit": { + "type": "object", + "title": "EmitTaskConfiguration", + "description": "The configuration of an event's emission.", + "unevaluatedProperties": false, + "properties": { + "event": { + "type": "object", + "title": "EmitEventDefinition", + "description": "The definition of the event to emit.", + "properties": { + "with": { + "$ref": "#/$defs/eventProperties", + "title": "EmitEventWith", + "description": "Defines the properties of event to emit.", + "required": [ + "source", + "type" + ] + } + }, + "additionalProperties": true + } + }, + "required": [ + "event" + ] + } + } + } + ] + }, + "forTask": { + "type": "object", + "title": "ForTask", + "description": "Allows workflows to iterate over a collection of items, executing a defined set of subtasks for each item in the collection. This task type is instrumental in handling scenarios such as batch processing, data transformation, and repetitive operations across datasets.", + "required": [ + "for", + "do" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "for": { + "type": "object", + "title": "ForTaskConfiguration", + "description": "The definition of the loop that iterates over a range of values.", + "unevaluatedProperties": false, + "properties": { + "each": { + "type": "string", + "title": "ForEach", + "description": "The name of the variable used to store the current item being enumerated.", + "default": "item" + }, + "in": { + "type": "string", + "title": "ForIn", + "description": "A runtime expression used to get the collection to enumerate." + }, + "at": { + "type": "string", + "title": "ForAt", + "description": "The name of the variable used to store the index of the current item being enumerated.", + "default": "index" + } + }, + "required": [ + "in" + ] + }, + "while": { + "type": "string", + "title": "While", + "description": "A runtime expression that represents the condition, if any, that must be met for the iteration to continue." + }, + "do": { + "$ref": "#/$defs/taskList", + "title": "ForTaskDo" + } + } + } + ] + }, + "listenTask": { + "type": "object", + "title": "ListenTask", + "description": "Provides a mechanism for workflows to await and react to external events, enabling event-driven behavior within workflow systems.", + "required": [ + "listen" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "listen": { + "type": "object", + "title": "ListenTaskConfiguration", + "description": "The configuration of the listener to use.", + "unevaluatedProperties": false, + "properties": { + "to": { + "$ref": "#/$defs/eventConsumptionStrategy", + "title": "ListenTo", + "description": "Defines the event(s) to listen to." + }, + "read": { + "type": "string", + "enum": [ + "data", + "envelope", + "raw" + ], + "default": "data", + "title": "ListenAndReadAs", + "description": "Specifies how events are read during the listen operation." + } + }, + "required": [ + "to" + ] + }, + "foreach": { + "$ref": "#/$defs/subscriptionIterator", + "title": "ListenIterator", + "description": "Configures the iterator, if any, for processing consumed event(s)." + } + } + } + ] + }, + "raiseTask": { + "type": "object", + "title": "RaiseTask", + "description": "Intentionally triggers and propagates errors.", + "required": [ + "raise" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "raise": { + "type": "object", + "title": "RaiseTaskConfiguration", + "description": "The definition of the error to raise.", + "unevaluatedProperties": false, + "properties": { + "error": { + "title": "RaiseTaskError", + "oneOf": [ + { + "$ref": "#/$defs/error", + "title": "RaiseErrorDefinition", + "description": "Defines the error to raise." + }, + { + "type": "string", + "title": "RaiseErrorReference", + "description": "The name of the error to raise" + } + ] + } + }, + "required": [ + "error" + ] + } + } + } + ] + }, + "runTask": { + "type": "object", + "title": "RunTask", + "description": "Provides the capability to execute external containers, shell commands, scripts, or workflows.", + "required": [ + "run" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "run": { + "type": "object", + "title": "RunTaskConfiguration", + "description": "The configuration of the process to execute.", + "unevaluatedProperties": false, + "properties": { + "await": { + "type": "boolean", + "default": true, + "title": "AwaitProcessCompletion", + "description": "Whether to await the process completion before continuing." + }, + "return": { + "type": "string", + "title": "ProcessReturnType", + "description": "Configures the output of the process.", + "enum": [ + "stdout", + "stderr", + "code", + "all", + "none" + ], + "default": "stdout" + } + }, + "oneOf": [ + { + "title": "RunContainer", + "description": "Enables the execution of external processes encapsulated within a containerized environment.", + "properties": { + "container": { + "type": "object", + "title": "Container", + "description": "The configuration of the container to run.", + "unevaluatedProperties": false, + "properties": { + "image": { + "type": "string", + "title": "ContainerImage", + "description": "The name of the container image to run." + }, + "name": { + "type": "string", + "title": "ContainerName", + "description": "A runtime expression, if any, used to give specific name to the container." + }, + "command": { + "type": "string", + "title": "ContainerCommand", + "description": "The command, if any, to execute on the container." + }, + "ports": { + "type": "object", + "title": "ContainerPorts", + "description": "The container's port mappings, if any." + }, + "volumes": { + "type": "object", + "title": "ContainerVolumes", + "description": "The container's volume mappings, if any." + }, + "environment": { + "type": "object", + "title": "ContainerEnvironment", + "description": "A key/value mapping of the environment variables, if any, to use when running the configured process." + }, + "lifetime": { + "$ref": "#/$defs/containerLifetime", + "title": "ContainerLifetime", + "description": "An object, if any, used to configure the container's lifetime" + } + }, + "required": [ + "image" + ] + } + }, + "required": [ + "container" + ] + }, + { + "title": "RunScript", + "description": "Enables the execution of custom scripts or code within a workflow, empowering workflows to perform specialized logic, data processing, or integration tasks by executing user-defined scripts written in various programming languages.", + "properties": { + "script": { + "type": "object", + "title": "Script", + "description": "The configuration of the script to run.", + "unevaluatedProperties": false, + "properties": { + "language": { + "type": "string", + "title": "ScriptLanguage", + "description": "The language of the script to run." + }, + "arguments": { + "type": "object", + "title": "ScriptArguments", + "description": "A key/value mapping of the arguments, if any, to use when running the configured script.", + "additionalProperties": true + }, + "environment": { + "type": "object", + "title": "ScriptEnvironment", + "description": "A key/value mapping of the environment variables, if any, to use when running the configured script process.", + "additionalProperties": true + } + }, + "oneOf": [ + { + "title": "InlineScript", + "type": "object", + "description": "The script's code.", + "properties": { + "code": { + "type": "string", + "title": "InlineScriptCode" + } + }, + "required": [ + "code" + ] + }, + { + "title": "ExternalScript", + "type": "object", + "description": "The script's resource.", + "properties": { + "source": { + "$ref": "#/$defs/externalResource", + "title": "ExternalScriptResource" + } + }, + "required": [ + "source" + ] + } + ], + "required": [ + "language" + ] + } + }, + "required": [ + "script" + ] + }, + { + "title": "RunShell", + "description": "Enables the execution of shell commands within a workflow, enabling workflows to interact with the underlying operating system and perform system-level operations, such as file manipulation, environment configuration, or system administration tasks.", + "properties": { + "shell": { + "type": "object", + "title": "Shell", + "description": "The configuration of the shell command to run.", + "unevaluatedProperties": false, + "properties": { + "command": { + "type": "string", + "title": "ShellCommand", + "description": "The shell command to run." + }, + "arguments": { + "type": "object", + "title": "ShellArguments", + "description": "A list of the arguments of the shell command to run.", + "additionalProperties": true + }, + "environment": { + "type": "object", + "title": "ShellEnvironment", + "description": "A key/value mapping of the environment variables, if any, to use when running the configured process.", + "additionalProperties": true + } + }, + "required": [ + "command" + ] + } + }, + "required": [ + "shell" + ] + }, + { + "title": "RunWorkflow", + "description": "Enables the invocation and execution of nested workflows within a parent workflow, facilitating modularization, reusability, and abstraction of complex logic or business processes by encapsulating them into standalone workflow units.", + "properties": { + "workflow": { + "type": "object", + "title": "SubflowConfiguration", + "description": "The configuration of the workflow to run.", + "unevaluatedProperties": false, + "properties": { + "namespace": { + "type": "string", + "title": "SubflowNamespace", + "description": "The namespace the workflow to run belongs to." + }, + "name": { + "type": "string", + "title": "SubflowName", + "description": "The name of the workflow to run." + }, + "version": { + "type": "string", + "default": "latest", + "title": "SubflowVersion", + "description": "The version of the workflow to run. Defaults to latest." + }, + "input": { + "type": "object", + "title": "SubflowInput", + "description": "The data, if any, to pass as input to the workflow to execute. The value should be validated against the target workflow's input schema, if specified.", + "additionalProperties": true + } + }, + "required": [ + "namespace", + "name", + "version" + ] + } + }, + "required": [ + "workflow" + ] + } + ] + } + } + } + ] + }, + "setTask": { + "type": "object", + "title": "SetTask", + "description": "A task used to set data.", + "required": [ + "set" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "set": { + "oneOf": [ + { + "type": "object", + "minProperties": 1, + "additionalProperties": true + }, + { + "type": "string" + } + ], + "title": "SetTaskConfiguration", + "description": "The data to set." + } + } + } + ] + }, + "switchTask": { + "type": "object", + "title": "SwitchTask", + "description": "Enables conditional branching within workflows, allowing them to dynamically select different paths based on specified conditions or criteria.", + "required": [ + "switch" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "switch": { + "type": "array", + "title": "SwitchTaskConfiguration", + "description": "The definition of the switch to use.", + "minItems": 1, + "items": { + "type": "object", + "title": "SwitchItem", + "minProperties": 1, + "maxProperties": 1, + "additionalProperties": { + "type": "object", + "title": "SwitchCase", + "description": "The definition of a case within a switch task, defining a condition and corresponding tasks to execute if the condition is met.", + "unevaluatedProperties": false, + "required": [ + "then" + ], + "properties": { + "when": { + "type": "string", + "title": "SwitchCaseCondition", + "description": "A runtime expression used to determine whether or not the case matches." + }, + "then": { + "$ref": "#/$defs/flowDirective", + "title": "SwitchCaseOutcome", + "description": "The flow directive to execute when the case matches." + } + } + } + } + } + } + } + ] + }, + "tryTask": { + "type": "object", + "title": "TryTask", + "description": "Serves as a mechanism within workflows to handle errors gracefully, potentially retrying failed tasks before proceeding with alternate ones.", + "required": [ + "try", + "catch" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "try": { + "$ref": "#/$defs/taskList", + "title": "TryTaskConfiguration", + "description": "The task(s) to perform." + }, + "catch": { + "type": "object", + "title": "TryTaskCatch", + "description": "The object used to define the errors to catch.", + "unevaluatedProperties": false, + "properties": { + "errors": { + "type": "object", + "title": "CatchErrors", + "properties": { + "with": { + "$ref": "#/$defs/errorFilter" + } + }, + "description": "static error filter" + }, + "as": { + "type": "string", + "title": "CatchAs", + "description": "The name of the runtime expression variable to save the error as. Defaults to 'error'." + }, + "when": { + "type": "string", + "title": "CatchWhen", + "description": "A runtime expression used to determine whether to catch the filtered error." + }, + "exceptWhen": { + "type": "string", + "title": "CatchExceptWhen", + "description": "A runtime expression used to determine whether not to catch the filtered error." + }, + "retry": { + "oneOf": [ + { + "$ref": "#/$defs/retryPolicy", + "title": "RetryPolicyDefinition", + "description": "The retry policy to use, if any, when catching errors." + }, + { + "type": "string", + "title": "RetryPolicyReference", + "description": "The name of the retry policy to use, if any, when catching errors." + } + ] + }, + "do": { + "$ref": "#/$defs/taskList", + "title": "TryTaskCatchDo", + "description": "The definition of the task(s) to run when catching an error." + } + } + } + } + } + ] + }, + "waitTask": { + "type": "object", + "title": "WaitTask", + "description": "Allows workflows to pause or delay their execution for a specified period of time.", + "required": [ + "wait" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/taskBase" + }, + { + "properties": { + "wait": { + "$ref": "#/$defs/duration", + "title": "WaitTaskConfiguration", + "description": "The amount of time to wait." + } + } + } + ] + }, + "flowDirective": { + "title": "FlowDirective", + "description": "Represents different transition options for a workflow.", + "anyOf": [ + { + "title": "FlowDirectiveEnum", + "type": "string", + "enum": [ + "continue", + "exit", + "end" + ], + "default": "continue" + }, + { + "type": "string" + } + ] + }, + "referenceableAuthenticationPolicy": { + "type": "object", + "title": "ReferenceableAuthenticationPolicy", + "description": "Represents a referenceable authentication policy.", + "unevaluatedProperties": false, + "oneOf": [ + { + "title": "AuthenticationPolicyReference", + "description": "The reference of the authentication policy to use.", + "properties": { + "use": { + "type": "string", + "minLength": 1, + "title": "ReferenceableAuthenticationPolicyName", + "description": "The name of the authentication policy to use." + } + }, + "required": [ + "use" + ] + }, + { + "$ref": "#/$defs/authenticationPolicy" + } + ] + }, + "secretBasedAuthenticationPolicy": { + "type": "object", + "title": "SecretBasedAuthenticationPolicy", + "description": "Represents an authentication policy based on secrets.", + "unevaluatedProperties": false, + "properties": { + "use": { + "type": "string", + "minLength": 1, + "title": "SecretBasedAuthenticationPolicyName", + "description": "The name of the authentication policy to use." + } + }, + "required": [ + "use" + ] + }, + "authenticationPolicy": { + "type": "object", + "title": "AuthenticationPolicy", + "description": "Defines an authentication policy.", + "oneOf": [ + { + "title": "BasicAuthenticationPolicy", + "description": "Use basic authentication.", + "properties": { + "basic": { + "type": "object", + "title": "BasicAuthenticationPolicyConfiguration", + "description": "The configuration of the basic authentication policy.", + "unevaluatedProperties": false, + "oneOf": [ + { + "title": "BasicAuthenticationProperties", + "description": "Inline configuration of the basic authentication policy.", + "properties": { + "username": { + "type": "string", + "description": "The username to use." + }, + "password": { + "type": "string", + "description": "The password to use." + } + }, + "required": [ + "username", + "password" + ] + }, + { + "$ref": "#/$defs/secretBasedAuthenticationPolicy", + "title": "BasicAuthenticationPolicySecret", + "description": "Secret based configuration of the basic authentication policy." + } + ] + } + }, + "required": [ + "basic" + ] + }, + { + "title": "BearerAuthenticationPolicy", + "description": "Use bearer authentication.", + "properties": { + "bearer": { + "type": "object", + "title": "BearerAuthenticationPolicyConfiguration", + "description": "The configuration of the bearer authentication policy.", + "unevaluatedProperties": false, + "oneOf": [ + { + "title": "BearerAuthenticationProperties", + "description": "Inline configuration of the bearer authentication policy.", + "properties": { + "token": { + "type": "string", + "description": "The bearer token to use." + } + }, + "required": [ + "token" + ] + }, + { + "$ref": "#/$defs/secretBasedAuthenticationPolicy", + "title": "BearerAuthenticationPolicySecret", + "description": "Secret based configuration of the bearer authentication policy." + } + ] + } + }, + "required": [ + "bearer" + ] + }, + { + "title": "DigestAuthenticationPolicy", + "description": "Use digest authentication.", + "properties": { + "digest": { + "type": "object", + "title": "DigestAuthenticationPolicyConfiguration", + "description": "The configuration of the digest authentication policy.", + "unevaluatedProperties": false, + "oneOf": [ + { + "title": "DigestAuthenticationProperties", + "description": "Inline configuration of the digest authentication policy.", + "properties": { + "username": { + "type": "string", + "description": "The username to use." + }, + "password": { + "type": "string", + "description": "The password to use." + } + }, + "required": [ + "username", + "password" + ] + }, + { + "$ref": "#/$defs/secretBasedAuthenticationPolicy", + "title": "DigestAuthenticationPolicySecret", + "description": "Secret based configuration of the digest authentication policy." + } + ] + } + }, + "required": [ + "digest" + ] + }, + { + "title": "OAuth2AuthenticationPolicy", + "description": "Use OAuth2 authentication.", + "properties": { + "oauth2": { + "type": "object", + "title": "OAuth2AuthenticationPolicyConfiguration", + "description": "The configuration of the OAuth2 authentication policy.", + "unevaluatedProperties": false, + "oneOf": [ + { + "type": "object", + "title": "OAuth2ConnectAuthenticationProperties", + "description": "The inline configuration of the OAuth2 authentication policy.", + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/oauth2AuthenticationProperties" + }, + { + "type": "object", + "properties": { + "endpoints": { + "type": "object", + "title": "OAuth2AuthenticationPropertiesEndpoints", + "description": "The endpoint configurations for OAuth2.", + "properties": { + "token": { + "type": "string", + "format": "uri-template", + "default": "/oauth2/token", + "title": "OAuth2TokenEndpoint", + "description": "The relative path to the token endpoint. Defaults to `/oauth2/token`." + }, + "revocation": { + "type": "string", + "format": "uri-template", + "default": "/oauth2/revoke", + "title": "OAuth2RevocationEndpoint", + "description": "The relative path to the revocation endpoint. Defaults to `/oauth2/revoke`." + }, + "introspection": { + "type": "string", + "format": "uri-template", + "default": "/oauth2/introspect", + "title": "OAuth2IntrospectionEndpoint", + "description": "The relative path to the introspection endpoint. Defaults to `/oauth2/introspect`." + } + } + } + } + } + ] + }, + { + "$ref": "#/$defs/secretBasedAuthenticationPolicy", + "title": "OAuth2AuthenticationPolicySecret", + "description": "Secret based configuration of the OAuth2 authentication policy." + } + ] + } + }, + "required": [ + "oauth2" + ] + }, + { + "title": "OpenIdConnectAuthenticationPolicy", + "description": "Use OpenIdConnect authentication.", + "properties": { + "oidc": { + "type": "object", + "title": "OpenIdConnectAuthenticationPolicyConfiguration", + "description": "The configuration of the OpenIdConnect authentication policy.", + "unevaluatedProperties": false, + "oneOf": [ + { + "$ref": "#/$defs/oauth2AuthenticationProperties", + "title": "OpenIdConnectAuthenticationProperties", + "description": "The inline configuration of the OpenIdConnect authentication policy.", + "unevaluatedProperties": false + }, + { + "$ref": "#/$defs/secretBasedAuthenticationPolicy", + "title": "OpenIdConnectAuthenticationPolicySecret", + "description": "Secret based configuration of the OpenIdConnect authentication policy." + } + ] + } + }, + "required": [ + "oidc" + ] + } + ] + }, + "oauth2AuthenticationProperties": { + "type": "object", + "title": "OAuth2AutenthicationData", + "description": "Inline configuration of the OAuth2 authentication policy.", + "properties": { + "authority": { + "$ref": "#/$defs/uriTemplate", + "title": "OAuth2AutenthicationDataAuthority", + "description": "The URI that references the OAuth2 authority to use." + }, + "grant": { + "type": "string", + "enum": [ + "authorization_code", + "client_credentials", + "password", + "refresh_token", + "urn:ietf:params:oauth:grant-type:token-exchange" + ], + "title": "OAuth2AutenthicationDataGrant", + "description": "The grant type to use." + }, + "client": { + "type": "object", + "title": "OAuth2AutenthicationDataClient", + "description": "The definition of an OAuth2 client.", + "unevaluatedProperties": false, + "properties": { + "id": { + "type": "string", + "title": "ClientId", + "description": "The client id to use." + }, + "secret": { + "type": "string", + "title": "ClientSecret", + "description": "The client secret to use, if any." + }, + "assertion": { + "type": "string", + "title": "ClientAssertion", + "description": "A JWT containing a signed assertion with your application credentials." + }, + "authentication": { + "type": "string", + "enum": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none" + ], + "default": "client_secret_post", + "title": "ClientAuthentication", + "description": "The authentication method to use to authenticate the client." + } + } + }, + "request": { + "type": "object", + "title": "OAuth2TokenRequest", + "description": "The configuration of an OAuth2 token request", + "properties": { + "encoding": { + "type": "string", + "enum": [ + "application/x-www-form-urlencoded", + "application/json" + ], + "default": "application/x-www-form-urlencoded", + "title": "Oauth2TokenRequestEncoding" + } + } + }, + "issuers": { + "type": "array", + "title": "OAuth2Issuers", + "description": "A list that contains that contains valid issuers that will be used to check against the issuer of generated tokens.", + "items": { + "type": "string" + } + }, + "scopes": { + "type": "array", + "title": "OAuth2AutenthicationDataScopes", + "description": "The scopes, if any, to request the token for.", + "items": { + "type": "string" + } + }, + "audiences": { + "type": "array", + "title": "OAuth2AutenthicationDataAudiences", + "description": "The audiences, if any, to request the token for.", + "items": { + "type": "string" + } + }, + "username": { + "type": "string", + "title": "OAuth2AutenthicationDataUsername", + "description": "The username to use. Used only if the grant type is Password." + }, + "password": { + "type": "string", + "title": "OAuth2AutenthicationDataPassword", + "description": "The password to use. Used only if the grant type is Password." + }, + "subject": { + "$ref": "#/$defs/oauth2Token", + "title": "OAuth2AutenthicationDataSubject", + "description": "The security token that represents the identity of the party on behalf of whom the request is being made." + }, + "actor": { + "$ref": "#/$defs/oauth2Token", + "title": "OAuth2AutenthicationDataActor", + "description": "The security token that represents the identity of the acting party." + } + } + }, + "oauth2Token": { + "type": "object", + "title": "OAuth2TokenDefinition", + "description": "Represents an OAuth2 token.", + "unevaluatedProperties": false, + "properties": { + "token": { + "type": "string", + "title": "OAuth2Token", + "description": "The security token to use." + }, + "type": { + "type": "string", + "title": "OAuth2TokenType", + "description": "The type of the security token to use." + } + }, + "required": [ + "token", + "type" + ] + }, + "duration": { + "oneOf": [ + { + "type": "object", + "minProperties": 1, + "unevaluatedProperties": false, + "properties": { + "days": { + "type": "integer", + "title": "DurationDays", + "description": "Number of days, if any." + }, + "hours": { + "type": "integer", + "title": "DurationHours", + "description": "Number of days, if any." + }, + "minutes": { + "type": "integer", + "title": "DurationMinutes", + "description": "Number of minutes, if any." + }, + "seconds": { + "type": "integer", + "title": "DurationSeconds", + "description": "Number of seconds, if any." + }, + "milliseconds": { + "type": "integer", + "title": "DurationMilliseconds", + "description": "Number of milliseconds, if any." + } + }, + "title": "DurationInline", + "description": "The inline definition of a duration." + }, + { + "type": "string", + "pattern": "^P(?!$)(\\d+(?:\\.\\d+)?Y)?(\\d+(?:\\.\\d+)?M)?(\\d+(?:\\.\\d+)?W)?(\\d+(?:\\.\\d+)?D)?(T(?=\\d)(\\d+(?:\\.\\d+)?H)?(\\d+(?:\\.\\d+)?M)?(\\d+(?:\\.\\d+)?S)?)?$", + "title": "DurationExpression", + "description": "The ISO 8601 expression of a duration." + } + ] + }, + "error": { + "type": "object", + "title": "Error", + "description": "Represents an error.", + "unevaluatedProperties": false, + "properties": { + "type": { + "title": "ErrorType", + "description": "A URI reference that identifies the error type.", + "oneOf": [ + { + "title": "LiteralErrorType", + "$ref": "#/$defs/uriTemplate", + "description": "The literal error type." + }, + { + "title": "ExpressionErrorType", + "$ref": "#/$defs/runtimeExpression", + "description": "An expression based error type." + } + ] + }, + "status": { + "type": "integer", + "title": "ErrorStatus", + "description": "The status code generated by the origin for this occurrence of the error." + }, + "instance": { + "title": "ErrorInstance", + "description": "A JSON Pointer used to reference the component the error originates from.", + "oneOf": [ + { + "title": "LiteralErrorInstance", + "description": "The literal error instance.", + "type": "string", + "format": "json-pointer" + }, + { + "$ref": "#/$defs/runtimeExpression", + "title": "ExpressionErrorInstance", + "description": "An expression based error instance." + } + ] + }, + "title": { + "description": "A short, human-readable summary of the error.", + "title": "ErrorTitle", + "anyOf": [ + { + "$ref": "#/$defs/runtimeExpression", + "title": "ExpressionErrorTitle" + }, + { + "type": "string", + "title": "LiteralErrorTitle" + } + ] + }, + "detail": { + "title": "ErrorDetails", + "description": "A human-readable explanation specific to this occurrence of the error.", + "anyOf": [ + { + "$ref": "#/$defs/runtimeExpression", + "title": "ExpressionErrorDetails" + }, + { + "type": "string", + "title": "LiteralErrorDetails" + } + ] + } + }, + "required": [ + "type", + "status" + ] + }, + "errorFilter": { + "type": "object", + "title": "ErrorFilter", + "description": "Error filtering base on static values. For error filtering on dynamic values, use catch.when property", + "minProperties": 1, + "properties": { + "type": { + "type": "string", + "description": "if present, means this value should be used for filtering" + }, + "status": { + "type": "integer", + "description": "if present, means this value should be used for filtering" + }, + "instance": { + "type": "string", + "description": "if present, means this value should be used for filtering" + }, + "title": { + "type": "string", + "description": "if present, means this value should be used for filtering" + }, + "details": { + "type": "string", + "description": "if present, means this value should be used for filtering" + } + } + }, + "uriTemplate": { + "title": "UriTemplate", + "anyOf": [ + { + "title": "LiteralUriTemplate", + "type": "string", + "format": "uri-template", + "pattern": "^[A-Za-z][A-Za-z0-9+\\-.]*://.*" + }, + { + "title": "LiteralUri", + "type": "string", + "format": "uri", + "pattern": "^[A-Za-z][A-Za-z0-9+\\-.]*://.*" + } + ] + }, + "endpoint": { + "title": "Endpoint", + "description": "Represents an endpoint.", + "oneOf": [ + { + "$ref": "#/$defs/runtimeExpression" + }, + { + "$ref": "#/$defs/uriTemplate" + }, + { + "title": "EndpointConfiguration", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "uri": { + "title": "EndpointUri", + "description": "The endpoint's URI.", + "oneOf": [ + { + "title": "LiteralEndpointURI", + "description": "The literal endpoint's URI.", + "$ref": "#/$defs/uriTemplate" + }, + { + "title": "ExpressionEndpointURI", + "$ref": "#/$defs/runtimeExpression", + "description": "An expression based endpoint's URI." + } + ] + }, + "authentication": { + "$ref": "#/$defs/referenceableAuthenticationPolicy", + "title": "EndpointAuthentication", + "description": "The authentication policy to use." + } + }, + "required": [ + "uri" + ] + } + ] + }, + "eventProperties": { + "type": "object", + "title": "EventProperties", + "description": "Describes the properties of an event.", + "properties": { + "id": { + "type": "string", + "title": "EventId", + "description": "The event's unique identifier." + }, + "source": { + "title": "EventSource", + "description": "Identifies the context in which an event happened.", + "oneOf": [ + { + "$ref": "#/$defs/uriTemplate" + }, + { + "$ref": "#/$defs/runtimeExpression" + } + ] + }, + "type": { + "type": "string", + "title": "EventType", + "description": "This attribute contains a value describing the type of event related to the originating occurrence." + }, + "time": { + "title": "EventTime", + "description": "When the event occured.", + "oneOf": [ + { + "title": "LiteralTime", + "type": "string", + "format": "date-time" + }, + { + "$ref": "#/$defs/runtimeExpression" + } + ] + }, + "subject": { + "type": "string", + "title": "EventSubject", + "description": "The subject of the event." + }, + "datacontenttype": { + "type": "string", + "title": "EventDataContentType", + "description": "Content type of data value. This attribute enables data to carry any type of content, whereby format and encoding might differ from that of the chosen event format." + }, + "dataschema": { + "title": "EventDataschema", + "description": "The schema describing the event format.", + "oneOf": [ + { + "title": "LiteralDataSchema", + "$ref": "#/$defs/uriTemplate", + "description": "The literal event data schema." + }, + { + "title": "ExpressionDataSchema", + "$ref": "#/$defs/runtimeExpression", + "description": "An expression based event data schema." + } + ] + }, + "data": { + "title": "EventData", + "description": "The event's payload data", + "anyOf": [ + { + "$ref": "#/$defs/runtimeExpression" + }, + {} + ] + } + }, + "additionalProperties": true + }, + "eventConsumptionStrategy": { + "type": "object", + "title": "EventConsumptionStrategy", + "description": "Describe the event consumption strategy to adopt.", + "unevaluatedProperties": false, + "oneOf": [ + { + "title": "AllEventConsumptionStrategy", + "properties": { + "all": { + "type": "array", + "title": "AllEventConsumptionStrategyConfiguration", + "description": "A list containing all the events that must be consumed.", + "items": { + "$ref": "#/$defs/eventFilter" + } + } + }, + "required": [ + "all" + ] + }, + { + "title": "AnyEventConsumptionStrategy", + "properties": { + "any": { + "type": "array", + "title": "AnyEventConsumptionStrategyConfiguration", + "description": "A list containing any of the events to consume.", + "items": { + "$ref": "#/$defs/eventFilter" + } + }, + "until": { + "oneOf": [ + { + "type": "string", + "title": "AnyEventUntilCondition", + "description": "A runtime expression condition evaluated after consuming an event and which determines whether or not to continue listening." + }, + { + "allOf": [ + { + "$ref": "#/$defs/eventConsumptionStrategy", + "description": "The strategy that defines the event(s) to consume to stop listening." + }, + { + "properties": { + "until": false + } + } + ], + "title": "AnyEventUntilConsumed" + } + ] + } + }, + "required": [ + "any" + ] + }, + { + "title": "OneEventConsumptionStrategy", + "properties": { + "one": { + "$ref": "#/$defs/eventFilter", + "title": "OneEventConsumptionStrategyConfiguration", + "description": "The single event to consume." + } + }, + "required": [ + "one" + ] + } + ] + }, + "eventFilter": { + "type": "object", + "title": "EventFilter", + "description": "An event filter is a mechanism used to selectively process or handle events based on predefined criteria, such as event type, source, or specific attributes.", + "unevaluatedProperties": false, + "properties": { + "with": { + "$ref": "#/$defs/eventProperties", + "minProperties": 1, + "title": "WithEvent", + "description": "An event filter is a mechanism used to selectively process or handle events based on predefined criteria, such as event type, source, or specific attributes." + }, + "correlate": { + "type": "object", + "title": "EventFilterCorrelate", + "description": "A correlation is a link between events and data, established by mapping event attributes to specific data attributes, allowing for coordinated processing or handling based on event characteristics.", + "additionalProperties": { + "type": "object", + "properties": { + "from": { + "type": "string", + "title": "CorrelateFrom", + "description": "A runtime expression used to extract the correlation value from the filtered event." + }, + "expect": { + "type": "string", + "title": "CorrelateExpect", + "description": "A constant or a runtime expression, if any, used to determine whether or not the extracted correlation value matches expectations. If not set, the first extracted value will be used as the correlation's expectation." + } + }, + "required": [ + "from" + ] + } + } + }, + "required": [ + "with" + ] + }, + "extension": { + "type": "object", + "title": "Extension", + "description": "The definition of an extension.", + "unevaluatedProperties": false, + "properties": { + "extend": { + "type": "string", + "enum": [ + "call", + "composite", + "emit", + "for", + "listen", + "raise", + "run", + "set", + "switch", + "try", + "wait", + "all" + ], + "title": "ExtensionTarget", + "description": "The type of task to extend." + }, + "when": { + "type": "string", + "title": "ExtensionCondition", + "description": "A runtime expression, if any, used to determine whether or not the extension should apply in the specified context." + }, + "before": { + "$ref": "#/$defs/taskList", + "title": "ExtensionDoBefore", + "description": "The task(s) to execute before the extended task, if any." + }, + "after": { + "$ref": "#/$defs/taskList", + "title": "ExtensionDoAfter", + "description": "The task(s) to execute after the extended task, if any." + } + }, + "required": [ + "extend" + ] + }, + "externalResource": { + "type": "object", + "title": "ExternalResource", + "description": "Represents an external resource.", + "unevaluatedProperties": false, + "properties": { + "name": { + "type": "string", + "title": "ExternalResourceName", + "description": "The name of the external resource, if any." + }, + "endpoint": { + "$ref": "#/$defs/endpoint", + "title": "ExternalResourceEndpoint", + "description": "The endpoint of the external resource." + } + }, + "required": [ + "endpoint" + ] + }, + "input": { + "type": "object", + "title": "Input", + "description": "Configures the input of a workflow or task.", + "unevaluatedProperties": false, + "properties": { + "schema": { + "$ref": "#/$defs/schema", + "title": "InputSchema", + "description": "The schema used to describe and validate the input of the workflow or task." + }, + "from": { + "title": "InputFrom", + "description": "A runtime expression, if any, used to mutate and/or filter the input of the workflow or task.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + } + ] + } + } + }, + "output": { + "type": "object", + "title": "Output", + "description": "Configures the output of a workflow or task.", + "unevaluatedProperties": false, + "properties": { + "schema": { + "$ref": "#/$defs/schema", + "title": "OutputSchema", + "description": "The schema used to describe and validate the output of the workflow or task." + }, + "as": { + "title": "OutputAs", + "description": "A runtime expression, if any, used to mutate and/or filter the output of the workflow or task.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + } + ] + } + } + }, + "export": { + "type": "object", + "title": "Export", + "description": "Set the content of the context. .", + "unevaluatedProperties": false, + "properties": { + "schema": { + "$ref": "#/$defs/schema", + "title": "ExportSchema", + "description": "The schema used to describe and validate the workflow context." + }, + "as": { + "title": "ExportAs", + "description": "A runtime expression, if any, used to export the output data to the context.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + } + ] + } + } + }, + "retryPolicy": { + "type": "object", + "title": "RetryPolicy", + "description": "Defines a retry policy.", + "unevaluatedProperties": false, + "properties": { + "when": { + "type": "string", + "title": "RetryWhen", + "description": "A runtime expression, if any, used to determine whether or not to retry running the task, in a given context." + }, + "exceptWhen": { + "type": "string", + "title": "RetryExcepWhen", + "description": "A runtime expression used to determine whether or not to retry running the task, in a given context." + }, + "delay": { + "$ref": "#/$defs/duration", + "title": "RetryDelay", + "description": "The duration to wait between retry attempts." + }, + "backoff": { + "type": "object", + "title": "RetryBackoff", + "description": "The retry duration backoff.", + "unevaluatedProperties": false, + "oneOf": [ + { + "title": "ConstantBackoff", + "properties": { + "constant": { + "type": "object", + "description": "The definition of the constant backoff to use, if any." + } + }, + "required": [ + "constant" + ] + }, + { + "title": "ExponentialBackOff", + "properties": { + "exponential": { + "type": "object", + "description": "The definition of the exponential backoff to use, if any." + } + }, + "required": [ + "exponential" + ] + }, + { + "title": "LinearBackoff", + "properties": { + "linear": { + "type": "object", + "description": "The definition of the linear backoff to use, if any." + } + }, + "required": [ + "linear" + ] + } + ] + }, + "limit": { + "type": "object", + "title": "RetryLimit", + "unevaluatedProperties": false, + "properties": { + "attempt": { + "type": "object", + "title": "RetryLimitAttempt", + "unevaluatedProperties": false, + "properties": { + "count": { + "type": "integer", + "title": "RetryLimitAttemptCount", + "description": "The maximum amount of retry attempts, if any." + }, + "duration": { + "$ref": "#/$defs/duration", + "title": "RetryLimitAttemptDuration", + "description": "The maximum duration for each retry attempt." + } + } + }, + "duration": { + "$ref": "#/$defs/duration", + "title": "RetryLimitDuration", + "description": "The duration limit, if any, for all retry attempts." + } + }, + "description": "The retry limit, if any." + }, + "jitter": { + "type": "object", + "title": "RetryPolicyJitter", + "description": "The parameters, if any, that control the randomness or variability of the delay between retry attempts.", + "unevaluatedProperties": false, + "properties": { + "from": { + "$ref": "#/$defs/duration", + "title": "RetryPolicyJitterFrom", + "description": "The minimum duration of the jitter range." + }, + "to": { + "$ref": "#/$defs/duration", + "title": "RetryPolicyJitterTo", + "description": "The maximum duration of the jitter range." + } + }, + "required": [ + "from", + "to" + ] + } + } + }, + "schema": { + "type": "object", + "title": "Schema", + "description": "Represents the definition of a schema.", + "unevaluatedProperties": false, + "properties": { + "format": { + "type": "string", + "default": "json", + "title": "SchemaFormat", + "description": "The schema's format. Defaults to 'json'. The (optional) version of the format can be set using `{format}:{version}`." + } + }, + "oneOf": [ + { + "title": "SchemaInline", + "properties": { + "document": { + "description": "The schema's inline definition." + } + }, + "required": [ + "document" + ] + }, + { + "title": "SchemaExternal", + "properties": { + "resource": { + "$ref": "#/$defs/externalResource", + "title": "SchemaExternalResource", + "description": "The schema's external resource." + } + }, + "required": [ + "resource" + ] + } + ] + }, + "timeout": { + "type": "object", + "title": "Timeout", + "description": "The definition of a timeout.", + "unevaluatedProperties": false, + "properties": { + "after": { + "$ref": "#/$defs/duration", + "title": "TimeoutAfter", + "description": "The duration after which to timeout." + } + }, + "required": [ + "after" + ] + }, + "catalog": { + "type": "object", + "title": "Catalog", + "description": "The definition of a resource catalog.", + "unevaluatedProperties": false, + "properties": { + "endpoint": { + "$ref": "#/$defs/endpoint", + "title": "CatalogEndpoint", + "description": "The root URL where the catalog is hosted." + } + }, + "required": [ + "endpoint" + ] + }, + "runtimeExpression": { + "type": "string", + "title": "RuntimeExpression", + "description": "A runtime expression.", + "pattern": "^\\s*\\$\\{.+\\}\\s*$" + }, + "containerLifetime": { + "type": "object", + "title": "ContainerLifetime", + "description": "The configuration of a container's lifetime", + "unevaluatedProperties": false, + "properties": { + "cleanup": { + "type": "string", + "title": "ContainerCleanupPolicy", + "description": "The container cleanup policy to use", + "enum": [ + "always", + "never", + "eventually" + ], + "default": "never" + }, + "after": { + "$ref": "#/$defs/duration", + "title": "ContainerLifetimeDuration", + "description": "The duration after which to cleanup the container, in case the cleanup policy has been set to 'eventually'" + } + }, + "required": [ + "cleanup" + ], + "if": { + "properties": { + "cleanup": { + "const": "eventually" + } + } + }, + "then": { + "required": [ + "after" + ] + }, + "else": { + "not": { + "required": [ + "after" + ] + } + } + }, + "processResult": { + "type": "object", + "title": "ProcessResult", + "description": "The object returned by a run task when its return type has been set 'all'.", + "unevaluatedProperties": false, + "properties": { + "code": { + "type": "integer", + "title": "ProcessExitCode", + "description": "The process's exit code." + }, + "stdout": { + "type": "string", + "title": "ProcessStandardOutput", + "description": "The content of the process's STDOUT." + }, + "stderr": { + "type": "string", + "title": "ProcessStandardError", + "description": "The content of the process's STDERR." + } + }, + "required": [ + "code", + "stdout", + "stderr" + ] + }, + "asyncApiServer": { + "type": "object", + "title": "AsyncApiServer", + "description": "Configures the target server of an AsyncAPI operation.", + "unevaluatedProperties": false, + "properties": { + "name": { + "type": "string", + "title": "AsyncApiServerName", + "description": "The target server's name." + }, + "variables": { + "type": "object", + "title": "AsyncApiServerVariables", + "description": "The target server's variables, if any." + } + }, + "required": [ + "name" + ] + }, + "asyncApiOutboundMessage": { + "type": "object", + "title": "AsyncApiOutboundMessage", + "description": "An object used to configure the message to publish using the target operation.", + "unevaluatedProperties": false, + "properties": { + "payload": { + "type": "object", + "title": "AsyncApiMessagePayload", + "description": "The message's payload, if any.", + "additionalProperties": true + }, + "headers": { + "type": "object", + "title": "AsyncApiMessageHeaders", + "description": "The message's headers, if any.", + "additionalProperties": true + } + } + }, + "asyncApiInboundMessage": { + "type": "object", + "title": "AsyncApiInboundMessage", + "description": "Represents a message counsumed by an AsyncAPI subscription.", + "allOf": [ + { + "$ref": "#/$defs/asyncApiOutboundMessage" + } + ], + "properties": { + "correlationId": { + "type": "string", + "title": "AsyncApiMessageCorrelationId", + "description": "The message's correlation id, if any." + } + } + }, + "asyncApiSubscription": { + "type": "object", + "title": "AsyncApiSubscription", + "description": "An object used to configure the subscription to messages consumed using the target operation.", + "unevaluatedProperties": false, + "properties": { + "filter": { + "$ref": "#/$defs/runtimeExpression", + "title": "AsyncApiSubscriptionCorrelation", + "description": "A runtime expression, if any, used to filter consumed messages." + }, + "consume": { + "$ref": "#/$defs/asyncApiMessageConsumptionPolicy", + "title": "AsyncApiMessageConsumptionPolicy", + "description": "An object used to configure the subscription's message consumption policy." + }, + "foreach": { + "$ref": "#/$defs/subscriptionIterator", + "title": "AsyncApiSubscriptionIterator", + "description": "Configures the iterator, if any, for processing consumed messages(s)." + } + }, + "required": [ + "consume" + ] + }, + "asyncApiMessageConsumptionPolicy": { + "type": "object", + "title": "AsyncApiMessageConsumptionPolicy", + "description": "An object used to configure a subscription's message consumption policy.", + "unevaluatedProperties": false, + "properties": { + "for": { + "$ref": "#/$defs/duration", + "title": "AsyncApiMessageConsumptionPolicyFor", + "description": "Specifies the time period over which messages will be consumed." + } + }, + "oneOf": [ + { + "properties": { + "amount": { + "type": "integer", + "description": "The amount of (filtered) messages to consume before disposing of the subscription." + } + }, + "title": "AsyncApiMessageConsumptionPolicyAmount", + "required": [ + "amount" + ] + }, + { + "properties": { + "while": { + "$ref": "#/$defs/runtimeExpression", + "description": "A runtime expression evaluated after each consumed (filtered) message to decide if message consumption should continue." + } + }, + "title": "AsyncApiMessageConsumptionPolicyWhile", + "required": [ + "while" + ] + }, + { + "properties": { + "until": { + "$ref": "#/$defs/runtimeExpression", + "description": "A runtime expression evaluated before each consumed (filtered) message to decide if message consumption should continue." + } + }, + "title": "AsyncApiMessageConsumptionPolicyUntil", + "required": [ + "until" + ] + } + ] + }, + "subscriptionIterator": { + "type": "object", + "title": "SubscriptionIterator", + "description": "Configures the iteration over each item (event or message) consumed by a subscription.", + "unevaluatedProperties": false, + "properties": { + "item": { + "type": "string", + "title": "SubscriptionIteratorItem", + "description": "The name of the variable used to store the current item being enumerated.", + "default": "item" + }, + "at": { + "type": "string", + "title": "SubscriptionIteratorIndex", + "description": "The name of the variable used to store the index of the current item being enumerated.", + "default": "index" + }, + "do": { + "$ref": "#/$defs/taskList", + "title": "SubscriptionIteratorTasks", + "description": "The tasks to perform for each consumed item." + }, + "output": { + "$ref": "#/$defs/output", + "title": "SubscriptionIteratorOutput", + "description": "An object, if any, used to customize the item's output and to document its schema." + }, + "export": { + "$ref": "#/$defs/export", + "title": "SubscriptionIteratorExport", + "description": "An object, if any, used to customize the content of the workflow context." + } + } + } + } +} \ No newline at end of file diff --git a/src/Authentication/AuthenticationPolicy.php b/src/Authentication/AuthenticationPolicy.php new file mode 100644 index 0000000..e7b4104 --- /dev/null +++ b/src/Authentication/AuthenticationPolicy.php @@ -0,0 +1,44 @@ +|null + */ + public array|null $issuers = null; - public string|null $username = null; - - public string|null $password = null; + /** + * @var list|null + */ + public array|null $scopes = null; - /** @var string[] */ + /** + * @var list|null + */ public array|null $audiences = null; - public string|null $subjectToken = null; + public string|null $username = null; - public string|null $requestedSubject = null; + public string|null $password = null; - public string|null $requestedIssuer = null; + public OAuth2Token|null $subject = null; - public Metadata|null $metadata = null; + public OAuth2Token|null $actor = null; } diff --git a/src/Authentication/OAuth2Token.php b/src/Authentication/OAuth2Token.php new file mode 100644 index 0000000..8ad3f2d --- /dev/null +++ b/src/Authentication/OAuth2Token.php @@ -0,0 +1,33 @@ + */ - public function __construct(array $data) - { - if ($data === []) { - return; - } - $this->initializeProperties($data); - } + private array $additionalProperties = []; /** * @param mixed[] $data */ - private function initializeProperties(array $data): void + public function __construct(array $data = []) { - $reflectionClass = new ReflectionClass($this); - foreach ($data as $key => $value) { - if ($reflectionClass->hasProperty($key)) { - $property = $reflectionClass->getProperty($key); - $this->assignValue($property, $value); - } - } + $this->additionalProperties = Hydrator::hydrate($this, $data); } - private function assignValue(ReflectionProperty $reflectionProperty, mixed $value): void + public static function fromArray(array $data): static { - $type = $reflectionProperty->getType(); - - if (! empty($this->getTypeFromProperty($reflectionProperty))) { - $this->handleDocCommentType($reflectionProperty, $value); - } elseif ($type instanceof ReflectionUnionType) { - $this->handleUnionType($reflectionProperty, $value, $type->getTypes()); - } elseif ($type instanceof ReflectionNamedType) { - $this->handleNamedType($reflectionProperty, $value, $type); - } + $class = static::resolveClass($data); + + return new $class($data); } /** - * @param mixed[] $types + * @return array */ - private function handleUnionType(ReflectionProperty $reflectionProperty, mixed $value, array $types): void + public function getAdditionalProperties(): array { - foreach ($types as $type) { - if ($this->canInstantiateType($type, $value)) { - $typeName = $type->getName(); - $this->instantiateAndSetValue($reflectionProperty, $value, $typeName); - return; - } - } - } - - private function handleNamedType( - ReflectionProperty $reflectionProperty, - mixed $value, - ReflectionNamedType $reflectionNamedType - ): void { - $typeName = $reflectionNamedType->getName(); - $this->instantiateAndSetValue($reflectionProperty, $value, $typeName); + return $this->additionalProperties; } - private function handleDocCommentType(ReflectionProperty $reflectionProperty, mixed $value): void + public function setAdditionalProperty(string $name, mixed $value): static { - $typeHints = $this->getTypeFromProperty($reflectionProperty); - if (! empty($typeHints)) { - foreach ($typeHints as $typeHint) { - if ($this->canInstantiateTypeFromDoc($typeHint, $value)) { - $this->instantiateAndSetValue($reflectionProperty, $value, $typeHint); - return; - } - } - } + $this->additionalProperties[$name] = $value; - $reflectionProperty->setValue($this, $value); + return $this; } /** - * @return string[] + * Returns the non-null property values, in serialization order, followed by the additional properties. + * + * @return array */ - private function getTypeFromProperty(ReflectionProperty $reflectionProperty): array + public function properties(): array { - $docComment = $reflectionProperty->getDocComment(); - if ($docComment === false) { - return []; - } - - $docBlockFactory = DocBlockFactory::createInstance(); - $docblock = $docBlockFactory->create( - $docComment, - (new ContextFactory())->createFromReflector($reflectionProperty) - ); + $properties = []; + foreach ((new ReflectionClass($this))->getProperties(ReflectionProperty::IS_PUBLIC) as $reflectionProperty) { + if ($reflectionProperty->isStatic()) { + continue; + } - $varTags = $docblock->getTagsByName('var'); - if ($varTags !== []) { - return array_map('trim', explode('|', $varTags[0]->__toString())); + $value = $reflectionProperty->getValue($this); + if ($value !== null) { + $properties[$reflectionProperty->getName()] = $value; + } } - return []; + return $properties + $this->additionalProperties; } - private function canInstantiateTypeFromDoc(string $typeName, mixed $value): bool + public function toArray(): array { - if (str_contains($typeName, '[]')) { - return is_array($value); - } - return class_exists($typeName) || is_scalar($value); + return Serializer::toArray($this); } - private function canInstantiateType(ReflectionNamedType $reflectionNamedType, mixed $value): bool + /** + * Returns the concrete class to instantiate for the given data. Polymorphic models override this. + * + * @param mixed[] $data + * @return class-string + */ + protected static function resolveClass(array $data): string { - $typeName = $reflectionNamedType->getName(); - - if ($typeName === 'bool') { - return is_bool($value); - } - - if ($typeName === 'string') { - return is_string($value); - } - - if ($typeName === 'array') { - return is_array($value); - } - - if (! class_exists($typeName)) { - return false; - } - - if (is_a($value, $typeName)) { - return true; - } - - if (! is_array($value)) { - return false; - } - - $reflectionClass = new ReflectionClass($typeName); - $properties = array_map( - static fn (ReflectionProperty $property): string => $property->getName(), - $reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC) - ); - - foreach (array_keys($value) as $key) { - if (! in_array($key, $properties, true)) { - return false; - } - } - - return true; - } - - private function instantiateAndSetValue( - ReflectionProperty $reflectionProperty, - mixed $value, - string $typeName - ): void { - if (str_contains($typeName, '[]')) { - $singleTypeName = str_replace('[]', '', $typeName); - $objects = []; - foreach ($value as $item) { - if ($singleTypeName === '\\' . \Serverless\Workflow\State::class && is_array($item)) { - foreach (glob(__DIR__ . '/*.php') as $filename) { - $class = '\\Serverless\\Workflow\\' . basename($filename, '.php'); - if (class_exists($class) && is_subclass_of($class, '\\' . \Serverless\Workflow\State::class)) { - $reflection = new ReflectionClass($class); - if ($reflection->hasProperty('type')) { - $instance = $reflection->newInstance([]); - $typeValue = $reflection->getProperty('type') - ->getValue($instance); - if ($typeValue === $item['type']) { - $singleTypeName = $class; - break; - } - } - } - } - } - - if (is_a($item, $singleTypeName)) { - $objects[] = $item; - } else { - $objects[] = new $singleTypeName($item); - } - } - $reflectionProperty->setValue($this, $objects); - } elseif (class_exists($typeName)) { - if (is_a($value, $typeName)) { - $reflectionProperty->setValue($this, $value); - } else { - $reflectionProperty->setValue($this, new $typeName($value)); - } - } else { - $reflectionProperty->setValue($this, $value); - } + return static::class; } } diff --git a/src/CallbackState.php b/src/CallbackState.php deleted file mode 100644 index 43290af..0000000 --- a/src/CallbackState.php +++ /dev/null @@ -1,60 +0,0 @@ -usedForCompensation ??= false; - $this->type ??= 'callback'; - } -} diff --git a/src/BearerPropsDef.php b/src/Catalog.php similarity index 83% rename from src/BearerPropsDef.php rename to src/Catalog.php index 86331d0..49ef88f 100644 --- a/src/BearerPropsDef.php +++ b/src/Catalog.php @@ -20,9 +20,10 @@ namespace Serverless\Workflow; -final class BearerPropsDef extends BaseObject +/** + * The definition of a resource catalog. + */ +final class Catalog extends BaseObject { - public string|null $token = null; - - public Metadata|null $metadata = null; + public string|Endpoint|null $endpoint = null; } diff --git a/src/Components.php b/src/Components.php new file mode 100644 index 0000000..6da908a --- /dev/null +++ b/src/Components.php @@ -0,0 +1,79 @@ +|null + */ + #[MapOf(AuthenticationPolicy::class)] + public array|null $authentications = null; + + /** + * @var array|null + */ + #[MapOf(Error::class)] + public array|null $errors = null; + + /** + * @var NamedList|null + */ + #[NamedListOf(Extension::class)] + public NamedList|null $extensions = null; + + /** + * @var array|null + */ + #[MapOf(Task::class)] + public array|null $functions = null; + + /** + * @var array|null + */ + #[MapOf(RetryPolicy::class)] + public array|null $retries = null; + + /** + * @var list|null + */ + public array|null $secrets = null; + + /** + * @var array|null + */ + #[MapOf(Timeout::class)] + public array|null $timeouts = null; + + /** + * @var array|null + */ + #[MapOf(Catalog::class)] + public array|null $catalogs = null; +} diff --git a/src/Document.php b/src/Document.php new file mode 100644 index 0000000..5e85212 --- /dev/null +++ b/src/Document.php @@ -0,0 +1,49 @@ +|null + */ + public array|null $tags = null; + + /** + * @var array|null + */ + public array|null $metadata = null; +} diff --git a/src/RetryDef.php b/src/Duration.php similarity index 67% rename from src/RetryDef.php rename to src/Duration.php index 961a0ac..449cb68 100644 --- a/src/RetryDef.php +++ b/src/Duration.php @@ -20,19 +20,18 @@ namespace Serverless\Workflow; -final class RetryDef extends BaseObject +/** + * The inline definition of a duration. Where the DSL allows it, an ISO 8601 duration string can be used instead. + */ +final class Duration extends BaseObject { - public string|null $name = null; - - public string|null $delay = null; - - public string|null $maxDelay = null; + public int|null $days = null; - public string|null $increment = null; + public int|null $hours = null; - public int|string|null $multiplier = null; + public int|null $minutes = null; - public int|string|null $maxAttempts = null; + public int|null $seconds = null; - public int|string|null $jitter = null; + public int|null $milliseconds = null; } diff --git a/src/Endpoint.php b/src/Endpoint.php new file mode 100644 index 0000000..005a74c --- /dev/null +++ b/src/Endpoint.php @@ -0,0 +1,33 @@ +|null + */ + #[ListOf(EventFilter::class)] + public array|null $all = null; + + /** + * @var list|null + */ + #[ListOf(EventFilter::class)] + public array|null $any = null; + + public EventFilter|null $one = null; + + /** + * A runtime expression, or the strategy defining the event(s) that stop the listening. + */ + public string|EventConsumptionStrategy|null $until = null; +} diff --git a/src/Event/EventCorrelation.php b/src/Event/EventCorrelation.php new file mode 100644 index 0000000..fcd4c1f --- /dev/null +++ b/src/Event/EventCorrelation.php @@ -0,0 +1,33 @@ +|null + */ + #[MapOf(EventCorrelation::class)] + public array|null $correlate = null; +} diff --git a/src/Event/EventProperties.php b/src/Event/EventProperties.php new file mode 100644 index 0000000..dbd497a --- /dev/null +++ b/src/Event/EventProperties.php @@ -0,0 +1,45 @@ +usedForCompensation ??= false; - $this->type ??= 'switch'; - } -} diff --git a/src/EventBasedSwitchStateTimeOut.php b/src/EventBasedSwitchStateTimeOut.php deleted file mode 100644 index 31023ae..0000000 --- a/src/EventBasedSwitchStateTimeOut.php +++ /dev/null @@ -1,28 +0,0 @@ -exclusive ??= true; - $this->type ??= 'event'; - } -} diff --git a/src/EventStateTimeout.php b/src/EventStateTimeout.php deleted file mode 100644 index 9689171..0000000 --- a/src/EventStateTimeout.php +++ /dev/null @@ -1,30 +0,0 @@ -usedForCompensation ??= false; - $this->mode ??= 'parallel'; - $this->type ??= 'foreach'; - } -} diff --git a/src/ForeachStateTimeout.php b/src/ForeachStateTimeout.php deleted file mode 100644 index d8bdea1..0000000 --- a/src/ForeachStateTimeout.php +++ /dev/null @@ -1,30 +0,0 @@ - + */ + public static function hydrate(object $target, array $data): array + { + $reflectionClass = new ReflectionClass($target); + $additional = []; + + foreach ($data as $key => $value) { + $name = (string) $key; + if (! $reflectionClass->hasProperty($name)) { + $additional[$name] = $value; + continue; + } + + $property = $reflectionClass->getProperty($name); + if (! $property->isPublic() || $property->isStatic()) { + $additional[$name] = $value; + continue; + } + + try { + $property->setValue($target, self::cast($property, $value)); + } catch (TypeError) { + // Keep values the model cannot represent so they survive serialization and schema validation. + $additional[$name] = $value; + } + } + + return $additional; + } + + private static function cast(ReflectionProperty $reflectionProperty, mixed $value): mixed + { + if ($value === null) { + return null; + } + + foreach ($reflectionProperty->getAttributes() as $reflectionAttribute) { + $attribute = $reflectionAttribute->newInstance(); + if ($attribute instanceof ListOf || $attribute instanceof MapOf) { + return is_array($value) ? self::castItems($attribute->class, $value) : $value; + } + + if ($attribute instanceof NamedListOf) { + if ($value instanceof NamedList) { + return $value; + } + + return is_array($value) ? NamedList::of($attribute->class, $value) : $value; + } + } + + $type = $reflectionProperty->getType(); + if ($type instanceof ReflectionUnionType) { + $types = $type->getTypes(); + } elseif ($type instanceof ReflectionNamedType) { + $types = [$type]; + } else { + $types = []; + } + + foreach ($types as $namedType) { + if (! $namedType instanceof ReflectionNamedType || $namedType->isBuiltin()) { + continue; + } + + $class = $namedType->getName(); + if ($value instanceof $class) { + return $value; + } + + if (is_array($value) && is_subclass_of($class, Hydratable::class)) { + return $class::fromArray($value); + } + } + + return $value; + } + + /** + * @param class-string $class + * @param mixed[] $items + * @return mixed[] + */ + private static function castItems(string $class, array $items): array + { + $result = []; + foreach ($items as $key => $item) { + $result[$key] = is_array($item) ? $class::fromArray($item) : $item; + } + + return $result; + } +} diff --git a/src/Hydration/ListOf.php b/src/Hydration/ListOf.php new file mode 100644 index 0000000..aa6db8e --- /dev/null +++ b/src/Hydration/ListOf.php @@ -0,0 +1,38 @@ + $class + */ + public function __construct( + public readonly string $class + ) { + } +} diff --git a/src/Hydration/MapOf.php b/src/Hydration/MapOf.php new file mode 100644 index 0000000..9a11917 --- /dev/null +++ b/src/Hydration/MapOf.php @@ -0,0 +1,38 @@ + $class + */ + public function __construct( + public readonly string $class + ) { + } +} diff --git a/src/Hydration/NamedListOf.php b/src/Hydration/NamedListOf.php new file mode 100644 index 0000000..d1761be --- /dev/null +++ b/src/Hydration/NamedListOf.php @@ -0,0 +1,38 @@ + $class + */ + public function __construct( + public readonly string $class + ) { + } +} diff --git a/src/InjectState.php b/src/InjectState.php deleted file mode 100644 index 72cee1f..0000000 --- a/src/InjectState.php +++ /dev/null @@ -1,57 +0,0 @@ -usedForCompensation ??= false; - $this->type ??= 'inject'; - } -} diff --git a/src/Input.php b/src/Input.php new file mode 100644 index 0000000..9adab78 --- /dev/null +++ b/src/Input.php @@ -0,0 +1,36 @@ + + */ +class NamedList implements ArrayConvertible, IteratorAggregate, Countable +{ + /** + * @var array + */ + private array $items = []; + + /** + * Accepts either a name/item map or the DSL form, a list of single-entry maps. Items may be instances of the item + * class or arrays to hydrate. + * + * @param class-string $itemClass + * @param mixed[] $items + */ + public function __construct( + private readonly string $itemClass, + array $items = [] + ) { + foreach ($items as $key => $value) { + if (is_int($key)) { + if (! is_array($value)) { + throw new InvalidArgumentException('A named list entry must be a single-entry map.'); + } + + foreach ($value as $name => $item) { + $this->add((string) $name, $item); + } + + continue; + } + + $this->add($key, $value); + } + } + + /** + * @template U of Hydratable + * @param class-string $itemClass + * @param mixed[] $items + * @return self + */ + public static function of(string $itemClass, array $items = []): self + { + return new self($itemClass, $items); + } + + /** + * @return class-string + */ + public function getItemClass(): string + { + return $this->itemClass; + } + + /** + * @param T|mixed[] $item + */ + public function add(string $name, mixed $item): static + { + if (is_array($item)) { + $item = ($this->itemClass)::fromArray($item); + } + + if (! $item instanceof $this->itemClass) { + throw new InvalidArgumentException(sprintf( + 'Item "%s" must be an instance of %s or an array, %s given.', + $name, + $this->itemClass, + get_debug_type($item) + )); + } + + $this->items[$name] = $item; + + return $this; + } + + public function remove(string $name): static + { + unset($this->items[$name]); + + return $this; + } + + public function has(string $name): bool + { + return array_key_exists($name, $this->items); + } + + /** + * @return T|null + */ + public function get(string $name): ?Hydratable + { + return $this->items[$name] ?? null; + } + + /** + * @return list + */ + public function names(): array + { + return array_keys($this->items); + } + + /** + * @return array + */ + public function all(): array + { + return $this->items; + } + + public function count(): int + { + return count($this->items); + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->items); + } + + public function toArray(): array + { + return Serializer::toArray($this); + } +} diff --git a/src/OnEvents.php b/src/OnEvents.php deleted file mode 100644 index 98ebe20..0000000 --- a/src/OnEvents.php +++ /dev/null @@ -1,34 +0,0 @@ -type ??= 'operation'; - $this->actionMode ??= 'sequential'; - $this->usedForCompensation ??= false; - } -} diff --git a/src/OperationStateTimeout.php b/src/OperationStateTimeout.php deleted file mode 100644 index 695027d..0000000 --- a/src/OperationStateTimeout.php +++ /dev/null @@ -1,28 +0,0 @@ -completionType ??= 'allOf'; - $this->usedForCompensation ??= false; - $this->type ??= 'parallel'; - } -} diff --git a/src/ParallelStateTimeout.php b/src/ParallelStateTimeout.php deleted file mode 100644 index c6d2870..0000000 --- a/src/ParallelStateTimeout.php +++ /dev/null @@ -1,28 +0,0 @@ -properties() as $name => $propertyValue) { + $properties[$name] = self::normalize($propertyValue, $preserveEmptyObjects); + } + + return $preserveEmptyObjects && $properties === [] ? new ArrayObject() : $properties; + } + + if ($value instanceof NamedList) { + $items = []; + foreach ($value->all() as $name => $item) { + $items[] = [ + $name => self::normalize($item, $preserveEmptyObjects), + ]; + } + + return $items; + } + + if ($value instanceof ArrayConvertible) { + return self::normalize($value->toArray(), $preserveEmptyObjects); + } + + if (is_array($value)) { + foreach ($value as $key => $item) { + $value[$key] = self::normalize($item, $preserveEmptyObjects); + } + + return $value; + } + + return $value; + } + + /** + * @return mixed[] + */ + public static function toArray(ArrayConvertible $arrayConvertible): array + { + $normalized = self::normalize($arrayConvertible); + + return is_array($normalized) ? $normalized : []; + } +} diff --git a/src/Sleep.php b/src/Sleep.php deleted file mode 100644 index e86dc8a..0000000 --- a/src/Sleep.php +++ /dev/null @@ -1,28 +0,0 @@ -usedForCompensation ??= false; - $this->type ??= 'sleep'; - } -} diff --git a/src/SleepStateTimeout.php b/src/SleepStateTimeout.php deleted file mode 100644 index ba880c2..0000000 --- a/src/SleepStateTimeout.php +++ /dev/null @@ -1,26 +0,0 @@ -type === 'event'; - } - - public function isOperationState(): bool - { - return $this->type === 'operation'; - } - - public function isSwitchState(): bool - { - return $this->type === 'switch'; - } - - public function isSleepState(): bool - { - return $this->type === 'sleep'; - } - - public function isParallelState(): bool - { - return $this->type === 'parallel'; - } - - public function isInjectState(): bool - { - return $this->type === 'inject'; - } - - public function isForeachState(): bool - { - return $this->type === 'foreach'; - } - - public function isCallbackState(): bool - { - return $this->type === 'callback'; - } -} diff --git a/src/StateExecTimeout.php b/src/StateExecTimeout.php deleted file mode 100644 index 8c9e7df..0000000 --- a/src/StateExecTimeout.php +++ /dev/null @@ -1,28 +0,0 @@ -|null + */ + public array|null $payload = null; + + /** + * @var array|null + */ + public array|null $headers = null; +} diff --git a/src/Task/Call/AsyncApiMessageConsumptionPolicy.php b/src/Task/Call/AsyncApiMessageConsumptionPolicy.php new file mode 100644 index 0000000..8463564 --- /dev/null +++ b/src/Task/Call/AsyncApiMessageConsumptionPolicy.php @@ -0,0 +1,38 @@ +|null + */ + public array|null $variables = null; } diff --git a/src/Task/Call/AsyncApiSubscription.php b/src/Task/Call/AsyncApiSubscription.php new file mode 100644 index 0000000..f58e16f --- /dev/null +++ b/src/Task/Call/AsyncApiSubscription.php @@ -0,0 +1,36 @@ +|null + */ + public array|null $arguments = null; } diff --git a/src/TransitionEventCondition.php b/src/Task/Call/GrpcService.php similarity index 67% rename from src/TransitionEventCondition.php rename to src/Task/Call/GrpcService.php index 97ebce4..ea38543 100644 --- a/src/TransitionEventCondition.php +++ b/src/Task/Call/GrpcService.php @@ -18,17 +18,21 @@ * limitations under the License. */ -namespace Serverless\Workflow; +namespace Serverless\Workflow\Task\Call; -final class TransitionEventCondition extends BaseObject +use Serverless\Workflow\Authentication\AuthenticationPolicy; +use Serverless\Workflow\BaseObject; + +/** + * The gRPC service to call. + */ +final class GrpcService extends BaseObject { public string|null $name = null; - public string|null $eventRef = null; - - public string|Transition|null $transition = null; + public string|null $host = null; - public EventDataFilter|null $eventDataFilter = null; + public int|null $port = null; - public Metadata|null $metadata = null; + public AuthenticationPolicy|null $authentication = null; } diff --git a/src/Task/Call/HttpArguments.php b/src/Task/Call/HttpArguments.php new file mode 100644 index 0000000..0c9869f --- /dev/null +++ b/src/Task/Call/HttpArguments.php @@ -0,0 +1,57 @@ +|string|null + */ + public array|string|null $headers = null; + + public mixed $body = null; + + /** + * A name/value mapping, or a runtime expression. + * + * @var array|string|null + */ + public array|string|null $query = null; + + /** + * The output format: "raw", "content" (default) or "response". + */ + public string|null $output = null; + + public bool|null $redirect = null; +} diff --git a/src/StateDataFilter.php b/src/Task/Call/OpenApiArguments.php similarity index 52% rename from src/StateDataFilter.php rename to src/Task/Call/OpenApiArguments.php index 96ae481..a8fe93b 100644 --- a/src/StateDataFilter.php +++ b/src/Task/Call/OpenApiArguments.php @@ -18,11 +18,32 @@ * limitations under the License. */ -namespace Serverless\Workflow; +namespace Serverless\Workflow\Task\Call; -final class StateDataFilter extends BaseObject +use Serverless\Workflow\Authentication\AuthenticationPolicy; +use Serverless\Workflow\BaseObject; +use Serverless\Workflow\ExternalResource; + +/** + * The OpenAPI call arguments. + */ +final class OpenApiArguments extends BaseObject { - public string|null $input = null; + public ExternalResource|null $document = null; + + public string|null $operationId = null; + /** + * @var array|null + */ + public array|null $parameters = null; + + public AuthenticationPolicy|null $authentication = null; + + /** + * The output format: "raw", "content" (default) or "response". + */ public string|null $output = null; + + public bool|null $redirect = null; } diff --git a/src/End.php b/src/Task/CallAsyncApi.php similarity index 68% rename from src/End.php rename to src/Task/CallAsyncApi.php index fa93c2e..8dca83a 100644 --- a/src/End.php +++ b/src/Task/CallAsyncApi.php @@ -18,23 +18,25 @@ * limitations under the License. */ -namespace Serverless\Workflow; +namespace Serverless\Workflow\Task; -final class End extends BaseObject -{ - public bool|null $terminate = null; - - /** @var ProduceEventDef[]|null */ - public array|null $produceEvents = null; +use Serverless\Workflow\Task\Call\AsyncApiArguments; - public bool|null $compensate = null; +/** + * Defines the AsyncAPI call to perform. + */ +final class CallAsyncApi extends CallTask +{ + public const CALL = 'asyncapi'; - public string|ContinueAsDef|null $continueAs = null; + public AsyncApiArguments|null $with = null; + /** + * @param mixed[] $data + */ public function __construct(array $data = []) { parent::__construct($data); - $this->terminate ??= false; - $this->compensate ??= false; + $this->call = self::CALL; } } diff --git a/src/Task/CallFunction.php b/src/Task/CallFunction.php new file mode 100644 index 0000000..733eab1 --- /dev/null +++ b/src/Task/CallFunction.php @@ -0,0 +1,34 @@ +|null + */ + public array|null $with = null; +} diff --git a/src/AuthDef.php b/src/Task/CallGrpc.php similarity index 74% rename from src/AuthDef.php rename to src/Task/CallGrpc.php index 06f443a..9589954 100644 --- a/src/AuthDef.php +++ b/src/Task/CallGrpc.php @@ -18,15 +18,18 @@ * limitations under the License. */ -namespace Serverless\Workflow; +namespace Serverless\Workflow\Task; -final class AuthDef extends BaseObject -{ - public string|null $name = null; +use Serverless\Workflow\Task\Call\GrpcArguments; - public string|null $scheme = null; +/** + * Defines the gRPC call to perform. + */ +final class CallGrpc extends CallTask +{ + public const CALL = 'grpc'; - public string|BasicPropsDef|BearerPropsDef|OAuth2PropsDef|null $properties = null; + public GrpcArguments|null $with = null; /** * @param mixed[] $data @@ -34,6 +37,6 @@ final class AuthDef extends BaseObject public function __construct(array $data = []) { parent::__construct($data); - $this->scheme ??= 'basic'; + $this->call = self::CALL; } } diff --git a/src/DataInputSchema.php b/src/Task/CallHttp.php similarity index 70% rename from src/DataInputSchema.php rename to src/Task/CallHttp.php index d2f40f3..d1ce6c4 100644 --- a/src/DataInputSchema.php +++ b/src/Task/CallHttp.php @@ -18,17 +18,25 @@ * limitations under the License. */ -namespace Serverless\Workflow; +namespace Serverless\Workflow\Task; -final class DataInputSchema extends BaseObject +use Serverless\Workflow\Task\Call\HttpArguments; + +/** + * Defines the HTTP call to perform. + */ +final class CallHttp extends CallTask { - public string|null $schema = null; + public const CALL = 'http'; - public bool|null $failOnValidationErrors = null; + public HttpArguments|null $with = null; + /** + * @param mixed[] $data + */ public function __construct(array $data = []) { parent::__construct($data); - $this->failOnValidationErrors ??= false; + $this->call = self::CALL; } } diff --git a/src/EventDef.php b/src/Task/CallOpenApi.php similarity index 61% rename from src/EventDef.php rename to src/Task/CallOpenApi.php index 84193dc..2ae2d60 100644 --- a/src/EventDef.php +++ b/src/Task/CallOpenApi.php @@ -18,29 +18,25 @@ * limitations under the License. */ -namespace Serverless\Workflow; +namespace Serverless\Workflow\Task; -final class EventDef extends BaseObject -{ - public string|null $name = null; - - public string|null $source = null; - - public string|null $type = null; - - public string|null $kind = null; +use Serverless\Workflow\Task\Call\OpenApiArguments; - /** @var CorrelationDef|CorrelationDef[]|null */ - public CorrelationDef|array|null $correlation = null; - - public bool|null $dataOnly = null; +/** + * Defines the OpenAPI call to perform. + */ +final class CallOpenApi extends CallTask +{ + public const CALL = 'openapi'; - public Metadata|null $metadata = null; + public OpenApiArguments|null $with = null; + /** + * @param mixed[] $data + */ public function __construct(array $data = []) { parent::__construct($data); - $this->kind ??= 'consumed'; - $this->dataOnly ??= true; + $this->call = self::CALL; } } diff --git a/src/InjectStateTimeout.php b/src/Task/CallTask.php similarity index 81% rename from src/InjectStateTimeout.php rename to src/Task/CallTask.php index 62f1bfb..cb3011b 100644 --- a/src/InjectStateTimeout.php +++ b/src/Task/CallTask.php @@ -18,9 +18,12 @@ * limitations under the License. */ -namespace Serverless\Workflow; +namespace Serverless\Workflow\Task; -final class InjectStateTimeOut extends BaseObject +/** + * Defines a call to perform. + */ +abstract class CallTask extends Task { - public StateExecTimeOut|null $stateExecTimeOut = null; + public string|null $call = null; } diff --git a/src/Task/CatchErrors.php b/src/Task/CatchErrors.php new file mode 100644 index 0000000..229d665 --- /dev/null +++ b/src/Task/CatchErrors.php @@ -0,0 +1,32 @@ +|null + */ + public array|null $ports = null; + + /** + * @var array|null + */ + public array|null $volumes = null; + + /** + * @var array|null + */ + public array|null $environment = null; + + public ContainerLifetime|null $lifetime = null; +} diff --git a/src/Task/Run/ContainerLifetime.php b/src/Task/Run/ContainerLifetime.php new file mode 100644 index 0000000..d11607a --- /dev/null +++ b/src/Task/Run/ContainerLifetime.php @@ -0,0 +1,37 @@ +|null + */ + public array|null $arguments = null; + + /** + * @var array|null + */ + public array|null $environment = null; + + public string|null $code = null; + + public ExternalResource|null $source = null; +} diff --git a/src/FunctionRef.php b/src/Task/Run/Shell.php similarity index 66% rename from src/FunctionRef.php rename to src/Task/Run/Shell.php index d7abc5c..25c5e2d 100644 --- a/src/FunctionRef.php +++ b/src/Task/Run/Shell.php @@ -18,16 +18,24 @@ * limitations under the License. */ -namespace Serverless\Workflow; +namespace Serverless\Workflow\Task\Run; -final class FunctionRef extends BaseObject +use Serverless\Workflow\BaseObject; + +/** + * The configuration of the shell command to run. + */ +final class Shell extends BaseObject { - public string|null $refName = null; + public string|null $command = null; - /** @var mixed[] */ + /** + * @var array|null + */ public array|null $arguments = null; - public string|null $selectionSet = null; - - public string|null $invoke = null; + /** + * @var array|null + */ + public array|null $environment = null; } diff --git a/src/SubFlowRef.php b/src/Task/Run/SubflowConfiguration.php similarity index 67% rename from src/SubFlowRef.php rename to src/Task/Run/SubflowConfiguration.php index 07a44cd..24a5738 100644 --- a/src/SubFlowRef.php +++ b/src/Task/Run/SubflowConfiguration.php @@ -18,15 +18,23 @@ * limitations under the License. */ -namespace Serverless\Workflow; +namespace Serverless\Workflow\Task\Run; -final class SubFlowRef extends BaseObject +use Serverless\Workflow\BaseObject; + +/** + * The configuration of the workflow to run. + */ +final class SubflowConfiguration extends BaseObject { - public string|null $workflowId = null; + public string|null $namespace = null; - public string|null $version = null; + public string|null $name = null; - public string|null $onParentComplete = null; + public string|null $version = null; - public string|null $invoke = null; + /** + * @var array|null + */ + public array|null $input = null; } diff --git a/src/Task/RunConfiguration.php b/src/Task/RunConfiguration.php new file mode 100644 index 0000000..f5166ff --- /dev/null +++ b/src/Task/RunConfiguration.php @@ -0,0 +1,48 @@ +|string|null + */ + public array|string|null $set = null; +} diff --git a/src/EventDataFilter.php b/src/Task/SwitchCase.php similarity index 74% rename from src/EventDataFilter.php rename to src/Task/SwitchCase.php index 7d31e9e..c71422a 100644 --- a/src/EventDataFilter.php +++ b/src/Task/SwitchCase.php @@ -18,13 +18,16 @@ * limitations under the License. */ -namespace Serverless\Workflow; +namespace Serverless\Workflow\Task; -final class EventDataFilter extends BaseObject -{ - public bool|null $useData = null; +use Serverless\Workflow\BaseObject; - public string|null $data = null; +/** + * A case within a switch task. + */ +final class SwitchCase extends BaseObject +{ + public string|null $when = null; - public string|null $toStateData = null; + public string|null $then = null; } diff --git a/src/Task/SwitchTask.php b/src/Task/SwitchTask.php new file mode 100644 index 0000000..b7850d8 --- /dev/null +++ b/src/Task/SwitchTask.php @@ -0,0 +1,36 @@ +|null + */ + #[NamedListOf(SwitchCase::class)] + public NamedList|null $switch = null; +} diff --git a/src/Task/Task.php b/src/Task/Task.php new file mode 100644 index 0000000..d0ffbda --- /dev/null +++ b/src/Task/Task.php @@ -0,0 +1,138 @@ +|null + */ + public array|null $metadata = null; + + /** + * @param mixed[] $data + * @return class-string + */ + public static function resolveTaskClass(array $data): string + { + if (isset($data['call']) && is_string($data['call'])) { + return match ($data['call']) { + 'http' => CallHttp::class, + 'openapi' => CallOpenApi::class, + 'grpc' => CallGrpc::class, + 'asyncapi' => CallAsyncApi::class, + default => CallFunction::class, + }; + } + + if (array_key_exists('for', $data)) { + return ForTask::class; + } + + $classes = [ + 'do' => DoTask::class, + 'fork' => ForkTask::class, + 'emit' => EmitTask::class, + 'listen' => ListenTask::class, + 'raise' => RaiseTask::class, + 'run' => RunTask::class, + 'set' => SetTask::class, + 'switch' => SwitchTask::class, + 'try' => TryTask::class, + 'wait' => WaitTask::class, + ]; + foreach ($classes as $key => $class) { + if (array_key_exists($key, $data)) { + return $class; + } + } + + throw new InvalidArgumentException(sprintf( + 'Unable to determine the task type from the properties [%s].', + implode(', ', array_map('strval', array_keys($data))) + )); + } + + public function properties(): array + { + $properties = parent::properties(); + $leading = ['if', 'input']; + $trailing = ['output', 'export', 'timeout', 'then', 'metadata']; + $ordered = []; + + foreach ($leading as $name) { + if (array_key_exists($name, $properties)) { + $ordered[$name] = $properties[$name]; + } + } + + foreach ($properties as $name => $value) { + if (! in_array($name, $leading, true) && ! in_array($name, $trailing, true)) { + $ordered[$name] = $value; + } + } + + foreach ($trailing as $name) { + if (array_key_exists($name, $properties)) { + $ordered[$name] = $properties[$name]; + } + } + + return $ordered; + } + + protected static function resolveClass(array $data): string + { + if (static::class !== self::class) { + return static::class; + } + + /** @var class-string $class */ + $class = self::resolveTaskClass($data); + + return $class; + } +} diff --git a/src/Task/TryCatch.php b/src/Task/TryCatch.php new file mode 100644 index 0000000..64fa7d9 --- /dev/null +++ b/src/Task/TryCatch.php @@ -0,0 +1,43 @@ + + */ +final class TaskList extends NamedList implements Hydratable +{ + /** + * @param mixed[] $tasks + */ + public function __construct(array $tasks = []) + { + parent::__construct(Task::class, $tasks); + } + + public static function fromArray(array $data): static + { + return new static($data); + } +} diff --git a/src/Serializable.php b/src/Timeout.php similarity index 84% rename from src/Serializable.php rename to src/Timeout.php index 776f9b0..4c9af5d 100644 --- a/src/Serializable.php +++ b/src/Timeout.php @@ -20,6 +20,10 @@ namespace Serverless\Workflow; -final class Serializable extends BaseObject +/** + * The definition of a timeout. + */ +final class Timeout extends BaseObject { + public string|Duration|null $after = null; } diff --git a/src/Transition.php b/src/Transition.php deleted file mode 100644 index ecac608..0000000 --- a/src/Transition.php +++ /dev/null @@ -1,31 +0,0 @@ -keepActive ??= true; - } + public Output|null $output = null; - /** - * @return mixed[] - */ - public function toArray(): array - { - $convertObjectsToArray = static function ($input) use (&$convertObjectsToArray) { - if (is_object($input)) { - $input = get_object_vars($input); - } - if (is_array($input)) { - $input = array_filter($input, static fn ($value): bool => $value !== null); - - foreach ($input as $key => $value) { - $input[$key] = $convertObjectsToArray($value); - } - } - return $input; - }; - - return $convertObjectsToArray($this); - } + public Schedule|null $schedule = null; public function toJson(): string { - return json_encode($this->toArray(), JSON_PRETTY_PRINT) ?: ''; + return json_encode( + Serializer::normalize($this, true), + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR + ); } public static function fromJson(string $json): self { - return new self(json_decode($json, true, 512, JSON_THROW_ON_ERROR)); + $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + if (! is_array($data)) { + throw new InvalidArgumentException('A workflow definition must be a JSON object.'); + } + + return new self($data); } public function toYaml(): string { - return Yaml::dump($this->toArray(), 100, 2, Yaml::DUMP_OBJECT_AS_MAP); + $yaml = Yaml::dump( + Serializer::normalize($this, true), + 100, + 2, + Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK + ); + + // Start list items that hold a mapping on the dash line ("- name:"), as the specification examples do. + return preg_replace('/^( *)-\n\1 (?=\S)/m', '$1- ', $yaml) ?? $yaml; } public static function fromYaml(string $yaml): self { - return new self(Yaml::parse($yaml)); + $data = Yaml::parse($yaml); + if (! is_array($data)) { + throw new InvalidArgumentException('A workflow definition must be a YAML mapping.'); + } + + return new self($data); + } + + /** + * Validates the definition against the DSL schema. + */ + public function validate(): void + { + WorkflowValidator::validate($this); } } diff --git a/src/WorkflowExecTimeout.php b/src/WorkflowExecTimeout.php deleted file mode 100644 index 5d38246..0000000 --- a/src/WorkflowExecTimeout.php +++ /dev/null @@ -1,30 +0,0 @@ -> $errors validation messages keyed by JSON pointer + */ + public function __construct( + private readonly array $errors + ) { + parent::__construct(self::buildMessage($errors)); + } + + /** + * @return array> + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * @param array> $errors + */ + private static function buildMessage(array $errors): string + { + $lines = []; + foreach ($errors as $pointer => $messages) { + foreach ($messages as $message) { + $lines[] = sprintf('%s: %s', $pointer === '' ? '/' : $pointer, $message); + } + } + + return "The workflow definition is invalid:\n" . implode("\n", $lines); + } +} diff --git a/src/WorkflowValidator.php b/src/WorkflowValidator.php index e981fe4..1a23856 100644 --- a/src/WorkflowValidator.php +++ b/src/WorkflowValidator.php @@ -20,15 +20,101 @@ namespace Serverless\Workflow; -use Swaggest\JsonSchema\Schema; +use InvalidArgumentException; +use Opis\JsonSchema\Errors\ErrorFormatter; +use Opis\JsonSchema\Validator; +use RuntimeException; +use Serverless\Workflow\Serialization\Serializer; +/** + * Validates workflow definitions against the DSL JSON schema bundled with the SDK. + */ final class WorkflowValidator { - public const SCHEMAS_WORKFLOW_JSON = 'https://serverlessworkflow.io/schemas/0.8/workflow.json'; + public const SCHEMA_PATH = __DIR__ . '/../schema/workflow.json'; + + private const MAX_ERRORS = 100; + + private static object|null $schema = null; + + /** + * @param Workflow|mixed[] $workflow + */ + public static function validate(Workflow|array $workflow): void + { + $errors = self::errors($workflow); + if ($errors !== []) { + throw new WorkflowValidationException($errors); + } + } + + /** + * Returns the validation errors keyed by JSON pointer, or an empty array when the definition is valid. + * + * @param Workflow|mixed[] $workflow + * @return array> + */ + public static function errors(Workflow|array $workflow): array + { + $decoded = json_decode( + json_encode(self::normalize($workflow), JSON_THROW_ON_ERROR), + false, + 512, + JSON_THROW_ON_ERROR + ); + + $validator = new Validator(); + $validator->setMaxErrors(self::MAX_ERRORS); + $result = $validator->validate($decoded, self::schema()); + $error = $result->error(); + if ($error === null) { + return []; + } - public static function validate(Workflow $workflow): void + $errors = []; + foreach ((new ErrorFormatter())->format($error) as $pointer => $messages) { + $errors[(string) $pointer] = array_values(array_map('strval', (array) $messages)); + } + + return $errors; + } + + /** + * Arrays are hydrated into the model first so that empty objects the model knows about (for example an empty + * backoff definition) are encoded as objects rather than empty lists. Data the model cannot hydrate is + * validated as is. + * + * @param Workflow|mixed[] $workflow + */ + private static function normalize(Workflow|array $workflow): mixed + { + if (is_array($workflow)) { + try { + $workflow = new Workflow($workflow); + } catch (InvalidArgumentException) { + return $workflow; + } + } + + return Serializer::normalize($workflow, true); + } + + private static function schema(): object { - $schemaContract = Schema::import(self::SCHEMAS_WORKFLOW_JSON); - $schemaContract->in(json_decode((string) $workflow->toJson(), null, 512, JSON_THROW_ON_ERROR)); + if (self::$schema === null) { + $contents = file_get_contents(self::SCHEMA_PATH); + if ($contents === false) { + throw new RuntimeException(sprintf('Unable to read the workflow schema at %s.', self::SCHEMA_PATH)); + } + + $schema = json_decode($contents, false, 512, JSON_THROW_ON_ERROR); + if (! is_object($schema)) { + throw new RuntimeException('The workflow schema must be a JSON object.'); + } + + self::$schema = $schema; + } + + return self::$schema; } } diff --git a/tests/Fixtures/examples/README.md b/tests/Fixtures/examples/README.md new file mode 100644 index 0000000..85fc9f0 --- /dev/null +++ b/tests/Fixtures/examples/README.md @@ -0,0 +1,5 @@ +# Specification examples + +These workflow definitions are copied unmodified from the `examples` directory of the +[Open Workflow Specification](https://github.com/open-workflow-specification/specification) +at tag `v1.0.1`. The test suite parses, serializes and validates every one of them. diff --git a/tests/Fixtures/examples/accumulate-room-readings.yaml b/tests/Fixtures/examples/accumulate-room-readings.yaml new file mode 100644 index 0000000..a1b56c7 --- /dev/null +++ b/tests/Fixtures/examples/accumulate-room-readings.yaml @@ -0,0 +1,44 @@ +document: + dsl: '1.0.1' + namespace: examples + name: accumulate-room-readings + version: '0.1.0' +do: + - consumeReading: + listen: + to: + all: + - with: + source: https://my.home.com/sensor + type: my.home.sensors.temperature + correlate: + roomId: + from: .roomid + - with: + source: https://my.home.com/sensor + type: my.home.sensors.humidity + correlate: + roomId: + from: .roomid + output: + as: .data.reading + - logReading: + for: + each: reading + in: .readings + do: + - callOrderService: + call: openapi + with: + document: + endpoint: http://myorg.io/ordersservices.json + operationId: logreading + - generateReport: + call: openapi + with: + document: + endpoint: http://myorg.io/ordersservices.json + operationId: produceReport +timeout: + after: + hours: 1 diff --git a/tests/Fixtures/examples/authentication-bearer-uri-format.yaml b/tests/Fixtures/examples/authentication-bearer-uri-format.yaml new file mode 100644 index 0000000..173df06 --- /dev/null +++ b/tests/Fixtures/examples/authentication-bearer-uri-format.yaml @@ -0,0 +1,15 @@ +document: + dsl: '1.0.1' + namespace: examples + name: bearer-auth + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + bearer: + token: ${ .token } diff --git a/tests/Fixtures/examples/authentication-bearer.yaml b/tests/Fixtures/examples/authentication-bearer.yaml new file mode 100644 index 0000000..76c4e3f --- /dev/null +++ b/tests/Fixtures/examples/authentication-bearer.yaml @@ -0,0 +1,15 @@ +document: + dsl: '1.0.1' + namespace: examples + name: bearer-auth-uri-format + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/1 + authentication: + bearer: + token: ${ .token } \ No newline at end of file diff --git a/tests/Fixtures/examples/authentication-oauth2-secret.yaml b/tests/Fixtures/examples/authentication-oauth2-secret.yaml new file mode 100644 index 0000000..e48e6a4 --- /dev/null +++ b/tests/Fixtures/examples/authentication-oauth2-secret.yaml @@ -0,0 +1,18 @@ +document: + dsl: '1.0.1' + namespace: examples + name: oauth2-authentication + version: '1.0.0' +use: + secrets: + - mySecret +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oauth2: + use: mySecret \ No newline at end of file diff --git a/tests/Fixtures/examples/authentication-oauth2.yaml b/tests/Fixtures/examples/authentication-oauth2.yaml new file mode 100644 index 0000000..cf166f6 --- /dev/null +++ b/tests/Fixtures/examples/authentication-oauth2.yaml @@ -0,0 +1,22 @@ +document: + dsl: '1.0.1' + namespace: examples + name: oauth2-authentication + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oauth2: + authority: http://keycloak/realms/fake-authority + endpoints: #optional + token: /auth/token #defaults to /oauth2/token + introspection: /auth/introspect #defaults to /oauth2/introspect + grant: client_credentials + client: + id: workflow-runtime-id + secret: workflow-runtime-secret \ No newline at end of file diff --git a/tests/Fixtures/examples/authentication-oidc-secret.yaml b/tests/Fixtures/examples/authentication-oidc-secret.yaml new file mode 100644 index 0000000..3522308 --- /dev/null +++ b/tests/Fixtures/examples/authentication-oidc-secret.yaml @@ -0,0 +1,18 @@ +document: + dsl: '1.0.1' + namespace: examples + name: oidc-authentication + version: '1.0.0' +use: + secrets: + - mySecret +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oidc: + use: mySecret \ No newline at end of file diff --git a/tests/Fixtures/examples/authentication-oidc.yaml b/tests/Fixtures/examples/authentication-oidc.yaml new file mode 100644 index 0000000..9154eb4 --- /dev/null +++ b/tests/Fixtures/examples/authentication-oidc.yaml @@ -0,0 +1,19 @@ +document: + dsl: '1.0.1' + namespace: examples + name: oidc-authentication + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oidc: + authority: http://keycloak/realms/fake-authority #endpoints are resolved using the OIDC configuration located at '/.well-known/openid-configuration' + grant: client_credentials + client: + id: workflow-runtime-id + secret: workflow-runtime-secret \ No newline at end of file diff --git a/tests/Fixtures/examples/authentication-reusable.yaml b/tests/Fixtures/examples/authentication-reusable.yaml new file mode 100644 index 0000000..43049db --- /dev/null +++ b/tests/Fixtures/examples/authentication-reusable.yaml @@ -0,0 +1,19 @@ +document: + dsl: '1.0.1' + namespace: examples + name: bearer-auth + version: '0.1.0' +use: + authentications: + petStoreAuth: + bearer: + token: ${ .token } +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + use: petStoreAuth diff --git a/tests/Fixtures/examples/call-asyncapi-publish.yaml b/tests/Fixtures/examples/call-asyncapi-publish.yaml new file mode 100644 index 0000000..ed9fb8c --- /dev/null +++ b/tests/Fixtures/examples/call-asyncapi-publish.yaml @@ -0,0 +1,20 @@ +document: + dsl: '1.0.1' + namespace: examples + name: bearer-auth + version: '0.1.0' +do: + - findPet: + call: asyncapi + with: + document: + endpoint: https://fake.com/docs/asyncapi.json + operation: findPetsByStatus + server: + name: staging + message: + payload: + petId: ${ .pet.id } + authentication: + bearer: + token: ${ .token } diff --git a/tests/Fixtures/examples/call-asyncapi-subscribe-consume-amount.yaml b/tests/Fixtures/examples/call-asyncapi-subscribe-consume-amount.yaml new file mode 100644 index 0000000..55ff065 --- /dev/null +++ b/tests/Fixtures/examples/call-asyncapi-subscribe-consume-amount.yaml @@ -0,0 +1,17 @@ +document: + dsl: '1.0.1' + namespace: examples + name: bearer-auth + version: '0.1.0' +do: + - getNotifications: + call: asyncapi + with: + document: + endpoint: https://fake.com/docs/asyncapi.json + operation: getNotifications + protocol: ws + subscription: + filter: '${ .correlationId == $context.userId and .payload.from.firstName == $context.contact.firstName and .payload.from.lastName == $context.contact.lastName }' + consume: + amount: 5 diff --git a/tests/Fixtures/examples/call-asyncapi-subscribe-consume-forever-foreach.yaml b/tests/Fixtures/examples/call-asyncapi-subscribe-consume-forever-foreach.yaml new file mode 100644 index 0000000..6a7860f --- /dev/null +++ b/tests/Fixtures/examples/call-asyncapi-subscribe-consume-forever-foreach.yaml @@ -0,0 +1,28 @@ +document: + dsl: '1.0.1' + namespace: examples + name: bearer-auth + version: '0.1.0' +do: + - getNotifications: + call: asyncapi + with: + document: + endpoint: https://fake.com/docs/asyncapi.json + operation: getNotifications + subscription: + filter: '${ .correlationId == $context.userId and .payload.from.firstName == $context.contact.firstName and .payload.from.lastName == $context.contact.lastName }' + consume: + while: '${ true }' + foreach: + item: message + do: + - publishCloudEvent: + emit: + event: + with: + source: https://serverlessworkflow.io/samples + type: io.serverlessworkflow.samples.asyncapi.message.consumed.v1 + data: + message: '${ $message }' + diff --git a/tests/Fixtures/examples/call-asyncapi-subscribe-consume-until.yaml b/tests/Fixtures/examples/call-asyncapi-subscribe-consume-until.yaml new file mode 100644 index 0000000..d931498 --- /dev/null +++ b/tests/Fixtures/examples/call-asyncapi-subscribe-consume-until.yaml @@ -0,0 +1,18 @@ +document: + dsl: '1.0.1' + namespace: examples + name: bearer-auth + version: '0.1.0' +do: + - getNotifications: + call: asyncapi + with: + document: + endpoint: https://fake.com/docs/asyncapi.json + channel: /notifications + subscription: + filter: '${ .correlationId == $context.userId and .payload.from.firstName == $context.contact.firstName and .payload.from.lastName == $context.contact.lastName }' + consume: + for: + minutes: 30 + until: '${ ($context.consumedMessages | length) == 5 }' diff --git a/tests/Fixtures/examples/call-asyncapi-subscribe-consume-while.yaml b/tests/Fixtures/examples/call-asyncapi-subscribe-consume-while.yaml new file mode 100644 index 0000000..9fd9fd6 --- /dev/null +++ b/tests/Fixtures/examples/call-asyncapi-subscribe-consume-while.yaml @@ -0,0 +1,16 @@ +document: + dsl: '1.0.1' + namespace: examples + name: bearer-auth + version: '0.1.0' +do: + - getNotifications: + call: asyncapi + with: + document: + endpoint: https://fake.com/docs/asyncapi.json + operation: getNotifications + subscription: + filter: '${ .correlationId == $context.userId and .payload.from.firstName == $context.contact.firstName and .payload.from.lastName == $context.contact.lastName }' + consume: + while: '${ ($context.consumedMessages | length) < 5 }' diff --git a/tests/Fixtures/examples/call-custom-function-cataloged.yaml b/tests/Fixtures/examples/call-custom-function-cataloged.yaml new file mode 100644 index 0000000..177a792 --- /dev/null +++ b/tests/Fixtures/examples/call-custom-function-cataloged.yaml @@ -0,0 +1,13 @@ +document: + dsl: '1.0.1' + namespace: samples + name: call-custom-function-cataloged + version: '0.1.0' +do: + - log: + call: https://raw.githubusercontent.com/serverlessworkflow/catalog/main/functions/log/1.0.0/function.yaml + with: + message: Hello, world! + level: information + timestamp: true + format: '{TIMESTAMP} [{LEVEL}] ({CONTEXT}): {MESSAGE}' \ No newline at end of file diff --git a/tests/Fixtures/examples/call-custom-function-inline.yaml b/tests/Fixtures/examples/call-custom-function-inline.yaml new file mode 100644 index 0000000..86c2e52 --- /dev/null +++ b/tests/Fixtures/examples/call-custom-function-inline.yaml @@ -0,0 +1,25 @@ +document: + dsl: '1.0.1' + namespace: samples + name: call-custom-function-inline + version: '0.1.0' +use: + functions: + getPetById: + input: + schema: + document: + type: object + properties: + petId: + type: string + required: [ petId ] + call: http + with: + method: get + endpoint: https://petstore.swagger.io/v2/pet/{petId} +do: + - getPet: + call: getPetById + with: + petId: 69 \ No newline at end of file diff --git a/tests/Fixtures/examples/call-grpc.yaml b/tests/Fixtures/examples/call-grpc.yaml new file mode 100644 index 0000000..84fa951 --- /dev/null +++ b/tests/Fixtures/examples/call-grpc.yaml @@ -0,0 +1,18 @@ +document: + dsl: '1.0.1' + namespace: test + name: grpc-example + version: '0.1.0' +do: + - greet: + call: grpc + with: + proto: + endpoint: file://app/greet.proto + service: + name: GreeterApi.Greeter + host: localhost + port: 5011 + method: SayHello + arguments: + name: ${ .user.preferredDisplayName } \ No newline at end of file diff --git a/tests/Fixtures/examples/call-http-endpoint-interpolation-shorthand.yaml b/tests/Fixtures/examples/call-http-endpoint-interpolation-shorthand.yaml new file mode 100644 index 0000000..33c769d --- /dev/null +++ b/tests/Fixtures/examples/call-http-endpoint-interpolation-shorthand.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.1' + namespace: examples + name: call-http-shorthand-endpoint + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: https://petstore.swagger.io/v2/pet/{petId} diff --git a/tests/Fixtures/examples/call-http-endpoint-interpolation.yaml b/tests/Fixtures/examples/call-http-endpoint-interpolation.yaml new file mode 100644 index 0000000..6f18d3b --- /dev/null +++ b/tests/Fixtures/examples/call-http-endpoint-interpolation.yaml @@ -0,0 +1,13 @@ +document: + dsl: '1.0.1' + namespace: examples + name: call-http-shorthand-endpoint + version: '0.1.0' +do: + - getPet: + call: http + with: + headers: + content-type: application/json + method: get + endpoint: ${ "https://petstore.swagger.io/v2/pet/\(.petId)" } \ No newline at end of file diff --git a/tests/Fixtures/examples/call-http-query-headers-expressions.yaml b/tests/Fixtures/examples/call-http-query-headers-expressions.yaml new file mode 100644 index 0000000..5ff3247 --- /dev/null +++ b/tests/Fixtures/examples/call-http-query-headers-expressions.yaml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=../schema/workflow.yaml +document: + dsl: '1.0.1' + namespace: examples + name: http-query-headers-expressions + version: '1.0.0' +input: + schema: + format: json + document: + type: object + required: + - searchQuery + properties: + searchQuery: + type: string +do: + - setQueryAndHeaders: + set: + query: + search: ${.searchQuery} + headers: + Accept: application/json + - searchStarWarsCharacters: + call: http + with: + method: get + endpoint: https://swapi.dev/api/people/ + headers: ${.headers} + query: ${.query} + diff --git a/tests/Fixtures/examples/call-http-query-parameters.yaml b/tests/Fixtures/examples/call-http-query-parameters.yaml new file mode 100644 index 0000000..f804dd1 --- /dev/null +++ b/tests/Fixtures/examples/call-http-query-parameters.yaml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=../schema/workflow.yaml +document: + dsl: '1.0.1' + namespace: examples + name: http-query-params + version: '1.0.0' +input: + schema: + format: json + document: + type: object + required: + - searchQuery + properties: + searchQuery: + type: string +do: + - searchStarWarsCharacters: + call: http + with: + method: get + endpoint: https://swapi.dev/api/people/ + query: + search: ${.searchQuery} + diff --git a/tests/Fixtures/examples/call-http-redirect.yaml b/tests/Fixtures/examples/call-http-redirect.yaml new file mode 100644 index 0000000..d628974 --- /dev/null +++ b/tests/Fixtures/examples/call-http-redirect.yaml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=../schema/workflow.yaml +document: + dsl: '1.0.1' + namespace: examples + name: http-query-params + version: '1.0.0' +input: + schema: + format: json + document: + type: object + required: + - searchQuery + properties: + searchQuery: + type: string +do: + - searchStarWarsCharacters: + call: http + with: + method: get + endpoint: https://swapi.dev/api/people/ + query: + search: ${.searchQuery} + redirect: true + diff --git a/tests/Fixtures/examples/call-openapi-redirect.yaml b/tests/Fixtures/examples/call-openapi-redirect.yaml new file mode 100644 index 0000000..a35c6e0 --- /dev/null +++ b/tests/Fixtures/examples/call-openapi-redirect.yaml @@ -0,0 +1,15 @@ +document: + dsl: '1.0.1' + namespace: test + name: openapi-example + version: '0.1.0' +do: + - findPet: + call: openapi + with: + document: + endpoint: https://petstore.swagger.io/v2/swagger.json + operationId: findPetsByStatus + parameters: + status: available + redirect: true \ No newline at end of file diff --git a/tests/Fixtures/examples/call-openapi.yaml b/tests/Fixtures/examples/call-openapi.yaml new file mode 100644 index 0000000..27ba348 --- /dev/null +++ b/tests/Fixtures/examples/call-openapi.yaml @@ -0,0 +1,14 @@ +document: + dsl: '1.0.1' + namespace: test + name: openapi-example + version: '0.1.0' +do: + - findPet: + call: openapi + with: + document: + endpoint: https://petstore.swagger.io/v2/swagger.json + operationId: findPetsByStatus + parameters: + status: available \ No newline at end of file diff --git a/tests/Fixtures/examples/conditional-task.yaml b/tests/Fixtures/examples/conditional-task.yaml new file mode 100644 index 0000000..556fa67 --- /dev/null +++ b/tests/Fixtures/examples/conditional-task.yaml @@ -0,0 +1,22 @@ +document: + dsl: '1.0.1' + namespace: default + name: conditional-task + version: '0.1.0' +do: + - raiseErrorIfUnderage: + if: .customer.age < 18 + raise: + error: + type: https://superbet-casinos.com/customer/access-forbidden + status: 400 + title: Access Forbidden + then: end + - placeBet: + call: http + with: + method: post + endpoint: https://superbet-casinos.com/api/bet/on/football + body: + customer: .customer + bet: .bet \ No newline at end of file diff --git a/tests/Fixtures/examples/do-multiple.yaml b/tests/Fixtures/examples/do-multiple.yaml new file mode 100644 index 0000000..dc78fc7 --- /dev/null +++ b/tests/Fixtures/examples/do-multiple.yaml @@ -0,0 +1,17 @@ +document: + dsl: '1.0.1' + namespace: examples + name: call-http-shorthand-endpoint + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: https://petstore.swagger.io/v2/pet/{petId} + - buyPet: + call: http + with: + method: put + endpoint: https://petstore.swagger.io/v2/pet/{petId} + body: '${ . + { status: "sold" } }' diff --git a/tests/Fixtures/examples/do-single.yaml b/tests/Fixtures/examples/do-single.yaml new file mode 100644 index 0000000..33c769d --- /dev/null +++ b/tests/Fixtures/examples/do-single.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.1' + namespace: examples + name: call-http-shorthand-endpoint + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: https://petstore.swagger.io/v2/pet/{petId} diff --git a/tests/Fixtures/examples/emit.yaml b/tests/Fixtures/examples/emit.yaml new file mode 100644 index 0000000..c90be7b --- /dev/null +++ b/tests/Fixtures/examples/emit.yaml @@ -0,0 +1,19 @@ +document: + dsl: '1.0.1' + namespace: test + name: emit + version: '0.1.0' +do: + - emitEvent: + emit: + event: + with: + source: https://petstore.com + type: com.petstore.order.placed.v1 + data: + client: + firstName: Cruella + lastName: de Vil + items: + - breed: dalmatian + quantity: 101 \ No newline at end of file diff --git a/tests/Fixtures/examples/for.yaml b/tests/Fixtures/examples/for.yaml new file mode 100644 index 0000000..0e07ada --- /dev/null +++ b/tests/Fixtures/examples/for.yaml @@ -0,0 +1,21 @@ +document: + dsl: '1.0.1' + namespace: test + name: for-example + version: '0.1.0' +do: + - checkup: + for: + each: pet + in: .pets + at: index + while: .vet != null + do: + - waitForCheckup: + listen: + to: + one: + with: + type: com.fake.petclinic.pets.checkup.completed.v2 + output: + as: '.pets + [{ "id": $pet.id }]' \ No newline at end of file diff --git a/tests/Fixtures/examples/fork.yaml b/tests/Fixtures/examples/fork.yaml new file mode 100644 index 0000000..7ecb245 --- /dev/null +++ b/tests/Fixtures/examples/fork.yaml @@ -0,0 +1,26 @@ +document: + dsl: '1.0.1' + namespace: test + name: fork-example + version: '0.1.0' +do: + - raiseAlarm: + fork: + compete: true + branches: + - callNurse: + call: http + with: + method: put + endpoint: https://fake-hospital.com/api/v3/alert/nurses + body: + patientId: ${ .patient.fullName } + room: ${ .room.number } + - callDoctor: + call: http + with: + method: put + endpoint: https://fake-hospital.com/api/v3/alert/doctor + body: + patientId: ${ .patient.fullName } + room: ${ .room.number } \ No newline at end of file diff --git a/tests/Fixtures/examples/listen-to-all read-envelope.yaml b/tests/Fixtures/examples/listen-to-all read-envelope.yaml new file mode 100644 index 0000000..a973404 --- /dev/null +++ b/tests/Fixtures/examples/listen-to-all read-envelope.yaml @@ -0,0 +1,17 @@ +document: + dsl: '1.0.1' + namespace: test + name: listen-to-all-read-envelope + version: '0.1.0' +do: + - callDoctor: + listen: + to: + all: + - with: + type: com.fake-hospital.vitals.measurements.temperature + data: ${ .temperature > 38 } + - with: + type: com.fake-hospital.vitals.measurements.bpm + data: ${ .bpm < 60 or .bpm > 100 } + read: envelope \ No newline at end of file diff --git a/tests/Fixtures/examples/listen-to-all.yaml b/tests/Fixtures/examples/listen-to-all.yaml new file mode 100644 index 0000000..982e476 --- /dev/null +++ b/tests/Fixtures/examples/listen-to-all.yaml @@ -0,0 +1,16 @@ +document: + dsl: '1.0.1' + namespace: test + name: listen-to-all + version: '0.1.0' +do: + - callDoctor: + listen: + to: + all: + - with: + type: com.fake-hospital.vitals.measurements.temperature + data: ${ .temperature > 38 } + - with: + type: com.fake-hospital.vitals.measurements.bpm + data: ${ .bpm < 60 or .bpm > 100 } \ No newline at end of file diff --git a/tests/Fixtures/examples/listen-to-any-filter.yaml b/tests/Fixtures/examples/listen-to-any-filter.yaml new file mode 100644 index 0000000..f5ee19f --- /dev/null +++ b/tests/Fixtures/examples/listen-to-any-filter.yaml @@ -0,0 +1,16 @@ +document: + dsl: '1.0.1' + namespace: test + name: listen-to-any-filter + version: '0.1.0' +do: + - callDoctor: + listen: + to: + any: + - with: + type: com.fake-hospital.vitals.measurements.temperature + data: ${ .temperature > 38 } + - with: + type: com.fake-hospital.vitals.measurements.bpm + data: ${ .bpm < 60 or .bpm > 100 } \ No newline at end of file diff --git a/tests/Fixtures/examples/listen-to-any-forever-foreach.yaml b/tests/Fixtures/examples/listen-to-any-forever-foreach.yaml new file mode 100644 index 0000000..840cbff --- /dev/null +++ b/tests/Fixtures/examples/listen-to-any-forever-foreach.yaml @@ -0,0 +1,22 @@ +document: + dsl: '1.0.1' + namespace: test + name: listen-to-any-while-foreach + version: '0.1.0' +do: + - listenToGossips: + listen: + to: + any: [] + until: '${ false }' + foreach: + item: event + at: i + do: + - postToChatApi: + call: http + with: + method: post + endpoint: https://fake-chat-api.com/room/{roomId} + body: + event: ${ $event } \ No newline at end of file diff --git a/tests/Fixtures/examples/listen-to-any-until-condition.yaml b/tests/Fixtures/examples/listen-to-any-until-condition.yaml new file mode 100644 index 0000000..4a1008e --- /dev/null +++ b/tests/Fixtures/examples/listen-to-any-until-condition.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.1' + namespace: test + name: listen-to-any + version: '0.1.0' +do: + - callDoctor: + listen: + to: + any: [] + until: ( . | length ) > 3 #wait until 3 events have been consumed \ No newline at end of file diff --git a/tests/Fixtures/examples/listen-to-any-until-consumed.yaml b/tests/Fixtures/examples/listen-to-any-until-consumed.yaml new file mode 100644 index 0000000..2aece6f --- /dev/null +++ b/tests/Fixtures/examples/listen-to-any-until-consumed.yaml @@ -0,0 +1,20 @@ +document: + dsl: '1.0.1' + namespace: test + name: listen-to-any + version: '0.1.0' +do: + - callDoctor: + listen: + to: + any: + - with: + type: com.fake-hospital.vitals.measurements.temperature + data: ${ .temperature > 38 } + - with: + type: com.fake-hospital.vitals.measurements.bpm + data: ${ .bpm < 60 or .bpm > 100 } + until: + one: + with: + type: com.fake-hospital.patient.checked-out \ No newline at end of file diff --git a/tests/Fixtures/examples/listen-to-any.yaml b/tests/Fixtures/examples/listen-to-any.yaml new file mode 100644 index 0000000..4f56769 --- /dev/null +++ b/tests/Fixtures/examples/listen-to-any.yaml @@ -0,0 +1,10 @@ +document: + dsl: '1.0.1' + namespace: test + name: listen-to-any + version: '0.1.0' +do: + - callDoctor: + listen: + to: + any: [] \ No newline at end of file diff --git a/tests/Fixtures/examples/listen-to-one.yaml b/tests/Fixtures/examples/listen-to-one.yaml new file mode 100644 index 0000000..884252d --- /dev/null +++ b/tests/Fixtures/examples/listen-to-one.yaml @@ -0,0 +1,18 @@ +document: + dsl: '1.0.1' + namespace: test + name: listen-to-one + version: '0.1.0' +do: + - waitForStartup: + listen: + to: + one: + with: + type: com.virtual-wf-powered-race.events.race.started.v1 + - startup: + call: http + with: + method: post + endpoint: + uri: https://virtual-wf-powered-race.com/api/v4/cars/{carId}/start \ No newline at end of file diff --git a/tests/Fixtures/examples/mock-service-extension.yaml b/tests/Fixtures/examples/mock-service-extension.yaml new file mode 100644 index 0000000..dfb29d6 --- /dev/null +++ b/tests/Fixtures/examples/mock-service-extension.yaml @@ -0,0 +1,27 @@ +document: + dsl: '1.0.1' + namespace: test + name: sample-workflow + version: 0.1.0 +use: + extensions: + - mockService: + extend: call + when: ($task.with.endpoint != null and ($task.with.endpoint | startswith("https://mocked.service.com"))) or ($task.with.endpoint.uri != null and ($task.with.endpoint.uri | startswith("https://mocked.service.com"))) + before: + - mockResponse: + set: + statusCode: 200 + headers: + Content-Type: application/json + content: + foo: + bar: baz + then: exit #using this, we indicate to the workflow we want to exit the extended task, thus just returning what we injected +do: + - callHttp: + call: http + with: + method: get + endpoint: + uri: https://fake.com/sample diff --git a/tests/Fixtures/examples/raise-inline.yaml b/tests/Fixtures/examples/raise-inline.yaml new file mode 100644 index 0000000..3592f21 --- /dev/null +++ b/tests/Fixtures/examples/raise-inline.yaml @@ -0,0 +1,13 @@ +document: + dsl: '1.0.1' + namespace: test + name: raise-not-implemented + version: '0.1.0' +do: + - notImplemented: + raise: + error: + type: https://serverlessworkflow.io/errors/not-implemented + status: 500 + title: Not Implemented + detail: ${ "The workflow '\( $workflow.definition.document.name ):\( $workflow.definition.document.version )' is a work in progress and cannot be run yet" } \ No newline at end of file diff --git a/tests/Fixtures/examples/raise-reusable.yaml b/tests/Fixtures/examples/raise-reusable.yaml new file mode 100644 index 0000000..16b4b24 --- /dev/null +++ b/tests/Fixtures/examples/raise-reusable.yaml @@ -0,0 +1,16 @@ +document: + dsl: '1.0.1' + namespace: test + name: raise-not-implemented + version: '0.1.0' +use: + errors: + notImplemented: + type: https://serverlessworkflow.io/errors/not-implemented + status: 500 + title: Not Implemented + detail: ${ "The workflow '\( $workflow.definition.document.name ):\( $workflow.definition.document.version )' is a work in progress and cannot be run yet" } +do: + - notImplemented: + raise: + error: notImplemented \ No newline at end of file diff --git a/tests/Fixtures/examples/run-container-cleanup-always.yaml b/tests/Fixtures/examples/run-container-cleanup-always.yaml new file mode 100644 index 0000000..5921ecb --- /dev/null +++ b/tests/Fixtures/examples/run-container-cleanup-always.yaml @@ -0,0 +1,12 @@ +document: + dsl: '1.0.1' + namespace: test + name: run-container + version: '0.1.0' +do: + - runContainer: + run: + container: + image: hello-world + lifetime: + cleanup: always \ No newline at end of file diff --git a/tests/Fixtures/examples/run-container-cleanup-eventually.yaml b/tests/Fixtures/examples/run-container-cleanup-eventually.yaml new file mode 100644 index 0000000..725113f --- /dev/null +++ b/tests/Fixtures/examples/run-container-cleanup-eventually.yaml @@ -0,0 +1,14 @@ +document: + dsl: '1.0.1' + namespace: test + name: run-container + version: '0.1.0' +do: + - runContainer: + run: + container: + image: hello-world + lifetime: + cleanup: eventually + after: + minutes: 30 \ No newline at end of file diff --git a/tests/Fixtures/examples/run-container-with-name.yaml b/tests/Fixtures/examples/run-container-with-name.yaml new file mode 100644 index 0000000..c10f697 --- /dev/null +++ b/tests/Fixtures/examples/run-container-with-name.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.1' + namespace: test + name: run-container-with-name + version: '0.1.0' +do: + - runContainer: + run: + container: + image: hello-world + name: ${ "hello-\(.workflow.document.name)-\(.task.name)-\(.workflow.id)" } \ No newline at end of file diff --git a/tests/Fixtures/examples/run-container.yaml b/tests/Fixtures/examples/run-container.yaml new file mode 100644 index 0000000..3e2ac95 --- /dev/null +++ b/tests/Fixtures/examples/run-container.yaml @@ -0,0 +1,10 @@ +document: + dsl: '1.0.1' + namespace: test + name: run-container + version: '0.1.0' +do: + - runContainer: + run: + container: + image: hello-world \ No newline at end of file diff --git a/tests/Fixtures/examples/run-return-all.yaml b/tests/Fixtures/examples/run-return-all.yaml new file mode 100644 index 0000000..ca9549c --- /dev/null +++ b/tests/Fixtures/examples/run-return-all.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.1' + namespace: test + name: run-container + version: '0.1.0' +do: + - runContainer: + run: + container: + image: hello-world + return: all \ No newline at end of file diff --git a/tests/Fixtures/examples/run-return-code.yaml b/tests/Fixtures/examples/run-return-code.yaml new file mode 100644 index 0000000..1844160 --- /dev/null +++ b/tests/Fixtures/examples/run-return-code.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.1' + namespace: test + name: run-container + version: '0.1.0' +do: + - runContainer: + run: + container: + image: hello-world + return: code \ No newline at end of file diff --git a/tests/Fixtures/examples/run-return-none.yaml b/tests/Fixtures/examples/run-return-none.yaml new file mode 100644 index 0000000..88e8d92 --- /dev/null +++ b/tests/Fixtures/examples/run-return-none.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.1' + namespace: test + name: run-container + version: '0.1.0' +do: + - runContainer: + run: + container: + image: hello-world + return: none \ No newline at end of file diff --git a/tests/Fixtures/examples/run-return-stderr.yaml b/tests/Fixtures/examples/run-return-stderr.yaml new file mode 100644 index 0000000..74d46ee --- /dev/null +++ b/tests/Fixtures/examples/run-return-stderr.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.1' + namespace: test + name: run-container + version: '0.1.0' +do: + - runContainer: + run: + container: + image: hello-world + return: stderr \ No newline at end of file diff --git a/tests/Fixtures/examples/run-script-with-arguments.yaml b/tests/Fixtures/examples/run-script-with-arguments.yaml new file mode 100644 index 0000000..db5fe32 --- /dev/null +++ b/tests/Fixtures/examples/run-script-with-arguments.yaml @@ -0,0 +1,14 @@ +document: + dsl: '1.0.1' + namespace: samples + name: run-script-with-arguments + version: 0.1.0 +do: + - log: + run: + script: + language: javascript + arguments: + message: ${ .message } + code: > + console.log(message) \ No newline at end of file diff --git a/tests/Fixtures/examples/run-subflow.yaml b/tests/Fixtures/examples/run-subflow.yaml new file mode 100644 index 0000000..17d3469 --- /dev/null +++ b/tests/Fixtures/examples/run-subflow.yaml @@ -0,0 +1,14 @@ +document: + dsl: '1.0.1' + namespace: test + name: run-subflow + version: '0.1.0' +do: + - registerCustomer: + run: + workflow: + namespace: test + name: register-customer + version: '0.1.0' + input: + customer: .user \ No newline at end of file diff --git a/tests/Fixtures/examples/schedule-cron.yaml b/tests/Fixtures/examples/schedule-cron.yaml new file mode 100644 index 0000000..b0a8872 --- /dev/null +++ b/tests/Fixtures/examples/schedule-cron.yaml @@ -0,0 +1,13 @@ +document: + dsl: '1.0.1' + namespace: examples + name: cron-schedule + version: '0.1.0' +schedule: + cron: 0 0 * * * +do: + - backup: + call: http + with: + method: post + endpoint: https://example.com/api/v1/backup/start \ No newline at end of file diff --git a/tests/Fixtures/examples/schedule-event-driven.yaml b/tests/Fixtures/examples/schedule-event-driven.yaml new file mode 100644 index 0000000..4fee65d --- /dev/null +++ b/tests/Fixtures/examples/schedule-event-driven.yaml @@ -0,0 +1,24 @@ +document: + dsl: '1.0.1' + namespace: examples + name: event-driven-schedule + version: '0.1.0' +schedule: + on: + one: + with: + type: com.example.hospital.events.patients.heartbeat.low +do: + - callNurse: + call: http + with: + method: post + endpoint: https://hospital.example.com/api/v1/notify + body: + patientId: ${ $workflow.input[0].data.patient.id } + patientName: ${ $workflow.input[0].data.patient.name } + roomNumber: ${ $workflow.input[0].data.patient.room.number } + vitals: + heartRate: ${ $workflow.input[0].data.patient.vitals.bpm } + timestamp: ${ $workflow.input[0].data.timestamp } + message: "Alert: Patient's heartbeat is critically low. Immediate attention required." \ No newline at end of file diff --git a/tests/Fixtures/examples/set-expression.yaml b/tests/Fixtures/examples/set-expression.yaml new file mode 100644 index 0000000..cb114a9 --- /dev/null +++ b/tests/Fixtures/examples/set-expression.yaml @@ -0,0 +1,13 @@ +document: + dsl: '1.0.1' + namespace: test + name: set + version: '0.1.0' +schedule: + on: + one: + with: + type: io.serverlessworkflow.samples.events.trigger.v1 +do: + - initialize: + set: ${ $workflow.input[0] } \ No newline at end of file diff --git a/tests/Fixtures/examples/set.yaml b/tests/Fixtures/examples/set.yaml new file mode 100644 index 0000000..4120813 --- /dev/null +++ b/tests/Fixtures/examples/set.yaml @@ -0,0 +1,14 @@ +document: + dsl: '1.0.1' + namespace: test + name: set + version: '0.1.0' +schedule: + on: + one: + with: + type: io.serverlessworkflow.samples.events.trigger.v1 +do: + - initialize: + set: + startEvent: ${ $workflow.input[0] } \ No newline at end of file diff --git a/tests/Fixtures/examples/star-wars-homeworld.yaml b/tests/Fixtures/examples/star-wars-homeworld.yaml new file mode 100644 index 0000000..c4f524b --- /dev/null +++ b/tests/Fixtures/examples/star-wars-homeworld.yaml @@ -0,0 +1,33 @@ +# yaml-language-server: $schema=../schema/workflow.yaml +document: + dsl: '1.0.1' + namespace: examples + name: star-wars-homeplanet + version: '1.0.0' +input: + schema: + format: json + document: + type: object + required: + - id + properties: + id: + type: integer + description: The id of the star wars character to get + minimum: 1 +do: + - getStarWarsCharacter: + call: http + with: + method: get + endpoint: https://swapi.dev/api/people/{id} + output: response + export: + as: + homeworld: ${ .content.homeworld } + - getStarWarsHomeworld: + call: http + with: + method: get + endpoint: ${ $context.homeworld } diff --git a/tests/Fixtures/examples/switch-then-string.yaml b/tests/Fixtures/examples/switch-then-string.yaml new file mode 100644 index 0000000..ccd307d --- /dev/null +++ b/tests/Fixtures/examples/switch-then-string.yaml @@ -0,0 +1,31 @@ +document: + dsl: '1.0.1' + namespace: test + name: sample-workflow + version: 0.1.0 +do: + - processOrder: + switch: + - case1: + when: .orderType == "electronic" + then: processElectronicOrder + - case2: + when: .orderType == "physical" + then: processPhysicalOrder + - default: + then: handleUnknownOrderType + - processElectronicOrder: + set: + validate: true + status: fulfilled + then: exit + - processPhysicalOrder: + set: + inventory: clear + items: 1 + address: Elmer St + then: exit + - handleUnknownOrderType: + set: + log: warn + message: something's wrong \ No newline at end of file diff --git a/tests/Fixtures/examples/try-catch-retry-inline.yaml b/tests/Fixtures/examples/try-catch-retry-inline.yaml new file mode 100644 index 0000000..257056d --- /dev/null +++ b/tests/Fixtures/examples/try-catch-retry-inline.yaml @@ -0,0 +1,26 @@ +document: + dsl: '1.0.1' + namespace: default + name: try-catch-retry + version: '0.1.0' +do: + - tryGetPet: + try: + - getPet: + call: http + with: + method: get + endpoint: https://petstore.swagger.io/v2/pet/{petId} + catch: + errors: + with: + type: https://serverlessworkflow.io/spec/1.0.0/errors/communication + status: 503 + retry: + delay: + seconds: 3 + backoff: + exponential: {} + limit: + attempt: + count: 5 \ No newline at end of file diff --git a/tests/Fixtures/examples/try-catch-retry-reusable.yaml b/tests/Fixtures/examples/try-catch-retry-reusable.yaml new file mode 100644 index 0000000..d009e8e --- /dev/null +++ b/tests/Fixtures/examples/try-catch-retry-reusable.yaml @@ -0,0 +1,30 @@ +document: + dsl: '1.0.1' + namespace: default + name: try-catch-retry + version: '0.1.0' +use: + retries: + default: + delay: + seconds: 3 + backoff: + exponential: {} + limit: + attempt: + count: 5 +do: + - tryGetPet: + try: + - getPet: + call: http + with: + method: get + endpoint: https://petstore.swagger.io/v2/pet/{petId} + catch: + errors: + with: + type: https://serverlessworkflow.io/spec/1.0.0/errors/communication + status: 503 + retry: default + \ No newline at end of file diff --git a/tests/Fixtures/examples/try-catch-then.yaml b/tests/Fixtures/examples/try-catch-then.yaml new file mode 100644 index 0000000..0c7cec1 --- /dev/null +++ b/tests/Fixtures/examples/try-catch-then.yaml @@ -0,0 +1,39 @@ +document: + dsl: '1.0.1' + namespace: default + name: try-catch + version: '0.1.0' +do: + - tryGetPet: + try: + - getPet: + call: http + with: + method: get + endpoint: https://petstore.swagger.io/v2/pet/{petId} + catch: + errors: + with: + type: https://serverlessworkflow.io/spec/1.0.0/errors/communication + status: 404 + as: error + do: + - notifySupport: + emit: + event: + with: + source: https://petstore.swagger.io + type: io.swagger.petstore.events.pets.not-found.v1 + data: ${ $error } + - setError: + set: + error: $error + export: + as: '$context + { error: $error }' + - buyPet: + if: $context.error == null + call: http + with: + method: put + endpoint: https://petstore.swagger.io/v2/pet/{petId} + body: '${ . + { status: "sold" } }' \ No newline at end of file diff --git a/tests/Fixtures/examples/try-catch.yaml b/tests/Fixtures/examples/try-catch.yaml new file mode 100644 index 0000000..5c9cdf9 --- /dev/null +++ b/tests/Fixtures/examples/try-catch.yaml @@ -0,0 +1,18 @@ +document: + dsl: '1.0.1' + namespace: default + name: try-catch + version: '0.1.0' +do: + - tryGetPet: + try: + - getPet: + call: http + with: + method: get + endpoint: https://petstore.swagger.io/v2/pet/{petId} + catch: + errors: + with: + type: https://serverlessworkflow.io/spec/1.0.0/errors/communication + status: 404 \ No newline at end of file diff --git a/tests/Fixtures/examples/wait-duration-inline.yaml b/tests/Fixtures/examples/wait-duration-inline.yaml new file mode 100644 index 0000000..e250fcf --- /dev/null +++ b/tests/Fixtures/examples/wait-duration-inline.yaml @@ -0,0 +1,9 @@ +document: + dsl: '1.0.1' + namespace: test + name: wait-duration-inline + version: '0.1.0' +do: + - wait30Seconds: + wait: + seconds: 30 \ No newline at end of file diff --git a/tests/Fixtures/examples/wait-duration-iso8601.yaml b/tests/Fixtures/examples/wait-duration-iso8601.yaml new file mode 100644 index 0000000..c3ca63c --- /dev/null +++ b/tests/Fixtures/examples/wait-duration-iso8601.yaml @@ -0,0 +1,8 @@ +document: + dsl: '1.0.1' + namespace: test + name: wait-duration-8601 + version: '0.1.0' +do: + - wait30Seconds: + wait: PT30S \ No newline at end of file diff --git a/tests/Fixtures/workflow.json b/tests/Fixtures/workflow.json index 73c3978..117f69a 100644 --- a/tests/Fixtures/workflow.json +++ b/tests/Fixtures/workflow.json @@ -1,37 +1,57 @@ { - "id": "greeting", - "name": "Greeting Workflow", - "description": "Greet Someone", - "version": "1.0", - "start": "Greet", - "specVersion": "0.8", - "keepActive": true, - "functions": [ - { - "name": "greetingFunction", - "operation": "file:\/\/myapis\/greetingapis.json#greeting" + "document": { + "dsl": "1.0.1", + "namespace": "examples", + "name": "buy-pet", + "version": "0.1.0" + }, + "use": { + "timeouts": { + "short": { + "after": { + "seconds": 30 + } + } } - ], - "states": [ + }, + "do": [ { - "type": "operation", - "name": "Greet", - "end": true, - "actionMode": "sequential", - "actions": [ - { - "functionRef": { - "refName": "greetingFunction", - "arguments": { - "name": "${ .person.name }" + "getPet": { + "with": { + "method": "get", + "endpoint": { + "uri": "https://petstore.swagger.io/v2/pet/{petId}", + "authentication": { + "bearer": { + "token": "${ .token }" + } } - }, - "actionDataFilter": { - "results": "${ .greeting }" } + }, + "call": "http", + "timeout": "short" + } + }, + { + "markAsSold": { + "set": { + "status": "sold" } - ], - "usedForCompensation": false + } + }, + { + "buyPet": { + "with": { + "method": "put", + "endpoint": "https://petstore.swagger.io/v2/pet/{petId}", + "body": "${ . + { status: .status } }" + }, + "call": "http", + "then": "end" + } } - ] + ], + "timeout": { + "after": "PT5M" + } } \ No newline at end of file diff --git a/tests/Fixtures/workflow.yaml b/tests/Fixtures/workflow.yaml index b28fb98..d08c0c9 100644 --- a/tests/Fixtures/workflow.yaml +++ b/tests/Fixtures/workflow.yaml @@ -1,26 +1,33 @@ -id: greeting -name: 'Greeting Workflow' -description: 'Greet Someone' -version: '1.0' -start: Greet -specVersion: '0.8' -keepActive: true -functions: - - - name: greetingFunction - operation: 'file://myapis/greetingapis.json#greeting' -states: - - - type: operation - name: Greet - end: true - actionMode: sequential - actions: - - - functionRef: - refName: greetingFunction - arguments: - name: '${ .person.name }' - actionDataFilter: - results: '${ .greeting }' - usedForCompensation: false +document: + dsl: 1.0.1 + namespace: examples + name: buy-pet + version: 0.1.0 +use: + timeouts: + short: + after: + seconds: 30 +do: + - getPet: + with: + method: get + endpoint: + uri: 'https://petstore.swagger.io/v2/pet/{petId}' + authentication: + bearer: + token: '${ .token }' + call: http + timeout: short + - markAsSold: + set: + status: sold + - buyPet: + with: + method: put + endpoint: 'https://petstore.swagger.io/v2/pet/{petId}' + body: '${ . + { status: .status } }' + call: http + then: end +timeout: + after: PT5M diff --git a/tests/Unit/ExamplesTest.php b/tests/Unit/ExamplesTest.php new file mode 100644 index 0000000..03f5378 --- /dev/null +++ b/tests/Unit/ExamplesTest.php @@ -0,0 +1,89 @@ + + */ + public static function examples(): iterable + { + $paths = glob(__DIR__ . '/../Fixtures/examples/*.yaml'); + self::assertNotFalse($paths); + self::assertNotEmpty($paths); + + foreach ($paths as $path) { + yield basename($path, '.yaml') => [$path]; + } + } + + #[DataProvider('examples')] + public function testRoundTripsWithoutLoss(string $path): void + { + $yaml = self::read($path); + $expected = Yaml::parse($yaml); + $workflow = Workflow::fromYaml($yaml); + + self::assertEquals($expected, $workflow->toArray()); + self::assertEquals($expected, Workflow::fromJson($workflow->toJson())->toArray()); + self::assertEquals($expected, Workflow::fromYaml($workflow->toYaml())->toArray()); + } + + #[DataProvider('examples')] + public function testConformsToTheSchema(string $path): void + { + $workflow = Workflow::fromYaml(self::read($path)); + + self::assertSame([], WorkflowValidator::errors($workflow)); + self::assertSame([], WorkflowValidator::errors(Yaml::parse(self::read($path)))); + } + + #[DataProvider('examples')] + public function testHydratesTypedTasks(string $path): void + { + $workflow = Workflow::fromYaml(self::read($path)); + + self::assertNotNull($workflow->document); + self::assertSame(Workflow::DSL_VERSION, $workflow->document->dsl); + self::assertNotNull($workflow->do); + self::assertGreaterThan(0, count($workflow->do)); + foreach ($workflow->do as $name => $task) { + self::assertNotSame('', $name); + self::assertInstanceOf(Task::class, $task); + } + } + + private static function read(string $path): string + { + $contents = file_get_contents($path); + self::assertNotFalse($contents); + + return $contents; + } +} diff --git a/tests/Unit/TaskListTest.php b/tests/Unit/TaskListTest.php new file mode 100644 index 0000000..f66dd8c --- /dev/null +++ b/tests/Unit/TaskListTest.php @@ -0,0 +1,119 @@ + [ + 'wait' => 'PT1S', + ], + ], + [ + 'second' => [ + 'set' => [ + 'a' => 1, + ], + ], + ], + ]); + + self::assertSame(['first', 'second'], $tasks->names()); + self::assertInstanceOf(WaitTask::class, $tasks->get('first')); + self::assertInstanceOf(SetTask::class, $tasks->get('second')); + self::assertCount(2, $tasks); + } + + public function testAcceptsANameToTaskMap(): void + { + $tasks = new TaskList([ + 'first' => new WaitTask([ + 'wait' => 'PT1S', + ]), + 'second' => [ + 'set' => [ + 'a' => 1, + ], + ], + ]); + + self::assertSame([ + [ + 'first' => [ + 'wait' => 'PT1S', + ], + ], + [ + 'second' => [ + 'set' => [ + 'a' => 1, + ], + ], + ], + ], $tasks->toArray()); + } + + public function testKeepsInsertionOrderAndReplacesByName(): void + { + $tasks = new TaskList(); + $tasks->add('b', new WaitTask([ + 'wait' => 'PT1S', + ])); + $tasks->add('a', new WaitTask([ + 'wait' => 'PT2S', + ])); + $tasks->add('b', new WaitTask([ + 'wait' => 'PT3S', + ])); + + self::assertSame(['b', 'a'], $tasks->names()); + self::assertTrue($tasks->has('a')); + self::assertNull($tasks->get('missing')); + + $tasks->remove('a'); + self::assertSame(['b'], $tasks->names()); + + $names = []; + foreach ($tasks as $name => $task) { + $names[] = $name; + self::assertInstanceOf(WaitTask::class, $task); + } + + self::assertSame(['b'], $names); + } + + public function testRejectsForeignItems(): void + { + $this->expectException(InvalidArgumentException::class); + new TaskList([ + 'first' => 'not a task', + ]); + } +} diff --git a/tests/Unit/TaskTest.php b/tests/Unit/TaskTest.php new file mode 100644 index 0000000..d188555 --- /dev/null +++ b/tests/Unit/TaskTest.php @@ -0,0 +1,185 @@ +}> + */ + public static function tasks(): iterable + { + yield 'call http' => [[ + 'call' => 'http', + 'with' => [ + 'method' => 'get', + 'endpoint' => 'https://example.com', + ], + ], CallHttp::class]; + yield 'call openapi' => [[ + 'call' => 'openapi', + 'with' => [ + 'operationId' => 'x', + ], + ], CallOpenApi::class]; + yield 'call grpc' => [[ + 'call' => 'grpc', + 'with' => [ + 'method' => 'x', + ], + ], CallGrpc::class]; + yield 'call asyncapi' => [[ + 'call' => 'asyncapi', + 'with' => [ + 'operation' => 'x', + ], + ], CallAsyncApi::class]; + yield 'call function' => [[ + 'call' => 'myFunction', + 'with' => [ + 'a' => 1, + ], + ], CallFunction::class]; + yield 'do' => [[ + 'do' => [], + ], DoTask::class]; + yield 'for' => [[ + 'for' => [ + 'in' => '.items', + ], + 'do' => [], + ], ForTask::class]; + yield 'fork' => [[ + 'fork' => [ + 'branches' => [], + ], + ], ForkTask::class]; + yield 'emit' => [[ + 'emit' => [ + 'event' => [], + ], + ], EmitTask::class]; + yield 'listen' => [[ + 'listen' => [ + 'to' => [ + 'any' => [], + ], + ], + ], ListenTask::class]; + yield 'raise' => [[ + 'raise' => [ + 'error' => 'x', + ], + ], RaiseTask::class]; + yield 'run' => [[ + 'run' => [ + 'shell' => [ + 'command' => 'ls', + ], + ], + ], RunTask::class]; + yield 'set' => [[ + 'set' => [ + 'a' => 1, + ], + ], SetTask::class]; + yield 'switch' => [[ + 'switch' => [], + ], SwitchTask::class]; + yield 'try' => [[ + 'try' => [], + 'catch' => [], + ], TryTask::class]; + yield 'wait' => [[ + 'wait' => 'PT1S', + ], WaitTask::class]; + } + + /** + * @param mixed[] $data + * @param class-string $expectedClass + */ + #[DataProvider('tasks')] + public function testResolvesTheTaskType(array $data, string $expectedClass): void + { + $task = Task::fromArray($data); + + self::assertInstanceOf($expectedClass, $task); + self::assertSame($expectedClass, Task::resolveTaskClass($data)); + self::assertEquals($data, $task->toArray()); + } + + public function testRejectsUnknownTasks(): void + { + $this->expectException(InvalidArgumentException::class); + Task::fromArray([ + 'unknown' => true, + ]); + } + + public function testCallTasksKeepTheirCallName(): void + { + $task = new CallHttp(); + self::assertSame('http', $task->call); + + $function = new CallFunction([ + 'call' => 'log', + ]); + self::assertSame('log', $function->call); + } + + public function testSerializesBaseProperticesAroundTheTaskBody(): void + { + $task = Task::fromArray([ + 'then' => 'end', + 'set' => [ + 'a' => 1, + ], + 'if' => '.a == 1', + 'output' => [ + 'as' => '.a', + ], + ]); + + self::assertSame(['if', 'set', 'output', 'then'], array_keys($task->toArray())); + } +} diff --git a/tests/Unit/WorkflowTest.php b/tests/Unit/WorkflowTest.php index b88af8e..661f98f 100644 --- a/tests/Unit/WorkflowTest.php +++ b/tests/Unit/WorkflowTest.php @@ -18,95 +18,202 @@ * limitations under the License. */ -use PHPUnit\Framework\TestCase; +namespace Serverless\Workflow\Tests\Unit; -use Serverless\Workflow\Action; -use Serverless\Workflow\ActionDataFilter; -use Serverless\Workflow\FunctionDef; -use Serverless\Workflow\FunctionRef; -use Serverless\Workflow\OperationState; +use PHPUnit\Framework\TestCase; +use Serverless\Workflow\Components; +use Serverless\Workflow\Document; +use Serverless\Workflow\Duration; +use Serverless\Workflow\Endpoint; +use Serverless\Workflow\Task\Call\HttpArguments; +use Serverless\Workflow\Task\CallHttp; +use Serverless\Workflow\Task\SetTask; +use Serverless\Workflow\Task\TryTask; +use Serverless\Workflow\TaskList; +use Serverless\Workflow\Timeout; use Serverless\Workflow\Workflow; +use Serverless\Workflow\WorkflowValidationException; use Serverless\Workflow\WorkflowValidator; final class WorkflowTest extends TestCase { - public string $json; + private string $json; - public string $yaml; + private string $yaml; - public Workflow $workflow; + private Workflow $workflow; protected function setUp(): void { - $this->json = file_get_contents(__DIR__ . '/../Fixtures/workflow.json'); - $this->yaml = file_get_contents(__DIR__ . '/../Fixtures/workflow.yaml'); + $json = file_get_contents(__DIR__ . '/../Fixtures/workflow.json'); + $yaml = file_get_contents(__DIR__ . '/../Fixtures/workflow.yaml'); + self::assertNotFalse($json); + self::assertNotFalse($yaml); + + $this->json = $json; + $this->yaml = $yaml; $this->workflow = Workflow::fromYaml($this->yaml); } public function testBuild(): void { $workflow = new Workflow([ - 'id' => 'greeting', - 'name' => 'Greeting Workflow', - 'description' => 'Greet Someone', - 'version' => '1.0', - 'specVersion' => '0.8', - 'start' => 'Greet', - 'states' => [ - new OperationState([ - 'name' => 'Greet', - 'type' => 'operation', - 'actions' => [ - new Action([ - 'functionRef' => new FunctionRef([ - 'refName' => 'greetingFunction', - 'arguments' => [ - 'name' => '${ .person.name }', + 'document' => new Document([ + 'dsl' => '1.0.1', + 'namespace' => 'examples', + 'name' => 'buy-pet', + 'version' => '0.1.0', + ]), + 'use' => new Components([ + 'timeouts' => [ + 'short' => new Timeout([ + 'after' => new Duration([ + 'seconds' => 30, + ]), + ]), + ], + ]), + 'do' => new TaskList([ + 'getPet' => new CallHttp([ + 'with' => new HttpArguments([ + 'method' => 'get', + 'endpoint' => new Endpoint([ + 'uri' => 'https://petstore.swagger.io/v2/pet/{petId}', + 'authentication' => [ + 'bearer' => [ + 'token' => '${ .token }', ], - ]), - 'actionDataFilter' => new ActionDataFilter([ - 'results' => '${ .greeting }', - ]), + ], ]), + ]), + 'timeout' => 'short', + ]), + 'markAsSold' => new SetTask([ + 'set' => [ + 'status' => 'sold', ], - 'end' => true, ]), - ], - 'functions' => [ - new FunctionDef([ - 'name' => 'greetingFunction', - 'operation' => 'file://myapis/greetingapis.json#greeting', + 'buyPet' => new CallHttp([ + 'with' => [ + 'method' => 'put', + 'endpoint' => 'https://petstore.swagger.io/v2/pet/{petId}', + 'body' => '${ . + { status: .status } }', + ], + 'then' => 'end', ]), - ], + ]), + 'timeout' => new Timeout([ + 'after' => 'PT5M', + ]), ]); - $this->assertEquals($this->workflow, $workflow); + + self::assertEquals($this->workflow, $workflow); } public function testToJson(): void { - $this->assertSame($this->json, $this->workflow->toJson()); + self::assertSame($this->json, $this->workflow->toJson()); } public function testFromJson(): void { - $workflow = Workflow::fromJson($this->json); - $this->assertEquals($this->workflow, $workflow); + self::assertEquals($this->workflow, Workflow::fromJson($this->json)); } public function testToYaml(): void { - $this->assertSame($this->yaml, $this->workflow->toYaml()); + self::assertSame($this->yaml, $this->workflow->toYaml()); } public function testFromYaml(): void { - $workflow = Workflow::fromYaml($this->yaml); - $this->assertEquals($this->workflow, $workflow); + self::assertEquals($this->workflow, Workflow::fromYaml($this->yaml)); + } + + public function testTypedAccess(): void + { + self::assertNotNull($this->workflow->do); + $getPet = $this->workflow->do->get('getPet'); + self::assertInstanceOf(CallHttp::class, $getPet); + self::assertSame('http', $getPet->call); + self::assertNotNull($getPet->with); + self::assertSame('get', $getPet->with->method); + self::assertInstanceOf(Endpoint::class, $getPet->with->endpoint); + self::assertNotNull($getPet->with->endpoint->authentication); + self::assertNotNull($getPet->with->endpoint->authentication->bearer); + self::assertSame('${ .token }', $getPet->with->endpoint->authentication->bearer->token); + self::assertSame('short', $getPet->timeout); + + $buyPet = $this->workflow->do->get('buyPet'); + self::assertInstanceOf(CallHttp::class, $buyPet); + self::assertNotNull($buyPet->with); + self::assertSame('https://petstore.swagger.io/v2/pet/{petId}', $buyPet->with->endpoint); + self::assertSame('end', $buyPet->then); + + self::assertSame(['getPet', 'markAsSold', 'buyPet'], $this->workflow->do->names()); + self::assertInstanceOf(Timeout::class, $this->workflow->timeout); + self::assertSame('PT5M', $this->workflow->timeout->after); + self::assertNotNull($this->workflow->use); + self::assertNotNull($this->workflow->use->timeouts); + self::assertInstanceOf(Duration::class, $this->workflow->use->timeouts['short']->after); + self::assertSame(30, $this->workflow->use->timeouts['short']->after->seconds); } public function testValidate(): void { - $this->expectNotToPerformAssertions(); - WorkflowValidator::validate($this->workflow); + $this->workflow->validate(); + + self::assertSame([], WorkflowValidator::errors($this->workflow)); + self::assertSame([], WorkflowValidator::errors($this->workflow->toArray())); + } + + public function testValidateReportsErrors(): void + { + $workflow = Workflow::fromYaml($this->yaml); + self::assertNotNull($workflow->document); + $workflow->document->version = null; + $getPet = $workflow->do?->get('getPet'); + self::assertInstanceOf(CallHttp::class, $getPet); + self::assertNotNull($getPet->with); + $getPet->with->setAdditionalProperty('unexpected', true); + + try { + $workflow->validate(); + self::fail('Expected a validation exception.'); + } catch (WorkflowValidationException $workflowValidationException) { + $errors = $workflowValidationException->getErrors(); + self::assertArrayHasKey('/document', $errors); + self::assertStringContainsString('version', implode(' ', $errors['/document'])); + self::assertArrayHasKey('/do/0/getPet/with', $errors); + self::assertStringContainsString('/document', $workflowValidationException->getMessage()); + } + } + + public function testPreservesUnknownProperties(): void + { + $workflow = Workflow::fromJson( + '{"document":{"dsl":"1.0.1","namespace":"a","name":"b","version":"1.0.0","x-custom":1},"do":[{"wait":{"wait":"PT1S","x-note":"n"}}]}' + ); + + self::assertNotNull($workflow->document); + self::assertSame([ + 'x-custom' => 1, + ], $workflow->document->getAdditionalProperties()); + self::assertSame(1, $workflow->toArray()['document']['x-custom']); + self::assertSame('n', $workflow->toArray()['do'][0]['wait']['x-note']); + self::assertArrayHasKey('/document', WorkflowValidator::errors($workflow)); + } + + public function testEmptyObjectsSerializeAsObjects(): void + { + $workflow = Workflow::fromYaml( + "document:\n dsl: '1.0.1'\n namespace: a\n name: b\n version: '1.0.0'\ndo:\n - tryIt:\n try:\n - wait:\n wait: PT1S\n catch:\n retry:\n backoff:\n exponential: {}\n" + ); + $tryIt = $workflow->do?->get('tryIt'); + + self::assertInstanceOf(TryTask::class, $tryIt); + self::assertStringContainsString('"exponential": {}', $workflow->toJson()); + self::assertStringContainsString('exponential: { }', $workflow->toYaml()); + self::assertSame([], WorkflowValidator::errors($workflow)); } }