Skip to content

Commit 5ad551d

Browse files
committed
Merge branch 'v3.2-dev' into fix/missing-ref-schema-media-and-parameter
Signed-off-by: Vincent Biret <[email protected]>
2 parents dff699b + 40bcf51 commit 5ad551d

File tree

9 files changed

+316
-232
lines changed

9 files changed

+316
-232
lines changed

package-lock.json

Lines changed: 215 additions & 163 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@
2020
"validate-markdown": "npx markdownlint-cli2 --config spec.markdownlint.yaml src/oas.md && npx markdownlint-cli2 *.md"
2121
},
2222
"dependencies": {
23-
"cheerio": "^1.0.0-rc.5",
23+
"cheerio": "^1.1.0",
2424
"highlight.js": "^11.11.1",
2525
"markdown-it": "^14.1.0",
2626
"respec": "35.4.0",
2727
"yargs": "^18.0.0"
2828
},
2929
"devDependencies": {
30-
"@hyperjump/json-schema": "^1.14.1",
30+
"@hyperjump/json-schema": "^1.15.1",
3131
"c8": "^10.1.3",
3232
"markdownlint-cli2": "^0.18.1",
33-
"vitest": "^3.2.1",
33+
"vitest": "^3.2.3",
3434
"yaml": "^2.8.0"
3535
},
3636
"keywords": [

scripts/schema-test-coverage.mjs

Lines changed: 68 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,13 @@ import { readdir, readFile } from "node:fs/promises";
22
import YAML from "yaml";
33
import { join } from "node:path";
44
import { argv } from "node:process";
5-
import "@hyperjump/json-schema/draft-2020-12";
5+
import { validate } from "@hyperjump/json-schema/draft-2020-12";
66
import "@hyperjump/json-schema/draft-04";
7-
import {
8-
compile,
9-
getSchema,
10-
interpret,
11-
Validation,
12-
BASIC,
13-
} from "@hyperjump/json-schema/experimental";
14-
import * as Instance from "@hyperjump/json-schema/instance/experimental";
7+
import { BASIC } from "@hyperjump/json-schema/experimental";
158

169
/**
17-
* @import { AST } from "@hyperjump/json-schema/experimental"
18-
* @import { Json } from "@hyperjump/json-schema"
10+
* @import { EvaluationPlugin } from "@hyperjump/json-schema/experimental"
11+
* @import { Json } from "@hyperjump/json-pointer"
1912
*/
2013

2114
import contentTypeParser from "content-type";
@@ -36,6 +29,41 @@ addMediaTypePlugin("application/schema+yaml", {
3629
fileMatcher: (path) => path.endsWith(".yaml"),
3730
});
3831

32+
/** @implements EvaluationPlugin */
33+
class TestCoveragePlugin {
34+
constructor() {
35+
/** @type Set<string> */
36+
this.visitedLocations = new Set();
37+
}
38+
39+
beforeSchema(_schemaUri, _instance, context) {
40+
if (this.allLocations) {
41+
return;
42+
}
43+
44+
/** @type Set<string> */
45+
this.allLocations = [];
46+
47+
for (const schemaLocation in context.ast) {
48+
if (schemaLocation === "metaData") {
49+
continue;
50+
}
51+
52+
if (Array.isArray(context.ast[schemaLocation])) {
53+
for (const keyword of context.ast[schemaLocation]) {
54+
if (Array.isArray(keyword)) {
55+
this.allLocations.push(keyword[1]);
56+
}
57+
}
58+
}
59+
}
60+
}
61+
62+
beforeKeyword([, schemaUri]) {
63+
this.visitedLocations.add(schemaUri);
64+
}
65+
}
66+
3967
/** @type (testDirectory: string) => AsyncGenerator<[string,Json]> */
4068
const tests = async function* (testDirectory) {
4169
for (const file of await readdir(testDirectory, {
@@ -53,70 +81,43 @@ const tests = async function* (testDirectory) {
5381
}
5482
};
5583

56-
/** @type (testDirectory: string) => Promise<void> */
57-
const runTests = async (testDirectory) => {
58-
for await (const [name, test] of tests(testDirectory)) {
59-
const instance = Instance.fromJs(test);
84+
/**
85+
* @typedef {{
86+
* allLocations: string[];
87+
* visitedLocations: Set<string>;
88+
* }} Coverage
89+
*/
6090

61-
const result = interpret(compiled, instance, BASIC);
91+
/** @type (schemaUri: string, testDirectory: string) => Promise<Coverage> */
92+
const runTests = async (schemaUri, testDirectory) => {
93+
const testCoveragePlugin = new TestCoveragePlugin();
94+
const validateOpenApi = await validate(schemaUri);
95+
96+
for await (const [name, test] of tests(testDirectory)) {
97+
const result = validateOpenApi(test, {
98+
outputFormat: BASIC,
99+
plugins: [testCoveragePlugin],
100+
});
62101

63102
if (!result.valid) {
64103
console.log("Failed:", name, result.errors);
65104
}
66105
}
67-
};
68-
69-
/** @type (ast: AST) => string[] */
70-
const keywordLocations = (ast) => {
71-
/** @type string[] */
72-
const locations = [];
73-
for (const schemaLocation in ast) {
74-
if (schemaLocation === "metaData") {
75-
continue;
76-
}
77-
78-
if (Array.isArray(ast[schemaLocation])) {
79-
for (const keyword of ast[schemaLocation]) {
80-
if (Array.isArray(keyword)) {
81-
locations.push(keyword[1]);
82-
}
83-
}
84-
}
85-
}
86106

87-
return locations;
107+
return {
108+
allLocations: testCoveragePlugin.allLocations ?? new Set(),
109+
visitedLocations: testCoveragePlugin.visitedLocations
110+
};
88111
};
89112

90113
///////////////////////////////////////////////////////////////////////////////
91114

92-
const schema = await getSchema(argv[2]);
93-
const compiled = await compile(schema);
94-
95-
/** @type Set<string> */
96-
const visitedLocations = new Set();
97-
const baseInterpret = Validation.interpret;
98-
Validation.interpret = (url, instance, context) => {
99-
if (Array.isArray(context.ast[url])) {
100-
for (const keywordNode of context.ast[url]) {
101-
if (Array.isArray(keywordNode)) {
102-
visitedLocations.add(keywordNode[1]);
103-
}
104-
}
105-
}
106-
return baseInterpret(url, instance, context);
107-
};
108-
109-
await runTests(argv[3]);
110-
Validation.interpret = baseInterpret;
111-
112-
// console.log("Covered:", visitedLocations);
113-
114-
const allKeywords = keywordLocations(compiled.ast);
115-
const notCovered = allKeywords.filter(
115+
const { allLocations, visitedLocations } = await runTests(argv[2], argv[3]);
116+
const notCovered = allLocations.filter(
116117
(location) => !visitedLocations.has(location),
117118
);
118119
if (notCovered.length > 0) {
119-
console.log("NOT Covered:", notCovered.length, "of", allKeywords.length);
120+
console.log("NOT Covered:", notCovered.length, "of", allLocations.length);
120121
const maxNotCovered = 20;
121122
const firstNotCovered = notCovered.slice(0, maxNotCovered);
122123
if (notCovered.length > maxNotCovered) firstNotCovered.push("...");
@@ -127,6 +128,10 @@ console.log(
127128
"Covered:",
128129
visitedLocations.size,
129130
"of",
130-
allKeywords.length,
131-
"(" + Math.floor((visitedLocations.size / allKeywords.length) * 100) + "%)",
131+
allLocations.length,
132+
"(" + Math.floor((visitedLocations.size / allLocations.length) * 100) + "%)",
132133
);
134+
135+
if (visitedLocations.size != allLocations.length) {
136+
process.exitCode = 1;
137+
}

scripts/schema-test-coverage.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@
66

77
[[ ! -e src/schemas ]] && exit 0
88

9+
branch=$(git branch --show-current)
10+
911
echo
1012
echo "Schema Test Coverage"
1113
echo
1214

1315
node scripts/schema-test-coverage.mjs src/schemas/validation/schema.yaml tests/schema/pass
16+
rc=$?
17+
18+
[[ "$branch" == "dev" ]] || exit $rc

src/oas.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ The fourth repeats `application/geo+json`-structured values, while the last repe
108108

109109
Implementations MUST support mapping sequential media types into the JSON Schema data model by treating them as if the values were in an array in the same order.
110110

111-
See [Complete vs Streaming Content](#complete-vs-streaming-content) for more information on handling sequential media type in a streaming context, including special considerations for `text/event-stream` content.
111+
See [Complete vs Streaming Content](#complete-vs-streaming-content) for more information on handling sequential media types in a streaming context, including special considerations for `text/event-stream` content.
112112

113113
#### Media Type Registry
114114

@@ -2424,7 +2424,9 @@ This object MAY be extended with [Specification Extensions](#specification-exten
24242424
For simpler scenarios, a [`schema`](#header-schema) and [`style`](#header-style) can describe the structure and syntax of the header.
24252425
When `example` or `examples` are provided in conjunction with the `schema` field, the example MUST follow the prescribed serialization strategy for the header.
24262426

2427-
Serializing with `schema` is NOT RECOMMENDED for headers with parameters (name=value pairs following a `;`) in their values, or where values might have non-URL-safe characters; see [Appendix D](#appendix-d-serializing-headers-and-cookies) for details.
2427+
Serializing headers with `schema` can be problematic due to the URI percent-encoding that is automatically applied, which would percent-encode characters such as `;` that are used to separate primary header values from their parameters.
2428+
The `allowReserved` field can disable most but not all of this behavior.
2429+
See [Appendix D](#appendix-d-serializing-headers-and-cookies) for details and further guidance.
24282430

24292431
When `example` or `examples` are provided in conjunction with the `schema` field, the example SHOULD match the specified schema and follow the prescribed serialization strategy for the header.
24302432
The `example` and `examples` fields are mutually exclusive, and if either is present it SHALL _override_ any `example` in the schema.
@@ -2433,6 +2435,7 @@ The `example` and `examples` fields are mutually exclusive, and if either is pre
24332435
| ---- | :----: | ---- |
24342436
| <a name="header-style"></a>style | `string` | Describes how the header value will be serialized. The default (and only legal value for headers) is `"simple"`. |
24352437
| <a name="header-explode"></a>explode | `boolean` | When this is true, header values of type `array` or `object` generate a single header whose value is a comma-separated list of the array items or key-value pairs of the map, see [Style Examples](#style-examples). For other data types this field has no effect. The default value is `false`. |
2438+
| <a name="header-allow-reserved"></a>allowReserved | `boolean` | When this is true, header values are serialized using reserved expansion, as defined by [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3), which allows [RFC3986's reserved character set](https://datatracker.ietf.org/doc/html/rfc3986#section-2.2), as well as percent-encoded triples, to pass through unchanged, while still percent-encoding all other disallowed characters (including `%` outside of percent-encoded triples). See [Appendix D: Serializing Headers and Cookies](#appendix-d-serializing-headers-and-cookies) for guidance on header encoding and escaping. The default value is `false`. |
24362439
| <a name="header-schema"></a>schema | [Schema Object](#schema-object) | The schema defining the type used for the header. |
24372440
| <a name="header-example"></a>example | Any | Example of the header's potential value; see [Working With Examples](#working-with-examples). |
24382441
| <a name="header-examples"></a>examples | Map[ `string`, [Example Object](#example-object) \| [Reference Object](#reference-object)] | Examples of the header's potential value; see [Working With Examples](#working-with-examples). |

src/schemas/validation/schema.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,9 @@ $defs:
728728
explode:
729729
default: false
730730
type: boolean
731+
allowReserved:
732+
default: false
733+
type: boolean
731734
$ref: '#/$defs/examples'
732735
$ref: '#/$defs/specification-extensions'
733736
unevaluatedProperties: false

tests/schema/pass/info-object-example.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# including External Documentation Object Example
22
openapi: 3.2.0
3+
$self: https://example.com/openapi
34
info:
45
title: Example Pet Store App
56
summary: A pet store manager.

tests/schema/pass/parameter-object-examples.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,18 @@ paths:
5151
lat:
5252
type: number
5353
long:
54-
type: number
54+
type: number
55+
/user:
56+
parameters:
57+
- in: querystring
58+
name: json
59+
content:
60+
application/json:
61+
schema:
62+
# Allow an arbitrary JSON object to keep
63+
# the example simple
64+
type: object
65+
example: {
66+
"numbers": [1, 2],
67+
"flag": null
68+
}

tests/schema/pass/response-object-examples.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ info:
55
components:
66
responses:
77
complex-object-array:
8+
summary: Complex object array
89
description: A complex object array response
910
content:
1011
application/json:

0 commit comments

Comments
 (0)