Skip to content

Commit 61a9c38

Browse files
committed
Add support for embedded schemas
1 parent 36d2f05 commit 61a9c38

File tree

5 files changed

+91
-23
lines changed

5 files changed

+91
-23
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ All files | 81.81 | 66.66 | 80 | 88.88 |
2828

2929
The following are known limitations I'm hopeful can be addressed.
3030

31-
- Coverage can't be reported for embedded schemas.
3231
- Coverage can only be reported for `**/*.schema.json` and `**/schema.json`
3332
files.
3433
- Schemas in YAML aren't supported.

src/coverage-util.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getKeyword } from "@hyperjump/json-schema/experimental";
2-
import { parseIri } from "@hyperjump/uri";
2+
import { parseIri, toAbsoluteIri } from "@hyperjump/uri";
33
import { getNodeFromPointer } from "./json-util.js";
44

55
/**
@@ -9,8 +9,8 @@ import { getNodeFromPointer } from "./json-util.js";
99
* @import { JsonNode } from "./jsonast.js"
1010
*/
1111

12-
/** @type (compiledSchema: CompiledSchema, schemaPath: string, tree: JsonNode) => CoverageMapData */
13-
export const astToCoverageMap = (compiledSchema, schemaPath, tree) => {
12+
/** @type (compiledSchema: CompiledSchema, schemaPath: string, schemaNodes: Record<string, JsonNode>) => CoverageMapData */
13+
export const astToCoverageMap = (compiledSchema, schemaPath, schemaNodes) => {
1414
/** @type FileCoverageData */
1515
const fileCoverage = {
1616
path: schemaPath,
@@ -23,12 +23,16 @@ export const astToCoverageMap = (compiledSchema, schemaPath, tree) => {
2323
};
2424

2525
for (const schemaLocation in compiledSchema.ast) {
26-
if (schemaLocation === "metaData" || schemaLocation === "plugins" || !schemaLocation.startsWith(compiledSchema.schemaUri)) {
26+
if (schemaLocation === "metaData" || schemaLocation === "plugins") {
27+
continue;
28+
}
29+
30+
if (!(toAbsoluteIri(schemaLocation) in schemaNodes)) {
2731
continue;
2832
}
2933

3034
const pointer = decodeURI(parseIri(schemaLocation).fragment ?? "");
31-
const node = getNodeFromPointer(tree, pointer);
35+
const node = getNodeFromPointer(schemaNodes[toAbsoluteIri(schemaLocation)], pointer, true);
3236

3337
const declRange = node.type === "json-property"
3438
? positionToRange(node.children[0].position)
@@ -58,7 +62,7 @@ export const astToCoverageMap = (compiledSchema, schemaPath, tree) => {
5862
const [keywordUri, keywordLocation] = keywordNode;
5963

6064
const pointer = decodeURI(parseIri(keywordLocation).fragment ?? "");
61-
const node = getNodeFromPointer(tree, pointer);
65+
const node = getNodeFromPointer(schemaNodes[toAbsoluteIri(keywordLocation)], pointer, true);
6266
const range = positionToRange(node.position);
6367

6468
// Create statement

src/json-util.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,24 @@ const tokenPosition = (startToken, endToken) => {
169169
};
170170
};
171171

172-
/** @type (tree: JsonNode, pointer: string) => JsonNode | JsonPropertyNode */
173-
export const getNodeFromPointer = (tree, pointer) => {
172+
/**
173+
* @overload
174+
* @param {JsonNode} tree
175+
* @param {string} pointer
176+
* @param {true} returnProperty
177+
* @return {JsonNode | JsonPropertyNode}
178+
*
179+
* @overload
180+
* @param {JsonNode} tree
181+
* @param {string} pointer
182+
* @return {JsonNode}
183+
*
184+
* @param {JsonNode} tree
185+
* @param {string} pointer
186+
* @param {true} [returnProperty]
187+
* @return {JsonNode | JsonPropertyNode}
188+
*/
189+
export const getNodeFromPointer = (tree, pointer, returnProperty) => {
174190
/** @type JsonNode | JsonPropertyNode | undefined */
175191
let node = tree;
176192

@@ -193,5 +209,5 @@ export const getNodeFromPointer = (tree, pointer) => {
193209
}
194210
}
195211

196-
return node;
212+
return node.type === "json-property" && !returnProperty ? node.children[1] : node;
197213
};

src/test-coverage-evaluation-plugin.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export class TestCoverageEvaluationPlugin {
2020

2121
/** @type NonNullable<EvaluationPlugin["beforeSchema"]> */
2222
beforeSchema(schemaUri) {
23-
const schemaLocation = toAbsoluteIri(schemaUri);
24-
if (!(schemaLocation in this.#filePathFor)) {
23+
if (!(schemaUri in this.#filePathFor)) {
24+
const schemaLocation = toAbsoluteIri(schemaUri);
2525
const fileHash = createHash("md5").update(`${schemaLocation}#`).digest("hex");
2626
const coverageFilePath = resolve(".json-schema-coverage", fileHash);
2727

@@ -30,17 +30,19 @@ export class TestCoverageEvaluationPlugin {
3030
/** @type CoverageMapData */
3131
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
3232
const coverageMapData = JSON.parse(json);
33-
const fileCoveragePath = Object.keys(coverageMapData)[0];
34-
Object.assign(this.coverageMap, coverageMapData);
35-
this.#filePathFor[schemaLocation] = fileCoveragePath;
33+
for (const fileCoveragePath in coverageMapData) {
34+
this.coverageMap[fileCoveragePath] = coverageMapData[fileCoveragePath];
35+
for (const location in this.coverageMap[fileCoveragePath].s) {
36+
this.#filePathFor[location] = fileCoveragePath;
37+
}
38+
}
3639
}
3740
}
3841
}
3942

4043
/** @type NonNullable<EvaluationPlugin["afterKeyword"]> */
4144
afterKeyword([, keywordLocation], _instance, _context, valid) {
42-
const schemaLocation = toAbsoluteIri(keywordLocation);
43-
const filePath = this.#filePathFor[schemaLocation];
45+
const filePath = this.#filePathFor[keywordLocation];
4446
if (!(filePath in this.coverageMap)) {
4547
return;
4648
}
@@ -54,8 +56,7 @@ export class TestCoverageEvaluationPlugin {
5456

5557
/** @type NonNullable<EvaluationPlugin["afterSchema"]> */
5658
afterSchema(schemaUri) {
57-
const schemaLocation = toAbsoluteIri(schemaUri);
58-
const filePath = this.#filePathFor[schemaLocation];
59+
const filePath = this.#filePathFor[schemaUri];
5960
if (!(filePath in this.coverageMap)) {
6061
return;
6162
}

src/vitest/coverage-provider.js

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import "@hyperjump/json-schema/draft-04";
1818
import "@hyperjump/json-schema/openapi-3-0";
1919
import "@hyperjump/json-schema/openapi-3-1";
2020
import { compile, getSchema } from "@hyperjump/json-schema/experimental";
21+
import * as JsonPointer from "@hyperjump/json-pointer";
22+
import { jrefTypeOf, Reference } from "@hyperjump/browser/jref";
2123
import { astToCoverageMap } from "../coverage-util.js";
22-
import { fromJson } from "../json-util.js";
24+
import { fromJson, getNodeFromPointer } from "../json-util.js";
2325

2426
/**
2527
* @import {
@@ -31,6 +33,8 @@ import { fromJson } from "../json-util.js";
3133
* } from "vitest"
3234
* @import { CoverageMap, CoverageMapData } from "istanbul-lib-coverage"
3335
* @import { SchemaObject } from "@hyperjump/json-schema"
36+
* @import { JRef } from "@hyperjump/browser/jref"
37+
* @import { JsonNode } from "../jsonast.js"
3438
*/
3539

3640
/** @type CoverageProviderModule */
@@ -142,16 +146,60 @@ class JsonSchemaCoverageProvider {
142146
const schemaPath = path.resolve(root, file);
143147
const schema = await getSchema(schemaPath);
144148
const compiledSchema = await compile(schema);
145-
const fileHash = createHash("md5").update(compiledSchema.schemaUri).digest("hex");
146-
const coverageFilePath = path.resolve(this.coverageFilesDirectory, fileHash);
149+
147150
const json = await fs.readFile(schemaPath, "utf-8");
148151
const tree = fromJson(json);
149-
const coverageMap = astToCoverageMap(compiledSchema, path.resolve(root, file), tree);
152+
/** @type Record<string, JsonNode> */
153+
const schemaNodes = {};
154+
for (const schemaUri in schema.document.embedded ?? {}) {
155+
const pointer = this.#findEmbedded(schema.document.root, schemaUri);
156+
schemaNodes[schemaUri] = getNodeFromPointer(tree, pointer);
157+
}
158+
const coverageMap = astToCoverageMap(compiledSchema, schemaPath, schemaNodes);
159+
160+
const fileHash = createHash("md5").update(compiledSchema.schemaUri).digest("hex");
161+
const coverageFilePath = path.resolve(this.coverageFilesDirectory, fileHash);
150162
await fs.writeFile(coverageFilePath, JSON.stringify(coverageMap));
151163
}
152164
}
153165
}
154166

167+
/** @type (node: JRef, uri: string, pointer?: string) => Generator<[string, JRef]> */
168+
* #allSchemaNodes(node, uri, pointer = "") {
169+
yield [pointer, node];
170+
171+
switch (jrefTypeOf(node)) {
172+
case "object":
173+
const jrefObject = /** @type Record<string, JRef> */ (node);
174+
for (const key in jrefObject) {
175+
yield* this.#allSchemaNodes(jrefObject[key], uri, JsonPointer.append(key, pointer));
176+
}
177+
break;
178+
179+
case "array":
180+
const jrefArray = /** @type JRef[] */ (node);
181+
let index = 0;
182+
for (const item of jrefArray) {
183+
yield* this.#allSchemaNodes(item, uri, JsonPointer.append(`${index++}`, pointer));
184+
}
185+
break;
186+
}
187+
}
188+
189+
/** @type (root: JRef, uri: string) => string */
190+
#findEmbedded(root, uri) {
191+
for (const [pointer, node] of this.#allSchemaNodes(root, uri)) {
192+
if (node instanceof Reference) {
193+
const json = node.toJSON();
194+
if (typeof json === "object" && json !== null && !("$ref" in json) && node.href === uri) {
195+
return pointer;
196+
}
197+
}
198+
}
199+
200+
return "";
201+
}
202+
155203
/** @type () => Promise<void> */
156204
async cleanAfterRun() {
157205
await fs.rm(this.coverageFilesDirectory, { recursive: true });

0 commit comments

Comments
 (0)