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: 2 additions & 1 deletion integration-tests/typescript-koa/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ for path in ../../integration-tests-definitions/*; do
node ../../packages/openapi-code-generator/dist/index.js \
--input="$path" \
--output="./src/$filename" \
--template=typescript-koa
--template=typescript-koa \
--schema-builder=zod
done
15 changes: 14 additions & 1 deletion packages/openapi-code-generator/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import convict from "convict"
import {OpenapiGenerator, templates} from "./templates"
import {templates} from "./templates"
import {OpenapiGenerator} from "./templates.types"
import {SchemaBuilderType} from "./typescript/common/schema-builders/schema-builder"

const convictConfig = convict({
input: {
Expand All @@ -23,6 +25,13 @@ const convictConfig = convict({
env: "TEMPLATE",
arg: "template",
},
schemaBuilder: {
doc: "Runtime parsing library to use",
format: ["zod", "joi"],
default: "zod",
env: "SCHEMA_BUILDER",
arg: "schema-builder",
}
})

export class Config {
Expand All @@ -34,6 +43,10 @@ export class Config {
return convictConfig.get("output")
}

get schemaBuilder(): SchemaBuilderType {
return convictConfig.get<any>("schemaBuilder")
}

get generator(): OpenapiGenerator {
const template = convictConfig.get("template")

Expand Down
6 changes: 5 additions & 1 deletion packages/openapi-code-generator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ async function main() {

logger.time("generation")
// TODO abort generation if not a git repo or there are uncommitted changes
await config.generator({input, dest: config.output})
await config.generator({
input,
dest: config.output,
schemaBuilder: config.schemaBuilder,
})
}

main()
Expand Down
7 changes: 1 addition & 6 deletions packages/openapi-code-generator/src/templates.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import {Input} from "./core/input"

import {generateTypescriptAngular} from "./typescript/typescript-angular/typescript-angular.generator"
import {generateTypescriptFetch} from "./typescript/typescript-fetch/typescript-fetch.generator"
import {generateTypescriptKoa} from "./typescript/typescript-koa/typescript-koa.generator"

export interface OpenapiGenerator {
(args: { dest: string, input: Input }): Promise<void>
}
import {OpenapiGenerator} from "./templates.types"

export const templates: Record<string, OpenapiGenerator> = {
"typescript-fetch": generateTypescriptFetch,
Expand Down
12 changes: 12 additions & 0 deletions packages/openapi-code-generator/src/templates.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {Input} from "./core/input"
import {SchemaBuilderType} from "./typescript/common/schema-builders/schema-builder"

export interface OpenapiGeneratorConfig {
dest: string,
input: Input,
schemaBuilder: SchemaBuilderType
}

export interface OpenapiGenerator {
(args: OpenapiGeneratorConfig): Promise<void>
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,48 @@ export class JoiBuilder extends AbstractSchemaBuilder {
imports: ImportBuilder,
) {
super(filename, input, imports)
}

this.importHelpers(imports)

imports.from("@nahkies/typescript-koa-runtime/joi")
.add("parseRequestInput", "Params", "responseValidationFactory")
}

protected importHelpers(importBuilder: ImportBuilder) {
importBuilder.addModule(this.joi, "@hapi/joi")

importBuilder.from("@nahkies/typescript-koa-runtime/joi")
.add("parseRequestInput", "Params")
protected importHelpers(imports: ImportBuilder) {
imports.addModule(this.joi, "@hapi/joi")
}

public any(): string {
// TODO: implement
throw new Error("Method not implemented.")
return [
this.joi,
"any()",
].filter(isDefined).join(".")
}

public void(): string {
// TODO: implement
throw new Error("Method not implemented.")
return [
this.joi,
"any()",
"valid(undefined)",
].filter(isDefined).join(".")
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars

protected intersect(schemas: string[]): string {
// TODO: implement
throw new Error("Method not implemented.")
return schemas.filter(isDefined).reduce((acc, it) => {
return `${acc}\n.concat(${it})`
})
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars

protected union(schemas: string[]): string {
// TODO: implement
throw new Error("Method not implemented.")
return [
this.joi,
`alternatives().try(${
schemas.filter(isDefined).map(it => it).join(",")
})`
].filter(isDefined).join(".")
}

protected nullable(schema: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ export class ZodBuilder extends AbstractSchemaBuilder {
) {
super(filename, input, imports)

imports.from("zod")
.add(this.zod)
this.importHelpers(imports)

imports.from("@nahkies/typescript-koa-runtime/zod")
.add("parseRequestInput", "Params", "responseValidationFactory")
Expand All @@ -40,7 +39,7 @@ export class ZodBuilder extends AbstractSchemaBuilder {
return [
this.zod,
`union([\n${
schemas.map(it => it + ",").join("\n")
schemas.filter(isDefined).map(it => it + ",").join("\n")
}\n])`
].filter(isDefined).join(".")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {TypeBuilder} from "../common/type-builder"
import {isDefined, titleCase} from "../../core/utils"
import {SchemaBuilder, schemaBuilderFactory} from "../common/schema-builders/schema-builder"
import {requestBodyAsParameter, statusStringToType} from "../common/typescript-common"
import {OpenapiGeneratorConfig} from "../../templates.types"

function reduceParamsToOpenApiSchema(parameters: IRParameter[]): IRModelObject {
return parameters.reduce((acc, parameter) => {
Expand Down Expand Up @@ -201,10 +202,11 @@ function route(route: string): string {
}, route)
}

export async function generateTypescriptKoa({dest, input}: { dest: string, input: Input }): Promise<void> {
export async function generateTypescriptKoa(config: OpenapiGeneratorConfig): Promise<void> {
const input = config.input
const imports = new ImportBuilder()
const types = TypeBuilder.fromInput("./models.ts", input).withImports(imports)
const schemaBuilder = schemaBuilderFactory("zod", input, imports)
const schemaBuilder = schemaBuilderFactory(config.schemaBuilder, input, imports)

const server = new ServerBuilder(
"generated.ts",
Expand All @@ -213,13 +215,13 @@ export async function generateTypescriptKoa({dest, input}: { dest: string, input
imports,
types,
schemaBuilder,
loadExistingImplementations(await loadPreviousResult(dest, {filename: "index.ts"}))
loadExistingImplementations(await loadPreviousResult(config.dest, {filename: "index.ts"}))
)

input.allOperations()
.map(it => server.add(it))

await emitGenerationResult(dest, [
await emitGenerationResult(config.dest, [
types,
server,
schemaBuilder,
Expand Down
41 changes: 39 additions & 2 deletions packages/typescript-koa-runtime/src/joi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,46 @@ export function parseRequestInput<Schema extends JoiSchema>(
const result = schema.validate(input, {stripUnknown: true})

if (result.error) {
// TODO: improve error
throw new Error("validation error")
throw result.error
}

return result.value
}

export function responseValidationFactory(possibleResponses: [string, JoiSchema][], defaultResponse?: JoiSchema) {

// Exploit the natural ordering matching the desired specificity of eg: 404 vs 4xx
possibleResponses.sort((x, y) => x[0] < y[0] ? -1 : 1)

return (status: number, value: unknown) => {

for (const [match, schema] of possibleResponses) {

const isMatch =
/^\d+$/.test(match) && String(status) === match ||
/^\d[xX]{2}$/.test(match) && String(status)[0] === match[0]

if (isMatch) {
const result = schema.validate(value)

if (result.error) {
throw result.error
}

return result.value
}
}

if (defaultResponse) {
const result = defaultResponse.validate(value)

if (result.error) {
throw result.error
}

return result.value
}

return value
}
}