diff --git a/.gitignore b/.gitignore index ad46b30..5bc2977 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ typings/ # next.js build output .next + +build/ diff --git a/PetStoreSchema.ts b/PetStoreSchema.ts index a384c88..5c1aabe 100644 --- a/PetStoreSchema.ts +++ b/PetStoreSchema.ts @@ -1,4 +1,4 @@ -import { OpenAPIObject } from './OpenAPI' +import { OpenAPIObject } from './src/OpenAPI' export default interface OpenAPISchema extends OpenAPIObject { openapi: '3.0.0' diff --git a/package.json b/package.json index e5115cc..30d3db3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "name": "express-openapi-typer", "version": "0.0.1", "description": "Code-generation-free conversion of OpenAPI schema into typed Express request handlers", - "main": "index.js", + "main": "build/index.js", + "types": "build/index.d.ts", "scripts": { "build": "tsc -p tsconfig.json", "lint": "tslint --project ." diff --git a/sample.ts b/sample.ts index a024328..4f238ae 100644 --- a/sample.ts +++ b/sample.ts @@ -1,6 +1,6 @@ import * as express from 'express' -import { OpenAPIRouter } from '.' import PetStoreSchema from './PetStoreSchema' +import { OpenAPIRouter } from './src' const router = (express.Router() as unknown) as OpenAPIRouter @@ -16,6 +16,5 @@ router.delete('/pets/{id}', (req, res, next) => { }) router.post('/pets', (req, res, next) => { - // req.body autocompletes to `{ [x: string]: JSONValue; name: string; tag?: string | undefined; }` res.send({ name: req.body.name, id: 1 }) }) diff --git a/src/JSONSchema.ts b/src/JSONSchema.ts new file mode 100644 index 0000000..f2fae17 --- /dev/null +++ b/src/JSONSchema.ts @@ -0,0 +1,27 @@ +import * as JSONSchemaTypeMapper from 'json-schema-type-mapper' +import { OpenAPIObject } from '.' +import { ValueOf } from './util' + +// Digging up schemas by hand since json-schema-type-mapper expects to find them +// under `/definitions`, not in OpenAPI's `/components/schemas`: +export type Schemas = S['components']['schemas'] + +export type SchemaIds = Exclude< + ValueOf>['$id'], + undefined +> + +export type SchemasById = { + [Id in SchemaIds]: ValueOf< + { + [P in keyof Schemas]: Schemas[P]['$id'] extends Id + ? Schemas[P] + : never + } + > +} + +export type JSONSchema< + T, + S extends OpenAPIObject +> = JSONSchemaTypeMapper.Schema> diff --git a/OpenAPI.ts b/src/OpenAPI.ts similarity index 100% rename from OpenAPI.ts rename to src/OpenAPI.ts diff --git a/index.ts b/src/index.ts similarity index 68% rename from index.ts rename to src/index.ts index 22f6dfb..1ead258 100644 --- a/index.ts +++ b/src/index.ts @@ -1,58 +1,15 @@ import * as express from 'express-serve-static-core' // tslint:disable-line:no-implicit-dependencies -import * as JSONSchemaTypeMapper from 'json-schema-type-mapper' +import { JSONSchema } from './JSONSchema' import * as oa from './OpenAPI' +import { Compute, UnionToIntersection, ValueOf } from './util' // TODO: for now assuming schema.components.schemas is populated and doesn't contain `$ref` objects -type OpenAPIObject = oa.OpenAPIObject & { +export type OpenAPIObject = oa.OpenAPIObject & { components: { schemas: Record } } -// Digging up schemas by hand since json-schema-type-mapper expects to find them -// under `/definitions`, not in OpenAPI's `/components/schemas`: - -export type Schemas = S['components']['schemas'] - -export type SchemaIds = Exclude< - ValueOf>['$id'], - undefined -> - -export type SchemasById = { - [Id in SchemaIds]: ValueOf< - { - [P in keyof Schemas]: Schemas[P]['$id'] extends Id - ? Schemas[P] - : never - } - > -} - -export type JSONSchema< - T, - S extends OpenAPIObject -> = JSONSchemaTypeMapper.Schema> - -/** - * Force TS to load a type that has not been computed - * https://github.com/pirix-gh/ts-toolbelt - */ -export type Compute = A extends Function // tslint:disable-line:ban-types - ? A - : { [K in keyof A]: A[K] } & {} - -/** - * Courtesy of @jcalz at https://stackoverflow.com/a/50375286/1763012 - */ -type UnionToIntersection = (U extends any -? (k: U) => void -: never) extends (k: infer I) => void - ? I - : never - -type ValueOf = T[keyof T] - export type RequestBody< S extends OpenAPIObject, Path extends keyof S['paths'], @@ -63,6 +20,19 @@ export type RequestBody< | (O['requestBody']['required'] extends true ? never : undefined) > +export type ResponseBody< + S extends OpenAPIObject, + Path extends keyof S['paths'], + Method extends keyof S['paths'][Path] +> = Compute< + JSONSchema< + ValueOf< + S['paths'][Path][Method]['responses'] + >['content']['application/json']['schema'], + S + > +> + export type ParametersIn< S extends OpenAPIObject, Path extends keyof S['paths'], @@ -98,16 +68,11 @@ export type Operation = ValueOf< params: Parameters query: Parameters - // TODO handle different content types and `required`: + // TODO handle different content types: requestBody: RequestBody // TODO handle different content types, headers and status codes: - responseBody: JSONSchema< - ValueOf< - S['paths'][Path][Method]['responses'] - >['content']['application/json']['schema'], - S - > + responseBody: ResponseBody } } > diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..76cf511 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,18 @@ +/** + * Force TS to load a type that has not been computed + * https://github.com/pirix-gh/ts-toolbelt + */ +export type Compute = A extends Function // tslint:disable-line:ban-types + ? A + : { [K in keyof A]: A[K] } & {} + +/** + * Courtesy of @jcalz at https://stackoverflow.com/a/50375286/1763012 + */ +export type UnionToIntersection = (U extends any +? (k: U) => void +: never) extends (k: infer I) => void + ? I + : never + +export type ValueOf = T[keyof T] diff --git a/tsconfig.json b/tsconfig.json index 5b54642..26f1893 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { + "declaration": true, "moduleResolution": "node", - "noEmit": true, - "strict": true - } + "outDir": "build", + "strict": true, + }, + "include": [ + "./src/**/*" + ] }