Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions src/error-handlers/typeConstEnum.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,33 @@ import { describeKeyword } from "../json-schema-errors.js";

const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "object", "integer"]);

const TYPE_KEYWORDS = [
"https://json-schema.org/keyword/type",
"https://spec.openapis.org/oas/3.0/keyword/type"
];

/** @type {ErrorHandler} */
const typeConstEnumErrorHandler = {
error: (normalizedErrors, instance, context) => {
let allowedTypes = new Set(ALL_TYPES);
/** @type {string[]} */
const failedTypeLocations = [];

for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/type"]) {
if (normalizedErrors["https://json-schema.org/keyword/type"][schemaLocation].valid === false) {
failedTypeLocations.push(schemaLocation);

/** @type {string | string[]} */
const value = /** @type {string | string[]} */ (normalizedErrors["https://json-schema.org/keyword/type"][schemaLocation].value);
const types = Array.isArray(value) ? value : [value];
/** @type {Set<string>} */
const keywordTypes = new Set(types);
if (keywordTypes.has("number")) {
keywordTypes.add("integer");
for (const typeKeyword of TYPE_KEYWORDS) {
for (const schemaLocation in normalizedErrors[typeKeyword] ?? {}) {
if (normalizedErrors[typeKeyword][schemaLocation].valid === false) {
failedTypeLocations.push(schemaLocation);

/** @type {string | string[]} */
const value = /** @type {string | string[]} */ (normalizedErrors[typeKeyword][schemaLocation].value);
const types = Array.isArray(value) ? value : [value];
/** @type {Set<string>} */
const keywordTypes = new Set(types);
if (keywordTypes.has("number")) {
keywordTypes.add("integer");
}
allowedTypes = allowedTypes.intersection(keywordTypes);
}
allowedTypes = allowedTypes.intersection(keywordTypes);
}
}
if (allowedTypes.has("number")) {
Expand Down Expand Up @@ -114,9 +121,9 @@ const typeConstEnumErrorHandler = {

success: (normalizedOutput, instance, context) => {
return [
...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/type", instance, (/** @type string | string[] */ type) => {
...TYPE_KEYWORDS.flatMap((typeKeyword) => describeKeyword(normalizedOutput, typeKeyword, instance, (/** @type string | string[] */ type) => {
return context.localization.getTypeSuccessMessage(Array.isArray(type) ? type : [type]);
}),
})),
...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/const", instance, (/** @type string */ constJson) => {
return context.localization.getEnumSuccessMessage([JSON.parse(constJson)]);
}),
Expand Down
15 changes: 12 additions & 3 deletions src/hyperjump-json-schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import "@hyperjump/json-schema/draft-07";
import "@hyperjump/json-schema/draft-06";
import "@hyperjump/json-schema/draft-04";
import "@hyperjump/json-schema/formats";
import "@hyperjump/json-schema/openapi-3-0";
import "@hyperjump/json-schema/openapi-3-1";
import "@hyperjump/json-schema/openapi-3-2";
import { BASIC } from "@hyperjump/json-schema/experimental";
import { JSE, jsonSchemaErrors } from "../src/index.js";
import { FluentBundle, FluentResource } from "@fluent/bundle";
Expand Down Expand Up @@ -55,8 +58,8 @@ import { translations } from "./translations/index.js";
* }} TestSuite
*/

/** @type (dialectUri: string, dialect: number) => void */
const runTests = (dialectUri, dialect) => {
/** @type (dialectUri: string, dialect: number, suite?: string) => void */
const runTests = (dialectUri, dialect, suite = "tests") => {
describe(`JSON Schema Errors - ${dialectUri}`, async () => {
setShouldValidateFormat(true);

Expand All @@ -66,7 +69,7 @@ const runTests = (dialectUri, dialect) => {
unregisterSchema(schemaUri);
});

for (const entry of await readdir(`${import.meta.dirname}/test-suite/tests`, { withFileTypes: true })) {
for (const entry of await readdir(`${import.meta.dirname}/test-suite/${suite}`, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith(".json")) {
continue;
}
Expand Down Expand Up @@ -215,3 +218,9 @@ runTests("https://json-schema.org/draft/2019-09/schema", 2019);
runTests("http://json-schema.org/draft-07/schema", 7);
runTests("http://json-schema.org/draft-06/schema", 6);
runTests("http://json-schema.org/draft-04/schema", 4);
runTests("https://spec.openapis.org/oas/3.2/dialect", 2020);
runTests("https://spec.openapis.org/oas/3.1/dialect/base", 2020);

runTests("https://spec.openapis.org/oas/3.2/dialect", 32, "openapi");
runTests("https://spec.openapis.org/oas/3.1/dialect/base", 31, "openapi");
runTests("https://spec.openapis.org/oas/3.0/dialect", 30, "openapi");
11 changes: 11 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ defineKeyword("https://json-schema.org/keyword/comment", { annotation: true });
defineKeyword("https://json-schema.org/keyword/const", {});
defineKeyword("https://json-schema.org/keyword/contains", containsKeyword);
defineKeyword("https://json-schema.org/keyword/draft-06/contains", containsDraft06Keyword);
defineKeyword("https://json-schema.org/keyword/contentEncoding", { annotation: true });
defineKeyword("https://json-schema.org/keyword/contentMediaType", { annotation: true });
defineKeyword("https://json-schema.org/keyword/contentSchema", { annotation: true });
defineKeyword("https://json-schema.org/keyword/default", { annotation: true });
defineKeyword("https://json-schema.org/keyword/definitions", { annotation: true });
defineKeyword("https://json-schema.org/keyword/draft-04/dependencies", dependenciesKeyword);
Expand Down Expand Up @@ -150,6 +153,14 @@ defineKeyword("https://json-schema.org/keyword/uniqueItems", {});
defineKeyword("https://json-schema.org/keyword/unknown", { annotation: true });
defineKeyword("https://json-schema.org/keyword/writeOnly", { annotation: true });

// OpenAPI keywords
defineKeyword("https://spec.openapis.org/oas/3.0/keyword/discriminator", { annotation: true });
defineKeyword("https://spec.openapis.org/oas/3.0/keyword/example", { annotation: true });
defineKeyword("https://spec.openapis.org/oas/3.0/keyword/externalDocs", { annotation: true });
defineKeyword("https://spec.openapis.org/oas/3.0/keyword/nullable", { annotation: true });
defineKeyword("https://spec.openapis.org/oas/3.0/keyword/type", {});
defineKeyword("https://spec.openapis.org/oas/3.0/keyword/xml", { annotation: true });

export {
allTrue,
countTrue,
Expand Down
9 changes: 9 additions & 0 deletions src/test-suite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,12 @@ Some of the message parameters take a list combined using "or". For example,
parameters, you would use `{ "or": ["string", "number", "boolean"]}`. The test
runner should combine those using an "or" appropriate for the language under
test.

## OpenAPI

The tests in the `tests` folder are also run against the OpenAPI 3.1 and 3.2
dialects. The `openapi` folder has tests for the keywords OpenAPI adds and is
only run against the OpenAPI dialects. In those tests, `compatibility` uses
`30`, `31`, and `32` for OpenAPI 3.0, 3.1, and 3.2.

**Example**: `"compatibility": "=30"`
41 changes: 41 additions & 0 deletions src/test-suite/openapi/annotations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "../test-suite.schema.json",

"description": "The OpenAPI annotation keywords",
"tests": [
{
"description": "OpenAPI annotation keywords are ignored",
"schema": {
"type": "string",
"example": "an example",
"externalDocs": { "url": "https://example.com" },
"xml": { "name": "value" },
"discriminator": { "propertyName": "value" }
},
"instance": 5,
"errors": [
{
"messageId": "type-message",
"messageParams": {
"count": 1,
"type": "string",
"expectedTypes": { "or": ["string"] }
},
"instanceLocation": "#",
"schemaLocations": ["#/type"]
}
]
},
{
"description": "a schema with only OpenAPI annotation keywords allows any value",
"schema": {
"example": "an example",
"externalDocs": { "url": "https://example.com" },
"xml": { "name": "value" },
"discriminator": { "propertyName": "value" }
},
"instance": 5,
"errors": []
}
]
}
38 changes: 38 additions & 0 deletions src/test-suite/openapi/nullable.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"$schema": "../test-suite.schema.json",

"description": "The OpenAPI nullable keyword",
"tests": [
{
"description": "nullable expands the allowed types",
"compatibility": "=30",
"schema": {
"type": "string",
"nullable": true
},
"instance": 5,
"errors": [
{
"messageId": "type-message",
"messageParams": {
"count": 2,
"type": "null",
"expectedTypes": { "or": ["null", "string"] }
},
"instanceLocation": "#",
"schemaLocations": ["#/type"]
}
]
},
{
"description": "nullable allows null",
"compatibility": "=30",
"schema": {
"type": "string",
"nullable": true
},
"instance": null,
"errors": []
}
]
}
34 changes: 34 additions & 0 deletions src/test-suite/openapi/type.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "../test-suite.schema.json",

"description": "The OpenAPI type keyword",
"tests": [
{
"description": "OpenAPI type",
"schema": {
"type": "string"
},
"instance": 5,
"errors": [
{
"messageId": "type-message",
"messageParams": {
"count": 1,
"type": "string",
"expectedTypes": { "or": ["string"] }
},
"instanceLocation": "#",
"schemaLocations": ["#/type"]
}
]
},
{
"description": "OpenAPI type pass",
"schema": {
"type": "string"
},
"instance": "foo",
"errors": []
}
]
}
61 changes: 61 additions & 0 deletions src/test-suite/tests/content.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"$schema": "../test-suite.schema.json",

"description": "The content keywords",
"tests": [
{
"description": "contentMediaType and contentEncoding are annotations",
"compatibility": "7",
"schema": {
"type": "string",
"contentMediaType": "application/json",
"contentEncoding": "base64"
},
"instance": 5,
"errors": [
{
"messageId": "type-message",
"messageParams": {
"count": 1,
"type": "string",
"expectedTypes": { "or": ["string"] }
},
"instanceLocation": "#",
"schemaLocations": ["#/type"]
}
]
},
{
"description": "contentSchema is an annotation",
"compatibility": "2019",
"schema": {
"type": "string",
"contentMediaType": "application/json",
"contentSchema": { "type": "object" }
},
"instance": 5,
"errors": [
{
"messageId": "type-message",
"messageParams": {
"count": 1,
"type": "string",
"expectedTypes": { "or": ["string"] }
},
"instanceLocation": "#",
"schemaLocations": ["#/type"]
}
]
},
{
"description": "a schema with only content keywords allows any value",
"compatibility": "7",
"schema": {
"contentMediaType": "application/json",
"contentEncoding": "base64"
},
"instance": 5,
"errors": []
}
]
}