Skip to content

Commit 6697e83

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

File tree

1 file changed

+200
-31
lines changed

1 file changed

+200
-31
lines changed

README.md

Lines changed: 200 additions & 31 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](istanbul) coverage format, so you
7+
can generate any reports that support [istanbul](istanbul).
78

89
Validation is done by `@hyperjump/json-schema`, so you can use any version of
9-
JSON Schema supported by that package.
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.
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](istanbul) coverage
216+
maps for your schemas and stores them for use by the
217+
`TestCoverageEvaluationPlugin`. A coverage map stores the file positions of all
218+
the keywords, schemas, and branches in a schema.
219+
220+
- **CoverageMapService.addFromFile** -- (schemaPath: string): Promise\<string>
221+
222+
This method takes a file path to a schema, generates a coverage map for it,
223+
and stores it for later use. It returns the identifier for the schema
224+
(usually the value of `$id`).
225+
- **CoverageMapService.addCoverageMap** -- (coverageMap: CoverageMapData): void
226+
227+
If you have a coverage map you created yourself or got from some other
228+
source, you can add it using this method. You probably don't need this. Use
229+
`addFromFile` to create and store the coverage map for you.
230+
- **CoverageMapService.getSchemaPath** -- (schemaUri: string): string
231+
232+
Get the file path for the schema that is identified by the given URI.
233+
- **CoverageMapService.getCoverageMap** -- (schemaUri: string): CoverageMapData
234+
235+
Retrieve a coverage map that was previously added through `addFromFile` or
236+
`addCoverageMap`.
237+
238+
### TestCoverageEvaluationPlugin
239+
240+
The `TestCoverageEvaluationPlugin` hooks into the evaluation process of the
241+
[@hyperjump/json-schema](https://www.npmjs.com/package/@hyperjump/json-schema)
242+
validator and uses the `CoverageMapService` to record when a keyword or schema
243+
is visited. Once the evaluation process is completed, it contains an
244+
[istanbul](istanbul) coverage file. These files can then be used to generate any
245+
report that supports [istanbul](istanbul). See the following example for an
246+
example of how to use the evaluation plugin.
247+
248+
### Nyc Example
249+
250+
The following is an example of using the Low-Level API to generate coverage
251+
without Vitest. This uses the [nyc](https://www.npmjs.com/package/nyc) CLI to
252+
generate reports from the coverage files that are generated. Once you run the
253+
script, you can run the following command to generate a report.
254+
255+
```bash
256+
npx nyc report --extension .schema.json
257+
```
258+
259+
```TypeScript
260+
import { randomUUID } from "node:crypto";
261+
import { existsSync } from "node:fs";
262+
import { mkdir, rm, writeFile } from "node:fs/promises";
263+
import { validate } from "@hyperjump/json-schema/draft-2020-12";
264+
import { BASIC } from "@hyperjump/json-schema/experimental";
265+
import {
266+
CoverageMapService,
267+
TestCoverageEvaluationPlugin
268+
} from "@hyperjump/json-schema-coverage";
269+
270+
const schemaUnderTest = `scratch/foo.schema.json`;
271+
272+
// Tell the CoverageMapService which schemas we want coverage for.
273+
const coverageService = new CoverageMapService();
274+
await coverageService.addFromFile(schemaUnderTest);
275+
276+
const validateFoo = await validate(schemaUnderTest);
277+
278+
// A function to run tests and write coverage files where nyc expects them.
279+
const test = async (instance: any, valid: boolean) => {
280+
// Validate with the TestCoverageEvaluationPlugin
281+
const testCoveragePlugin = new TestCoverageEvaluationPlugin(coverageService);
282+
const output = validateFoo(instance, {
283+
outputFormat: BASIC,
284+
plugins: [testCoveragePlugin]
285+
});
286+
287+
// Write the coverage file
288+
const filePath = `.nyc_output/${randomUUID()}.json`;
289+
await writeFile(filePath, JSON.stringify(testCoveragePlugin.coverage));
290+
291+
// Report failures
292+
if (output.valid !== valid) {
293+
const instanceJson = JSON.stringify(instance, null, " ");
294+
const outputJson = JSON.stringify(output, null, " ");
295+
console.log("TEST FAILED:", instanceJson, "\nOUTPUT:", outputJson);
296+
}
297+
};
298+
299+
// Initialize coverage directory
300+
if (existsSync(".nyc_output")) {
301+
await rm(".nyc_output", { recursive: true });
302+
}
303+
await mkdir(".nyc_output");
304+
305+
// Run the tests
306+
await test({ foo: 42 }, true);
307+
await test({ foo: null }, false);
308+
```
140309

141-
TODO
310+
[istanbul](https://istanbul.js.org/)

0 commit comments

Comments
 (0)