diff --git a/packages/rest/src/runtime/decorators.mts b/packages/rest/src/runtime/decorators.mts index 4126b5f..153faaf 100644 --- a/packages/rest/src/runtime/decorators.mts +++ b/packages/rest/src/runtime/decorators.mts @@ -1,5 +1,5 @@ import {Nornir} from "@nornir/core"; -import {HttpRequest, HttpResponse} from "./http-event.mjs"; +import {HttpRequest, HttpResponse, HttpStatusCode, MimeType} from "./http-event.mjs"; import {InstanceOf} from "ts-morph"; const UNTRANSFORMED_ERROR = new Error("nornir/rest decorators have not been transformed. Have you setup ts-patch/ttypescript and added the originator to your tsconfig.json?"); @@ -15,11 +15,31 @@ export function Controller( - _target: (chain: Nornir) => Nornir, +const routeChainDecorator = ( + _target: (chain: Nornir>) => Nornir, ValidateResponseType>, _propertyKey: ClassMethodDecoratorContext, ): never => {throw UNTRANSFORMED_ERROR}; +export type ValidateRequestType = RequestResponseWithBodyHasContentType extends true ? T : "Request type with a body must have a content-type header"; +export type ValidateResponseType = RequestResponseWithBodyHasContentType extends true ? + OutputHasSpecifiedStatusCode extends true + ? T : "Response type must have a status code specified" : "Response type with a body must have a content-type header"; + +type OutputHasSpecifiedStatusCode = IfEquals; + +type RequestResponseWithBodyHasContentType = + // No body spec is valid + HasBody extends false ? true : + // Empty body is valid + T extends { body?: undefined | null } ? true : + T["headers"]["content-type"] extends string ? + IfEquals + : false; + +type HasBody = T extends { body: any } ? true : false + +type Test = { statusCode: HttpStatusCode.Ok, headers: NonNullable, body: string} + /** * Use to mark a method as a GET route * diff --git a/packages/rest/src/runtime/http-event.mts b/packages/rest/src/runtime/http-event.mts index 2c7cf5b..16f17d8 100644 --- a/packages/rest/src/runtime/http-event.mts +++ b/packages/rest/src/runtime/http-event.mts @@ -62,6 +62,9 @@ export interface HttpResponseEmpty extends HttpResponse { readonly body?: undefined } +/** + * @ignore + */ export enum HttpStatusCode { Continue = "100", SwitchingProtocols = "101", @@ -123,69 +126,15 @@ export enum HttpStatusCode { LoopDetected = "508", NotExtended = "510", } -// -// export type HttpStatusCode = -// | "100" -// | "101" -// | "102" -// | "200" -// | "201" -// | "202" -// | "203" -// | "204" -// | "205" -// | "206" -// | "207" -// | "208" -// | "226" -// | "300" -// | "301" -// | "302" -// | "303" -// | "304" -// | "305" -// | "307" -// | "308" -// | "400" -// | "401" -// | "402" -// | "403" -// | "404" -// | "405" -// | "406" -// | "407" -// | "408" -// | "409" -// | "410" -// | "411" -// | "412" -// | "413" -// | "414" -// | "415" -// | "416" -// | "417" -// | "418" -// | "421" -// | "422" -// | "423" -// | "424" -// | "426" -// | "428" -// | "429" -// | "431" -// | "451" -// | "500" -// | "501" -// | "502" -// | "503" -// | "504" -// | "505" -// | "506" -// | "507" -// | "508" -// | "510"; +/** + * @ignore + */ export enum MimeType { + /** + * @ignore + */ + None = "", ApplicationJson = "application/json", ApplicationOctetStream = "application/octet-stream", ApplicationPdf = "application/pdf", diff --git a/packages/rest/src/runtime/index.mts b/packages/rest/src/runtime/index.mts index fb94fa6..04d5afa 100644 --- a/packages/rest/src/runtime/index.mts +++ b/packages/rest/src/runtime/index.mts @@ -9,7 +9,7 @@ import {httpErrorHandler} from "./error.mjs"; export { GetChain, Controller, PostChain, DeleteChain, HeadChain, OptionsChain, PatchChain, PutChain, - Provider + Provider, ValidateRequestType, ValidateResponseType } from './decorators.mjs'; export { HttpResponse, HttpRequest, HttpEvent, HttpMethod, HttpRequestEmpty, HttpResponseEmpty, diff --git a/packages/rest/src/runtime/serialize.mts b/packages/rest/src/runtime/serialize.mts index d123731..d5dce68 100644 --- a/packages/rest/src/runtime/serialize.mts +++ b/packages/rest/src/runtime/serialize.mts @@ -2,14 +2,14 @@ import {HttpResponse, MimeType, SerializedHttpResponse} from "./http-event.mjs"; import {getContentType} from "./utils.mjs"; -export type HttpBodySerializer = (body: T | undefined) => Buffer +export type HttpBodySerializer = (body: unknown | undefined) => Buffer -export type HttpBodySerializerMap = Partial>> +export type HttpBodySerializerMap = Partial> -const DEFAULT_BODY_SERIALIZERS: HttpBodySerializerMap & {default: HttpBodySerializer} = { - "application/json": (body?: object) => Buffer.from(JSON.stringify(body) || "", "utf8"), - "text/plain": (body?: string) => Buffer.from(body?.toString() || "", "utf8"), - "default": (body?: never) => Buffer.from(JSON.stringify(body) || "", "utf8") +const DEFAULT_BODY_SERIALIZERS: HttpBodySerializerMap & {default: HttpBodySerializer} = { + "application/json": (body) => Buffer.from(JSON.stringify(body) || "", "utf8"), + "text/plain": (body) => Buffer.from(body?.toString() || "", "utf8"), + "default": (body) => Buffer.from(JSON.stringify(body) || "", "utf8") } export function httpResponseSerializer(bodySerializerMap?: HttpBodySerializerMap) { diff --git a/packages/rest/src/transform/controller-meta.ts b/packages/rest/src/transform/controller-meta.ts index fa140f2..d5a907a 100644 --- a/packages/rest/src/transform/controller-meta.ts +++ b/packages/rest/src/transform/controller-meta.ts @@ -8,6 +8,7 @@ import { getSchemaOrAllOf, getUnifiedPropertySchemas, joinSchemas, + moveExamplesToExample, moveRefsToAllOf, resolveDiscriminantProperty, rewriteRefsForOpenApi, @@ -224,14 +225,20 @@ export class ControllerMeta { input: routeInfo.input, }; - OpenApiSpecHolder.addSpecForFile(this.source, this.generateRouteSpec(modifiedRouteInfo)); - + try { + OpenApiSpecHolder.addSpecForFile(this.source, this.generateRouteSpec(modifiedRouteInfo)); + } catch (e) { + if (e instanceof TransformationError) { + throw e; + } + console.error(e); + throw new TransformationError("Could not generate OpenAPI spec for route", modifiedRouteInfo); + } methods.set(index.method, modifiedRouteInfo); } private generateRouteSpec(route: RouteInfo): OpenAPIV3_1.Document { const inputSchema = moveRefsToAllOf(route.inputSchema); - const routeIndex = this.getRouteIndex(route); const dereferencedInputSchema = dereferenceSchema(inputSchema); const outputSchema = moveRefsToAllOf(route.outputSchema); const dereferencedOutputSchema = dereferenceSchema(outputSchema); @@ -261,8 +268,8 @@ export class ControllerMeta { }, components: { schemas: { - ...rewriteRefsForOpenApi(inputSchema).definitions, - ...rewriteRefsForOpenApi(outputSchema).definitions, + ...rewriteRefsForOpenApi(moveExamplesToExample(inputSchema)).definitions, + ...rewriteRefsForOpenApi(moveExamplesToExample(outputSchema)).definitions, }, parameters: {}, }, diff --git a/packages/rest/src/transform/error.ts b/packages/rest/src/transform/error.ts index f47814c..0245d29 100644 --- a/packages/rest/src/transform/error.ts +++ b/packages/rest/src/transform/error.ts @@ -1,18 +1,18 @@ import ts from "typescript"; import { RouteIndex } from "./controller-meta"; export class TransformationError extends Error { - constructor(message: string, public readonly routeIndex: RouteIndex, public readonly node?: ts.Node) { + constructor(message: string, routeIndex: RouteIndex, node?: ts.Node) { super(message); - this.message += this.getMessageAddendum(); + this.message += this.getMessageAddendum(routeIndex, node); } - public getMessageAddendum() { - const routeMessage = ` - ${this.routeIndex.method} ${this.routeIndex.path}`; + public getMessageAddendum(routeIndex: RouteIndex, node?: ts.Node) { + const routeMessage = ` - ${routeIndex.method} ${routeIndex.path}`; let message = routeMessage; - if (this.node) { - const file: ts.SourceFile = this.node.getSourceFile(); + if (node) { + const file: ts.SourceFile = node.getSourceFile(); const { line, character } = file.getLineAndCharacterOfPosition( - this.node.pos, + node.pos, ); message += `\n${file.fileName}:${line + 1}:${character + 1}`; } @@ -24,6 +24,6 @@ export class StrictTransformationError extends TransformationError { public readonly warningMessage: string; constructor(errorMessage: string, warningMessage: string, routeIndex: RouteIndex, node?: ts.Node) { super(errorMessage, routeIndex, node); - this.warningMessage = warningMessage + this.getMessageAddendum(); + this.warningMessage = warningMessage + this.getMessageAddendum(routeIndex, node); } } diff --git a/packages/rest/src/transform/json-schema-utils.ts b/packages/rest/src/transform/json-schema-utils.ts index bdbaabc..b190025 100644 --- a/packages/rest/src/transform/json-schema-utils.ts +++ b/packages/rest/src/transform/json-schema-utils.ts @@ -13,7 +13,7 @@ export function dereferenceSchema(schema: JSONSchema7) { refParser.schema = clonedSchema; dereference(refParser, { dereference: { - circular: true, + circular: "ignore", onDereference(path: string, value: JSONSchema7) { (value as { "x-resolved-ref": string })["x-resolved-ref"] = path; }, @@ -88,30 +88,37 @@ export function getUnifiedPropertySchemas( }> = {}; let parentSchemas = 0; - traverse(schema, { - allKeys: false, - cb: { - pre(schema, jsonPtr, rootSchema, parentJsonPtr) { - const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr); - if (convertedPath === parentPath && parentJsonPtr != "") { - parentSchemas++; - } + try { + traverse(schema, { + allKeys: false, + cb: { + pre(schema, jsonPtr, rootSchema, parentJsonPtr) { + const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr); + if (convertedPath === parentPath && parentJsonPtr != "") { + parentSchemas++; + } + }, + post: (schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) => { + const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr); + + if (convertedPath != null && isDirectChildPath(convertedPath, parentPath)) { + const schemaSet = schemas[keyIndex || ""] || { + schemaSet: [], + required: true, + }; + schemaSet.required = !schemaSet.required ? false : parentSchema?.required?.includes(keyIndex) ?? false; + schemaSet.schemaSet.push(schema); + schemas[keyIndex || ""] = schemaSet; + } + }, }, - post: (schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) => { - const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr); - - if (convertedPath != null && isDirectChildPath(convertedPath, parentPath)) { - const schemaSet = schemas[keyIndex || ""] || { - schemaSet: [], - required: true, - }; - schemaSet.required = !schemaSet.required ? false : parentSchema?.required?.includes(keyIndex) ?? false; - schemaSet.schemaSet.push(schema); - schemas[keyIndex || ""] = schemaSet; - } - }, - }, - }); + }); + } catch (e) { + if (e instanceof RangeError) { + throw new Error("Infinite loop detected in json schema"); + } + throw e; + } return Object.fromEntries( Object.entries(schemas).map(([key, value]) => { @@ -165,6 +172,22 @@ export function moveRefsToAllOf(schema: JSONSchema7) { return clonedSchema; } +export function moveExamplesToExample(schema: JSONSchema7) { + const clonedSchema = cloneDeep(schema); + traverse(clonedSchema, { + cb: { + pre: (schema) => { + if (schema.example == null && schema.examples != null && schema.examples.length > 0) { + schema.example = schema.examples[0]; + delete schema.examples; + } + }, + }, + }); + + return clonedSchema; +} + export function rewriteRefsForOpenApi(schema: JSONSchema7) { const clonedSchema = cloneDeep(schema); traverse(clonedSchema, { @@ -239,18 +262,19 @@ export function resolveDiscriminantProperty(schema: JSONSchema7, propertyPath: s const discriminatorValues: string[] = []; for (const schema of discriminantProperty.schemaSet) { + const resolvedAllOfSchema = getSchemaOrAllOf(schema); if ( - !(schema.type === "string" || schema.type === "number") - || (schema.const == null && schema.enum == null) + !(resolvedAllOfSchema.type === "string" || resolvedAllOfSchema.type === "number") + || (resolvedAllOfSchema.const == null && resolvedAllOfSchema.enum == null) ) { return; } - if (schema.const != null) { - discriminatorValues.push(schema.const as string); + if (resolvedAllOfSchema.const != null) { + discriminatorValues.push(resolvedAllOfSchema.const as string); } - if (schema.enum != null) { - discriminatorValues.push(...(schema.enum as string[])); + if (resolvedAllOfSchema.enum != null) { + discriminatorValues.push(...(resolvedAllOfSchema.enum as string[])); } } diff --git a/packages/rest/src/transform/project.ts b/packages/rest/src/transform/project.ts index 3bd0f16..ce47cb9 100644 --- a/packages/rest/src/transform/project.ts +++ b/packages/rest/src/transform/project.ts @@ -1,5 +1,17 @@ import type { Options as AJVBaseOptions } from "ajv"; -import { Config, NodeParser, SchemaGenerator, TypeFormatter } from "ts-json-schema-generator"; +import { + BaseType, + Config, + createParser, + NodeParser, + ReferenceType, + SchemaGenerator, + StringType, + SubNodeParser, + TypeFormatter, + UndefinedType, + UnknownType, +} from "ts-json-schema-generator"; import ts from "typescript"; export interface Project { @@ -48,3 +60,54 @@ export const SCHEMA_DEFAULTS: Config = { }; export type Options = AJVOptions & SchemaConfig; + +export class TemplateExpressionNodeParser implements SubNodeParser { + supportsNode(node: ts.Node): boolean { + return ts.isTemplateExpression(node); + } + + createType(): BaseType { + return new StringType(); + } +} + +export class UndefinedIdentifierParser implements SubNodeParser { + supportsNode(node: ts.Node): boolean { + return ts.isIdentifier(node) && node.text === "undefined"; + } + + createType(): BaseType { + return new UndefinedType(); + } +} + +export class NornirIgnoreParser implements SubNodeParser { + supportsNode(node: ts.Node): boolean { + // check if the ignore tag is present + return ts.getJSDocTags(node).some(tag => tag.tagName.getText() === "ignore"); + } + + createType(): BaseType { + return new UnknownType(); + } +} + +export class NornirParserThrow implements SubNodeParser { + supportsNode(node: ts.Node) { + return ts.getJSDocTags(node).some(tag => tag.tagName.getText() === "nodeParseThrow"); + } + + createType(node: ts.Node): BaseType { + const throwTagText = ts.getJSDocTags(node).find(tag => tag.tagName.getText() === "nodeParseThrow")?.comment; + throw new Error(`ParserFailure: ${throwTagText}`); + } +} + +export function getSchemaNodeParser(program: ts.Program, config: Config): NodeParser { + return createParser(program as unknown as Parameters[0], config, prs => { + prs.addNodeParser(new TemplateExpressionNodeParser()); + prs.addNodeParser(new UndefinedIdentifierParser()); + prs.addNodeParser(new NornirIgnoreParser()); + prs.addNodeParser(new NornirParserThrow()); + }); +} diff --git a/packages/rest/src/transform/transform.ts b/packages/rest/src/transform/transform.ts index d5b289b..4f93203 100644 --- a/packages/rest/src/transform/transform.ts +++ b/packages/rest/src/transform/transform.ts @@ -10,31 +10,19 @@ import { UndefinedType, } from "ts-json-schema-generator"; import ts from "typescript"; -import { AJV_DEFAULTS, AJVOptions, Options, Project, SCHEMA_DEFAULTS, SchemaConfig } from "./project.js"; +import { + AJV_DEFAULTS, + AJVOptions, + getSchemaNodeParser, + Options, + Project, + SCHEMA_DEFAULTS, + SchemaConfig, +} from "./project.js"; import { FileTransformer } from "./transformers/file-transformer.js"; let project: Project; -export class TemplateExpressionNodeParser implements SubNodeParser { - supportsNode(node: ts.Node): boolean { - return ts.isTemplateExpression(node); - } - - createType(): BaseType { - return new StringType(); - } -} - -export class UndefinedIdentifierParser implements SubNodeParser { - supportsNode(node: ts.Node): boolean { - return ts.isIdentifier(node) && node.text === "undefined"; - } - - createType(): BaseType { - return new UndefinedType(); - } -} - export class UndefinedFormatter implements SubTypeFormatter { public supportsType(type: BaseType): boolean { return type instanceof UndefinedType; @@ -48,7 +36,6 @@ export class UndefinedFormatter implements SubTypeFormatter { return {}; } } - export function transform(program: ts.Program, options?: Options): ts.TransformerFactory { const { loopEnum, @@ -83,13 +70,7 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme allErrors: allErrors ?? AJV_DEFAULTS.allErrors, }; - const nodeParser = createParser(program as unknown as Parameters[0], { - ...schemaConfig, - }, (prs) => { - // prs.addNodeParser(new NornirIgnoreParser()); - prs.addNodeParser(new TemplateExpressionNodeParser()); - prs.addNodeParser(new UndefinedIdentifierParser()); - }); + const nodeParser = getSchemaNodeParser(program, schemaConfig); const typeFormatter = createFormatter({ ...schemaConfig, }, frm => { diff --git a/packages/rest/src/transform/transformers/controller-method-transformer.ts b/packages/rest/src/transform/transformers/controller-method-transformer.ts index 16f993b..83a2965 100644 --- a/packages/rest/src/transform/transformers/controller-method-transformer.ts +++ b/packages/rest/src/transform/transformers/controller-method-transformer.ts @@ -32,6 +32,7 @@ export abstract class ControllerMethodTransformer { try { return METHOD_DECORATOR_PROCESSORS[method](methodDecorator, project, source, node, controller); } catch (e) { + throw e; console.error(e); if (e instanceof TransformationError) { throw e; diff --git a/packages/rest/src/transform/transformers/processors/chain-route-processor.ts b/packages/rest/src/transform/transformers/processors/chain-route-processor.ts index d30beb5..6880cbe 100644 --- a/packages/rest/src/transform/transformers/processors/chain-route-processor.ts +++ b/packages/rest/src/transform/transformers/processors/chain-route-processor.ts @@ -48,15 +48,16 @@ export abstract class ChainRouteProcessor { const routeIndex = controller.getRouteIndex({ method, path }); - // const wrappedNode = createWrappedNode(node, { typeChecker: project.checker }) as MethodDeclaration; - const { typeNode: inputTypeNode } = ChainRouteProcessor.resolveInputType(project, node, routeIndex); const outputType = ChainRouteProcessor.resolveOutputType(project, node, routeIndex); - const outputSchema = project.schemaGenerator.createSchemaFromNodes([outputType.node]); - - const inputSchema = project.schemaGenerator.createSchemaFromNodes([inputTypeNode]); + const { inputSchema, outputSchema } = ChainRouteProcessor.generateInputOutputSchema( + project, + routeIndex, + inputTypeNode, + outputType.node, + ); const inputValidator = schemaToValidator(moveRefsToAllOf(inputSchema), project.options.validation); @@ -105,6 +106,25 @@ export abstract class ChainRouteProcessor { return recreatedNode; } + private static generateInputOutputSchema( + project: Project, + routeIndex: RouteIndex, + inputTypeNode: ts.TypeNode, + outputTypeNode: ts.TypeNode, + ) { + try { + const inputSchema = project.schemaGenerator.createSchemaFromNodes([inputTypeNode]); + const outputSchema = project.schemaGenerator.createSchemaFromNodes([outputTypeNode]); + return { + inputSchema, + outputSchema, + }; + } catch (e) { + console.error(e); + throw new TransformationError(`Could not generate schema for route`, routeIndex); + } + } + private static parseJSDoc(_project: Project, method: ts.MethodDeclaration): RouteTags { const docs = ts.getJSDocCommentsAndTags(ts.getOriginalNode(method)); const topLevel = docs[0]; @@ -218,7 +238,7 @@ export abstract class ChainRouteProcessor { project: Project, methodDeclaration: ts.MethodDeclaration, routeIndex: RouteIndex, - ): { type: ts.Type; node: ts.Node } { + ): { type: ts.Type; node: ts.TypeNode } { const wrapped = tsp.createWrappedNode(methodDeclaration, { typeChecker: project.checker }); const returnedTypeNode = wrapped.getReturnTypeNode()?.compilerNode; if (returnedTypeNode == null) { diff --git a/packages/test/openapi.json b/packages/test/openapi.json index d501cb6..d8cb91b 100644 --- a/packages/test/openapi.json +++ b/packages/test/openapi.json @@ -1,15 +1,12 @@ { - "openapi": "3.1.0", + "openapi": "3.0.3", "info": { - "title": "Test API", - "version": "1.0.0", - "description": "A test api" + "title": "Nornir API", + "version": "1.0.0" }, "paths": { - "/basepath/2/route": { + "/docs": { "get": { - "summary": "Get route", - "description": "The second simple GET route.", "responses": { "200": { "description": "", @@ -21,12 +18,12 @@ "deprecated": false, "schema": { "type": "string", - "const": "text/plain" + "const": "text/html" } } }, "content": { - "text/plain": { + "text/html": { "schema": { "type": "string" } @@ -34,59 +31,13 @@ } } }, - "parameters": [ - { - "name": "content-type", - "in": "header", - "required": false, - "deprecated": false, - "schema": { - "anyOf": [ - { - "allOf": [ - { - "$ref": "#/components/schemas/MimeType" - } - ] - }, - { - "not": {} - } - ] - } - } - ] - }, - "put": { - "summary": "Put route", - "description": "The second simple PUT route.", + "parameters": [] + } + }, + "/openapi.json": { + "get": { "responses": { - "201": { - "description": "", - "headers": { - "content-type": { - "name": "content-type", - "in": "header", - "required": false, - "deprecated": false, - "schema": { - "anyOf": [ - { - "allOf": [ - { - "$ref": "#/components/schemas/MimeType" - } - ] - }, - { - "not": {} - } - ] - } - } - } - }, - "422": { + "200": { "description": "", "headers": { "content-type": { @@ -104,62 +55,13 @@ "application/json": { "schema": { "type": "object", - "properties": { - "potato": { - "type": "boolean" - } - }, - "required": [ - "potato" - ] + "additionalProperties": {} } } } } }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "cool": { - "type": "string" - } - }, - "required": [ - "cool" - ] - } - }, - "text/csv": { - "schema": { - "type": "string" - } - } - } - }, - "parameters": [ - { - "name": "content-type", - "in": "header", - "required": true, - "deprecated": false, - "schema": { - "anyOf": [ - { - "type": "string", - "const": "application/json" - }, - { - "type": "string", - "const": "text/csv" - } - ] - } - } - ] + "parameters": [] } }, "/root/basepath/route/{cool}": { @@ -195,7 +97,8 @@ "required": [ "bleep", "bloop" - ] + ], + "additionalProperties": false } } } @@ -229,7 +132,8 @@ "required": [ "bleep", "bloop" - ] + ], + "additionalProperties": false } } } @@ -249,27 +153,7 @@ } }, "content": { - "application/json": { - "example": { - "message": "Bad Request" - }, - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "examples": [ - { - "message": "Bad Request" - } - ] - } - } + "application/json": {} } } }, @@ -287,26 +171,6 @@ } ] } - }, - { - "name": "content-type", - "in": "header", - "required": false, - "deprecated": false, - "schema": { - "anyOf": [ - { - "allOf": [ - { - "$ref": "#/components/schemas/MimeType" - } - ] - }, - { - "not": {} - } - ] - } } ] }, @@ -356,6 +220,7 @@ "required": [ "cool" ], + "additionalProperties": false, "description": "A cool json input", "examples": [ { @@ -380,6 +245,7 @@ "required": [ "cool" ], + "additionalProperties": false, "description": "A cool json input", "examples": [ { @@ -489,63 +355,16 @@ }, "components": { "schemas": { - "HttpHeadersWithContentType": { - "type": "object", - "properties": { - "content-type": { - "allOf": [ - { - "$ref": "#/components/schemas/MimeType" - } - ] - } - }, - "required": [ - "content-type" - ] - }, - "MimeType": { - "type": "string", - "enum": [ - "*/*", - "application/json", - "application/octet-stream", - "application/pdf", - "application/x-www-form-urlencoded", - "application/zip", - "application/gzip", - "application/bzip", - "application/bzip2", - "application/ld+json", - "font/woff", - "font/woff2", - "font/ttf", - "font/otf", - "audio/mpeg", - "audio/x-wav", - "image/gif", - "image/jpeg", - "image/png", - "multipart/form-data", - "text/css", - "text/csv", - "text/html", - "text/plain", - "text/xml", - "video/mpeg", - "video/mp4", - "video/quicktime", - "video/x-msvideo", - "video/x-flv", - "video/webm" - ] - }, "HttpHeadersWithoutContentType": { "type": "object", + "additionalProperties": { + "type": [ + "number", + "string" + ] + }, "properties": { - "content-type": { - "not": {} - } + "content-type": {} } }, "TestStringType": { @@ -585,7 +404,8 @@ }, "required": [ "content-type" - ] + ], + "additionalProperties": false }, "body": { "type": "object", @@ -600,7 +420,8 @@ "required": [ "bleep", "bloop" - ] + ], + "additionalProperties": false } }, "required": [ @@ -608,6 +429,7 @@ "headers", "statusCode" ], + "additionalProperties": false, "description": "This is a comment" }, "RouteGetOutputError": { @@ -627,30 +449,16 @@ }, "required": [ "content-type" - ] - }, - "body": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" ], - "examples": [ - { - "message": "Bad Request" - } - ] - } + "additionalProperties": false + }, + "body": {} }, "required": [ - "body", "headers", "statusCode" ], + "additionalProperties": false, "description": "This is a comment on RouteGetOutputError" }, "RoutePostInputJSONAlias": { @@ -668,7 +476,8 @@ }, "required": [ "content-type" - ] + ], + "additionalProperties": false }, { "type": "object", @@ -680,7 +489,8 @@ }, "required": [ "content-type" - ] + ], + "additionalProperties": false } ] }, @@ -694,7 +504,8 @@ }, "required": [ "test" - ] + ], + "additionalProperties": false }, "body": { "type": "object", @@ -708,6 +519,7 @@ "required": [ "cool" ], + "additionalProperties": false, "description": "A cool json input", "examples": [ { @@ -737,7 +549,8 @@ }, "required": [ "reallyCool" - ] + ], + "additionalProperties": false } }, "required": [ @@ -745,7 +558,8 @@ "headers", "pathParams", "query" - ] + ], + "additionalProperties": false } }, "parameters": {} diff --git a/packages/test/src/controller.ts b/packages/test/src/controller.ts index a7315f1..51f3c11 100644 --- a/packages/test/src/controller.ts +++ b/packages/test/src/controller.ts @@ -8,10 +8,12 @@ import { HttpStatusCode, MimeType, PostChain, + ValidateRequestType, + ValidateResponseType, } from "@nornir/rest"; import { assertValid } from "@nrfcloud/ts-json-schema-transformer"; -interface RouteGetInput extends HttpRequestEmpty { +interface RouteGetInput extends HttpRequest { pathParams: { /** * @pattern ^[a-z]+$ @@ -193,9 +195,9 @@ export class TestController { return chain .use(_contentType => ({ statusCode: HttpStatusCode.Ok, - body: undefined, // body: `Content-Type: ${contentType}`, headers: {}, + body: "", })); } } diff --git a/packages/test/src/rest.ts b/packages/test/src/rest.ts index a976c39..0a8f63f 100644 --- a/packages/test/src/rest.ts +++ b/packages/test/src/rest.ts @@ -19,6 +19,7 @@ import type { } from "aws-lambda"; import "./controller.js"; import "./controller2.js"; +import "./docs-controller.js"; import { getMockObject } from "@nrfcloud/ts-json-schema-transformer"; export class TestError implements NodeJS.ErrnoException { @@ -42,6 +43,8 @@ const frameworkChain = nornir() ])) .use(httpResponseSerializer({ ["application/bzip"]: () => Buffer.from(""), + ["text/csv"]: (input) => Buffer.from(input as string), + ["text/html"]: (input) => Buffer.from(input as string), })); export const handler: APIGatewayProxyHandlerV2 = nornir()