From a2f544c58cd7cfe4178d6b25b897c9b8f6880bce Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Wed, 21 Oct 2026 20:07:16 -0500 Subject: [PATCH 1/3] Model the Open Workflow Specification DSL 1.0.1 Replaces the Serverless Workflow 0.8 object model (states, actions, functions) with typed classes for the DSL 1.0.1 document: document, input/output/export, reusable components, the named task list and every task type (call http/openapi/grpc/asyncapi/function, do, emit, for, fork, listen, raise, run, set, switch, try, wait), events, authentication policies, retries, timeouts and schedules. A small attribute-driven hydrator builds the model from arrays, resolves concrete task types from their properties and preserves unknown properties so definitions survive a parse/serialize round trip. Empty objects are serialized as objects, not empty lists. Validation now uses opis/json-schema (JSON Schema 2020-12) against the unmodified v1.0.1 schema bundled in schema/workflow.json, so it works offline. phpdocumentor/reflection-docblock and swaggest/json-schema are no longer needed. PHP 8.2 or later is required and CI pins PHP 8.3. Signed-off-by: Richard McDaniel --- .github/workflows/php.yml | 12 +- composer.json | 32 +- composer.lock | 986 ++---- schema/README.md | 10 + schema/workflow.json | 2835 +++++++++++++++++ src/Authentication/AuthenticationPolicy.php | 44 + src/Authentication/BasicAuthentication.php | 38 + src/Authentication/BearerAuthentication.php | 36 + src/Authentication/DigestAuthentication.php | 38 + src/Authentication/OAuth2Authentication.php | 29 + src/Authentication/OAuth2Client.php | 37 + src/Authentication/OAuth2Endpoints.php | 35 + .../OAuth2Properties.php} | 45 +- src/Authentication/OAuth2Token.php | 33 + .../OAuth2TokenRequest.php} | 16 +- .../OpenIdConnectAuthentication.php} | 10 +- ...ranchTimeout.php => BackoffDefinition.php} | 8 +- src/BaseObject.php | 214 +- src/CallbackState.php | 60 - src/{BearerPropsDef.php => Catalog.php} | 9 +- src/Components.php | 79 + src/Document.php | 49 + src/{RetryDef.php => Duration.php} | 19 +- src/Endpoint.php | 33 + src/Error.php | 14 +- src/{FunctionDef.php => ErrorFilter.php} | 15 +- src/Event/EventConsumptionStrategy.php | 49 + src/Event/EventCorrelation.php | 33 + src/Event/EventFilter.php | 38 + src/Event/EventProperties.php | 45 + src/Event/SubscriptionIterator.php | 42 + src/EventBasedSwitchState.php | 55 - src/EventBasedSwitchStateTimeOut.php | 28 - src/EventState.php | 57 - src/EventStateTimeout.php | 30 - src/Export.php | 36 + src/{EndDataCondition.php => Extension.php} | 13 +- src/{ErrorDef.php => ExternalResource.php} | 9 +- ...backStateTimeout.php => FlowDirective.php} | 11 +- src/ForeachState.php | 68 - src/ForeachStateTimeout.php | 30 - src/Hydration/ArrayConvertible.php | 31 + src/Hydration/Hydratable.php | 29 + src/Hydration/Hydrator.php | 132 + src/Hydration/ListOf.php | 38 + src/Hydration/MapOf.php | 38 + src/Hydration/NamedListOf.php | 38 + src/InjectState.php | 57 - src/Input.php | 36 + src/NamedList.php | 168 + src/OnEvents.php | 34 - src/OperationState.php | 60 - src/OperationStateTimeout.php | 28 - src/Output.php | 36 + src/ParallelState.php | 62 - src/ParallelStateTimeout.php | 28 - src/ProduceEventDef.php | 32 - src/RetryBackoff.php | 33 + src/{ActionDataFilter.php => RetryJitter.php} | 13 +- src/{CorrelationDef.php => RetryLimit.php} | 9 +- ...asicPropsDef.php => RetryLimitAttempt.php} | 11 +- src/{WorkflowTimeout.php => RetryPolicy.php} | 17 +- src/Schedule.php | 13 +- src/Schema.php | 33 + src/Serialization/Serializer.php | 85 + src/Sleep.php | 28 - src/SleepState.php | 56 - src/SleepStateTimeout.php | 26 - src/StartDef.php | 28 - src/State.php | 66 - src/StateExecTimeout.php | 28 - .../Call/AsyncApiArguments.php} | 35 +- src/Task/Call/AsyncApiMessage.php | 39 + .../Call/AsyncApiMessageConsumptionPolicy.php | 38 + .../Call/AsyncApiServer.php} | 20 +- src/Task/Call/AsyncApiSubscription.php | 36 + .../Call/GrpcArguments.php} | 27 +- .../Call/GrpcService.php} | 18 +- src/Task/Call/HttpArguments.php | 57 + .../Call/OpenApiArguments.php} | 27 +- src/{End.php => Task/CallAsyncApi.php} | 24 +- src/Task/CallFunction.php | 34 + src/{AuthDef.php => Task/CallGrpc.php} | 17 +- .../CallHttp.php} | 18 +- src/{EventDef.php => Task/CallOpenApi.php} | 30 +- .../CallTask.php} | 9 +- src/Task/CatchErrors.php | 32 + src/Task/DoTask.php | 31 + src/Task/EmitConfiguration.php | 31 + src/Task/EmitEvent.php | 32 + src/{Metadata.php => Task/EmitTask.php} | 8 +- .../ForConfiguration.php} | 18 +- src/Task/ForTask.php | 35 + src/Task/ForkConfiguration.php | 34 + src/Task/ForkTask.php | 29 + src/Task/ListenConfiguration.php | 37 + src/Task/ListenTask.php | 33 + src/Task/RaiseConfiguration.php | 32 + .../RaiseTask.php} | 11 +- src/Task/Run/Container.php | 52 + src/Task/Run/ContainerLifetime.php | 37 + src/Task/Run/Script.php | 46 + src/{FunctionRef.php => Task/Run/Shell.php} | 22 +- .../Run/SubflowConfiguration.php} | 20 +- src/Task/RunConfiguration.php | 48 + src/Task/RunTask.php | 29 + src/Task/SetTask.php | 32 + .../SwitchCase.php} | 15 +- src/Task/SwitchTask.php | 36 + src/Task/Task.php | 138 + src/Task/TryCatch.php | 43 + src/Task/TryTask.php | 33 + src/Task/WaitTask.php | 31 + src/TaskList.php | 45 + src/{Serializable.php => Timeout.php} | 6 +- src/Transition.php | 31 - src/TransitionDataCondition.php | 32 - src/Workflow.php | 128 +- src/WorkflowExecTimeout.php | 30 - src/WorkflowValidationException.php | 61 + src/WorkflowValidator.php | 96 +- tests/Fixtures/workflow.json | 37 - tests/Fixtures/workflow.yaml | 26 - tests/Unit/WorkflowTest.php | 112 - 124 files changed, 6288 insertions(+), 2263 deletions(-) create mode 100644 schema/README.md create mode 100644 schema/workflow.json create mode 100644 src/Authentication/AuthenticationPolicy.php create mode 100644 src/Authentication/BasicAuthentication.php create mode 100644 src/Authentication/BearerAuthentication.php create mode 100644 src/Authentication/DigestAuthentication.php create mode 100644 src/Authentication/OAuth2Authentication.php create mode 100644 src/Authentication/OAuth2Client.php create mode 100644 src/Authentication/OAuth2Endpoints.php rename src/{OAuth2PropsDef.php => Authentication/OAuth2Properties.php} (53%) create mode 100644 src/Authentication/OAuth2Token.php rename src/{Branch.php => Authentication/OAuth2TokenRequest.php} (74%) rename src/{CronDef.php => Authentication/OpenIdConnectAuthentication.php} (78%) rename src/{BranchTimeout.php => BackoffDefinition.php} (80%) delete mode 100644 src/CallbackState.php rename src/{BearerPropsDef.php => Catalog.php} (83%) create mode 100644 src/Components.php create mode 100644 src/Document.php rename src/{RetryDef.php => Duration.php} (67%) create mode 100644 src/Endpoint.php rename src/{FunctionDef.php => ErrorFilter.php} (75%) create mode 100644 src/Event/EventConsumptionStrategy.php create mode 100644 src/Event/EventCorrelation.php create mode 100644 src/Event/EventFilter.php create mode 100644 src/Event/EventProperties.php create mode 100644 src/Event/SubscriptionIterator.php delete mode 100644 src/EventBasedSwitchState.php delete mode 100644 src/EventBasedSwitchStateTimeOut.php delete mode 100644 src/EventState.php delete mode 100644 src/EventStateTimeout.php create mode 100644 src/Export.php rename src/{EndDataCondition.php => Extension.php} (74%) rename src/{ErrorDef.php => ExternalResource.php} (83%) rename src/{CallbackStateTimeout.php => FlowDirective.php} (76%) delete mode 100644 src/ForeachState.php delete mode 100644 src/ForeachStateTimeout.php create mode 100644 src/Hydration/ArrayConvertible.php create mode 100644 src/Hydration/Hydratable.php create mode 100644 src/Hydration/Hydrator.php create mode 100644 src/Hydration/ListOf.php create mode 100644 src/Hydration/MapOf.php create mode 100644 src/Hydration/NamedListOf.php delete mode 100644 src/InjectState.php create mode 100644 src/Input.php create mode 100644 src/NamedList.php delete mode 100644 src/OnEvents.php delete mode 100644 src/OperationState.php delete mode 100644 src/OperationStateTimeout.php create mode 100644 src/Output.php delete mode 100644 src/ParallelState.php delete mode 100644 src/ParallelStateTimeout.php delete mode 100644 src/ProduceEventDef.php create mode 100644 src/RetryBackoff.php rename src/{ActionDataFilter.php => RetryJitter.php} (75%) rename src/{CorrelationDef.php => RetryLimit.php} (80%) rename src/{BasicPropsDef.php => RetryLimitAttempt.php} (79%) rename src/{WorkflowTimeout.php => RetryPolicy.php} (67%) create mode 100644 src/Schema.php create mode 100644 src/Serialization/Serializer.php delete mode 100644 src/Sleep.php delete mode 100644 src/SleepState.php delete mode 100644 src/SleepStateTimeout.php delete mode 100644 src/StartDef.php delete mode 100644 src/State.php delete mode 100644 src/StateExecTimeout.php rename src/{Action.php => Task/Call/AsyncApiArguments.php} (50%) create mode 100644 src/Task/Call/AsyncApiMessage.php create mode 100644 src/Task/Call/AsyncApiMessageConsumptionPolicy.php rename src/{EndEventCondition.php => Task/Call/AsyncApiServer.php} (72%) create mode 100644 src/Task/Call/AsyncApiSubscription.php rename src/{EventRef.php => Task/Call/GrpcArguments.php} (61%) rename src/{TransitionEventCondition.php => Task/Call/GrpcService.php} (67%) create mode 100644 src/Task/Call/HttpArguments.php rename src/{StateDataFilter.php => Task/Call/OpenApiArguments.php} (52%) rename src/{End.php => Task/CallAsyncApi.php} (68%) create mode 100644 src/Task/CallFunction.php rename src/{AuthDef.php => Task/CallGrpc.php} (74%) rename src/{DataInputSchema.php => Task/CallHttp.php} (70%) rename src/{EventDef.php => Task/CallOpenApi.php} (61%) rename src/{InjectStateTimeout.php => Task/CallTask.php} (81%) create mode 100644 src/Task/CatchErrors.php create mode 100644 src/Task/DoTask.php create mode 100644 src/Task/EmitConfiguration.php create mode 100644 src/Task/EmitEvent.php rename src/{Metadata.php => Task/EmitTask.php} (81%) rename src/{ContinueAsDef.php => Task/ForConfiguration.php} (69%) create mode 100644 src/Task/ForTask.php create mode 100644 src/Task/ForkConfiguration.php create mode 100644 src/Task/ForkTask.php create mode 100644 src/Task/ListenConfiguration.php create mode 100644 src/Task/ListenTask.php create mode 100644 src/Task/RaiseConfiguration.php rename src/{DefaultConditionDef.php => Task/RaiseTask.php} (78%) create mode 100644 src/Task/Run/Container.php create mode 100644 src/Task/Run/ContainerLifetime.php create mode 100644 src/Task/Run/Script.php rename src/{FunctionRef.php => Task/Run/Shell.php} (66%) rename src/{SubFlowRef.php => Task/Run/SubflowConfiguration.php} (67%) create mode 100644 src/Task/RunConfiguration.php create mode 100644 src/Task/RunTask.php create mode 100644 src/Task/SetTask.php rename src/{EventDataFilter.php => Task/SwitchCase.php} (74%) create mode 100644 src/Task/SwitchTask.php create mode 100644 src/Task/Task.php create mode 100644 src/Task/TryCatch.php create mode 100644 src/Task/TryTask.php create mode 100644 src/Task/WaitTask.php create mode 100644 src/TaskList.php rename src/{Serializable.php => Timeout.php} (84%) delete mode 100644 src/Transition.php delete mode 100644 src/TransitionDataCondition.php delete mode 100644 src/WorkflowExecTimeout.php create mode 100644 src/WorkflowValidationException.php delete mode 100644 tests/Fixtures/workflow.json delete mode 100644 tests/Fixtures/workflow.yaml delete mode 100644 tests/Unit/WorkflowTest.php 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/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/workflow.json b/tests/Fixtures/workflow.json deleted file mode 100644 index 73c3978..0000000 --- a/tests/Fixtures/workflow.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "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 - } - ] -} \ No newline at end of file diff --git a/tests/Fixtures/workflow.yaml b/tests/Fixtures/workflow.yaml deleted file mode 100644 index b28fb98..0000000 --- a/tests/Fixtures/workflow.yaml +++ /dev/null @@ -1,26 +0,0 @@ -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 diff --git a/tests/Unit/WorkflowTest.php b/tests/Unit/WorkflowTest.php deleted file mode 100644 index b88af8e..0000000 --- a/tests/Unit/WorkflowTest.php +++ /dev/null @@ -1,112 +0,0 @@ -json = file_get_contents(__DIR__ . '/../Fixtures/workflow.json'); - $this->yaml = file_get_contents(__DIR__ . '/../Fixtures/workflow.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 }', - ], - ]), - 'actionDataFilter' => new ActionDataFilter([ - 'results' => '${ .greeting }', - ]), - ]), - ], - 'end' => true, - ]), - ], - 'functions' => [ - new FunctionDef([ - 'name' => 'greetingFunction', - 'operation' => 'file://myapis/greetingapis.json#greeting', - ]), - ], - ]); - $this->assertEquals($this->workflow, $workflow); - } - - public function testToJson(): void - { - $this->assertSame($this->json, $this->workflow->toJson()); - } - - public function testFromJson(): void - { - $workflow = Workflow::fromJson($this->json); - $this->assertEquals($this->workflow, $workflow); - } - - public function testToYaml(): void - { - $this->assertSame($this->yaml, $this->workflow->toYaml()); - } - - public function testFromYaml(): void - { - $workflow = Workflow::fromYaml($this->yaml); - $this->assertEquals($this->workflow, $workflow); - } - - public function testValidate(): void - { - $this->expectNotToPerformAssertions(); - WorkflowValidator::validate($this->workflow); - } -} From 6f8a748d1c5329e578adf2259a6fd4e6a6a75277 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Wed, 21 Oct 2026 20:07:19 -0500 Subject: [PATCH 2/3] Test the SDK against every specification example Copies the 63 workflow examples of specification v1.0.1 as fixtures. Each one is parsed, serialized to arrays, JSON and YAML without loss, validated against the bundled schema and checked to hydrate typed tasks. Unit tests cover task type resolution, named task lists, programmatic building, typed navigation, validation errors and the preservation of unknown properties. Signed-off-by: Richard McDaniel --- tests/Fixtures/examples/README.md | 5 + .../examples/accumulate-room-readings.yaml | 44 ++++ .../authentication-bearer-uri-format.yaml | 15 ++ .../examples/authentication-bearer.yaml | 15 ++ .../authentication-oauth2-secret.yaml | 18 ++ .../examples/authentication-oauth2.yaml | 22 ++ .../examples/authentication-oidc-secret.yaml | 18 ++ .../examples/authentication-oidc.yaml | 19 ++ .../examples/authentication-reusable.yaml | 19 ++ .../examples/call-asyncapi-publish.yaml | 20 ++ ...all-asyncapi-subscribe-consume-amount.yaml | 17 ++ ...api-subscribe-consume-forever-foreach.yaml | 28 +++ ...call-asyncapi-subscribe-consume-until.yaml | 18 ++ ...call-asyncapi-subscribe-consume-while.yaml | 16 ++ .../call-custom-function-cataloged.yaml | 13 ++ .../examples/call-custom-function-inline.yaml | 25 ++ tests/Fixtures/examples/call-grpc.yaml | 18 ++ ...http-endpoint-interpolation-shorthand.yaml | 11 + .../call-http-endpoint-interpolation.yaml | 13 ++ .../call-http-query-headers-expressions.yaml | 31 +++ .../examples/call-http-query-parameters.yaml | 25 ++ .../Fixtures/examples/call-http-redirect.yaml | 26 +++ .../examples/call-openapi-redirect.yaml | 15 ++ tests/Fixtures/examples/call-openapi.yaml | 14 ++ tests/Fixtures/examples/conditional-task.yaml | 22 ++ tests/Fixtures/examples/do-multiple.yaml | 17 ++ tests/Fixtures/examples/do-single.yaml | 11 + tests/Fixtures/examples/emit.yaml | 19 ++ tests/Fixtures/examples/for.yaml | 21 ++ tests/Fixtures/examples/fork.yaml | 26 +++ .../examples/listen-to-all read-envelope.yaml | 17 ++ tests/Fixtures/examples/listen-to-all.yaml | 16 ++ .../examples/listen-to-any-filter.yaml | 16 ++ .../listen-to-any-forever-foreach.yaml | 22 ++ .../listen-to-any-until-condition.yaml | 11 + .../listen-to-any-until-consumed.yaml | 20 ++ tests/Fixtures/examples/listen-to-any.yaml | 10 + tests/Fixtures/examples/listen-to-one.yaml | 18 ++ .../examples/mock-service-extension.yaml | 27 +++ tests/Fixtures/examples/raise-inline.yaml | 13 ++ tests/Fixtures/examples/raise-reusable.yaml | 16 ++ .../run-container-cleanup-always.yaml | 12 + .../run-container-cleanup-eventually.yaml | 14 ++ .../examples/run-container-with-name.yaml | 11 + tests/Fixtures/examples/run-container.yaml | 10 + tests/Fixtures/examples/run-return-all.yaml | 11 + tests/Fixtures/examples/run-return-code.yaml | 11 + tests/Fixtures/examples/run-return-none.yaml | 11 + .../Fixtures/examples/run-return-stderr.yaml | 11 + .../examples/run-script-with-arguments.yaml | 14 ++ tests/Fixtures/examples/run-subflow.yaml | 14 ++ tests/Fixtures/examples/schedule-cron.yaml | 13 ++ .../examples/schedule-event-driven.yaml | 24 ++ tests/Fixtures/examples/set-expression.yaml | 13 ++ tests/Fixtures/examples/set.yaml | 14 ++ .../examples/star-wars-homeworld.yaml | 33 +++ .../Fixtures/examples/switch-then-string.yaml | 31 +++ .../examples/try-catch-retry-inline.yaml | 26 +++ .../examples/try-catch-retry-reusable.yaml | 30 +++ tests/Fixtures/examples/try-catch-then.yaml | 39 ++++ tests/Fixtures/examples/try-catch.yaml | 18 ++ .../examples/wait-duration-inline.yaml | 9 + .../examples/wait-duration-iso8601.yaml | 8 + tests/Fixtures/workflow.json | 57 +++++ tests/Fixtures/workflow.yaml | 33 +++ tests/Unit/ExamplesTest.php | 89 +++++++ tests/Unit/TaskListTest.php | 119 ++++++++++ tests/Unit/TaskTest.php | 185 +++++++++++++++ tests/Unit/WorkflowTest.php | 219 ++++++++++++++++++ 69 files changed, 1846 insertions(+) create mode 100644 tests/Fixtures/examples/README.md create mode 100644 tests/Fixtures/examples/accumulate-room-readings.yaml create mode 100644 tests/Fixtures/examples/authentication-bearer-uri-format.yaml create mode 100644 tests/Fixtures/examples/authentication-bearer.yaml create mode 100644 tests/Fixtures/examples/authentication-oauth2-secret.yaml create mode 100644 tests/Fixtures/examples/authentication-oauth2.yaml create mode 100644 tests/Fixtures/examples/authentication-oidc-secret.yaml create mode 100644 tests/Fixtures/examples/authentication-oidc.yaml create mode 100644 tests/Fixtures/examples/authentication-reusable.yaml create mode 100644 tests/Fixtures/examples/call-asyncapi-publish.yaml create mode 100644 tests/Fixtures/examples/call-asyncapi-subscribe-consume-amount.yaml create mode 100644 tests/Fixtures/examples/call-asyncapi-subscribe-consume-forever-foreach.yaml create mode 100644 tests/Fixtures/examples/call-asyncapi-subscribe-consume-until.yaml create mode 100644 tests/Fixtures/examples/call-asyncapi-subscribe-consume-while.yaml create mode 100644 tests/Fixtures/examples/call-custom-function-cataloged.yaml create mode 100644 tests/Fixtures/examples/call-custom-function-inline.yaml create mode 100644 tests/Fixtures/examples/call-grpc.yaml create mode 100644 tests/Fixtures/examples/call-http-endpoint-interpolation-shorthand.yaml create mode 100644 tests/Fixtures/examples/call-http-endpoint-interpolation.yaml create mode 100644 tests/Fixtures/examples/call-http-query-headers-expressions.yaml create mode 100644 tests/Fixtures/examples/call-http-query-parameters.yaml create mode 100644 tests/Fixtures/examples/call-http-redirect.yaml create mode 100644 tests/Fixtures/examples/call-openapi-redirect.yaml create mode 100644 tests/Fixtures/examples/call-openapi.yaml create mode 100644 tests/Fixtures/examples/conditional-task.yaml create mode 100644 tests/Fixtures/examples/do-multiple.yaml create mode 100644 tests/Fixtures/examples/do-single.yaml create mode 100644 tests/Fixtures/examples/emit.yaml create mode 100644 tests/Fixtures/examples/for.yaml create mode 100644 tests/Fixtures/examples/fork.yaml create mode 100644 tests/Fixtures/examples/listen-to-all read-envelope.yaml create mode 100644 tests/Fixtures/examples/listen-to-all.yaml create mode 100644 tests/Fixtures/examples/listen-to-any-filter.yaml create mode 100644 tests/Fixtures/examples/listen-to-any-forever-foreach.yaml create mode 100644 tests/Fixtures/examples/listen-to-any-until-condition.yaml create mode 100644 tests/Fixtures/examples/listen-to-any-until-consumed.yaml create mode 100644 tests/Fixtures/examples/listen-to-any.yaml create mode 100644 tests/Fixtures/examples/listen-to-one.yaml create mode 100644 tests/Fixtures/examples/mock-service-extension.yaml create mode 100644 tests/Fixtures/examples/raise-inline.yaml create mode 100644 tests/Fixtures/examples/raise-reusable.yaml create mode 100644 tests/Fixtures/examples/run-container-cleanup-always.yaml create mode 100644 tests/Fixtures/examples/run-container-cleanup-eventually.yaml create mode 100644 tests/Fixtures/examples/run-container-with-name.yaml create mode 100644 tests/Fixtures/examples/run-container.yaml create mode 100644 tests/Fixtures/examples/run-return-all.yaml create mode 100644 tests/Fixtures/examples/run-return-code.yaml create mode 100644 tests/Fixtures/examples/run-return-none.yaml create mode 100644 tests/Fixtures/examples/run-return-stderr.yaml create mode 100644 tests/Fixtures/examples/run-script-with-arguments.yaml create mode 100644 tests/Fixtures/examples/run-subflow.yaml create mode 100644 tests/Fixtures/examples/schedule-cron.yaml create mode 100644 tests/Fixtures/examples/schedule-event-driven.yaml create mode 100644 tests/Fixtures/examples/set-expression.yaml create mode 100644 tests/Fixtures/examples/set.yaml create mode 100644 tests/Fixtures/examples/star-wars-homeworld.yaml create mode 100644 tests/Fixtures/examples/switch-then-string.yaml create mode 100644 tests/Fixtures/examples/try-catch-retry-inline.yaml create mode 100644 tests/Fixtures/examples/try-catch-retry-reusable.yaml create mode 100644 tests/Fixtures/examples/try-catch-then.yaml create mode 100644 tests/Fixtures/examples/try-catch.yaml create mode 100644 tests/Fixtures/examples/wait-duration-inline.yaml create mode 100644 tests/Fixtures/examples/wait-duration-iso8601.yaml create mode 100644 tests/Fixtures/workflow.json create mode 100644 tests/Fixtures/workflow.yaml create mode 100644 tests/Unit/ExamplesTest.php create mode 100644 tests/Unit/TaskListTest.php create mode 100644 tests/Unit/TaskTest.php create mode 100644 tests/Unit/WorkflowTest.php 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 new file mode 100644 index 0000000..117f69a --- /dev/null +++ b/tests/Fixtures/workflow.json @@ -0,0 +1,57 @@ +{ + "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" + } +} \ No newline at end of file diff --git a/tests/Fixtures/workflow.yaml b/tests/Fixtures/workflow.yaml new file mode 100644 index 0000000..d08c0c9 --- /dev/null +++ b/tests/Fixtures/workflow.yaml @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..661f98f --- /dev/null +++ b/tests/Unit/WorkflowTest.php @@ -0,0 +1,219 @@ +json = $json; + $this->yaml = $yaml; + $this->workflow = Workflow::fromYaml($this->yaml); + } + + public function testBuild(): void + { + $workflow = new Workflow([ + '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 }', + ], + ], + ]), + ]), + 'timeout' => 'short', + ]), + 'markAsSold' => new SetTask([ + 'set' => [ + 'status' => 'sold', + ], + ]), + 'buyPet' => new CallHttp([ + 'with' => [ + 'method' => 'put', + 'endpoint' => 'https://petstore.swagger.io/v2/pet/{petId}', + 'body' => '${ . + { status: .status } }', + ], + 'then' => 'end', + ]), + ]), + 'timeout' => new Timeout([ + 'after' => 'PT5M', + ]), + ]); + + self::assertEquals($this->workflow, $workflow); + } + + public function testToJson(): void + { + self::assertSame($this->json, $this->workflow->toJson()); + } + + public function testFromJson(): void + { + self::assertEquals($this->workflow, Workflow::fromJson($this->json)); + } + + public function testToYaml(): void + { + self::assertSame($this->yaml, $this->workflow->toYaml()); + } + + public function testFromYaml(): void + { + 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->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)); + } +} From 7b2928a53e986b457534712688216c3dd7b54ce3 Mon Sep 17 00:00:00 2001 From: Richard McDaniel Date: Wed, 21 Oct 2026 20:07:21 -0500 Subject: [PATCH 3/3] Document the DSL 1.0 SDK Signed-off-by: Richard McDaniel --- README.md | 147 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 104 insertions(+), 43 deletions(-) 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)