Skip to content
Merged
181 changes: 181 additions & 0 deletions .ci/validation/src/uri-pattern.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright 2023-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as fs from "node:fs";
import * as path from "node:path";
import * as yaml from "js-yaml";

const schemaPath = path.resolve(__dirname, "../../../schema/workflow.yaml");
const schema = yaml.load(
fs.readFileSync(schemaPath, "utf-8")
) as any;

const uriTemplateDef = schema["$defs"]["uriTemplate"];
const literalUriTemplateDef = uriTemplateDef.anyOf.find(
(v: any) => v.title === "LiteralUriTemplate"
);
const literalUriDef = uriTemplateDef.anyOf.find(
(v: any) => v.title === "LiteralUri"
);

const LITERAL_URI_PATTERN = new RegExp(literalUriDef.pattern);
const LITERAL_URI_TEMPLATE_PATTERN = new RegExp(literalUriTemplateDef.pattern);

describe("Schema pattern consistency", () => {
test("LiteralUri and LiteralUriTemplate use the same pattern", () => {
expect(literalUriDef.pattern).toBe(literalUriTemplateDef.pattern);
});
});

describe("LiteralUri pattern (RFC 3986 URI-reference)", () => {
const absoluteUris = [
"http://example.com",
"https://example.com/path",
"https://example.com/path?query=1",
"https://example.com/path#fragment",
"https://example.com/path?query=1#fragment",
"https://user:pass@example.com:8080/path",
"ftp://files.example.com/public",
"grpc://localhost:50051",
"file:///etc/hosts",
"custom-scheme://authority/path",
];

const relativePathUris = [
"openapi/petstore.json",
"proto/greeter.proto",
"schemas/user.yaml",
"docs/api.json",
"../parent/file.json",
"./sibling/file.yaml",
"file.json",
];

const absolutePathUris = [
"/api/v1/users",
"/openapi/petstore.json",
"/proto/greeter.proto",
];

const urisWithQueryFragment = [
"openapi/petstore.json?version=3",
"/api/docs#section",
"resource?key=value&other=1",
"path/to/resource#anchor",
];

const networkPathUris = ["//example.com/path", "//localhost:8080/api"];

test.each(absoluteUris)("accepts absolute URI: %s", (uri) => {
expect(LITERAL_URI_PATTERN.test(uri)).toBe(true);
});

test.each(relativePathUris)("accepts relative path URI: %s", (uri) => {
expect(LITERAL_URI_PATTERN.test(uri)).toBe(true);
});

test.each(absolutePathUris)("accepts absolute-path URI: %s", (uri) => {
expect(LITERAL_URI_PATTERN.test(uri)).toBe(true);
});

test.each(urisWithQueryFragment)(
"accepts URI with query/fragment: %s",
(uri) => {
expect(LITERAL_URI_PATTERN.test(uri)).toBe(true);
}
);

test.each(networkPathUris)("accepts network-path URI: %s", (uri) => {
expect(LITERAL_URI_PATTERN.test(uri)).toBe(true);
});
});

describe("LiteralUriTemplate pattern (RFC 3986 URI-reference)", () => {
const absoluteTemplates = [
"http://example.com",
"https://example.com/path/{id}",
"https://server.com/{path}",
"https://api.example.com/v1/{resource}?limit={limit}",
"ftp://files.example.com",
"grpc://localhost:50051",
"custom-scheme://authority/path",
];

const relativeTemplates = [
"openapi/{version}/petstore.json",
"{basePath}/resource",
"proto/greeter.proto",
"../{parent}/file.json",
"/api/{version}/users",
"docs/{file}.json",
];

test.each(absoluteTemplates)(
"accepts absolute URI template: %s",
(uri) => {
expect(LITERAL_URI_TEMPLATE_PATTERN.test(uri)).toBe(true);
}
);

test.each(relativeTemplates)(
"accepts relative URI template: %s",
(uri) => {
expect(LITERAL_URI_TEMPLATE_PATTERN.test(uri)).toBe(true);
}
);
});

describe("URI pattern rejects invalid or ambiguous values", () => {
const runtimeExpressions = [
"${ .foo }",
" ${ .bar } ",
"${.baz}",
"${ .context.endpoint }",
];

test.each(runtimeExpressions)(
"rejects runtime expression: %s",
(expr) => {
expect(LITERAL_URI_PATTERN.test(expr)).toBe(false);
}
);

test("rejects empty string", () => {
expect(LITERAL_URI_PATTERN.test("")).toBe(false);
});

test("rejects whitespace-only string", () => {
expect(LITERAL_URI_PATTERN.test(" ")).toBe(false);
});

const urisWithWhitespace = [
"http://example.com/path with spaces",
"openapi/pet store.json",
"/api/v1/my resource",
"https://example.com/path?query=has space",
"https://example.com/path#frag ment",
"proto/greeter .proto",
"//example.com/some path",
];

test.each(urisWithWhitespace)(
"rejects URI containing whitespace: %s",
(uri) => {
expect(LITERAL_URI_PATTERN.test(uri)).toBe(false);
expect(LITERAL_URI_TEMPLATE_PATTERN.test(uri)).toBe(false);
}
);
});
2 changes: 2 additions & 0 deletions dsl-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2464,6 +2464,8 @@ headers:

The DSL has limited support for URI template syntax as defined by [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570). Specifically, only the [Simple String Expansion](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2) is supported, which allows authors to embed variables in a URI.

URI-typed string fields that use the schema’s `uriTemplate` type accept [URI-references](https://datatracker.ietf.org/doc/html/rfc3986#section-4.1) as defined by [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986), meaning both absolute URIs (e.g., `https://example.com/path`) and relative references (e.g., `openapi/petstore.json`, `/api/v1/users`) are accepted. In the JSON Schema, both `LiteralUri` and `LiteralUriTemplate` are validated using the RFC 3986 Appendix B URI-reference regex (with an extra guard to avoid matching runtime expressions).

To substitute a variable within a URI, use the `{}` syntax. The identifier inside the curly braces will be replaced with its value during runtime evaluation. If no value is found for the identifier, an empty string will be used.

This has the following limitations compared to runtime expressions:
Expand Down
6 changes: 3 additions & 3 deletions schema/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1455,11 +1455,11 @@ $defs:
- title: LiteralUriTemplate
type: string
format: uri-template
pattern: "^[A-Za-z][A-Za-z0-9+\\-.]*://.*"
pattern: "^(?!\\s*\\$\\{)(?=\\S)(([^:/?#]+):)?(//([^/?#\\s]*))?([^?#\\s]*)(\\?([^#\\s]*))?(#(\\S*))?$"
- title: LiteralUri
type: string
format: uri
pattern: "^[A-Za-z][A-Za-z0-9+\\-.]*://.*"
format: uri-reference
pattern: "^(?!\\s*\\$\\{)(?=\\S)(([^:/?#]+):)?(//([^/?#\\s]*))?([^?#\\s]*)(\\?([^#\\s]*))?(#(\\S*))?$"
endpoint:
title: Endpoint
description: Represents an endpoint.
Expand Down
Loading