diff --git a/examples/valid/arazzo/wikimedia.json b/examples/valid/arazzo/wikimedia.json new file mode 100644 index 00000000..7eeac11f --- /dev/null +++ b/examples/valid/arazzo/wikimedia.json @@ -0,0 +1,111 @@ +{ + "arazzo": "1.0.1", + "info": { + "title": "Wikimedia Math API", + "version": "1.0.0", + "description": "Based on Jentic automatically generated Arazzo specification but adapted by humans to make it actually work" + }, + "sourceDescriptions": [ + { + "name": "openapi_source", + "url": "./wikimedia/openapi.json", + "type": "openapi" + } + ], + "workflows": [ + { + "workflowId": "render-mathematical-formula", + "summary": "Checks a TeX formula for validity and correctness, then renders it into a specified visual format (SVG, MML, or PNG). This is useful for displaying mathematical notation dynamically.", + "description": "Checks a TeX formula for validity and correctness, then renders it into a specified visual format (SVG, MML, or PNG). This is useful for displaying mathematical notation dynamically.", + "inputs": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["tex", "inline-tex", "chem"], + "default": "tex" + }, + "formula": { + "type": "string" + }, + "format": { + "type": "string", + "enum": ["svg", "mml", "png"], + "default": "svg" + }, + "email": { + "type": "string", + "description": "Wikimedia API usage policy asks for an email or url to prevent abuse. Please use this fairly. https://foundation.wikimedia.org/wiki/Policy:Wikimedia_Foundation_User-Agent_Policy" + } + } + }, + "steps": [ + { + "stepId": "check-formula", + "description": "Submits a TeX or inline-TeX formula for validation and normalization. Returns formula information and a hash identifier in the 'x-resource-location' header for rendering.", + "operationPath": "openapi_source#/paths/~1media~1math~1check~1{type}/post", + "parameters": [ + { + "name": "type", + "in": "path", + "value": "$inputs.type" + }, + { + "name": "User-Agent", + "in": "header", + "value": "Arazzo Workflow (From:$inputs.email)" + } + ], + "requestBody": { + "contentType": "multipart/form-data", + "payload": { + "q": "$inputs.formula" + } + }, + "successCriteria": [ + { + "condition": "$statusCode == 200" + }, + { + "condition": "$response.body#/success == true" + } + ], + "outputs": { + "formula_info": "$response.body", + "formula_hash": "$response.header.x-resource-location" + } + }, + { + "stepId": "render-formula", + "description": "Retrieves the rendered representation (SVG, MML, or PNG) of a previously checked formula using its hash.", + "operationPath": "openapi_source#/paths/~1media~1math~1render~1{format}~1{hash}/get", + "parameters": [ + { + "name": "format", + "in": "path", + "value": "$inputs.format" + }, + { + "name": "hash", + "in": "path", + "value": "$steps.check-formula.outputs.formula_hash" + } + ], + "successCriteria": [ + { + "condition": "$statusCode == 200" + } + ], + "outputs": { + "rendered_formula": "$response.body" + } + } + ], + "outputs": { + "formula_info": "$steps.check-formula.outputs.formula_info", + "formula_hash": "$steps.check-formula.outputs.formula_hash", + "rendered_formula": "$steps.render-formula.outputs.rendered_formula" + } + } + ] +} diff --git a/examples/valid/arazzo/wikimedia/openapi.json b/examples/valid/arazzo/wikimedia/openapi.json new file mode 100644 index 00000000..ef1cc899 --- /dev/null +++ b/examples/valid/arazzo/wikimedia/openapi.json @@ -0,0 +1,483 @@ +{ + "openapi": "3.0.1", + "info": { + "version": "1.0.0", + "title": "Wikimedia REST API", + "description": "This API provides cacheable and straightforward access to Wikimedia content and data, in machine-readable formats.\n### Global Rules\n- Limit your clients to no more than 200 requests/s to this API.\n Each API endpoint's documentation may detail more specific usage limits.\n- Set a unique `User-Agent` or `Api-User-Agent` header that\n allows us to contact you quickly. Email addresses or URLs\n of contact pages work well.\n\nBy using this API, you agree to Wikimedia's [Terms of Use](https://wikimediafoundation.org/wiki/Terms_of_Use) and [Privacy Policy](https://wikimediafoundation.org/wiki/Privacy_policy). Unless otherwise specified in the endpoint documentation below, content accessed via this API is licensed under the [CC-BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) and [GFDL](https://www.gnu.org/copyleft/fdl.html) licenses, and you irrevocably agree to release modifications or additions made through this API under these licenses. See https://www.mediawiki.org/wiki/REST_API for background and details.\n ### Endpoint documentation\nPlease consult each endpoint's documentation for details on:\n- Licensing information for the specific type of content\n and data served via the endpoint.\n- Stability markers to inform you about development status and\n change policy, according to\n [our API version policy](https://www.mediawiki.org/wiki/API_versioning).\n - Endpoint specific usage limits.\n### Metrics endpoints\nFor documentation for `/metrics` endpoints, including pageviews, unique devices, edited pages, editors, edits, registered users, bytes difference, and mediarequests data, see the [Wikimedia Analytics API documentation](https://doc.wikimedia.org/analytics-api).\n", + "termsOfService": "https://wikimediafoundation.org/wiki/Terms_of_Use", + "contact": { + "name": "the Wikimedia Services team", + "url": "http://mediawiki.org/wiki/REST_API" + }, + "license": { + "name": "Software available under the Apache 2 license", + "url": "http://www.apache.org/licenses/LICENSE-2.0" + }, + "x-jentic-source-url": "https://wikimedia.org/api/rest_v1/?spec" + }, + "servers": [ + { + "url": "https://wikimedia.org/api/rest_v1" + } + ], + "paths": { + "/media/math/check/{type}": { + "post": { + "tags": [ + "Math" + ], + "summary": "Check and normalize a TeX formula.", + "description": "Checks the supplied TeX formula for correctness and returns the\n normalised formula representation as well as information about\nidentifiers. Available types are tex and inline-tex. The response\ncontains the `x-resource-location` header which can be used to retrieve\nthe render of the checked formula in one of the supported rendering\nformats. Just append the value of the header to `/media/math/{format}/`\nand perform a GET request against that URL.\n\n Stability: [stable](https://www.mediawiki.org/wiki/API_versioning#Stable).\n", + "parameters": [ + { + "name": "type", + "in": "path", + "description": "The input type of the given formula; can be tex or inline-tex", + "required": true, + "schema": { + "type": "string", + "enum": [ + "tex", + "inline-tex", + "chem" + ] + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "q" + ], + "properties": { + "q": { + "type": "string", + "description": "The formula to check" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Information about the checked formula", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "description": "Invalid type", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/problem" + } + } + } + }, + "default": { + "description": "Error", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/problem" + } + } + } + } + }, + "x-monitor": true, + "x-amples": [ + { + "title": "Mathoid - check test formula", + "request": { + "params": { + "domain": "wikimedia.org", + "type": "tex" + }, + "body": { + "q": "E=mc^{2}" + } + }, + "response": { + "status": 200, + "headers": { + "content-type": "/^application\\/json/", + "x-resource-location": "/.+/", + "cache-control": "no-cache" + }, + "body": { + "success": true, + "checked": "/.+/" + } + } + } + ] + } + }, + "/media/math/formula/{hash}": { + "get": { + "tags": [ + "Math" + ], + "summary": "Get a previously-stored formula", + "description": "Returns the previously-stored formula via `/media/math/check/{type}` for\nthe given hash.\n\nStability: [stable](https://www.mediawiki.org/wiki/API_versioning#Stable).\n", + "parameters": [ + { + "name": "hash", + "in": "path", + "description": "The hash string of the previous POST data", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Information about the checked formula", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "404": { + "description": "Data for the given hash cannot be found", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/problem" + } + } + } + }, + "default": { + "description": "Error", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/problem" + } + } + } + } + }, + "x-monitor": false + } + }, + "/media/math/render/{format}/{hash}": { + "get": { + "tags": [ + "Math" + ], + "summary": "Get rendered formula in the given format.", + "description": "Given a request hash, renders a TeX formula into its mathematic\n representation in the given format. When a request is issued to the\n`/media/math/check/{format}` POST endpoint, the response contains the\n`x-resource-location` header denoting the hash ID of the POST data. Once\nobtained, this endpoint has to be used to obtain the actual render.\n\nStability: [stable](https://www.mediawiki.org/wiki/API_versioning#Stable).\n", + "parameters": [ + { + "name": "format", + "in": "path", + "description": "The output format; can be svg or mml", + "required": true, + "schema": { + "type": "string", + "enum": [ + "svg", + "mml", + "png" + ] + } + }, + { + "name": "hash", + "in": "path", + "description": "The hash string of the previous POST data", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The rendered formula", + "content": { + "image/svg+xml": { + "schema": { + "type": "string" + } + }, + "application/mathml+xml": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Unknown format or hash ID", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/problem" + } + } + } + }, + "default": { + "description": "Error", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/problem" + } + } + } + } + }, + "x-monitor": false + } + } + }, + "components": { + "schemas": { + "problem": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "instance": { + "type": "string" + } + } + }, + "originalimage": { + "type": "object", + "properties": { + "source": { + "type": "string", + "description": "Original image URI" + }, + "width": { + "type": "integer", + "description": "Original image width" + }, + "height": { + "type": "integer", + "description": "Original image height" + } + }, + "required": [ + "height", + "source", + "width" + ] + }, + "thumbnail": { + "type": "object", + "properties": { + "source": { + "type": "string", + "description": "Thumbnail image URI" + }, + "width": { + "type": "integer", + "description": "Thumbnail width" + }, + "height": { + "type": "integer", + "description": "Thumnail height" + } + }, + "required": [ + "height", + "source", + "width" + ] + }, + "titles_set": { + "type": "object", + "description": "a good example of the differences can be seen in https://en.wikipedia.org/api/rest_v1/page/summary/IOS_13", + "properties": { + "canonical": { + "type": "string", + "description": "the DB key (non-prefixed), e.g. may have _ instead of spaces, best for making request URIs, still requires Percent-encoding" + }, + "normalized": { + "type": "string", + "description": "the normalized title (https://www.mediawiki.org/wiki/API:Query#Example_2:_Title_normalization), e.g. may have spaces instead of _" + }, + "display": { + "type": "string", + "description": "the title as it should be displayed to the user" + } + }, + "required": [ + "canonical", + "normalized", + "display" + ] + }, + "summary": { + "type": "object", + "properties": { + "titles": { + "$ref": "#/components/schemas/titles_set" + }, + "title": { + "deprecated": true, + "type": "string", + "description": "The page title.\nDeprecated: Use `titles.normalized` instead.\n" + }, + "displaytitle": { + "deprecated": true, + "type": "string", + "description": "The page title how it should be shown to the user.\nDeprecated: Use `titles.display` instead.\n" + }, + "pageid": { + "type": "integer", + "description": "The page ID" + }, + "extract": { + "type": "string", + "description": "First several sentences of an article in plain text" + }, + "extract_html": { + "type": "string", + "description": "First several sentences of an article in simple HTML format" + }, + "thumbnail": { + "$ref": "#/components/schemas/thumbnail" + }, + "originalimage": { + "$ref": "#/components/schemas/originalimage" + }, + "lang": { + "type": "string", + "description": "The page language code", + "example": "en" + }, + "dir": { + "type": "string", + "description": "The page language direction code", + "example": "ltr" + }, + "timestamp": { + "type": "string", + "description": "The time when the page was last edited in the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format", + "example": {} + }, + "description": { + "type": "string", + "description": "Wikidata description for the page", + "example": "American poet" + }, + "coordinates": { + "type": "object", + "description": "The coordinates of the item", + "properties": { + "lat": { + "type": "number", + "description": "The latitude" + }, + "lon": { + "type": "number", + "description": "The longitude" + } + }, + "required": [ + "lat", + "lon" + ] + } + }, + "required": [ + "dir", + "extract", + "lang", + "titles" + ] + }, + "cx_mt": { + "type": "object", + "properties": { + "contents": { + "type": "string", + "description": "the translated content" + } + } + }, + "cx_dict": { + "type": "object", + "properties": { + "source": { + "type": "string", + "description": "the original word to look up" + }, + "translations": { + "type": "array", + "description": "the translations found", + "items": { + "type": "object", + "properties": { + "phrase": { + "type": "string", + "description": "the translated phrase" + }, + "info": { + "type": "string", + "description": "extra information about the phrase" + }, + "sources": { + "type": "string", + "description": "the source dictionary used for the translation" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "Math", + "description": "formula rendering" + } + ], + "securityDefinitions": { + "mediawiki_auth": { + "description": "Checks permissions using MW api", + "type": "apiKey", + "in": "header", + "name": "cookie", + "x-internal-request-whitelist": [ + "/http:\\/\\/[a-zA-Z0-9\\.]+\\/w\\/api\\.php/" + ] + } + }, + "x-host-basePath": "/api/rest_v1", + "x-default-params": {} +} \ No newline at end of file diff --git a/examples/valid/flower/parking.yml b/examples/valid/flower/parking.yml index 5d185903..ad2b1e2d 100644 --- a/examples/valid/flower/parking.yml +++ b/examples/valid/flower/parking.yml @@ -45,7 +45,7 @@ flows: url: https://data.angers.fr/api/explore/v2.1/catalog/datasets/parking-angers/records query: limit: "20" - refine: nom%3A"$inputs.parking" + refine: 'nom:"$inputs.parking"' outputs: remaining_slots: $response.body.results.0.disponible diff --git a/src/core/schemas/arazzo-schemas/index.ts b/src/core/schemas/arazzo-schemas/index.ts new file mode 100644 index 00000000..3b1267b6 --- /dev/null +++ b/src/core/schemas/arazzo-schemas/index.ts @@ -0,0 +1,11 @@ +import type {JSONSchema7} from 'json-schema' + +// Spec definitions are copied from the official +// spec repo https://spec.openapis.org/arazzo/ +import schemaV10 from './v1.0/schema.json' with {type: 'json'} + +export default { + schemas: { + '1.0': schemaV10 as JSONSchema7, + }, +} diff --git a/src/core/schemas/arazzo-schemas/v1.0/schema.json b/src/core/schemas/arazzo-schemas/v1.0/schema.json new file mode 100644 index 00000000..907fe2cc --- /dev/null +++ b/src/core/schemas/arazzo-schemas/v1.0/schema.json @@ -0,0 +1,792 @@ +{ + "$id": "https://spec.openapis.org/arazzo/1.0/schema/2025-10-15", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "The description of Arazzo v1.0.x documents", + "type": "object", + "properties": { + "arazzo": { + "description": "The version number of the Arazzo Specification", + "type": "string", + "pattern": "^1\\.0\\.\\d+(-.+)?$" + }, + "info": { + "$ref": "#/$defs/info" + }, + "sourceDescriptions": { + "description": "A list of source descriptions such as Arazzo or OpenAPI", + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/$defs/source-description-object" + } + }, + "workflows": { + "description": "A list of workflows", + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/$defs/workflow-object" + } + }, + "components": { + "$ref": "#/$defs/components-object" + } + }, + "required": [ + "arazzo", + "info", + "sourceDescriptions", + "workflows" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "info": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#info-object", + "description": "Provides metadata about the Arazzo description", + "type": "object", + "properties": { + "title": { + "description": "A human readable title of the Arazzo Description", + "type": "string" + }, + "summary": { + "description": "A short summary of the Arazzo Description", + "type": "string" + }, + "description": { + "description": "A description of the purpose of the workflows defined. CommonMark syntax MAY be used for rich text representation", + "type": "string" + }, + "version": { + "description": "The version identifier of the Arazzo document (which is distinct from the Arazzo Specification version)", + "type": "string" + } + }, + "required": [ + "title", + "version" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "source-description-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#source-description-object", + "description": "Describes a source description (such as an OpenAPI description)\nthat will be referenced by one or more workflows described within\nan Arazzo description", + "type": "object", + "properties": { + "name": { + "description": "A unique name for the source description", + "type": "string", + "pattern": "^[A-Za-z0-9_\\-]+$" + }, + "url": { + "description": "A URL to a source description to be used by a workflow", + "type": "string", + "format": "uri-reference" + }, + "type": { + "description": "The type of source description", + "enum": [ + "arazzo", + "openapi" + ] + } + }, + "required": [ + "name", + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "workflow-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#workflow-object", + "description": "Describes the steps to be taken across one or more APIs to achieve an objective", + "type": "object", + "properties": { + "workflowId": { + "description": "Unique string to represent the workflow", + "$anchor": "workflowId", + "type": "string" + }, + "summary": { + "description": "A summary of the purpose or objective of the workflow", + "type": "string" + }, + "description": { + "description": "A description of the workflow. CommonMark syntax MAY be used for rich text representation", + "type": "string" + }, + "inputs": { + "description": "A JSON Schema 2020-12 object representing the input parameters used by this workflow", + "$ref": "#/$defs/schema" + }, + "dependsOn": { + "description": "A list of workflows that MUST be completed before this workflow can be processed", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "steps": { + "description": "An ordered list of steps where each step represents a call to an API operation or to another workflow", + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/$defs/step-object" + } + }, + "successActions": { + "description": "A list of success actions that are applicable for all steps described under this workflow", + "type": "array", + "uniqueItems": true, + "items": { + "oneOf": [ + { + "$ref": "#/$defs/success-action-object" + }, + { + "$ref": "#/$defs/reusable-object" + } + ] + } + }, + "failureActions": { + "description": "A list of failure actions that are applicable for all steps described under this workflow", + "type": "array", + "uniqueItems": true, + "items": { + "oneOf": [ + { + "$ref": "#/$defs/failure-action-object" + }, + { + "$ref": "#/$defs/reusable-object" + } + ] + } + }, + "outputs": { + "description": "A map between a friendly name and a dynamic output value", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "type": "string" + } + } + }, + "parameters": { + "description": "A list of parameters that are applicable for all steps described under this workflow", + "type": "array", + "uniqueItems": true, + "items": { + "oneOf": [ + { + "$ref": "#/$defs/parameter-object" + }, + { + "$ref": "#/$defs/reusable-object" + } + ] + } + } + }, + "required": [ + "workflowId", + "steps" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "step-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#step-object'", + "description": "Describes a single workflow step which MAY be a call to an\nAPI operation (OpenAPI Operation Object or another Workflow Object)", + "type": "object", + "properties": { + "stepId": { + "description": "Unique string to represent the step", + "$anchor": "stepId", + "type": "string" + }, + "description": { + "description": "A description of the step. CommonMark syntax MAY be used for rich text representation", + "type": "string" + }, + "operationId": { + "description": "The name of an existing, resolvable operation, as defined with a unique operationId and existing within one of the sourceDescriptions", + "type": "string" + }, + "operationPath": { + "description": "A reference to a Source combined with a JSON Pointer to reference an operation", + "type": "string" + }, + "workflowId": { + "description": "The workflowId referencing an existing workflow within the Arazzo description", + "$ref": "#workflowId" + }, + "parameters": { + "description": "A list of parameters that MUST be passed to an operation or workflow as referenced by operationId, operationPath, or workflowId", + "type": "array", + "uniqueItems": true, + "items": true + }, + "requestBody": { + "$ref": "#/$defs/request-body-object" + }, + "successCriteria": { + "description": "A list of assertions to determine the success of the step", + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/$defs/criterion-object" + } + }, + "onSuccess": { + "description": "An array of success action objects that specify what to do upon step success", + "type": "array", + "uniqueItems": true, + "items": { + "oneOf": [ + { + "$ref": "#/$defs/success-action-object" + }, + { + "$ref": "#/$defs/reusable-object" + } + ] + } + }, + "onFailure": { + "description": "An array of failure action objects that specify what to do upon step failure", + "type": "array", + "uniqueItems": true, + "items": { + "oneOf": [ + { + "$ref": "#/$defs/failure-action-object" + }, + { + "$ref": "#/$defs/reusable-object" + } + ] + } + }, + "outputs": { + "description": "A map between a friendly name and a dynamic output value defined using a runtime expression", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "type": "string" + } + } + } + }, + "required": [ + "stepId" + ], + "oneOf": [ + { + "required": [ + "operationId" + ] + }, + { + "required": [ + "operationPath" + ] + }, + { + "required": [ + "workflowId" + ] + } + ], + "allOf": [ + { + "if": { + "oneOf": [ + { + "required": [ + "operationPath" + ] + }, + { + "required": [ + "operationId" + ] + } + ] + }, + "then": { + "properties": { + "parameters": { + "items": { + "oneOf": [ + { + "$ref": "#/$defs/reusable-object" + }, + { + "$ref": "#/$defs/parameter-object", + "required": [ + "in" + ] + } + ] + } + } + } + } + }, + { + "if": { + "required": [ + "workflowId" + ] + }, + "then": { + "properties": { + "parameters": { + "items": { + "oneOf": [ + { + "$ref": "#/$defs/parameter-object" + }, + { + "$ref": "#/$defs/reusable-object" + } + ] + } + } + } + } + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "request-body-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#request-body-object", + "description": "The request body to pass to an operation as referenced by operationId or operationPath", + "type": "object", + "properties": { + "contentType": { + "description": "The Content-Type for the request content", + "type": "string" + }, + "payload": true, + "replacements": { + "description": "A list of locations and values to set within a payload", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/payload-replacement-object" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "criterion-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#criterion-object", + "description": "An object used to specify the context, conditions, and condition types\nthat can be used to prove or satisfy assertions specified in Step Object successCriteria,\nSuccess Action Object criteria, and Failure Action Object criteria", + "type": "object", + "properties": { + "context": { + "description": "A runtime expression used to set the context for the condition to be applied on", + "type": "string" + }, + "condition": { + "description": "The condition to apply", + "type": "string" + } + }, + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "description": "The type of condition to be applied", + "enum": [ + "simple", + "regex", + "jsonpath", + "xpath" + ], + "default": "simple" + } + } + }, + { + "$ref": "#/$defs/criterion-expression-type-object" + } + ], + "required": [ + "condition" + ], + "dependentRequired": { + "type": [ + "context" + ] + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "criterion-expression-type-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#criterion-expression-type-object", + "description": "An object used to describe the type and version of an expression used within a Criterion Object", + "type": "object", + "properties": { + "type": { + "description": "The type of condition to be applied", + "enum": [ + "jsonpath", + "xpath" + ] + }, + "version": { + "description": "A short hand string representing the version of the expression type", + "type": "string" + } + }, + "required": [ + "type", + "version" + ], + "allOf": [ + { + "if": { + "required": [ + "type" + ], + "properties": { + "type": { + "const": "jsonpath" + } + } + }, + "then": { + "properties": { + "version": { + "const": "draft-goessner-dispatch-jsonpath-00" + } + } + } + }, + { + "if": { + "required": [ + "type" + ], + "properties": { + "type": { + "const": "xpath" + } + } + }, + "then": { + "properties": { + "version": { + "enum": [ + "xpath-10", + "xpath-20", + "xpath-30" + ] + } + } + } + } + ], + "$ref": "#/$defs/specification-extensions" + }, + "success-action-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#success-action-object", + "description": "A single success action which describes an action to take upon success of a workflow step", + "type": "object", + "properties": { + "name": { + "description": "The name of the success action", + "type": "string" + }, + "type": { + "description": "The type of action to take", + "enum": [ + "end", + "goto" + ] + }, + "workflowId": { + "description": "The workflowId referencing an existing workflow within the Arazzo description to transfer to upon success of the step", + "$ref": "#workflowId" + }, + "stepId": { + "description": "The stepId to transfer to upon success of the step", + "$ref": "#stepId" + }, + "criteria": { + "description": "A list of assertions to determine if this action SHALL be executed", + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/$defs/criterion-object" + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "goto" + } + } + }, + "then": { + "oneOf": [ + { + "required": [ + "workflowId" + ] + }, + { + "required": [ + "stepId" + ] + } + ] + } + } + ], + "required": [ + "name", + "type" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "failure-action-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#failure-action-object", + "description": "A single failure action which describes an action to take upon failure of a workflow step", + "type": "object", + "properties": { + "name": { + "description": "The name of the failure action", + "type": "string" + }, + "type": { + "description": "The type of action to take", + "enum": [ + "end", + "goto", + "retry" + ] + }, + "workflowId": { + "description": "The workflowId referencing an existing workflow within the Arazzo description to transfer to upon failure of the step", + "$ref": "#workflowId" + }, + "stepId": { + "description": "The stepId to transfer to upon failure of the step", + "$ref": "#stepId" + }, + "retryAfter": { + "description": "A non-negative decimal indicating the seconds to delay after the step failure before another attempt SHALL be made", + "type": "number", + "minimum": 0 + }, + "retryLimit": { + "description": "A non-negative integer indicating how many attempts to retry the step MAY be attempted before failing the overall step", + "type": "integer", + "minimum": 0 + }, + "criteria": { + "description": "A list of assertions to determine if this action SHALL be executed", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/criterion-object" + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "enum": [ + "goto" + ] + } + } + }, + "then": { + "oneOf": [ + { + "required": [ + "workflowId" + ] + }, + { + "required": [ + "stepId" + ] + } + ] + } + } + ], + "required": [ + "name", + "type" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "reusable-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#reusable-object", + "description": "A simple object to allow referencing of objects contained within the Components Object", + "type": "object", + "properties": { + "reference": { + "description": "A runtime expression used to reference the desired object", + "type": "string" + }, + "value": { + "description": "Sets a value of the referenced parameter", + "type": [ + "string", + "boolean", + "object", + "array", + "number", + "null" + ] + } + }, + "required": [ + "reference" + ], + "unevaluatedProperties": false + }, + "parameter-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#parameter-object", + "description": "Describes a single step parameter", + "type": "object", + "properties": { + "name": { + "description": "The name of the parameter", + "type": "string" + }, + "in": { + "description": "The named location of the parameter", + "enum": [ + "path", + "query", + "header", + "cookie" + ] + }, + "value": { + "description": "The value to pass in the parameter", + "type": [ + "string", + "boolean", + "object", + "array", + "number", + "null" + ] + } + }, + "required": [ + "name", + "value" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "payload-replacement-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#payload-replacement-object", + "description": "Describes a location within a payload (e.g., a request body) and a value to set within the location", + "type": "object", + "properties": { + "target": { + "description": "A JSON Pointer or XPath Expression which MUST be resolved against the request body", + "type": "string" + }, + "value": { + "description": "The value set within the target location", + "type": "string" + } + }, + "required": [ + "target", + "value" + ], + "unevaluatedProperties": false, + "$ref": "#/$defs/specification-extensions" + }, + "components-object": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#components-object", + "description": "Holds a set of reusable objects for different aspects of the Arazzo Specification", + "type": "object", + "properties": { + "inputs": { + "description": "An object to hold reusable JSON Schema 2020-12 schemas to be referenced from workflow inputs", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/schema" + } + }, + "parameters": { + "description": "An object to hold reusable Parameter Objects", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/parameter-object" + } + }, + "successActions": { + "description": "An object to hold reusable Success Actions Objects", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/success-action-object" + } + }, + "failureActions": { + "description": "An object to hold reusable Failure Actions Objects", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/failure-action-object" + } + } + }, + "patternProperties": { + "^(inputs|parameters|successActions|failureActions)$": { + "$comment": "Enumerating all of the property names in the regex is necessary for unevaluatedProperties to work as expected", + "propertyNames": { + "pattern": "^[a-zA-Z0-9\\.\\-_]+$" + } + } + }, + "unevaluatedProperties": false, + "$ref": "#/$defs/specification-extensions" + }, + "specification-extensions": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#specification-extensions", + "description": "While the Arazzo Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points", + "patternProperties": { + "^x-": true + } + }, + "schema": { + "$comment": "https://spec.openapis.org/arazzo/v1.0#schema-object", + "$ref": "https://json-schema.org/draft/2020-12/schema" + } + } +} diff --git a/src/definition.ts b/src/definition.ts index 462aecbf..8150df99 100644 --- a/src/definition.ts +++ b/src/definition.ts @@ -14,13 +14,16 @@ import { import {default as nodePath} from 'node:path' import {Overlay} from './core/overlay.js' +import arazzoSchemas from './core/schemas/arazzo-schemas/index.js' import flowerSchemas from './core/schemas/flower-schemas/index.js' import openapiSchemas from './core/schemas/oas-schemas/index.js' -type SpecSchema = JSONSchema4 | JSONSchema6 | JSONSchema7 +type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7 class SupportedFormat { - static readonly asyncapi: Record = { + static readonly arazzo: Record = arazzoSchemas.schemas + + static readonly asyncapi: Record = { '2.0': asyncapi.schemas['2.0.0'], '2.1': asyncapi.schemas['2.1.0'], '2.2': asyncapi.schemas['2.2.0'], @@ -30,16 +33,9 @@ class SupportedFormat { '2.6': asyncapi.schemas['2.6.0'], } - static readonly flower: Record = { - '0.1': flowerSchemas.schemas['0.1'], - } + static readonly flower: Record = flowerSchemas.schemas - static readonly openapi: Record = { - '2.0': openapiSchemas.schemas['2.0'], - '3.0': openapiSchemas.schemas['3.0'], - '3.1': openapiSchemas.schemas['3.1'], - '3.2': openapiSchemas.schemas['3.2'], - } + static readonly openapi: Record = openapiSchemas.schemas } class UnsupportedFormat extends CLIError { @@ -65,14 +61,14 @@ class API { readonly specName?: string readonly version?: string - constructor(location: string, values: SpecSchema) { + constructor(location: string, data: Record) { this.location = location - this.references = [] - const [raw, parsed] = this.resolveContent(values) + const [raw, parsed, references] = this._resolveContentFrom(data) + this.references = references || [] this.rawDefinition = raw as string - this.definition = parsed + this.specName = this.getSpecName(parsed) this.version = this.getVersion(parsed) @@ -81,6 +77,10 @@ class API { } } + static isArazzo(definition: JSONSchema4Object | JSONSchema6Object): definition is Arazzo { + return 'arazzo' in definition + } + static isAsyncAPI(definition: JSONSchema4Object | JSONSchema6Object): definition is AsyncAPI { return 'asyncapi' in definition } @@ -97,6 +97,16 @@ class API { return 'overlay' in definition } + static isSupportedFormat(definition: JSONSchema4Object | JSONSchema6Object): definition is APIDefinition { + return ( + API.isArazzo(definition) || + API.isAsyncAPI(definition) || + API.isFlower(definition) || + API.isOpenAPI(definition) || + API.isOpenAPIOverlay(definition) + ) + } + static async load(path: string): Promise { const {json, text, yaml} = getJsonSchemaRefParserDefaultOptions().parse // Not sure why the lib types the parser as potentially @@ -144,8 +154,17 @@ class API { }, }) .then(($refs) => { - const values = $refs.values() - return new API(path, values) + // JSON schema refs parser lib doesn't type the output of this + // method well (it types it as a generic JSON schema) where as + // it builds a Map of string (the path/URLs of each reference) + // to JSONSchema (the reference value) + // + // We also change the reference values in our custom parsers + // defined above to include the raw values which gets “widen” + // by the lib. We thus need to force the type output to a more + // precise type. + const data = $refs.values() as Record + return new API(path, data) }) .catch((error: Error) => { throw new CLIError(error) @@ -185,26 +204,27 @@ class API { /* eslint-enable no-await-in-loop */ } + if (API.isArazzo(this.definition)) { + await this._resolveArazzoSourceDescriptions() + } + const references = [] for (let i = 0; i < this.references.length; i++) { - const reference = this.references[i] - references.push({ - content: reference.content, - location: reference.location, - }) + const {content, location, name} = this.references[i] + references.push({content, location, name}) } return [this.serializeDefinition(outputPath), references] } - getSpec(definition: APIDefinition): SpecSchema | undefined { - if (API.isAsyncAPI(definition)) { - return SupportedFormat.asyncapi[this.versionWithoutPatch()] + getSpec(definition: APIDefinition): JSONSchema | undefined { + if (API.isArazzo(definition)) { + return SupportedFormat.arazzo[this.versionWithoutPatch()] } - if (API.isOpenAPIOverlay(definition)) { - return {overlay: {type: 'string'}} + if (API.isAsyncAPI(definition)) { + return SupportedFormat.asyncapi[this.versionWithoutPatch()] } if (API.isFlower(definition)) { @@ -215,27 +235,38 @@ class API { return SupportedFormat.openapi[this.versionWithoutPatch()] } + if (API.isOpenAPIOverlay(definition)) { + return {overlay: {type: 'string'}} + } + return undefined } getSpecName(definition: APIDefinition): string | undefined { + if (API.isArazzo(definition)) { + return 'Arazzo' + } if (API.isAsyncAPI(definition)) { return 'AsyncAPI' } if (API.isFlower(definition)) { return 'Flower' } - if (API.isOpenAPIOverlay(definition)) { - return 'OpenAPIOverlay' - } if (API.isOpenAPI(definition)) { return 'OpenAPI' } + if (API.isOpenAPIOverlay(definition)) { + return 'OpenAPIOverlay' + } return undefined } getVersion(definition: APIDefinition): string | undefined { + if (API.isArazzo(definition)) { + return definition.arazzo + } + if (API.isAsyncAPI(definition)) { return definition.asyncapi } @@ -244,14 +275,14 @@ class API { return definition.flower } - if (API.isOpenAPIOverlay(definition)) { - return definition.overlay - } - if (API.isOpenAPI(definition)) { return (definition.openapi || definition.swagger) as string } + if (API.isOpenAPIOverlay(definition)) { + return definition.overlay + } + return undefined } @@ -271,84 +302,116 @@ class API { return path === this.location || path === resolvedAbsLocation } - resolveContent(values: SpecSchema): [string, APIDefinition] { - let mainReference: JSONSchemaWithRaw = {parsed: {}, raw: ''} + serializeDefinition(outputPath?: string): string { + if (this.overlayedDefinition) { + const {comments} = parseWithPointers(this.rawDefinition, {attachComments: true}) + const dumpOptions = {comments, lineWidth: Number.POSITIVE_INFINITY, noRefs: true} + return this.guessFormat(outputPath) === 'json' + ? JSON.stringify(this.overlayedDefinition) + : safeStringify(this.overlayedDefinition, dumpOptions) + } + + return this.rawDefinition + } + + versionWithoutPatch(): string { + if (!this.version) { + return '' + } + const [major, minor] = this.version.split('.', 3) + + return `${major}.${minor}` + } + + private async _resolveArazzoSourceDescriptions(): Promise { + if (!API.isArazzo(this.definition)) { + debug('bump-cli:definition')('This is not an Arazzo definition, no source descriptions to resolve') + } else if (this.definition.sourceDescriptions) { + const sources = this.definition.sourceDescriptions as ArazzoSourceDescription[] + for (const {name, type: sourceType, url: location} of sources) { + if (sourceType === 'openapi') { + const relativeLocation = this._resolveRelativeLocation(location) + + /* eslint-disable no-await-in-loop */ + const api = await API.load(relativeLocation) + const [content] = await api.extractDefinition() + /* eslint-enable no-await-in-loop */ + + this.references.push({content, location: relativeLocation, name}) + } else { + debug('bump-cli:definition')(`Arazzo source description of type ${sourceType} is not yet supported.`) + } + } + } else { + debug('bump-cli:definition')("Arazzo definition doesn't have any sourceDescriptions") + } + } + + private _resolveContentFrom(data: Record): [string, APIDefinition, APIReference[]] { + let definition: JSONSchema | string | undefined + let rawDefinition: string | undefined + const references: APIReference[] = [] - for (const [absPath, reference] of Object.entries(values)) { + // data contains all refs as a map of paths/URLs and their + // correspond values + for (const [absPath, reference] of Object.entries(data)) { if (this.isMainRefPath(absPath)) { - // $refs.values is not properly typed so we need to force it - // with the resulting type of our custom defined parser - mainReference = reference as JSONSchemaWithRaw + ;({parsed: definition, raw: rawDefinition} = reference) } else { - // $refs.values is not properly typed so we need to force it - // with the resulting type of our custom defined parser - const {raw} = reference as JSONSchemaWithRaw - - if (!raw) { + if (!reference.raw) { throw new UnsupportedFormat(`Reference ${absPath} is empty`) } - this.references.push({ - content: raw, - location: this.resolveRelativeLocation(absPath), + references.push({ + content: reference.raw, + location: this._resolveRelativeLocation(absPath), }) } } - const {parsed, raw} = mainReference - - if (!parsed || !raw || !(parsed instanceof Object) || !('info' in parsed || 'flower' in parsed)) { + if ( + !definition || + !rawDefinition || + !(definition instanceof Object) || + !('info' in definition || 'flower' in definition) + ) { debug('bump-cli:definition')( - `Main location (${this.location}) not found or empty (within ${JSON.stringify(Object.keys(values))})`, + `Main location (${this.location}) not found or empty (within ${JSON.stringify(Object.keys(data))})`, ) throw new UnsupportedFormat('Definition needs to be a valid Object') } - if (!API.isOpenAPI(parsed) && !API.isAsyncAPI(parsed) && !API.isOpenAPIOverlay(parsed) && !API.isFlower(parsed)) { + if (!API.isSupportedFormat(definition)) { throw new UnsupportedFormat() } - return [raw, parsed] + return [rawDefinition, definition, references] } - /* Resolve reference absolute paths to the main api location when possible */ - resolveRelativeLocation(absPath: string): string { + /* Resolve reference paths to the main api location when possible */ + private _resolveRelativeLocation(path: string): string { const definitionUrl = this.url() - const refUrl = this.url(absPath) - - if ( - (refUrl.hostname === '' && // filesystem path - (/^\//.test(absPath) || // Unix style - /^[A-Za-z]+:[/\\]/.test(absPath))) || // Windows style - (/^https?:\/\//.test(absPath) && definitionUrl.hostname === refUrl.hostname) // Same domain URLs - ) { - const relativeLocation = nodePath.relative(nodePath.dirname(this.location), absPath) - debug('bump-cli:definition')(`Resolved relative $ref location: ${relativeLocation}`) - return relativeLocation + const refUrl = this.url(path) + const unixStyle: boolean = /^\//.test(path) + const windowsStyle: boolean = /^[A-Za-z]+:[/\\]/.test(path) + const isUrl = /^https?:\/\//.test(path) + + // Guard: Absolute URL on different domain we return an untouched + // path + if (isUrl && definitionUrl.hostname !== refUrl.hostname) { + return path } - return absPath - } - - serializeDefinition(outputPath?: string): string { - if (this.overlayedDefinition) { - const {comments} = parseWithPointers(this.rawDefinition, {attachComments: true}) - const dumpOptions = {comments, lineWidth: Number.POSITIVE_INFINITY, noRefs: true} - return this.guessFormat(outputPath) === 'json' - ? JSON.stringify(this.overlayedDefinition) - : safeStringify(this.overlayedDefinition, dumpOptions) - } - - return this.rawDefinition - } + const isAbsolutePath: boolean = refUrl.hostname === '' && (unixStyle || windowsStyle) + // Absolute path or URL on **same domain** + const isAbsolute: boolean = isAbsolutePath || isUrl - versionWithoutPatch(): string { - if (!this.version) { - return '' - } - const [major, minor] = this.version.split('.', 3) + const relativeLocation: string = isAbsolute + ? nodePath.relative(nodePath.dirname(this.location), path) + : nodePath.join(nodePath.dirname(this.location), path) - return `${major}.${minor}` + debug('bump-cli:definition')(`Resolved relative $ref location: ${relativeLocation}`) + return relativeLocation } private url(location: string = this.location): {hostname: string} | Location { @@ -368,9 +431,10 @@ type JSONSchemaWithRaw = { type APIReference = { content: string location: string + name?: string } -type APIDefinition = AsyncAPI | Flower | OpenAPI | OpenAPIOverlay +type APIDefinition = Arazzo | AsyncAPI | Flower | OpenAPI | OpenAPIOverlay type InfoObject = { readonly description?: string @@ -401,4 +465,15 @@ type Flower = { readonly flower: string } & JSONSchema4Object +type Arazzo = { + readonly arazzo: string + readonly info: InfoObject +} & JSONSchema4Object + +type ArazzoSourceDescription = { + readonly name: string + readonly type?: string + readonly url: string +} + export {API, APIDefinition, OpenAPI, OpenAPIOverlay, SupportedFormat} diff --git a/test/unit/definition.test.ts b/test/unit/definition.test.ts index 296e830b..fe4e4923 100644 --- a/test/unit/definition.test.ts +++ b/test/unit/definition.test.ts @@ -53,6 +53,16 @@ describe('API class', () => { const api = await API.load('examples/valid/asyncapi.v2.5.yml') expect(api.version).to.equal('2.5.0') }) + + it('parses successfully a Flower definition', async () => { + const api = await API.load('examples/valid/flower/parking.yml') + expect(api.version).to.equal('0.1') + }) + + it('parses successfully an Arazzo definition', async () => { + const api = await API.load('examples/valid/arazzo/wikimedia.json') + expect(api.version).to.equal('1.0.1') + }) }) describe('with file & http references', () => { @@ -126,6 +136,24 @@ describe('API class', () => { }) }) + describe('extractDefinition()', () => { + describe('with an Arazzo definition with sources', () => { + it('returns the raw definition of the arazzo definition and the list of source descriptions', async () => { + const api = await API.load('examples/valid/arazzo/wikimedia.json') + const [definition, sources] = await api.extractDefinition() + expect(sources[0].name).to.equal('openapi_source') + expect(sources[0].location).to.equal( + ['examples', 'valid', 'arazzo', 'wikimedia', 'openapi.json'].join(path.sep), + ) + + expect(sources[0].content).to.include( + "By using this API, you agree to Wikimedia's [Terms of Use](https://wikimediafoundation.org/wiki/Terms_of_Use)", + ) + expect(definition).to.equal(api.rawDefinition) + }) + }) + }) + describe('serializeDefinition()', () => { describe('with no overlay applied', () => { it('returns the rawDefinition, no matter the argument', async () => {