Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,7 +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 linguist-generated=true
packages/openapi-code-generater/src/core/schemas/openapi-3.1-specification-validator.js linguist-generated=true
integration-tests/*/src/generated/** linguist-generated=true
e2e/src/generated/** linguist-generated=true
integration-tests-definitions/** linguist-vendored=true
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ next-env.d.ts
.plandex-v2
.plandexignore
.junie
.run
4 changes: 0 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ This is a monorepo managed with `pnpm`. It produces a CLI tool for generating hi
- `integration-tests-definitions/`: Contains source specifications.
- `integration-tests/*/src/generated/`: Contains generated code.
- `tsconfig.tsbuildinfo`: Compiler state files.
- **Generated Files**:
- `src/core/schemas/openapi-3.0-specification-validator.js`
- `src/core/schemas/openapi-3.1-specification-validator.js`
- (These are generated by `ajv` during the build process).

## Development Workflow

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This document describes the high-level architecture, package structure, and comm

The system follows a typical compiler/generator pipeline:
1. **Loading**: Specifications are loaded and resolved (including remote/local references).
2. **Validation**: Loaded specifications are validated against their respective schemas using pre-compiled AJV validators.
2. **Validation**: Loaded specifications are validated against their respective schemas using `@hyperjump/json-schema`
3. **Normalization**: The specification is transformed into a normalized Intermediate Representation (IR).
4. **Building**: Template-specific builders transform the IR into code structures (types, schemas, clients/routers).
5. **Emission**: The generated structures are formatted and written to the file system.
Expand Down
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"docs:generate": "node ./scripts/generate-toc.mjs",
"refresh": "./scripts/refresh-data.sh",
"lint": "biome check --fix .",
"build": "node ./scripts/generate-ajv-validator.js && pnpm run -r --workspace-concurrency=4 bundle && tsc -b tsconfig.json",
"build": "pnpm run -r --workspace-concurrency=4 bundle && tsc -b tsconfig.json",
"build:docs": "pnpm --filter @nahkies/openapi-code-generator-documentation run build",
"build:watch": "tsc -b tsconfig.json -w",
"test": "vitest run",
Expand Down Expand Up @@ -51,9 +51,6 @@
"@tsconfig/strictest": "^2.0.8",
"@types/node": "^22.20.0",
"@vitest/coverage-v8": "catalog:",
"ajv": "^8.20.0",
"ajv-draft-04": "^1.0.0",
"ajv-formats": "^3.0.1",
"commander": "catalog:",
"conventional-changelog-conventionalcommits": "catalog:",
"husky": "^9.1.7",
Expand Down
3 changes: 1 addition & 2 deletions packages/documentation/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ const nextConfig = {
turbopack: {
resolveAlias: {
"next-mdx-import-source-file": "./src/mdx-components.tsx",
// todo: ajv validator uses `require` in esm context, causing `node:module` to be used
// https://github.com/ajv-validator/ajv/issues/2598 should solve upstream
// todo: @hyperjump/json-schema isn't playing nice in the esm build
"@nahkies/openapi-code-generator/web":
"./node_modules/@nahkies/openapi-code-generator/dist/cjs/web.cjs",
},
Expand Down
5 changes: 2 additions & 3 deletions packages/openapi-code-generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@
"@biomejs/js-api": "catalog:",
"@biomejs/wasm-nodejs": "catalog:",
"@commander-js/extra-typings": "catalog:",
"ajv": "^8.20.0",
"ajv-draft-04": "^1.0.0",
"ajv-formats": "^3.0.1",
"@hyperjump/json-schema": "catalog:",
"@hyperjump/json-schema-errors": "github:hyperjump-io/json-schema-errors#429b18eb914fa1e01b23f38519e4da240e4113d8",
"commander": "catalog:",
"js-yaml": "catalog:",
"json5": "^2.2.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ export class TypespecLoader {
throw new Error("no versions returned")
}

return newestVersion.document
// remove undefined properties, as the validator will reject these.
return JSON.parse(JSON.stringify(newestVersion.document))
}

static async create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe("core/openapi-validator", () => {
true,
),
).rejects.toThrow(
"Validation failed: -> must NOT have fewer than 1 properties at path '/paths/~1something/get/responses'",
"Validation failed: -> Expected an object with at least '1' properties at path '#/paths/~1something/get/responses' ({\"schemaLocations\":[\"https://spec.openapis.org/oas/3.0/schema/2021-09-28#/definitions/Responses/minProperties\"]})",
)
})
})
Expand Down Expand Up @@ -89,7 +89,7 @@ describe("core/openapi-validator", () => {
).resolves.toBeUndefined()
})

it.skip("should reject an invalid specification", async () => {
it("should reject an invalid specification", async () => {
const validator = await OpenapiValidator.create()
await expect(
validator.validate(
Expand All @@ -111,7 +111,8 @@ describe("core/openapi-validator", () => {
true,
),
).rejects.toThrow(
"Validation failed: -> must NOT have fewer than 1 properties at path '/paths/~1something/get/responses'",
"Validation failed: -> Expected an object with at least '1' properties at path '#/paths/~1something/get/responses' ({\"schemaLocations\":[\"https://spec.openapis.org/oas/3.1/schema#/$defs/responses/minProperties\"]})\n" +
"-> Missing required 'property: 'default'' at path '#/paths/~1something/get/responses' ({\"schemaLocations\":[\"https://spec.openapis.org/oas/3.1/schema#/$defs/responses/then/required\"]})",
)
})
})
Expand Down
85 changes: 65 additions & 20 deletions packages/openapi-code-generator/src/core/openapi-validator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import type {
Output,
OutputFormat,
ValidationOptions,
} from "@hyperjump/json-schema"
import {validate as validate3_0} from "@hyperjump/json-schema/openapi-3-0"
import {validate as validate3_1} from "@hyperjump/json-schema/openapi-3-1"
import {
jsonSchemaErrors,
setNormalizationHandler,
} from "@hyperjump/json-schema-errors"
import {logger} from "./logger.ts"
import type {ValidateFunction} from "./schemas/IValidateFunction.ts"
import validate3_0 from "./schemas/openapi-3.0-specification-validator.ts"
import validate3_1 from "./schemas/openapi-3.1-specification-validator.ts"

export interface IOpenapiValidator {
validate(filename: string, schema: unknown, strict?: boolean): Promise<void>
Expand Down Expand Up @@ -41,7 +50,7 @@ export class OpenapiValidator implements IOpenapiValidator {
"unknown"
const validate = this.validationFunction(version)

const isValid = validate(schema)
const {isValid, errors} = await validate(schema)

if (!isValid) {
logger.warn(`Found errors validating '${filename}'.`)
Expand All @@ -50,10 +59,16 @@ export class OpenapiValidator implements IOpenapiValidator {
)

const messages =
validate.errors?.map((err) => {
errors.map((err) => {
return [
`-> ${err.message} at path '${err.instancePath}'`,
err.params,
`-> ${err.message} at path '${err.instanceLocation}'`.replace(
/[\u202A-\u202E\u2066-\u2069]/g,
"'",
),
{
schemaLocations: err.schemaLocations,
alternatives: err.alternatives,
},
] as const
}) ?? []

Expand All @@ -76,22 +91,52 @@ export class OpenapiValidator implements IOpenapiValidator {
static async create(
onValidationFailed: (filename: string) => Promise<void> = async () => {},
): Promise<OpenapiValidator> {
const skipValidationLoadSpecificationError: ValidateFunction = () => {
return true
}
setNormalizationHandler("https://json-schema.org/keyword/comment", {
evaluate() {
// Only applicator keywords need to return a value
},
})

return new OpenapiValidator(
wrapHyperjump(validate3_1, "https://spec.openapis.org/oas/3.1/schema"),
wrapHyperjump(validate3_0, "https://spec.openapis.org/oas/3.0/schema"),
onValidationFailed,
)
}
}

function wrapHyperjump(
validate: (
url: string,
// biome-ignore lint/suspicious/noExplicitAny: unknown input
value: any,
options?: OutputFormat | ValidationOptions,
) => Promise<Output>,
uri: string,
): ValidateFunction {
// biome-ignore lint/suspicious/noExplicitAny: unknown input
return async (it: any) => {
try {
return new OpenapiValidator(validate3_1, validate3_0, onValidationFailed)
} catch (err) {
logger.warn(
"Skipping validation as failed to load schema specification",
{err},
)
return new OpenapiValidator(
skipValidationLoadSpecificationError,
skipValidationLoadSpecificationError,
onValidationFailed,
)
const res = await validate(uri, it, "BASIC")

if (res.valid) {
return {isValid: true, errors: []}
}

const errors = await jsonSchemaErrors(res, uri, it)
return {isValid: false, errors}
} catch (err: unknown) {
return {
isValid: false,
errors: [
{
message: err instanceof Error ? err.message : String(err),
alternatives: [],
schemaLocations: [],
instanceLocation: "",
},
],
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type {ErrorObject} from "ajv"
import type {JsonSchemaErrors} from "@hyperjump/json-schema-errors"

export interface ValidateFunction {
(data: unknown): boolean

errors?: null | ErrorObject[]
}
export type ValidateFunction = (
// biome-ignore lint/suspicious/noExplicitAny: unknown input
data: any,
) => Promise<
{isValid: true; errors: never[]} | {isValid: false; errors: JsonSchemaErrors}
>
Loading