typera-openapi
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -17,5 +17,5 @@ #!/usr/bin/env node | ||
const args = parseArgs(); | ||
const sourceFiles = args._.map(x => x.toString()); | ||
const sourceFiles = args._.map((x) => x.toString()); | ||
const ext = args.format === 'ts' ? '.openapi.ts' : '.json'; | ||
const results = _1.generate(sourceFiles, { strict: true }, { log }).map(result => (Object.assign(Object.assign({}, result), { outputFileName: outputFileName(result.fileName, ext) }))); | ||
const results = _1.generate(sourceFiles, { strict: true }, { log }).map((result) => (Object.assign(Object.assign({}, result), { outputFileName: outputFileName(result.fileName, ext) }))); | ||
results.forEach(({ outputFileName, paths }) => { | ||
@@ -22,0 +22,0 @@ const resultObject = JSON.stringify({ paths }); |
import * as ts from 'typescript'; | ||
declare type LogLevel = 'verbose' | 'info' | 'warn' | 'error'; | ||
export declare type LogLevel = 'verbose' | 'info' | 'warn' | 'error'; | ||
export declare type Logger = (location: string, level: LogLevel, ...messages: any[]) => void; | ||
@@ -12,2 +12,1 @@ export interface Context { | ||
export declare const withLocation: (ctx: Context, location: ts.Node) => Context; | ||
export {}; |
@@ -17,3 +17,3 @@ "use strict"; | ||
continue; | ||
ts.forEachChild(sourceFile, node => { | ||
ts.forEachChild(sourceFile, (node) => { | ||
const paths = visit(context_1.context(checker, sourceFile, log, node), node); | ||
@@ -35,3 +35,3 @@ if (paths) { | ||
const paths = {}; | ||
argSymbols.forEach(symbol => { | ||
argSymbols.forEach((symbol) => { | ||
const location = symbol.valueDeclaration; | ||
@@ -57,3 +57,3 @@ const routeDeclaration = getRouteDeclaration(context_1.withLocation(ctx, location), symbol); | ||
.filter(ts.isIdentifier) | ||
.map(arg => ctx.checker.getSymbolAtLocation(arg)) | ||
.map((arg) => ctx.checker.getSymbolAtLocation(arg)) | ||
.filter(utils_1.isDefined); | ||
@@ -65,2 +65,3 @@ if (argSymbols.length !== args.length) | ||
const getRouteDeclaration = (ctx, symbol) => { | ||
const description = getRouteDescription(ctx, symbol); | ||
const routeInput = getRouteInput(ctx, symbol); | ||
@@ -84,6 +85,10 @@ if (!routeInput) | ||
{ | ||
[method]: Object.assign(Object.assign(Object.assign({}, (parameters.length > 0 ? { parameters } : undefined)), operationRequestBody(requestBody)), { responses }), | ||
[method]: Object.assign(Object.assign(Object.assign(Object.assign({}, (description ? { description } : undefined)), (parameters.length > 0 ? { parameters } : undefined)), operationRequestBody(requestBody)), { responses }), | ||
}, | ||
]; | ||
}; | ||
const getRouteDescription = (ctx, symbol) => symbol | ||
.getDocumentationComment(ctx.checker) | ||
.map((part) => part.text) | ||
.join(''); | ||
const operationRequestBody = (contentSchema) => { | ||
@@ -188,2 +193,3 @@ if (!contentSchema) | ||
const getResponseTypes = (ctx, symbol) => { | ||
const descriptions = getResponseDescriptions(symbol); | ||
const routeType = ctx.checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration); | ||
@@ -201,3 +207,3 @@ if (!routeType.aliasSymbol || | ||
if (utils_1.isObjectType(responseType)) { | ||
const responseDef = getResponseDefinition(ctx, responseType); | ||
const responseDef = getResponseDefinition(ctx, descriptions, responseType); | ||
if (responseDef) | ||
@@ -207,4 +213,4 @@ result[responseDef.status] = responseDef.response; | ||
else if (responseType.isUnion()) { | ||
responseType.types.forEach(type => { | ||
const responseDef = getResponseDefinition(ctx, type); | ||
responseType.types.forEach((type) => { | ||
const responseDef = getResponseDefinition(ctx, descriptions, type); | ||
if (responseDef) | ||
@@ -220,3 +226,15 @@ result[responseDef.status] = responseDef.response; | ||
}; | ||
const getResponseDefinition = (ctx, responseType) => { | ||
const getResponseDescriptions = (symbol) => Object.fromEntries(symbol | ||
.getJsDocTags() | ||
.filter((tag) => tag.name === 'response') | ||
.map((tag) => tag.text) | ||
.filter(utils_1.isDefined) | ||
.map((text) => { | ||
const match = /(\d{3}) (.+)/.exec(text); | ||
if (!match) | ||
return undefined; | ||
return [match[1], match[2]]; | ||
}) | ||
.filter(utils_1.isDefined)); | ||
const getResponseDefinition = (ctx, descriptions, responseType) => { | ||
const statusSymbol = responseType.getProperty('status'); | ||
@@ -247,5 +265,10 @@ const bodySymbol = responseType.getProperty('body'); | ||
: undefined; | ||
let description = descriptions[status]; | ||
if (!description) { | ||
ctx.log('warn', `No description for response ${status}`); | ||
description = status; | ||
} | ||
return { | ||
status, | ||
response: Object.assign(Object.assign({ description: status }, (bodySchema | ||
response: Object.assign(Object.assign({ description }, (bodySchema | ||
? { | ||
@@ -271,3 +294,3 @@ content: utils_1.isStringType(bodyType) || utils_1.isNumberType(bodyType) | ||
const props = ctx.checker.getPropertiesOfType(type); | ||
return props.map(prop => ({ | ||
return props.map((prop) => ({ | ||
name: prop.name, | ||
@@ -281,3 +304,3 @@ in: in_, | ||
const props = ctx.checker.getPropertiesOfType(type); | ||
props.forEach(prop => { | ||
props.forEach((prop) => { | ||
result[prop.name] = { | ||
@@ -294,3 +317,3 @@ required: !utils_1.isOptional(prop), | ||
if (optional) { | ||
elems = type.types.filter(elem => !utils_1.isUndefinedType(elem)); | ||
elems = type.types.filter((elem) => !utils_1.isUndefinedType(elem)); | ||
} | ||
@@ -300,3 +323,3 @@ if (elems.some(utils_1.isNullType)) { | ||
nullable = { nullable: true }; | ||
elems = elems.filter(elem => !utils_1.isNullType(elem)); | ||
elems = elems.filter((elem) => !utils_1.isNullType(elem)); | ||
} | ||
@@ -309,11 +332,11 @@ if (elems.every(utils_1.isBooleanLiteralType)) { | ||
// All elements are number literals => enum | ||
return Object.assign({ type: 'number', enum: elems.map(elem => elem.value) }, nullable); | ||
return Object.assign({ type: 'number', enum: elems.map((elem) => elem.value) }, nullable); | ||
} | ||
else if (elems.every(utils_1.isStringLiteralType)) { | ||
// All elements are string literals => enum | ||
return Object.assign({ type: 'string', enum: elems.map(elem => elem.value) }, nullable); | ||
return Object.assign({ type: 'string', enum: elems.map((elem) => elem.value) }, nullable); | ||
} | ||
else if (elems.length >= 2) { | ||
// 2 or more types remain => anyOf | ||
return Object.assign({ anyOf: elems.map(elem => typeToSchema(ctx, elem)).filter(utils_1.isDefined) }, nullable); | ||
return Object.assign({ anyOf: elems.map((elem) => typeToSchema(ctx, elem)).filter(utils_1.isDefined) }, nullable); | ||
} | ||
@@ -327,6 +350,8 @@ else { | ||
if (utils_1.isObjectType(type) || | ||
(type.isIntersection() && type.types.every(part => utils_1.isObjectType(part)))) { | ||
(type.isIntersection() && type.types.every((part) => utils_1.isObjectType(part)))) { | ||
const props = ctx.checker.getPropertiesOfType(type); | ||
return Object.assign(Object.assign({ type: 'object', required: props.filter(prop => !utils_1.isOptional(prop)).map(prop => prop.name) }, nullable), { properties: Object.fromEntries(props | ||
.map(prop => { | ||
return Object.assign(Object.assign({ type: 'object', required: props | ||
.filter((prop) => !utils_1.isOptional(prop)) | ||
.map((prop) => prop.name) }, nullable), { properties: Object.fromEntries(props | ||
.map((prop) => { | ||
const propType = ctx.checker.getTypeOfSymbolAtLocation(prop, ctx.location); | ||
@@ -333,0 +358,0 @@ if (!propType) { |
export { generate } from './generate'; | ||
export { LogLevel } from './context'; | ||
import { OpenAPIV3 } from 'openapi-types'; | ||
export declare const prefix: (prefix: string, paths: OpenAPIV3.PathsObject) => OpenAPIV3.PathsObject; |
{ | ||
"name": "typera-openapi", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Generate OpenAPI spec from typera routes", | ||
@@ -18,3 +18,4 @@ "main": "index.js", | ||
"build": "tsc", | ||
"lint": "eslint '**/*.ts'", | ||
"lint": "eslint --max-warnings 0 '**/*.ts' && prettier --check \"**/*.{json,md}\"", | ||
"lint:fix": "eslint --fix '**/*.ts' && prettier --write '**/*.{json,md}'", | ||
"test": "jest", | ||
@@ -41,4 +42,4 @@ "prepublishOnly": "yarn build" | ||
"ts-jest": "^26.4.4", | ||
"typera-express": "2.0.0-alpha.2" | ||
"typera-express": "2.0.0" | ||
} | ||
} |
@@ -16,4 +16,4 @@ # typera-openapi - typera to OpenAPI generator | ||
Your route files must have a single default export that exports a typera router, | ||
like this: | ||
Your route files must have a single default export that exports a typera router. | ||
JSDoc comments serve as additional documentation: | ||
@@ -23,3 +23,12 @@ ```typescript | ||
const myRoute: Route<...> = route.get(...).handler(...) | ||
/** | ||
* The JSDoc text is used as a description for the route (optional). | ||
* | ||
* @response 200 Success response description. | ||
* @response 400 Another description for a response. This one | ||
* spans multile lines. | ||
*/ | ||
const myRoute: Route<Response.Ok<string> | Response.BadRequest<string>> = | ||
route.get(...).handler(...) | ||
// ... | ||
@@ -30,2 +39,7 @@ | ||
In the OpenAPI v3 spec, the `description` field of a | ||
[Response Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject) | ||
is required, so `typera-openapi` prints a warning if a JSDoc tag for a response | ||
is not found. | ||
Run the `typera-openapi` tool giving paths to your route files as command line | ||
@@ -32,0 +46,0 @@ arguments. Assuming you have two route files in your project: |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
25310
519
94