Skip to content

Commit 52d1544

Browse files
committed
Add Low-Level API documentation to README
1 parent 0829ffb commit 52d1544

File tree

1 file changed

+201
-32
lines changed

1 file changed

+201
-32
lines changed

README.md

Lines changed: 201 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# Hyperjump - JSON Schema Test Coverage
22

3-
This package provides tools for testing JSON Schemas and providing test coverage
4-
for schema files in JSON or YAML in your code base. Integration is provided for
5-
Vitest, but the component for collecting the coverage data is also exposed if
6-
you want to do some other integration.
3+
This package provides test coverage support for JSON Schemas files in JSON and
4+
YAML in your project. Integration is provided for Vitest, but the low level
5+
components for collecting the coverage data is also exposed if you want to do
6+
some other integration. It uses the [istanbul] coverage format, so you can
7+
generate any reports that support [istanbul].
78

8-
Validation is done by `@hyperjump/json-schema`, so you can use any version of
9-
JSON Schema supported by that package.
9+
Validation is done by [@hyperjump/json-schema], so you can use any version of
10+
JSON Schema.
1011

1112
```
1213
-------------|---------|----------|---------|---------|-------------------
@@ -19,10 +20,14 @@ All files | 81.81 | 66.66 | 80 | 88.88 |
1920

2021
![HTML coverage example](coverage.png)
2122

23+
Istanbul reporters report in terms of Statements, Branches, and Functions, which
24+
aren't terms that makes sense for JSON Schema. I've mapped those concepts to
25+
what makes sense for schemas.
26+
2227
**Legend**
2328
- **Statements** = Keywords and subschemas
2429
- **Branches** = true/false branches for each keyword (except for keywords that
25-
don't branch such as annotation-only keywords)
30+
don't branch such as annotation-only keywords like `title` and `description`)
2631
- **Functions** = Subschemas
2732

2833
## Limitations
@@ -31,9 +36,11 @@ The following are a list of known limitations. Some might be able to be
3136
addressed at some point, while others might not.
3237

3338
- Keywords can pass/fail for multiple reasons, but not all branches are captured
34-
- Example: `type: ["object", "boolean"]`. If you test with an object and a
35-
number, you've covered pass/fail, but haven't tested that a boolean should
36-
pass.
39+
- Example: `type: ["object", "boolean"]`. If you test with an object and then
40+
test with a number, you've covered the pass and fail branches, but haven't
41+
tested that a boolean should also pass.
42+
- There's currently no way to produce a report that uses JSON Schema-friendly
43+
terms rather than "statements" and "functions".
3744

3845
## Vitest
3946

@@ -47,10 +54,10 @@ By default, it will track coverage for any file with a `*.schema.json`,
4754

4855
**Options**
4956

50-
- **include** -- An array of glob paths of schemas you want to track coverage
51-
for. For example, if you keep your schemas in a folder called `schemas` and
52-
they just have plain extensions (`*.json`) instead of schema extensions
53-
`*.schema.json`, you could use `["./schemas/**/*.json"]`.
57+
- **include** -- An array of paths of schemas you want to track coverage for.
58+
For example, if you keep your schemas in a folder called `schemas` and they
59+
just have plain extensions (`*.json`) instead of schema extensions
60+
`*.schema.json`, you could use `["schemas/**/*.json"]`.
5461

5562
`vitest-schema.config.js`
5663
```TypeScript
@@ -59,10 +66,11 @@ import type { JsonSchemaCoverageProviderOptions } from "@hyperjump/json-schema-c
5966

6067
export default defineConfig({
6168
test: {
69+
include: ["schema-tests/"],
6270
coverage: {
6371
provider: "custom",
6472
customProviderModule: "@hyperjump/json-schema-coverage/vitest-coverage-provider",
65-
include: ["./schemas/**/*.json"] // Optional
73+
include: ["schemas/**/*.json"] // Optional
6674
} as JsonSchemaCoverageProviderOptions
6775
}
6876
});
@@ -73,47 +81,51 @@ vitest run --config=vitest-schema.config.js --coverage
7381
```
7482

7583
When you use the provided custom matcher `matchJsonSchema`/`toMatchJsonSchema`,
76-
if vitest has coverage is enabled, it will collect coverage data from those
77-
tests.
84+
if vitest has coverage enabled, it will collect coverage data from those tests.
7885

7986
```JavaScript
8087
import { describe, expect, test } from "vitest";
8188
import "@hyperjump/json-schema-coverage/vitest-matchers";
8289

8390
describe("Worksheet", () => {
84-
test("matches with uri", async () => {
85-
await expect({ foo: 42 }).toMatchJsonSchema("./schema.json");
91+
test("matches with chai-style matcher", async () => {
92+
// 🚨 DON'T FORGET THE `await` 🚨
93+
await expect({ foo: 42 }).to.matchJsonSchema("./schema.json");
8694
});
8795

88-
test("doesn't match with uri", async () => {
96+
test("doesn't match with jest-style matcher", async () => {
97+
// 🚨 DON'T FORGET THE `await` 🚨
8998
await expect({ foo: null }).not.toMatchJsonSchema("./schema.json");
9099
});
91100
});
92101
```
93102

94-
Instead of referring to the file path, you can register the schema and use its
95-
`$id`. Another reason to register a schema is if your schema references another
96-
schema.
103+
Instead of referring to the file path, you can use `registerSchema` to register
104+
the schema and then use its `$id`. Another reason to register a schema is if
105+
your schema references external schema and you need to register those schemas
106+
for the validation to work.
97107

98108
```JavaScript
99109
import { describe, expect, test } from "vitest";
100110
import { registerSchema, unregisterSchema } from "@hyperjump/json-schema-coverage/vitest-matchers";
101111

102112
describe("Worksheet", () => {
103-
beforeEach(() => {
104-
registerSchema("./schema.json");
113+
beforeEach(async () => {
114+
await registerSchema("./schema.json");
105115
});
106116

107-
afterEach(() => {
108-
unregisterSchema("./schema.json");
117+
afterEach(async () => {
118+
await unregisterSchema("./schema.json");
109119
});
110120

111-
test("matches with uri", async () => {
121+
test("matches with jest-style matcher", async () => {
122+
// 🚨 DON'T FORGET THE `await` 🚨
112123
await expect({ foo: 42 }).toMatchJsonSchema("https://example.com/main");
113124
});
114125

115-
test("doesn't match with uri", async () => {
116-
await expect({ foo: null }).not.toMatchJsonSchema("https://example.com/main");
126+
test("doesn't match with chai-style matcher", async () => {
127+
// 🚨 DON'T FORGET THE `await` 🚨
128+
await expect({ foo: null }).not.to.matchJsonSchema("https://example.com/main");
117129
});
118130
});
119131
```
@@ -127,15 +139,172 @@ import "@hyperjump/json-schema-coverage/vitest-matchers";
127139

128140
describe("Worksheet", () => {
129141
test("matches with schema", async () => {
142+
// 🚨 DON'T FORGET THE `await` 🚨
130143
await expect("foo").to.matchJsonSchema({ type: "string" });
131144
});
132145

133146
test("doesn't match with schema", async () => {
147+
// 🚨 DON'T FORGET THE `await` 🚨
134148
await expect(42).to.not.matchJsonSchema({ type: "string" });
135149
});
136150
});
137151
```
138152

139-
## TestCoverageEvaluationPlugin
153+
## Vitest API
154+
155+
These are the functions available when working with the vitest integration.
156+
157+
```JavaScript
158+
import { ... } from "@hyperjump/json-schema-coverage/vitest-matchers"
159+
```
160+
161+
- **matchJsonSchema**: (uriOrSchema: string | SchemaObject | boolean) => Promise\<void>
162+
163+
A vitest matcher that can be used to validate a JSON-compatible value. It
164+
can take a relative or full URI for a schema in your codebase. Use relative
165+
URIs to reference a file and full URIs to reference the `$id` of a schema
166+
you registered using the `registerSchema` function.
167+
168+
You can use this matcher with an inline schema as well, but you will only
169+
get coverage for schemas that are in files.
170+
- **toMatchJsonSchema**: (uriOrSchema: string | SchemaObject | boolean) => Promise\<void>
171+
172+
An alias for `matchJsonSchema` for those who prefer Jest-style matchers.
173+
- **registerSchema**: (path: string) => Promise<void>
174+
175+
Register a schema in your code base by it's path.
176+
177+
_**NOTE**: This is **not** the same as the function from
178+
[@hyperjump/json-schema] that takes a schema._
179+
- **unregisterSchema**: (path: string) => Promise<void>
180+
181+
Remove a registered schema in your code base by it's path.
182+
183+
_**NOTE**: This is **not** the same as the function from
184+
[@hyperjump/json-schema] that takes the schema's `$id`._
185+
- **defineVocabulary**: (vocabularyUri: string, keywords: Record<string, string>) => void
186+
187+
If your schemas use a custom vocabulary, you can register your vocabulary
188+
with this function.
189+
190+
_**NOTE**: This is the same as the function from [@hyperjump/json-schema]_
191+
- **addKeyword**: (keywordHandler: Keyword) => void
192+
193+
If your schemas use a custom vocabulary, you can register your custom
194+
keywords with this function.
195+
196+
_**NOTE**: This is the same as the function from [@hyperjump/json-schema]_
197+
- **loadDialect**: (dialectUri: string, dialect: Record<string, boolean>, allowUnknowKeywords?: boolean) => void
198+
199+
If your schemas use a custom dialect, you can register it with this
200+
function.
201+
202+
_**NOTE**: This is the same as the function from [@hyperjump/json-schema]_
203+
204+
## Low-Level API
205+
206+
These are used internally. They can be used to get coverage without using the
207+
Vitest integration.
208+
209+
```JavaScript
210+
import { ... } from "@hyperjump/json-schema-coverage"
211+
```
212+
213+
### CoverageMapService
214+
215+
The `CoverageMapService` creates [istanbul] coverage maps for your schemas and
216+
stores them for use by the `TestCoverageEvaluationPlugin`. A coverage map stores
217+
the file positions of all the keywords, schemas, and branches in a schema.
218+
219+
- **CoverageMapService.addFromFile** -- (schemaPath: string): Promise\<string>
220+
221+
This method takes a file path to a schema, generates a coverage map for it,
222+
and stores it for later use. It returns the identifier for the schema
223+
(usually the value of `$id`).
224+
- **CoverageMapService.addCoverageMap** -- (coverageMap: CoverageMapData): void
225+
226+
If you have a coverage map you created yourself or got from some other
227+
source, you can add it using this method. You probably don't need this. Use
228+
`addFromFile` to create and store the coverage map for you.
229+
- **CoverageMapService.getSchemaPath** -- (schemaUri: string): string
230+
231+
Get the file path for the schema that is identified by the given URI.
232+
- **CoverageMapService.getCoverageMap** -- (schemaUri: string): CoverageMapData
233+
234+
Retrieve a coverage map that was previously added through `addFromFile` or
235+
`addCoverageMap`.
236+
237+
### TestCoverageEvaluationPlugin
238+
239+
The `TestCoverageEvaluationPlugin` hooks into the evaluation process of the
240+
[@hyperjump/json-schema] validator and uses the `CoverageMapService` to record
241+
when a keyword or schema is visited. Once the evaluation process is completed,
242+
it contains an [istanbul] coverage file. These files can then be used to
243+
generate any report that supports [istanbul]. See the following example for an
244+
example of how to use the evaluation plugin.
245+
246+
### Nyc Example
247+
248+
The following is an example of using the Low-Level API to generate coverage
249+
without Vitest. This uses the [nyc] CLI to generate reports from the coverage
250+
files that are generated. Once you run the script, you can run the following
251+
command to generate a report.
252+
253+
```bash
254+
npx nyc report --extension .schema.json
255+
```
256+
257+
```TypeScript
258+
import { randomUUID } from "node:crypto";
259+
import { existsSync } from "node:fs";
260+
import { mkdir, rm, writeFile } from "node:fs/promises";
261+
import { validate } from "@hyperjump/json-schema/draft-2020-12";
262+
import { BASIC } from "@hyperjump/json-schema/experimental";
263+
import {
264+
CoverageMapService,
265+
TestCoverageEvaluationPlugin
266+
} from "@hyperjump/json-schema-coverage";
267+
268+
const schemaUnderTest = `scratch/foo.schema.json`;
269+
270+
// Tell the CoverageMapService which schemas we want coverage for.
271+
const coverageService = new CoverageMapService();
272+
await coverageService.addFromFile(schemaUnderTest);
273+
274+
const validateFoo = await validate(schemaUnderTest);
275+
276+
// A function to run tests and write coverage files where nyc expects them.
277+
const test = async (instance: any, valid: boolean) => {
278+
// Validate with the TestCoverageEvaluationPlugin
279+
const testCoveragePlugin = new TestCoverageEvaluationPlugin(coverageService);
280+
const output = validateFoo(instance, {
281+
outputFormat: BASIC,
282+
plugins: [testCoveragePlugin]
283+
});
284+
285+
// Write the coverage file
286+
const filePath = `.nyc_output/${randomUUID()}.json`;
287+
await writeFile(filePath, JSON.stringify(testCoveragePlugin.coverage));
288+
289+
// Report failures
290+
if (output.valid !== valid) {
291+
const instanceJson = JSON.stringify(instance, null, " ");
292+
const outputJson = JSON.stringify(output, null, " ");
293+
console.log("TEST FAILED:", instanceJson, "\nOUTPUT:", outputJson);
294+
}
295+
};
296+
297+
// Initialize coverage directory
298+
if (existsSync(".nyc_output")) {
299+
await rm(".nyc_output", { recursive: true });
300+
}
301+
await mkdir(".nyc_output");
302+
303+
// Run the tests
304+
await test({ foo: 42 }, true);
305+
await test({ foo: null }, false);
306+
```
140307

141-
TODO
308+
[@hyperjump/json-schema]: https://www.npmjs.com/package/@hyperjump/json-schema
309+
[istanbul]: https://istanbul.js.org/
310+
[nyc]: https://www.npmjs.com/package/nyc

0 commit comments

Comments
 (0)