Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mrc-6113 JSON schema #6

Merged
merged 14 commits into from
Jan 13, 2025
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,24 @@ Run lint with `npm run lint`. To do any possible automatic fixes run `npm run li

## Docker
See the `docker` folder for Dockerfile and scripts to build, push, run, stop, and push branch tag for the image. The run
script always pulls. The image is pushed to the ghcr.io registry.
script always pulls. The image is pushed to the ghcr.io registry.

## JSON Schema

We use [JSON Schema](https://json-schema.org/) to define the format of JSON responses, and to validate that expected data formats are returned
during integration tests. JSON schema files can be found in the `./schema` folder. The [Ajv package](https://ajv.js.org/) is used for validation.

`Response.schema.json` defines the generic schema for all responses (with `status`, `data` and `errors` properties),
while the other schema files define the expected format of the `data` property for individual endpoints. When defining a new
endpoint, or modifying the format of an existing one, you should add or edit the corresponding
schema file, for the `data` part of the response.

The Response schema includes a `$ref` for the `data` property, with value `grout-data`. In order for this `$ref` to be
satisfied during validation, the expected data schema needs to be given `$id: "grout-data"`. This happens in the integration
test helper method `validateResponse` (called from `getData`), which takes a data schema name parameter. It loads both the Response
and expected data schema, and appends `$id: "grout-data` to the data schema before performing validation. This works so
long as there is only one loaded schema with that `$id`.

A better solution for dynamic nested schema would be to use `$dynamicRef`. However, a [longstanding bug](https://github.com/ajv-validator/ajv/issues/1573)
with `$dynamicRef` in Ajv prevents this.

125 changes: 115 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/node": "^22.9.3",
"@types/supertest": "^6.0.2",
"@vitest/coverage-istanbul": "^2.1.8",
"ajv": "^8.17.1",
"eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
Expand Down
12 changes: 12 additions & 0 deletions schema/Index.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"version": {
"type": "string",
"pattern": "^[0-9]+.[0-9]+.[0-9]+$"
}
},
"additionalProperties": false,
"required": ["version"]
}
28 changes: 28 additions & 0 deletions schema/Metadata.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"datasets": {
"type": "object",
"properties": {
"tile": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"levels": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["levels"]
}
}
},
"additionalProperties": false,
"required": ["tile"]
}
},
"additionalProperties": false,
"required": ["datasets"]
}
48 changes: 48 additions & 0 deletions schema/Response.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "grout-response",
"type": "object",
"properties": {
"status": {
"enum": [
"success",
"failure"
]
},
"errors": {
"type": [
"array",
"null"
],
"items": {
"type": "object",
"properties": {
"error": {
"enum": [
"BAD_REQUEST",
"NOT_FOUND",
"UNEXPECTED_ERROR"
]
},
"detail": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false,
"required": [ "error", "detail" ]
}
},
"data": {
"description": "Grout integration tests append the 'grout-data' $id to the expected data schema at runtime so this $ref can be found",
"anyOf": [
{ "type": "null" },
{ "$ref": "grout-data" }
]
}
},
"additionalProperties": false,
"required": [ "status", "errors", "data" ]
}
5 changes: 4 additions & 1 deletion tests/integration/errors.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { describe, expect, test, beforeAll, afterAll } from "vitest";
import { grout } from "./integrationTest";
import { grout, validateResponse } from "./integrationTest";
import * as fs from "fs";

const expect404Response = async (url: string) => {
const response = await grout.get(url);
await validateResponse(response);
expect(response.status).toBe(404);
expect(response.body).toStrictEqual({
status: "failure",
Expand All @@ -14,6 +15,7 @@ const expect404Response = async (url: string) => {

const expect400Response = async (url: string, error: string) => {
const response = await grout.get(url);
await validateResponse(response);
expect(response.status).toBe(400);
expect(response.body).toStrictEqual({
status: "failure",
Expand Down Expand Up @@ -68,6 +70,7 @@ describe("500 error response", () => {
// NB The error-test endpoint is only enabled when the GROUT_ERROR_TEST env var is set when the server starts
// up, e.g. when running through ./docker/run
const response = await grout.get("/error-test");
await validateResponse(response);
expect(response.status).toBe(500);
expect(response.body.status).toBe("failure");
expect(response.body.data).toBe(null);
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getData } from "./integrationTest";

describe("index endpoint", () => {
test("returns package version", async () => {
const data = await getData("/");
const data = await getData("/", "Index");
const expectedVersion = process.env.npm_package_version;
expect(data.version).toBe(expectedVersion);
});
Expand Down
Loading
Loading