Skip to content

Commit

Permalink
feat: use pre-compiled ajv validators at runtime (#141)
Browse files Browse the repository at this point in the history
this builds on the speed up from memorizing the validator in
#140,
further improving the speed of the zod spec file from `~1s` -> `~200ms`
(a lot better than the original `~6s`!) 
(though note, it's a bit of a wash for time in CI given it re-compiles
there)

there's no longer any need to memorize the validator, since the require
cache effectively does this for us now.

the compiled validation function is pretty large, but committing it
will keep me honest and prove reproducibility thanks to the CI check
for uncommitted changes after running the build / tests.
  • Loading branch information
mnahkies authored Apr 6, 2024
1 parent 21f2a76 commit 5439e9d
Show file tree
Hide file tree
Showing 13 changed files with 19,275 additions and 214 deletions.
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Pre-compiled ajv validators generated by ./scripts/generate-ajv-validator.js
# Any uncommitted changes will be rejected by CI using ./scripts/assert-clean-working-directory.sh
packages/openapi-code-generater/src/core/schemas/openapi-3.0-specification-validator.js binary
packages/openapi-code-generater/src/core/schemas/openapi-3.1-specification-validator.js binary
2 changes: 2 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"files": {
"ignore": [
"integration-tests/**/*",
"integration-tests-definitions/**/*",
"schemas/*.json",
"packages/*/package.json",
"package.json"
]
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
"refresh": "./scripts/refresh-data.sh",
"lint": "eslint . --cache --report-unused-disable-directives --fix",
"format": "biome check --apply .",
"build": "lerna run build --scope '@nahkies/*'",
"build": "node ./scripts/generate-ajv-validator.js && lerna run build --stream --scope '@nahkies/*'",
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
"integration:generate": "./scripts/generate.all.sh",
"integration:validate": "lerna run validate --stream",
"ci-build": "lerna run build --stream",
"ci-build": "yarn build",
"ci-test": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
"ci-lint": "eslint . --cache --report-unused-disable-directives",
"ci-format": "biome ci .",
Expand Down
56 changes: 56 additions & 0 deletions packages/openapi-code-generator/src/core/openapi-validator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {describe, expect} from "@jest/globals"
import {OpenapiValidator} from "./openapi-validator"

describe("core/openapi-validator", () => {
describe("openapi 3.0", () => {
it("should accept a valid specification", async () => {
const validator = await OpenapiValidator.create()
await expect(
validator.validate(
"valid-spec.yaml",
{
openapi: "3.0.0",
info: {
title: "Valid Specification",
version: "1.0.0",
},
paths: {
"/something": {
get: {
responses: {default: {description: "whatever"}},
},
},
},
},
true,
),
).resolves.toBeUndefined()
})

it("should reject an invalid specification", async () => {
const validator = await OpenapiValidator.create()
await expect(
validator.validate(
"invalid-spec.yaml",
{
openapi: "3.0.0",
info: {
title: "Valid Specification",
version: "1.0.0",
},
paths: {
"/something": {
get: {
responses: {},
},
},
},
},
true,
),
).rejects.toThrow(
"Validation failed: -> must NOT have fewer than 1 properties at path '/paths/~1something/get/responses'",
)
})
})
})
74 changes: 29 additions & 45 deletions packages/openapi-code-generator/src/core/openapi-validator.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import addFormats from "ajv-formats"
import Ajv2020 from "ajv/dist/2020"
import {promptContinue} from "./cli-utils"
import openapi3_1_specification = require("./schemas/openapi-3.1-specification-base.json")
import openapi3_0_specification = require("./schemas/openapi-3.0-specification.json")
import AjvDraft04 from "ajv-draft-04"

import {logger} from "./logger"

import {ErrorObject} from "ajv"

import validate3_0 = require("./schemas/openapi-3.0-specification-validator")
import validate3_1 = require("./schemas/openapi-3.1-specification-validator")

interface ValidateFunction {
(data: any): boolean

Expand All @@ -32,7 +31,11 @@ export class OpenapiValidator {
throw new Error(`unsupported openapi version '${version}'`)
}

async validate(filename: string, schema: unknown): Promise<void> {
async validate(
filename: string,
schema: unknown,
strict = false,
): Promise<void> {
const version =
(schema &&
typeof schema === "object" &&
Expand All @@ -48,12 +51,26 @@ export class OpenapiValidator {
"Note errors may cascade, and should be investigated top to bottom. Errors:\n",
)

validate.errors?.forEach((err) => {
logger.warn(
`-> ${err.message} at path '${err.instancePath}'`,
err.params,
const messages =
validate.errors?.map((err) => {
return [
`-> ${err.message} at path '${err.instancePath}'`,
err.params,
] as const
}) ?? []

if (strict) {
throw new Error(
"Validation failed: " +
messages
.map((it) => `${it[0]} (${JSON.stringify(it[1])})`)
.join("\n"),
)
})
} else {
messages.forEach(([message, metadata]) => {
logger.warn(message, metadata)
})
}

logger.warn("")

Expand All @@ -65,45 +82,12 @@ export class OpenapiValidator {
}

static async create(): Promise<OpenapiValidator> {
const skipValidationOA31: ValidateFunction = () => {
logger.warn(
"Skipping validation due to https://github.com/mnahkies/openapi-code-generator/issues/103",
)
return true
}

const skipValidationLoadSpecificationError: ValidateFunction = () => {
return true
}

try {
const loadSchema = async (uri: string): Promise<any> => {
const res = await fetch(uri)
return (await res.json()) as any
}

const ajv2020 = new Ajv2020({
strict: false,
verbose: true,
loadSchema,
})
addFormats(ajv2020)
ajv2020.addFormat("media-range", true)

const validate3_1 = await ajv2020.compileAsync(openapi3_1_specification)

const ajv4 = new AjvDraft04({
strict: false,
loadSchema,
})
addFormats(ajv4)

const validate3_0 = await ajv4.compileAsync(openapi3_0_specification)

return new OpenapiValidator(
skipValidationOA31 || validate3_1,
validate3_0,
)
return new OpenapiValidator(validate3_1, validate3_0)
} catch (err) {
logger.warn(
"Skipping validation as failed to load schema specification",
Expand Down
Loading

0 comments on commit 5439e9d

Please sign in to comment.