From ef4ccb7e687192e39d87711dfcc5d63fdabf0287 Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Fri, 1 Dec 2023 12:39:58 -0800
Subject: [PATCH 01/16] in progress work
---
eslint.config.js | 15 +-
package.json | 2 -
packages/rest/__tests__/src/routing.spec.mts | 1 -
packages/rest/package.json | 6 +-
packages/rest/src/runtime/decorators.mts | 34 +-
packages/rest/src/runtime/router.mts | 1 -
.../rest/src/transform/controller-meta.ts | 63 +++
packages/rest/src/transform/lib.ts | 30 +-
.../rest/src/transform/openapi-generator.ts | 2 -
.../openapi-merge/component-equivalence.ts | 108 +++++
.../rest/src/transform/openapi-merge/data.ts | 148 ++++++
.../src/transform/openapi-merge/dispute.ts | 35 ++
.../src/transform/openapi-merge/extensions.ts | 51 ++
.../rest/src/transform/openapi-merge/index.ts | 56 +++
.../rest/src/transform/openapi-merge/info.ts | 39 ++
.../openapi-merge/operation-selection.ts | 84 ++++
.../openapi-merge/paths-and-components.ts | 434 ++++++++++++++++++
.../openapi-merge/reference-walker.ts | 315 +++++++++++++
.../rest/src/transform/openapi-merge/tags.ts | 39 ++
packages/rest/src/transform/project.ts | 1 +
packages/rest/src/transform/transform.ts | 26 +-
.../controller-method-transformer.ts | 4 +-
.../transformers/file-transformer.ts | 21 +-
.../processors/chain-route-processor.ts | 50 +-
.../processors/controller-processor.ts | 11 +-
packages/test/__tests__/src/test.spec.ts | 1 -
packages/test/src/controller.ts | 25 +-
packages/test/src/controller2.ts | 8 +
pnpm-lock.yaml | 53 ++-
29 files changed, 1584 insertions(+), 79 deletions(-)
create mode 100644 packages/rest/src/transform/openapi-merge/component-equivalence.ts
create mode 100644 packages/rest/src/transform/openapi-merge/data.ts
create mode 100644 packages/rest/src/transform/openapi-merge/dispute.ts
create mode 100644 packages/rest/src/transform/openapi-merge/extensions.ts
create mode 100644 packages/rest/src/transform/openapi-merge/index.ts
create mode 100644 packages/rest/src/transform/openapi-merge/info.ts
create mode 100644 packages/rest/src/transform/openapi-merge/operation-selection.ts
create mode 100644 packages/rest/src/transform/openapi-merge/paths-and-components.ts
create mode 100644 packages/rest/src/transform/openapi-merge/reference-walker.ts
create mode 100644 packages/rest/src/transform/openapi-merge/tags.ts
diff --git a/eslint.config.js b/eslint.config.js
index 4db1c0f..f0d7b41 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -18,21 +18,16 @@ const test = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
- "plugin:unicorn/recommended",
"plugin:jest/recommended",
- "plugin:jest/style",
"plugin:workspaces/recommended",
"plugin:eslint-comments/recommended",
- "plugin:sonarjs/recommended",
],
parser: "@typescript-eslint/parser",
plugins: [
"@typescript-eslint",
"eslint-plugin-workspaces",
- "eslint-plugin-unicorn",
"eslint-plugin-jest",
"eslint-plugin-eslint-comments",
- "eslint-plugin-sonarjs",
"eslint-plugin-no-secrets",
],
root: true,
@@ -44,15 +39,8 @@ const test = {
],
rules: {
// Reduce is confusing, but it shouldn't be banned
- "unicorn/no-array-reduce": ["off"],
- "unicorn/filename-case": ["error", {
- case: "kebabCase",
- }],
- "unicorn/no-empty-files": ["off"],
"workspaces/require-dependency": ["off"],
- "unicorn/prevent-abbreviations": ["off"],
"no-secrets/no-secrets": ["warn", {"tolerance": 5.0}],
- "unicorn/numeric-separators-style": ["off"],
"@typescript-eslint/no-unused-vars": ["warn", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
@@ -72,8 +60,7 @@ const test = {
"match": false
}
},
- ],
- "sonarjs/no-duplicate-string": ["off"],
+ ]
},
};
diff --git a/package.json b/package.json
index 8776bca..716d7d3 100644
--- a/package.json
+++ b/package.json
@@ -19,8 +19,6 @@
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jest": "^27.2.3",
"eslint-plugin-no-secrets": "^0.8.9",
- "eslint-plugin-sonarjs": "^0.19.0",
- "eslint-plugin-unicorn": "^48.0.0",
"eslint-plugin-workspaces": "^0.9.0",
"husky": "^8.0.3",
"jest": "^29.5.0",
diff --git a/packages/rest/__tests__/src/routing.spec.mts b/packages/rest/__tests__/src/routing.spec.mts
index bb18976..d5addc6 100644
--- a/packages/rest/__tests__/src/routing.spec.mts
+++ b/packages/rest/__tests__/src/routing.spec.mts
@@ -87,7 +87,6 @@ class TestController {
statusCode: HttpStatusCode.Ok,
body: `cool`,
headers: {
- // eslint-disable-next-line sonarjs/no-duplicate-string
"content-type": MimeType.TextPlain,
},
}));
diff --git a/packages/rest/package.json b/packages/rest/package.json
index ffa4e7a..7927889 100644
--- a/packages/rest/package.json
+++ b/packages/rest/package.json
@@ -7,15 +7,19 @@
"@nrfcloud/ts-json-schema-transformer": "^1.2.4",
"@types/aws-lambda": "^8.10.115",
"ajv": "^8.12.0",
+ "atlassian-openapi": "^1.0.18",
+ "lodash": "^4.17.21",
"openapi-types": "^12.1.0",
"trouter": "^3.2.1",
+ "ts-is-present": "^1.2.2",
"ts-json-schema-generator": "^1.4.0",
- "ts-morph": "^19.0.0",
+ "ts-morph": "^20.0.0",
"tsutils": "^3.21.0"
},
"devDependencies": {
"@jest/globals": "^29.5.0",
"@types/jest": "^29.4.0",
+ "@types/lodash": "^4.14.202",
"@types/node": "^18.15.11",
"eslint": "^8.45.0",
"jest": "^29.5.0",
diff --git a/packages/rest/src/runtime/decorators.mts b/packages/rest/src/runtime/decorators.mts
index 82fdc8a..01b9525 100644
--- a/packages/rest/src/runtime/decorators.mts
+++ b/packages/rest/src/runtime/decorators.mts
@@ -24,9 +24,11 @@ const routeChainDecorator = (_path: Path)
+ {
+ return routeChainDecorator as IfEquals;
}
/**
@@ -34,8 +36,8 @@ export function GetChain(_path: string) {
*
* @originator nornir/rest
*/
-export function PostChain(_path: string) {
- return routeChainDecorator;
+export function PostChain(_path: Path) {
+ return routeChainDecorator as IfEquals;
}
/**
@@ -43,8 +45,8 @@ export function PostChain(_path: string) {
*
* @originator nornir/rest
*/
-export function PutChain(_path: string) {
- return routeChainDecorator;
+export function PutChain(_path: Path) {
+ return routeChainDecorator as IfEquals;
}
/**
@@ -52,8 +54,8 @@ export function PutChain(_path: string) {
*
* @originator nornir/rest
*/
-export function PatchChain(_path: string) {
- return routeChainDecorator;
+export function PatchChain(_path: Path) {
+ return routeChainDecorator as IfEquals;
}
/**
@@ -61,8 +63,8 @@ export function PatchChain(_path: string) {
*
* @originator nornir/rest
*/
-export function DeleteChain(_path: string) {
- return routeChainDecorator;
+export function DeleteChain(_path: Path) {
+ return routeChainDecorator as IfEquals;
}
/**
@@ -70,8 +72,8 @@ export function DeleteChain(_path: string) {
*
* @originator nornir/rest
*/
-export function HeadChain(_path: string) {
- return routeChainDecorator;
+export function HeadChain(_path: Path) {
+ return routeChainDecorator as IfEquals;
}
/**
@@ -79,8 +81,8 @@ export function HeadChain(_path: string) {
*
* @originator nornir/rest
*/
-export function OptionsChain(_path: string) {
- return routeChainDecorator;
+export function OptionsChain(_path: Path) {
+ return routeChainDecorator as IfEquals;
}
/**
@@ -93,3 +95,7 @@ export function Provider() {
throw UNTRANSFORMED_ERROR
}
}
+
+type Exact = IfEquals;
+type IfEquals = (() => G extends T ? 1 : 2) extends (() => G extends U ? 1 : 2) ? Y
+ : N;
diff --git a/packages/rest/src/runtime/router.mts b/packages/rest/src/runtime/router.mts
index 0164499..20b2331 100644
--- a/packages/rest/src/runtime/router.mts
+++ b/packages/rest/src/runtime/router.mts
@@ -63,7 +63,6 @@ export class Router {
return async (event, registry): Promise => {
- // eslint-disable-next-line unicorn/no-array-method-this-argument, unicorn/no-array-callback-reference
const {params, handlers: [handler]} = this.router.find(event.method, event.path);
const request: HttpRequest = {
...event,
diff --git a/packages/rest/src/transform/controller-meta.ts b/packages/rest/src/transform/controller-meta.ts
index 0cba723..4629b4a 100644
--- a/packages/rest/src/transform/controller-meta.ts
+++ b/packages/rest/src/transform/controller-meta.ts
@@ -1,7 +1,36 @@
+import { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
import ts from "typescript";
+import { isErrorResult, merge, MergeInput } from "./openapi-merge";
import { Project } from "./project";
import { FileTransformer } from "./transformers/file-transformer";
+export abstract class OpenApiSpecHolder {
+ private static specFileMap = new Map();
+
+ public static addSpecForFile(file: ts.SourceFile, spec: OpenAPIV3_1.Document) {
+ const fileSpecs = this.specFileMap.get(file.fileName) || [];
+ fileSpecs.push(spec);
+ this.specFileMap.set(file.fileName, fileSpecs);
+ }
+
+ public static getSpecForFile(file: ts.SourceFile): OpenAPIV3_1.Document {
+ const mergeInputs: MergeInput = (this.specFileMap.get(file.fileName) || [])
+ .map((spec) => ({
+ oas: spec,
+ dispute: {
+ alwaysApply: true,
+ },
+ })) as MergeInput;
+
+ const merged = merge(mergeInputs);
+ if (isErrorResult(merged)) {
+ throw new Error(merged.message);
+ }
+
+ return merged.output as OpenAPIV3_1.Document;
+ }
+}
+
export class ControllerMeta {
private static cache = new Map();
private static routes = new Map>();
@@ -174,6 +203,9 @@ export class ControllerMeta {
input: ts.Type;
output: ts.Type;
filePath: string;
+ tags?: string[];
+ deprecated?: boolean;
+ operationId?: string;
}) {
if (this.project.transformOnly) {
return;
@@ -186,6 +218,8 @@ export class ControllerMeta {
throw new Error(`Route already registered: ${index.method} ${index.path}`);
}
+ OpenApiSpecHolder.addSpecForFile(this.source, this.generateRouteSpec(routeInfo));
+
methods.set(index.method, {
method: routeInfo.method,
path: this.basePath + routeInfo.path.toLowerCase(),
@@ -197,6 +231,32 @@ export class ControllerMeta {
});
}
+ private generateRouteSpec(route: RouteInfo): OpenAPIV3_1.Document {
+ return {
+ openapi: "3.0.3",
+ info: {
+ title: "Nornir API",
+ version: "1.0.0",
+ },
+ paths: {
+ [this.basePath + route.path.toLowerCase()]: {
+ [route.method.toLowerCase()]: {
+ deprecated: route.deprecated,
+ tags: route.tags,
+ operationId: route.operationId,
+ summary: route.summary,
+ description: route.description,
+ responses: {
+ 200: {
+ description: "OK",
+ },
+ },
+ },
+ },
+ },
+ } as OpenAPIV3_1.Document;
+ }
+
// private buildRequestInfo(routeIndex: RouteIndex, inputType: ts.Type): RequestInfo {
// const paramterData: { [key in ParameterType]: { [name: string]: ParameterMeta } } = {
// path: {},
@@ -385,6 +445,9 @@ export interface RouteInfo {
path: string;
description?: string;
summary?: string;
+ deprecated?: boolean;
+ operationId?: string;
+ tags?: string[];
// requestInfo: RequestInfo;
// responseInfo: ResponseInfo;
filePath: string;
diff --git a/packages/rest/src/transform/lib.ts b/packages/rest/src/transform/lib.ts
index 9f0814f..7dac363 100644
--- a/packages/rest/src/transform/lib.ts
+++ b/packages/rest/src/transform/lib.ts
@@ -29,7 +29,8 @@ export function getStringLiteralOrConst(project: Project, node: ts.Expression):
export interface NornirDecoratorInfo {
decorator: ts.Decorator;
- signature: ts.Signature;
+ symbol: ts.Symbol;
+ // signature: ts.Signature;
declaration: ts.Declaration;
}
@@ -42,19 +43,34 @@ export function separateNornirDecorators(
} {
const nornirDecorators: {
decorator: ts.Decorator;
- signature: ts.Signature;
+ symbol: ts.Symbol;
+ // signature: ts.Signature;
declaration: ts.Declaration;
}[] = [];
const decorators: ts.Decorator[] = [];
for (const decorator of originalDecorators) {
- const signature = project.checker.getResolvedSignature(decorator);
- const parentDeclaration = signature?.getDeclaration()?.parent;
- if (parentDeclaration && signature && signature.declaration && isNornirRestNode(parentDeclaration)) {
+ const identifier = ts.isIdentifier(decorator.expression)
+ ? decorator.expression
+ : ts.isCallExpression(decorator.expression)
+ ? decorator.expression.expression as ts.Identifier
+ : undefined;
+ if (!identifier) continue;
+ const identifierSymbol = project.checker.getSymbolAtLocation(identifier);
+ if (!identifierSymbol) continue;
+ const symbol = project.checker.getAliasedSymbol(identifierSymbol);
+ const declaration = symbol?.declarations?.[0];
+ if (!declaration) continue;
+
+ // const signature = project.checker.getResolvedSignature(decorator);
+ //
+ // const parentDeclaration = signature?.getDeclaration()?.parent;
+ if (isNornirRestNode(declaration)) {
nornirDecorators.push({
decorator,
- signature,
- declaration: signature.declaration,
+ symbol,
+ // signature,
+ declaration,
});
} else {
decorators.push(decorator);
diff --git a/packages/rest/src/transform/openapi-generator.ts b/packages/rest/src/transform/openapi-generator.ts
index 3a1aeef..5a29ac4 100644
--- a/packages/rest/src/transform/openapi-generator.ts
+++ b/packages/rest/src/transform/openapi-generator.ts
@@ -1,5 +1,3 @@
-/* eslint-disable eslint-comments/disable-enable-pair,unicorn/no-empty-file */
-
// import type { OpenAPIV3 } from "openapi-types";
// import { Metadata } from "typia/lib/metadata/Metadata";
// import { ApplicationProgrammer } from "typia/lib/programmers/ApplicationProgrammer";
diff --git a/packages/rest/src/transform/openapi-merge/component-equivalence.ts b/packages/rest/src/transform/openapi-merge/component-equivalence.ts
new file mode 100644
index 0000000..1a613f8
--- /dev/null
+++ b/packages/rest/src/transform/openapi-merge/component-equivalence.ts
@@ -0,0 +1,108 @@
+import { Swagger, SwaggerLookup as Lookup, SwaggerTypeChecks as TC } from "atlassian-openapi";
+import _ from "lodash";
+import { Modify } from "./reference-walker";
+
+export type ReferenceWalker = (component: A, modify: Modify) => void;
+
+function referenceCount(walker: ReferenceWalker, component: A): number {
+ let count = 0;
+ walker(component, ref => {
+ count++;
+ return ref;
+ });
+ return count;
+}
+
+export function shallowEquality(
+ referenceWalker: ReferenceWalker,
+): (x: A | Swagger.Reference, y: A | Swagger.Reference) => boolean {
+ return (x: A | Swagger.Reference, y: A | Swagger.Reference): boolean => {
+ if (!_.isEqual(x, y)) {
+ return false;
+ }
+
+ if (TC.isReference(x)) {
+ return false;
+ }
+
+ return referenceCount(referenceWalker, x) === 0;
+ };
+}
+
+function isSchemaOrThrowError(ref: Swagger.Reference, s: Swagger.Schema | undefined): Swagger.Schema {
+ if (s === undefined) {
+ throw new Error(`Could not resolve reference: ${ref.$ref}`);
+ }
+ return s;
+}
+
+function arraysEquivalent(leftOriginal: Array, rightOriginal: Array): boolean {
+ if (leftOriginal.length !== rightOriginal.length) {
+ return false;
+ }
+
+ const left = [...leftOriginal].sort();
+ const right = [...rightOriginal].sort();
+
+ for (let index = 0; index < left.length; index++) {
+ if (left[index] !== right[index]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// The idea is that, if you have made this comparison before, then don't do it again, just return true becauese you have a cycle
+type SeenResult = "seen-before" | "new";
+
+class ReferenceRecord {
+ private leftRightSeen: { [leftRef: string]: { [rightRef: string]: boolean } } = {};
+
+ public checkAndStore(left: Swagger.Reference, right: Swagger.Reference): SeenResult {
+ if (this.leftRightSeen[left.$ref] === undefined) {
+ this.leftRightSeen[left.$ref] = {};
+ }
+
+ const leftLookup = this.leftRightSeen[left.$ref];
+
+ const result: SeenResult = leftLookup[right.$ref] === true ? "seen-before" : "new";
+ leftLookup[right.$ref] = true;
+ return result;
+ }
+}
+
+export function deepEquality(
+ xLookup: Lookup.Lookup,
+ yLookup: Lookup.Lookup,
+): (x: A | Swagger.Reference, y: A | Swagger.Reference) => boolean {
+ const refRecord = new ReferenceRecord();
+
+ function compare(x: T | Swagger.Reference, y: T | Swagger.Reference): boolean {
+ // If both are references then look up the references and compare them for equality
+ if (TC.isReference(x) && TC.isReference(y)) {
+ if (refRecord.checkAndStore(x, y) === "seen-before") {
+ return true;
+ }
+
+ const xResult = isSchemaOrThrowError(x, xLookup.getSchema(x));
+ const yResult = isSchemaOrThrowError(y, yLookup.getSchema(y));
+ return compare(xResult, yResult);
+ } else if (TC.isReference(x) || TC.isReference(y)) {
+ return false;
+ } else if (typeof x === "object" && typeof y === "object") {
+ // If both are objects then they should have all of the same keys and the values of those keys should match
+ if (!arraysEquivalent(Object.keys(x as object), Object.keys(y as object))) {
+ return false;
+ }
+
+ const xKeys = Object.keys(x as object) as Array;
+ return xKeys.every(key => compare(x?.[key], y?.[key]));
+ }
+
+ // If they are not objects or references then you can just run a direct equality
+ return _.isEqual(x, y);
+ }
+
+ return compare;
+}
diff --git a/packages/rest/src/transform/openapi-merge/data.ts b/packages/rest/src/transform/openapi-merge/data.ts
new file mode 100644
index 0000000..91cb2ae
--- /dev/null
+++ b/packages/rest/src/transform/openapi-merge/data.ts
@@ -0,0 +1,148 @@
+import { Swagger } from "atlassian-openapi";
+
+export type OperationSelection = {
+ /**
+ * Only Operations that have these tags will be taken from this OpenAPI file. If a single Operation contains
+ * an includeTag and an excludeTag then it will be excluded; exclusion takes precedence.
+ */
+ includeTags?: string[];
+
+ /**
+ * Any Operation that has any one of these tags will be excluded from the final result. If a single Operation contains
+ * an includeTag and an excludeTag then it will be excluded; exclusion takes precedence.
+ */
+ excludeTags?: string[];
+};
+
+export interface DisputeBase {
+ /**
+ * If this is set to true, then this prefix will always be applied to every Schema, even if there is no dispute
+ * for that particular schema. This may prevent the deduplication of common schemas from different OpenApi files.
+ */
+ alwaysApply?: boolean;
+ /**
+ * If this is set to true, then this well deep merge components, bringing all keys and values from identically
+ * named components in to the one object
+ */
+ mergeDispute?: boolean;
+}
+
+export interface DisputePrefix extends DisputeBase {
+ /**
+ * The prefix to use when a schema is in dispute.
+ */
+ prefix: string;
+}
+
+export interface DisputeSuffix extends DisputeBase {
+ /**
+ * The suffix to use when a schema is in dispute.
+ */
+ suffix: string;
+}
+
+export type Dispute = DisputePrefix | DisputeSuffix;
+
+export interface SingleMergeInputBase {
+ oas: Swagger.SwaggerV3;
+
+ pathModification?: PathModification;
+
+ /**
+ * Any Operation tagged with one of the paths in this definition will be excluded from the merge result. Any tag
+ * mentioned in this list will also be excluded from the top level list of tags.
+ */
+ operationSelection?: OperationSelection;
+
+ /**
+ * This configuration setting lets you configure how the info.description from this OpenAPI file will be merged
+ * into the final resulting OpenAPI file
+ */
+ description?: DescriptionMergeBehaviour;
+}
+
+/**
+ * The original SingelMergeInput, now deprecated. This is included for backwards compatibility, to prevent a breaking
+ * change and should be removed in the next major version.
+ *
+ * @deprecated
+ */
+export interface SingleMergeInputV1 extends SingleMergeInputBase {
+ /**
+ * The prefix to use in the event of a dispute.
+ *
+ * @deprecated
+ */
+ disputePrefix?: string;
+}
+
+/**
+ * The current expected format of the SingleMergeInput.
+ */
+export interface SingleMergeInputV2 extends SingleMergeInputBase {
+ /**
+ * This dictates how any disputes will be resolved between similar elements across multiple OpenAPI files.
+ */
+ dispute?: Dispute;
+ /**
+ * When set to false, allows operation IDs to be non-uniqiue. Default behaviour is to force a unique suffix unless
+ * specifically set
+ */
+ uniqueOperations?: boolean;
+}
+
+export type SingleMergeInput = SingleMergeInputV1 | SingleMergeInputV2;
+
+export type PathModification = {
+ stripStart?: string;
+ prepend?: string;
+};
+
+export type DescriptionMergeBehaviour = {
+ /**
+ * Wether or not the description for this OpenAPI file will be merged into the description of the final file.
+ */
+ append: boolean;
+
+ /**
+ * You may optionally include a Markdown Title to demarcate this particular section of the merged description files.
+ */
+ title?: DescriptionTitle;
+};
+
+export type DescriptionTitle = {
+ /**
+ * The value of the included title.
+ *
+ * @minLength 1
+ */
+ value: string;
+
+ /**
+ * What heading level this heading will be at: from h1 through to h6. The default value is 1 and will create h1 elements
+ * in Markdown format.
+ *
+ * @minimum 1
+ * @maximum 6
+ */
+ headingLevel?: number;
+};
+
+export type MergeInput = Array;
+
+export type SuccessfulMergeResult = {
+ output: Swagger.SwaggerV3;
+};
+
+export type ErrorType = "no-inputs" | "duplicate-paths" | "component-definition-conflict" | "operation-id-conflict";
+
+export type ErrorMergeResult = {
+ type: ErrorType;
+ message: string;
+};
+
+export function isErrorResult(t: object): t is ErrorMergeResult {
+ return "type" in t && "message" in t;
+}
+
+export type MergeResult = SuccessfulMergeResult | ErrorMergeResult;
diff --git a/packages/rest/src/transform/openapi-merge/dispute.ts b/packages/rest/src/transform/openapi-merge/dispute.ts
new file mode 100644
index 0000000..0df94e6
--- /dev/null
+++ b/packages/rest/src/transform/openapi-merge/dispute.ts
@@ -0,0 +1,35 @@
+import { Dispute, DisputePrefix, SingleMergeInput } from "./data";
+
+export function getDispute(input: SingleMergeInput): Dispute | undefined {
+ if ("disputePrefix" in input) {
+ if (input.disputePrefix !== undefined) {
+ return {
+ prefix: input.disputePrefix,
+ };
+ }
+
+ return undefined;
+ } else if ("dispute" in input) {
+ return input.dispute;
+ }
+
+ return undefined;
+}
+
+export type DisputeStatus = "disputed" | "undisputed";
+
+function isDisputePrefix(dispute: Dispute): dispute is DisputePrefix {
+ return "prefix" in dispute;
+}
+
+export function applyDispute(dispute: Dispute | undefined, input: string, status: DisputeStatus): string {
+ if (dispute === undefined) {
+ return input;
+ }
+
+ if ((status === "disputed" && !dispute.mergeDispute) || dispute.alwaysApply) {
+ return isDisputePrefix(dispute) ? `${dispute.prefix}${input}` : `${input}${dispute.suffix}`;
+ }
+
+ return input;
+}
diff --git a/packages/rest/src/transform/openapi-merge/extensions.ts b/packages/rest/src/transform/openapi-merge/extensions.ts
new file mode 100644
index 0000000..015deaf
--- /dev/null
+++ b/packages/rest/src/transform/openapi-merge/extensions.ts
@@ -0,0 +1,51 @@
+import { Swagger } from "atlassian-openapi";
+
+/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+type Extensions = { [extensionKey: string]: any };
+
+function extractExtensions(input: Swagger.SwaggerV3): Extensions {
+ const result: Extensions = {};
+
+ const plainObject: Extensions = input;
+
+ for (const key in plainObject) {
+ /* eslint-disable-next-line no-prototype-builtins */
+ if (key.startsWith("x-") && plainObject.hasOwnProperty(key)) {
+ result[key] = plainObject[key];
+ }
+ }
+
+ return result;
+}
+
+function mergeExtensionsHelper(extensions: Extensions[]): Extensions {
+ if (extensions.length === 0) {
+ return {};
+ }
+
+ if (extensions.length === 1) {
+ return extensions[0];
+ }
+
+ const result = { ...extensions[0] };
+
+ for (let extensionIndex = 1; extensionIndex < extensions.length; extensionIndex++) {
+ const ext = extensions[extensionIndex];
+
+ for (const extensionKey in ext) {
+ /* eslint-disable-next-line no-prototype-builtins */
+ if (result[extensionKey] === undefined && ext.hasOwnProperty(extensionKey)) {
+ result[extensionKey] = ext[extensionKey];
+ }
+ }
+ }
+
+ return result;
+}
+
+export function mergeExtensions(mergeTarget: Swagger.SwaggerV3, oass: Swagger.SwaggerV3[]): Swagger.SwaggerV3 {
+ return {
+ ...mergeTarget,
+ ...mergeExtensionsHelper([extractExtensions(mergeTarget), ...oass.map(extractExtensions)]),
+ };
+}
diff --git a/packages/rest/src/transform/openapi-merge/index.ts b/packages/rest/src/transform/openapi-merge/index.ts
new file mode 100644
index 0000000..afb11c2
--- /dev/null
+++ b/packages/rest/src/transform/openapi-merge/index.ts
@@ -0,0 +1,56 @@
+import { Swagger } from "atlassian-openapi";
+import { isPresent } from "ts-is-present";
+import { isErrorResult, MergeInput, MergeResult, OperationSelection, PathModification } from "./data";
+import { mergeExtensions } from "./extensions";
+import { mergeInfos } from "./info";
+import { mergePathsAndComponents } from "./paths-and-components";
+import { mergeTags } from "./tags";
+
+export { isErrorResult, MergeInput, MergeResult, OperationSelection, PathModification };
+
+function getFirst(inputs: Array): A | undefined {
+ if (inputs.length > 0) {
+ return inputs[0];
+ }
+
+ return undefined;
+}
+
+function getFirstMatching(inputs: Array, extract: (input: A) => B | undefined): B | undefined {
+ return getFirst(inputs.map(extract).filter(isPresent));
+}
+
+/**
+ * Swagger Merge Tool
+ */
+export function merge(inputs: MergeInput): MergeResult {
+ if (inputs.length === 0) {
+ return { type: "no-inputs", message: "You must provide at least one OAS file as an input." };
+ }
+
+ const pathAndComponentResult = mergePathsAndComponents(inputs);
+
+ if (isErrorResult(pathAndComponentResult)) {
+ return pathAndComponentResult;
+ }
+
+ const { paths, components: retComponents } = pathAndComponentResult;
+
+ const components = Object.keys(retComponents).length === 0 ? undefined : retComponents;
+
+ const output: Swagger.SwaggerV3 = mergeExtensions(
+ {
+ openapi: "3.0.3",
+ info: mergeInfos(inputs),
+ servers: getFirstMatching(inputs, input => input.oas.servers),
+ externalDocs: getFirstMatching(inputs, input => input.oas.externalDocs),
+ security: getFirstMatching(inputs, input => input.oas.security),
+ tags: mergeTags(inputs),
+ paths,
+ components,
+ },
+ inputs.map(input => input.oas),
+ );
+
+ return { output };
+}
diff --git a/packages/rest/src/transform/openapi-merge/info.ts b/packages/rest/src/transform/openapi-merge/info.ts
new file mode 100644
index 0000000..fa932a5
--- /dev/null
+++ b/packages/rest/src/transform/openapi-merge/info.ts
@@ -0,0 +1,39 @@
+import { Swagger } from "atlassian-openapi";
+import _ from "lodash";
+import { isPresent } from "ts-is-present";
+import { MergeInput, SingleMergeInput } from "./data";
+
+function getInfoDescriptionWithHeading(mergeInput: SingleMergeInput): string | undefined {
+ const { description } = mergeInput.oas.info;
+
+ if (description === undefined) {
+ return undefined;
+ }
+
+ const trimmedDescription = description.trimRight();
+
+ if (mergeInput.description === undefined || mergeInput.description.title === undefined) {
+ return trimmedDescription;
+ }
+
+ const { title } = mergeInput.description;
+
+ const headingLevel = title.headingLevel || 1;
+
+ return `${"#".repeat(headingLevel)} ${title.value}\n\n${trimmedDescription}`;
+}
+
+export function mergeInfos(mergeInput: MergeInput): Swagger.Info {
+ const finalInfo = _.cloneDeep(mergeInput[0].oas.info);
+
+ const appendedDescriptions = mergeInput
+ .filter(i => i.description && i.description.append)
+ .map(getInfoDescriptionWithHeading)
+ .filter(isPresent);
+
+ if (appendedDescriptions.length > 0) {
+ finalInfo.description = appendedDescriptions.join("\n\n");
+ }
+
+ return finalInfo;
+}
diff --git a/packages/rest/src/transform/openapi-merge/operation-selection.ts b/packages/rest/src/transform/openapi-merge/operation-selection.ts
new file mode 100644
index 0000000..f1bd508
--- /dev/null
+++ b/packages/rest/src/transform/openapi-merge/operation-selection.ts
@@ -0,0 +1,84 @@
+import { Swagger } from "atlassian-openapi";
+import _ from "lodash";
+import { OperationSelection } from "./data";
+
+const allMethods: Swagger.Method[] = [
+ "get",
+ "put",
+ "post",
+ "delete",
+ "options",
+ "head",
+ "patch",
+ "trace",
+];
+
+function operationContainsAnyTag(operation: Swagger.Operation, tags: string[]): boolean {
+ return operation.tags !== undefined && operation.tags.some(tag => tags.includes(tag));
+}
+
+function dropOperationsThatHaveTags(originalOas: Swagger.SwaggerV3, excludedTags: string[]): Swagger.SwaggerV3 {
+ if (excludedTags.length === 0) {
+ return originalOas;
+ }
+
+ const oas = _.cloneDeep(originalOas);
+
+ for (const path in oas.paths) {
+ /* eslint-disable-next-line no-prototype-builtins */
+ if (oas.paths.hasOwnProperty(path)) {
+ const pathItem = oas.paths[path];
+
+ for (let i = 0; i < allMethods.length; i++) {
+ const method = allMethods[i];
+ const operation = pathItem[method];
+
+ if (operation !== undefined && operationContainsAnyTag(operation, excludedTags)) {
+ delete pathItem[method];
+ }
+ }
+ }
+ }
+
+ return oas;
+}
+
+function includeOperationsThatHaveTags(originalOas: Swagger.SwaggerV3, includeTags: string[]): Swagger.SwaggerV3 {
+ if (includeTags.length === 0) {
+ return originalOas;
+ }
+
+ const oas = _.cloneDeep(originalOas);
+
+ for (const path in oas.paths) {
+ /* eslint-disable-next-line no-prototype-builtins */
+ if (oas.paths.hasOwnProperty(path)) {
+ const pathItem = oas.paths[path];
+
+ for (let i = 0; i < allMethods.length; i++) {
+ const method = allMethods[i];
+ const operation = pathItem[method];
+
+ if (operation !== undefined && !operationContainsAnyTag(operation, includeTags)) {
+ delete pathItem[method];
+ }
+ }
+ }
+ }
+
+ return oas;
+}
+
+export function runOperationSelection(
+ originalOas: Swagger.SwaggerV3,
+ operationSelection: OperationSelection | undefined,
+): Swagger.SwaggerV3 {
+ if (operationSelection === undefined) {
+ return originalOas;
+ }
+
+ return dropOperationsThatHaveTags(
+ includeOperationsThatHaveTags(originalOas, operationSelection.includeTags || []),
+ operationSelection.excludeTags || [],
+ );
+}
diff --git a/packages/rest/src/transform/openapi-merge/paths-and-components.ts b/packages/rest/src/transform/openapi-merge/paths-and-components.ts
new file mode 100644
index 0000000..b69ba7c
--- /dev/null
+++ b/packages/rest/src/transform/openapi-merge/paths-and-components.ts
@@ -0,0 +1,434 @@
+import { Swagger, SwaggerLookup } from "atlassian-openapi";
+import _ from "lodash";
+import { deepEquality } from "./component-equivalence";
+import { Dispute, ErrorMergeResult, MergeInput } from "./data";
+import { applyDispute, getDispute } from "./dispute";
+import { runOperationSelection } from "./operation-selection";
+import { walkAllReferences } from "./reference-walker";
+
+export type PathAndComponents = {
+ paths: Swagger.Paths;
+ components: Swagger.Components;
+};
+
+function removeFromStart(input: string, trim: string): string {
+ if (input.startsWith(trim)) {
+ return input.substring(trim.length);
+ }
+
+ return input;
+}
+
+type Components = { [key: string]: A };
+type Equal = (x: A, y: A) => boolean;
+type AddModRef = (from: string, to: string) => void;
+
+function processComponents(
+ results: Components,
+ components: Components,
+ areEqual: Equal,
+ dispute: Dispute | undefined,
+ addModifiedReference: AddModRef,
+): ErrorMergeResult | undefined {
+ for (const key in components) {
+ /* eslint-disable-next-line no-prototype-builtins */
+ if (components.hasOwnProperty(key)) {
+ const component = components[key];
+
+ const modifiedKey = applyDispute(dispute, key, "undisputed");
+ if (modifiedKey !== key) {
+ addModifiedReference(key, modifiedKey);
+ }
+
+ if (results[modifiedKey] === undefined || areEqual(results[modifiedKey], component)) {
+ // Add the schema
+ results[modifiedKey] = component;
+ } else {
+ // Distnguish the name and then add the element
+ let schemaPlaced = false;
+
+ // Try and use the dispute prefix first
+ if (dispute !== undefined) {
+ const preferredSchemaKey = applyDispute(dispute, key, "disputed");
+ if (results[preferredSchemaKey] === undefined || areEqual(results[preferredSchemaKey], component)) {
+ results[preferredSchemaKey] = component;
+ addModifiedReference(key, preferredSchemaKey);
+ schemaPlaced = true;
+ } // Merge deeply if the flag is set in the dispute object
+ else if (dispute.mergeDispute && Object.keys(results).includes(preferredSchemaKey)) {
+ results[preferredSchemaKey] = _.merge(results[preferredSchemaKey], component);
+ addModifiedReference(key, preferredSchemaKey);
+ schemaPlaced = true;
+ }
+ }
+
+ // Incrementally find the right prefix
+ for (let antiConflict = 1; schemaPlaced === false && antiConflict < 1000; antiConflict++) {
+ const trySchemaKey = `${key}${antiConflict}`;
+
+ if (results[trySchemaKey] === undefined) {
+ results[trySchemaKey] = component;
+ addModifiedReference(key, trySchemaKey);
+ schemaPlaced = true;
+ }
+ }
+
+ // In the unlikely event that we can't find a duplicate, return an error
+ if (schemaPlaced === false) {
+ return {
+ type: "component-definition-conflict",
+ message: `The "${key}" definition had a duplicate in a previous input and could not be deduplicated.`,
+ };
+ }
+ }
+ }
+ }
+}
+
+function countOperationsInPathItem(pathItem: Swagger.PathItem): number {
+ let count = 0;
+ count += pathItem.get !== undefined ? 1 : 0;
+ count += pathItem.put !== undefined ? 1 : 0;
+ count += pathItem.post !== undefined ? 1 : 0;
+ count += pathItem.delete !== undefined ? 1 : 0;
+ count += pathItem.options !== undefined ? 1 : 0;
+ count += pathItem.head !== undefined ? 1 : 0;
+ count += pathItem.patch !== undefined ? 1 : 0;
+ count += pathItem.trace !== undefined ? 1 : 0;
+ return count;
+}
+
+function dropPathItemsWithNoOperations(originalOas: Swagger.SwaggerV3): Swagger.SwaggerV3 {
+ const oas = _.cloneDeep(originalOas);
+
+ for (const path in oas.paths) {
+ /* eslint-disable-next-line no-prototype-builtins */
+ if (oas.paths.hasOwnProperty(path)) {
+ const pathItem = oas.paths[path];
+
+ if (countOperationsInPathItem(pathItem) === 0) {
+ delete oas.paths[path];
+ }
+ }
+ }
+
+ return oas;
+}
+
+function findUniqueOperationId(
+ operationId: string,
+ seenOperationIds: Set,
+ dispute: Dispute | undefined,
+): string | ErrorMergeResult {
+ if (!seenOperationIds.has(operationId)) {
+ return operationId;
+ }
+
+ // Try the dispute prefix
+ if (dispute !== undefined) {
+ const disputeOpId = applyDispute(dispute, operationId, "disputed");
+ if (!seenOperationIds.has(disputeOpId)) {
+ return disputeOpId;
+ }
+ }
+
+ // Incrementally find the right prefix
+ for (let antiConflict = 1; antiConflict < 1000; antiConflict++) {
+ const tryOpId = `${operationId}${antiConflict}`;
+ if (!seenOperationIds.has(tryOpId)) {
+ return tryOpId;
+ }
+ }
+
+ // Fail with an error
+ return {
+ type: "operation-id-conflict",
+ message: `Could not resolve a conflict for the operationId '${operationId}'`,
+ };
+}
+
+function ensureUniqueOperationId(
+ operation: Swagger.Operation,
+ seenOperationIds: Set,
+ dispute: Dispute | undefined,
+): ErrorMergeResult | undefined {
+ if (operation.operationId !== undefined) {
+ const opId = findUniqueOperationId(operation.operationId, seenOperationIds, dispute);
+ if (typeof opId === "string") {
+ operation.operationId = opId;
+ seenOperationIds.add(opId);
+ } else {
+ return opId;
+ }
+ }
+}
+
+function ensureUniqueOperationIds(
+ pathItem: Swagger.PathItem,
+ seenOperationIds: Set,
+ dispute: Dispute | undefined,
+): ErrorMergeResult | undefined {
+ const operations = [
+ pathItem.get,
+ pathItem.put,
+ pathItem.post,
+ pathItem.delete,
+ pathItem.patch,
+ pathItem.head,
+ pathItem.trace,
+ pathItem.options,
+ ];
+
+ for (let opIndex = 0; opIndex < operations.length; opIndex++) {
+ const operation = operations[opIndex];
+
+ if (operation !== undefined) {
+ const result = ensureUniqueOperationId(operation, seenOperationIds, dispute);
+ if (result !== undefined) {
+ return result;
+ }
+ }
+ }
+}
+
+/**
+ * Merge algorithm:
+ *
+ * Generate reference mappings for the components. Eliminating duplicates.
+ * Generate reference mappings for the paths.
+ * Copy the elements into the new location.
+ * Update all of the paths and components to the new references.
+ *
+ * @param inputs
+ */
+export function mergePathsAndComponents(inputs: MergeInput): PathAndComponents | ErrorMergeResult {
+ const seenOperationIds = new Set();
+
+ const result: PathAndComponents = {
+ paths: {},
+ components: {},
+ };
+
+ for (let inputIndex = 0; inputIndex < inputs.length; inputIndex++) {
+ const input = inputs[inputIndex];
+
+ const { oas: originalOas, pathModification, operationSelection } = input;
+ const dispute = getDispute(input);
+
+ const oas = dropPathItemsWithNoOperations(runOperationSelection(_.cloneDeep(originalOas), operationSelection));
+
+ // Original references will be transformed to new non-conflicting references
+ const referenceModification: { [originalReference: string]: string } = {};
+
+ // For each component in the original input, place it in the output with deduplicate taking place
+ if (oas.components !== undefined) {
+ const resultLookup = new SwaggerLookup.InternalLookup({
+ openapi: "3.0.1",
+ info: { title: "dummy", version: "0" },
+ paths: {},
+ components: result.components,
+ });
+ const currentLookup = new SwaggerLookup.InternalLookup(oas);
+ if (oas.components.schemas !== undefined) {
+ result.components.schemas = result.components.schemas || {};
+
+ processComponents(
+ result.components.schemas,
+ oas.components.schemas,
+ deepEquality(resultLookup, currentLookup),
+ dispute,
+ (from: string, to: string) => {
+ referenceModification[`#/components/schemas/${from}`] = `#/components/schemas/${to}`;
+ },
+ );
+ }
+
+ if (oas.components.responses !== undefined) {
+ result.components.responses = result.components.responses || {};
+
+ processComponents(
+ result.components.responses,
+ oas.components.responses,
+ deepEquality(resultLookup, currentLookup),
+ dispute,
+ (from: string, to: string) => {
+ referenceModification[`#/components/responses/${from}`] = `#/components/responses/${to}`;
+ },
+ );
+ }
+
+ if (oas.components.parameters !== undefined) {
+ result.components.parameters = result.components.parameters || {};
+
+ processComponents(
+ result.components.parameters,
+ oas.components.parameters,
+ deepEquality(resultLookup, currentLookup),
+ dispute,
+ (from: string, to: string) => {
+ referenceModification[`#/components/parameters/${from}`] = `#/components/parameters/${to}`;
+ },
+ );
+ }
+
+ // examples
+ if (oas.components.examples !== undefined) {
+ result.components.examples = result.components.examples || {};
+
+ processComponents(
+ result.components.examples,
+ oas.components.examples,
+ deepEquality(resultLookup, currentLookup),
+ dispute,
+ (from: string, to: string) => {
+ referenceModification[`#/components/examples/${from}`] = `#/components/examples/${to}`;
+ },
+ );
+ }
+
+ // requestBodies
+ if (oas.components.requestBodies !== undefined) {
+ result.components.requestBodies = result.components.requestBodies || {};
+
+ processComponents(
+ result.components.requestBodies,
+ oas.components.requestBodies,
+ deepEquality(resultLookup, currentLookup),
+ dispute,
+ (from: string, to: string) => {
+ referenceModification[`#/components/requestBodies/${from}`] = `#/components/requestBodies/${to}`;
+ },
+ );
+ }
+
+ // headers
+ if (oas.components.headers !== undefined) {
+ result.components.headers = result.components.headers || {};
+
+ processComponents(
+ result.components.headers,
+ oas.components.headers,
+ deepEquality(resultLookup, currentLookup),
+ dispute,
+ (from: string, to: string) => {
+ referenceModification[`#/components/headers/${from}`] = `#/components/headers/${to}`;
+ },
+ );
+ }
+
+ // security schemes
+ if (oas.components.securitySchemes !== undefined) {
+ result.components.securitySchemes = result.components.securitySchemes || {};
+
+ processComponents(
+ result.components.securitySchemes,
+ oas.components.securitySchemes,
+ deepEquality(resultLookup, currentLookup),
+ { prefix: "", mergeDispute: true, ...dispute }, // security should always be merged
+ (from: string, to: string) => {
+ referenceModification[`#/components/securitySchemes/${from}`] = `#/components/securitySchemes/${to}`;
+ },
+ );
+ }
+
+ // links
+ if (oas.components.links !== undefined) {
+ result.components.links = result.components.links || {};
+
+ processComponents(
+ result.components.links,
+ oas.components.links,
+ deepEquality(resultLookup, currentLookup),
+ dispute,
+ (from: string, to: string) => {
+ referenceModification[`#/components/links/${from}`] = `#/components/links/${to}`;
+ },
+ );
+ }
+
+ // callbacks
+ if (oas.components.callbacks !== undefined) {
+ result.components.callbacks = result.components.callbacks || {};
+
+ processComponents(
+ result.components.callbacks,
+ oas.components.callbacks,
+ deepEquality(resultLookup, currentLookup),
+ dispute,
+ (from: string, to: string) => {
+ referenceModification[`#/components/callbacks/${from}`] = `#/components/callbacks/${to}`;
+ },
+ );
+ }
+ }
+
+ // For each path, convert it into the right format (looking out for duplicates)
+ const paths = Object.keys(oas.paths || {});
+
+ for (let pathIndex = 0; pathIndex < paths.length; pathIndex++) {
+ const originalPath = paths[pathIndex];
+
+ const newPath = pathModification === undefined
+ ? originalPath
+ : `${pathModification.prepend || ""}${removeFromStart(originalPath, pathModification.stripStart || "")}`;
+
+ if (originalPath !== newPath) {
+ referenceModification[`#/paths/${originalPath}`] = `#/paths/${newPath}`;
+ }
+
+ if (result.paths[newPath] !== undefined) {
+ const operations = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
+ const newMethods = Object.keys(input.oas.paths[originalPath]).filter((method) =>
+ Object.values(operations).includes(method.toLowerCase())
+ );
+ try {
+ newMethods.forEach((method) => {
+ const pathMethod = method.toLowerCase() as Swagger.Method;
+ if (result.paths[newPath][pathMethod]) {
+ throw {
+ type: "duplicate-paths",
+ message:
+ `Input ${inputIndex}: The method '${originalPath}:${pathMethod}' is already mapped to '${newPath}:${pathMethod}' and has already been added by another input file`,
+ };
+ } else {
+ const copyPathItem: Swagger.PathItem = { [method]: oas.paths[originalPath][pathMethod] };
+ if (!("uniqueOperations" in input && input.uniqueOperations === false)) {
+ ensureUniqueOperationIds(copyPathItem, seenOperationIds, dispute);
+ }
+ result.paths[newPath][pathMethod] = copyPathItem[pathMethod];
+ }
+ });
+ } catch (err) {
+ return err as ErrorMergeResult;
+ }
+ } else {
+ const copyPathItem = oas.paths[originalPath];
+ if (!("uniqueOperations" in input && input.uniqueOperations === false)) {
+ ensureUniqueOperationIds(copyPathItem, seenOperationIds, dispute);
+ }
+
+ result.paths[newPath] = copyPathItem;
+ }
+ }
+
+ // Update the references to point to the right location
+ const modifiedKeys = Object.keys(referenceModification);
+ walkAllReferences(oas, (ref) => {
+ if (referenceModification[ref] !== undefined) {
+ return referenceModification[ref];
+ }
+
+ const matchingKeys = modifiedKeys.filter((key) => key.startsWith(`${ref}/`));
+
+ if (matchingKeys.length > 1) {
+ throw new Error(`Found more than one matching key for reference '${ref}': ${JSON.stringify(matchingKeys)}`);
+ } else if (matchingKeys.length === 1) {
+ return referenceModification[matchingKeys[0]];
+ }
+
+ return ref;
+ });
+ }
+
+ return result;
+}
diff --git a/packages/rest/src/transform/openapi-merge/reference-walker.ts b/packages/rest/src/transform/openapi-merge/reference-walker.ts
new file mode 100644
index 0000000..1d3ddc1
--- /dev/null
+++ b/packages/rest/src/transform/openapi-merge/reference-walker.ts
@@ -0,0 +1,315 @@
+import { Swagger, SwaggerTypeChecks as TC } from "atlassian-openapi";
+
+export type Modify = (input: string) => string;
+
+export function walkSchemaReferences(schema: Swagger.Schema | Swagger.Reference, modify: Modify): void {
+ if (TC.isReference(schema)) {
+ schema.$ref = modify(schema.$ref);
+ } else {
+ if (schema.not !== undefined) walkSchemaReferences(schema.not, modify);
+
+ if (schema.allOf !== undefined) {
+ for (const childSchema of schema.allOf) {
+ walkSchemaReferences(childSchema, modify);
+ }
+ }
+
+ if (schema.oneOf !== undefined) {
+ for (const childSchema of schema.oneOf) {
+ walkSchemaReferences(childSchema, modify);
+ }
+ }
+
+ if (schema.anyOf !== undefined) {
+ for (const childSchema of schema.anyOf) {
+ walkSchemaReferences(childSchema, modify);
+ }
+ }
+
+ if (schema.items !== undefined) {
+ walkSchemaReferences(schema.items, modify);
+ }
+
+ for (const propertyKey in schema.properties) {
+ if (schema.properties.hasOwnProperty(propertyKey)) {
+ const property = schema.properties[propertyKey];
+ walkSchemaReferences(property, modify);
+ }
+ }
+
+ if (schema.additionalProperties !== undefined && typeof schema.additionalProperties !== "boolean") {
+ walkSchemaReferences(schema.additionalProperties, modify);
+ }
+ }
+}
+
+export function walkExampleReferences(example: Swagger.Example | Swagger.Reference, modify: Modify): void {
+ if (TC.isReference(example)) {
+ example.$ref = modify(example.$ref);
+ }
+}
+
+function walkMediaTypeReferences(mediaType: Swagger.MediaType, modify: Modify): void {
+ if (mediaType.schema !== undefined) walkSchemaReferences(mediaType.schema, modify);
+
+ if (TC.isMediaTypeWithExamples(mediaType)) {
+ if (mediaType.schema !== undefined) walkSchemaReferences(mediaType.schema, modify);
+
+ for (const exampleKey of Object.keys(mediaType.examples)) {
+ const example = mediaType.examples[exampleKey];
+ walkExampleReferences(example, modify);
+ }
+ }
+}
+
+export function walkParameterReferences(parameterOrRef: Swagger.ParameterOrRef, modify: Modify): void {
+ if (TC.isReference(parameterOrRef)) {
+ parameterOrRef.$ref = modify(parameterOrRef.$ref);
+ } else if (TC.isParameterWithSchema(parameterOrRef)) {
+ walkSchemaReferences(parameterOrRef.schema, modify);
+
+ if ("examples" in parameterOrRef) {
+ for (const exampleKey in parameterOrRef.examples) {
+ if (parameterOrRef.examples.hasOwnProperty(exampleKey)) {
+ const example = parameterOrRef.examples[exampleKey];
+ walkExampleReferences(example, modify);
+ }
+ }
+ }
+ } else {
+ for (const contentKey in parameterOrRef.content) {
+ if (parameterOrRef.content.hasOwnProperty(contentKey)) {
+ const mediaType = parameterOrRef.content[contentKey];
+ walkMediaTypeReferences(mediaType, modify);
+ }
+ }
+ }
+}
+
+export function walkRequestBodyReferences(requestBody: Swagger.RequestBody | Swagger.Reference, modify: Modify): void {
+ if (TC.isReference(requestBody)) {
+ requestBody.$ref = modify(requestBody.$ref);
+ } else {
+ for (const contentKey in requestBody.content) {
+ if (requestBody.content.hasOwnProperty(contentKey)) {
+ const mediaType = requestBody.content[contentKey];
+ walkMediaTypeReferences(mediaType, modify);
+ }
+ }
+ }
+}
+
+export function walkHeaderReferences(header: Swagger.Header | Swagger.Reference, modify: Modify): void {
+ if (TC.isReference(header)) {
+ header.$ref = modify(header.$ref);
+ } else if (TC.isHeaderWithSchema(header)) {
+ if (header.schema !== undefined) walkSchemaReferences(header.schema, modify);
+
+ if ("examples" in header) {
+ for (const exampleKey in header.examples) {
+ if (header.examples.hasOwnProperty(exampleKey)) {
+ const example = header.examples[exampleKey];
+ walkExampleReferences(example, modify);
+ }
+ }
+ }
+ } else {
+ for (const contentKey in header.content) {
+ if (header.content.hasOwnProperty(contentKey)) {
+ const mediaType = header.content[contentKey];
+ walkMediaTypeReferences(mediaType, modify);
+ }
+ }
+ }
+}
+
+export function walkLinkReferences(link: Swagger.Link | Swagger.Reference, modify: Modify): void {
+ if (TC.isReference(link)) {
+ link.$ref = modify(link.$ref);
+ } else {
+ // TODO work out if there are any references in here that should be updated
+ }
+}
+
+export function walkResponseReferences(response: Swagger.Response | Swagger.Reference, modify: Modify): void {
+ if (TC.isReference(response)) {
+ response.$ref = modify(response.$ref);
+ } else {
+ if (response.headers !== undefined) {
+ for (const headerKey of Object.keys(response.headers)) {
+ const headerOrRef = response.headers[headerKey];
+ walkHeaderReferences(headerOrRef, modify);
+ }
+ }
+
+ if (response.content !== undefined) {
+ const contentKeys = Object.keys(response.content);
+ for (let contentKeyIndex = 0; contentKeyIndex < contentKeys.length; contentKeyIndex++) {
+ const contentKey = contentKeys[contentKeyIndex];
+ const mediaType = response.content[contentKey];
+ walkMediaTypeReferences(mediaType, modify);
+ }
+ }
+
+ if (response.links !== undefined) {
+ const linkKeys = Object.keys(response.links);
+ for (let linkKeyIndex = 0; linkKeyIndex < linkKeys.length; linkKeyIndex++) {
+ const linkKey = linkKeys[linkKeyIndex];
+ const linkOrRef = response.links[linkKey];
+ walkLinkReferences(linkOrRef, modify);
+ }
+ }
+ }
+}
+
+export function walkCallbackReferences(callback: Swagger.Callback | Swagger.Reference, modify: Modify): void {
+ if (TC.isReference(callback)) {
+ callback.$ref = modify(callback.$ref);
+ } else {
+ for (const pathItemKey in callback) {
+ if (callback.hasOwnProperty(pathItemKey)) {
+ const pathItem = callback[pathItemKey];
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ walkPathItemReferences(pathItem, modify);
+ }
+ }
+ }
+}
+
+function walkOperationReferences(operation: Swagger.Operation, modify: Modify): void {
+ if (operation.parameters !== undefined) {
+ for (const parameterOrRef of operation.parameters) {
+ walkParameterReferences(parameterOrRef, modify);
+ }
+ }
+
+ if (operation.requestBody !== undefined) {
+ walkRequestBodyReferences(operation.requestBody, modify);
+ }
+
+ for (const responseKey in operation.responses) {
+ if (operation.responses.hasOwnProperty(responseKey)) {
+ const response = operation.responses[responseKey];
+ walkResponseReferences(response, modify);
+ }
+ }
+
+ if (operation.callbacks !== undefined) {
+ const callbackKeys = Object.keys(operation.callbacks);
+ for (let callbackKeyIndex = 0; callbackKeyIndex < callbackKeys.length; callbackKeyIndex++) {
+ const callbackKey = callbackKeys[callbackKeyIndex];
+ const callback = operation.callbacks[callbackKey];
+ walkCallbackReferences(callback, modify);
+ }
+ }
+}
+
+function walkPathItemReferences(pathItem: Swagger.PathItem, modify: Modify): void {
+ if (pathItem["$ref"] !== undefined) {
+ pathItem["$ref"] = modify(pathItem["$ref"]);
+ } else {
+ if (pathItem.get !== undefined) walkOperationReferences(pathItem.get, modify);
+ if (pathItem.put !== undefined) walkOperationReferences(pathItem.put, modify);
+ if (pathItem.post !== undefined) walkOperationReferences(pathItem.post, modify);
+ if (pathItem.delete !== undefined) walkOperationReferences(pathItem.delete, modify);
+ if (pathItem.options !== undefined) walkOperationReferences(pathItem.options, modify);
+ if (pathItem.head !== undefined) walkOperationReferences(pathItem.head, modify);
+ if (pathItem.patch !== undefined) walkOperationReferences(pathItem.patch, modify);
+ if (pathItem.trace !== undefined) walkOperationReferences(pathItem.trace, modify);
+
+ if (pathItem.parameters !== undefined) {
+ for (let parameterIndex = 0; parameterIndex < pathItem.parameters.length; parameterIndex++) {
+ walkParameterReferences(pathItem.parameters[parameterIndex], modify);
+ }
+ }
+ }
+}
+
+export function walkComponentReferences(components: Swagger.Components, modify: Modify): void {
+ if (components.schemas !== undefined) {
+ for (const schemaKey in components.schemas) {
+ if (components.schemas.hasOwnProperty(schemaKey)) {
+ const schema = components.schemas[schemaKey];
+ walkSchemaReferences(schema, modify);
+ }
+ }
+ }
+
+ if (components.responses !== undefined) {
+ for (const responsesKey in components.responses) {
+ if (components.responses.hasOwnProperty(responsesKey)) {
+ const response = components.responses[responsesKey];
+
+ walkResponseReferences(response, modify);
+ }
+ }
+ }
+
+ if (components.parameters !== undefined) {
+ for (const parameterKey in components.parameters) {
+ if (components.parameters.hasOwnProperty(parameterKey)) {
+ const parameter = components.parameters[parameterKey];
+ walkParameterReferences(parameter, modify);
+ }
+ }
+ }
+
+ if (components.examples !== undefined) {
+ for (const exampleKey in components.examples) {
+ if (components.examples.hasOwnProperty(exampleKey)) {
+ const example = components.examples[exampleKey];
+ walkExampleReferences(example, modify);
+ }
+ }
+ }
+
+ if (components.requestBodies !== undefined) {
+ for (const requestBodyKey in components.requestBodies) {
+ if (components.requestBodies.hasOwnProperty(requestBodyKey)) {
+ const requestBody = components.requestBodies[requestBodyKey];
+ walkRequestBodyReferences(requestBody, modify);
+ }
+ }
+ }
+
+ if (components.headers !== undefined) {
+ for (const headerKey in components.headers) {
+ if (components.headers.hasOwnProperty(headerKey)) {
+ const header = components.headers[headerKey];
+ walkHeaderReferences(header, modify);
+ }
+ }
+ }
+
+ if (components.links !== undefined) {
+ for (const linkKey in components.links) {
+ if (components.links.hasOwnProperty(linkKey)) {
+ const link = components.links[linkKey];
+ walkLinkReferences(link, modify);
+ }
+ }
+ }
+
+ if (components.callbacks !== undefined) {
+ for (const componentKey in components.callbacks) {
+ if (components.callbacks.hasOwnProperty(componentKey)) {
+ const callback = components.callbacks[componentKey];
+ walkCallbackReferences(callback, modify);
+ }
+ }
+ }
+}
+
+export function walkPathReferences(paths: Swagger.Paths, modify: Modify): void {
+ for (const pathKey in paths) {
+ if (paths.hasOwnProperty(pathKey)) {
+ const path = paths[pathKey];
+ walkPathItemReferences(path, modify);
+ }
+ }
+}
+
+export function walkAllReferences(oas: Swagger.SwaggerV3, modify: Modify): void {
+ walkPathReferences(oas.paths, modify);
+ if (oas.components !== undefined) walkComponentReferences(oas.components, modify);
+}
diff --git a/packages/rest/src/transform/openapi-merge/tags.ts b/packages/rest/src/transform/openapi-merge/tags.ts
new file mode 100644
index 0000000..de1df6f
--- /dev/null
+++ b/packages/rest/src/transform/openapi-merge/tags.ts
@@ -0,0 +1,39 @@
+import { Swagger } from "atlassian-openapi";
+import { MergeInput } from "./data";
+
+function getNonExcludedTags(originalTags: Swagger.Tag[], excludedTagNames: string[]): Swagger.Tag[] {
+ if (excludedTagNames.length === 0) {
+ return originalTags;
+ }
+
+ return originalTags.filter(tag => !excludedTagNames.includes(tag.name));
+}
+
+export function mergeTags(inputs: MergeInput): Swagger.Tag[] | undefined {
+ const result = new Array();
+
+ const seenTags = new Set();
+ inputs.forEach(input => {
+ const { operationSelection } = input;
+ const { tags } = input.oas;
+ if (tags !== undefined) {
+ const excludeTags = operationSelection !== undefined && operationSelection.excludeTags !== undefined
+ ? operationSelection.excludeTags
+ : [];
+ const nonExcludedTags = getNonExcludedTags(tags, excludeTags);
+
+ nonExcludedTags.forEach(tag => {
+ if (!seenTags.has(tag.name)) {
+ seenTags.add(tag.name);
+ result.push(tag);
+ }
+ });
+ }
+ });
+
+ if (result.length === 0) {
+ return undefined;
+ }
+
+ return result;
+}
diff --git a/packages/rest/src/transform/project.ts b/packages/rest/src/transform/project.ts
index e8e3f7b..0a96af7 100644
--- a/packages/rest/src/transform/project.ts
+++ b/packages/rest/src/transform/project.ts
@@ -16,6 +16,7 @@ export interface Project {
schemaGenerator: SchemaGenerator;
typeFormatter: TypeFormatter;
nodeParser: NodeParser;
+ parsedCommandLine: ts.ParsedCommandLine;
}
export type AJVOptions = Pick<
diff --git a/packages/rest/src/transform/transform.ts b/packages/rest/src/transform/transform.ts
index 3626edf..7e7e2ab 100644
--- a/packages/rest/src/transform/transform.ts
+++ b/packages/rest/src/transform/transform.ts
@@ -54,6 +54,23 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme
schemaConfig,
);
+ const compilerHost = program.getCompilerOptions().incremental
+ ? ts.createIncrementalCompilerHost(program.getCompilerOptions())
+ : ts.createCompilerHost(program.getCompilerOptions());
+
+ const parseConfigHost: ts.ParseConfigFileHost = {
+ useCaseSensitiveFileNames: true,
+ fileExists: fileName => compilerHost!.fileExists(fileName),
+ readFile: fileName => compilerHost!.readFile(fileName),
+ directoryExists: f => compilerHost!.directoryExists!(f),
+ getDirectories: f => compilerHost!.getDirectories!(f),
+ realpath: compilerHost.realpath,
+ readDirectory: (...args) => compilerHost!.readDirectory!(...args),
+ trace: compilerHost.trace,
+ getCurrentDirectory: compilerHost.getCurrentDirectory,
+ onUnRecoverableConfigFileDiagnostic: () => {},
+ };
+
project = {
transformOnly: false,
program,
@@ -67,9 +84,12 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme
schemaGenerator,
nodeParser,
typeFormatter,
- compilerHost: program.getCompilerOptions().incremental
- ? ts.createIncrementalCompilerHost(program.getCompilerOptions())
- : ts.createCompilerHost(program.getCompilerOptions()),
+ compilerHost,
+ parsedCommandLine: ts.getParsedCommandLineOfConfigFile(
+ ts.findConfigFile(process.cwd(), ts.sys.fileExists, "tsconfig.json") as string,
+ program.getCompilerOptions(),
+ parseConfigHost,
+ )!,
};
return (context) => {
diff --git a/packages/rest/src/transform/transformers/controller-method-transformer.ts b/packages/rest/src/transform/transformers/controller-method-transformer.ts
index 30382b9..47c7bec 100644
--- a/packages/rest/src/transform/transformers/controller-method-transformer.ts
+++ b/packages/rest/src/transform/transformers/controller-method-transformer.ts
@@ -18,13 +18,13 @@ export abstract class ControllerMethodTransformer {
if (nornirDecorators.length === 0) return node;
const methodDecorator = nornirDecorators.find(decorator => {
- const name = project.checker.getTypeAtLocation(decorator.declaration.parent).symbol.name;
+ const name = decorator.symbol.name;
return METHOD_DECORATOR_PROCESSORS[name] != undefined;
});
if (!methodDecorator) return node;
- const method = project.checker.getTypeAtLocation(methodDecorator.declaration.parent).symbol.name;
+ const method = methodDecorator.symbol.name;
if (!method) return node;
diff --git a/packages/rest/src/transform/transformers/file-transformer.ts b/packages/rest/src/transform/transformers/file-transformer.ts
index 487ad93..3ce73f2 100644
--- a/packages/rest/src/transform/transformers/file-transformer.ts
+++ b/packages/rest/src/transform/transformers/file-transformer.ts
@@ -1,5 +1,7 @@
+import { rmSync } from "fs";
+import { parseJsDocOfNode } from "tsutils";
import ts from "typescript";
-import { ControllerMeta } from "../controller-meta";
+import { ControllerMeta, OpenApiSpecHolder } from "../controller-meta";
import { TransformationError } from "../error";
import { Project } from "../project.js";
import { NodeTransformer } from "./node-transformer";
@@ -10,8 +12,18 @@ export abstract class FileTransformer {
// return file;
const transformed = FileTransformer.iterateNode(project, context, file, file);
+ const outputFileNames = ts.getOutputFileNames(project.parsedCommandLine, file.fileName, false);
+ const schemaFileName = `${outputFileNames[0]}.nornir.oas.json`;
+
+ const { compilerHost } = project;
const nodesToAdd = FileTransformer.getStatementsForFile(file);
- if (nodesToAdd == undefined) return transformed;
+ if (nodesToAdd == undefined) {
+ // delete schema file if it exists
+ if (compilerHost.fileExists(schemaFileName)) {
+ rmSync(schemaFileName);
+ }
+ return transformed;
+ }
const updated = ts.factory.updateSourceFile(
transformed,
@@ -27,6 +39,11 @@ export abstract class FileTransformer {
file.libReferenceDirectives,
);
+ const mergedSpec = OpenApiSpecHolder.getSpecForFile(file);
+ console.log("Merged Spec:", JSON.stringify(mergedSpec, null, 2));
+
+ compilerHost.writeFile(schemaFileName, JSON.stringify(mergedSpec, null, 2), false, undefined, []);
+
FileTransformer.FILE_NODE_MAP.delete(file.fileName);
FileTransformer.IMPORT_MAP.delete(file.fileName);
ControllerMeta.clearCache();
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 e741607..a15c3c0 100644
--- a/packages/rest/src/transform/transformers/processors/chain-route-processor.ts
+++ b/packages/rest/src/transform/transformers/processors/chain-route-processor.ts
@@ -15,6 +15,20 @@ export const ChainMethodDecoratorTypeMap = {
HeadChain: "HEAD",
} as const;
+const TagMapper = {
+ summary: (value?: string) => value,
+ description: (value?: string) => value,
+ deprecated: (value?: string) => value != "false",
+ operationId: (value?: string) => value,
+ tags: (value?: string) => value?.split(",").map(tag => tag.trim()) || [],
+} as const;
+
+const SupportedTags = Object.keys(TagMapper) as (keyof typeof TagMapper)[];
+
+type RouteTags = {
+ -readonly [K in keyof typeof TagMapper]?: Exclude, undefined>;
+};
+
export const ChainMethodDecoratorTypes = Object.keys(
ChainMethodDecoratorTypeMap,
) as (keyof typeof ChainMethodDecoratorTypeMap)[];
@@ -41,6 +55,8 @@ export abstract class ChainRouteProcessor {
const inputValidator = schemaToValidator(inputSchema, project.options.validation);
+ const parsedDocComments = ChainRouteProcessor.parseJSDoc(project, node);
+
controller.registerRoute(node, {
method,
path,
@@ -48,9 +64,12 @@ export abstract class ChainRouteProcessor {
// FIXME: this should be the output type not the input type
output: inputType,
- // description,
- // summary,
+ description: parsedDocComments.description,
+ summary: parsedDocComments.summary,
filePath: source.fileName,
+ deprecated: parsedDocComments.deprecated,
+ operationId: parsedDocComments.operationId,
+ tags: parsedDocComments.tags,
});
controller.addInitializationStatement(
@@ -64,7 +83,7 @@ export abstract class ChainRouteProcessor {
),
);
const { otherDecorators } = separateNornirDecorators(project, ts.getDecorators(node) || []);
- return ts.factory.createMethodDeclaration(
+ const recreatedNode = ts.factory.createMethodDeclaration(
[...(ts.getModifiers(node) || []), ...otherDecorators],
node.asteriskToken,
node.name,
@@ -74,6 +93,28 @@ export abstract class ChainRouteProcessor {
node.type,
node.body,
);
+
+ ts.setTextRange(recreatedNode, node);
+ ts.setOriginalNode(recreatedNode, node);
+
+ return recreatedNode;
+ }
+
+ private static parseJSDoc(project: Project, method: ts.MethodDeclaration): RouteTags {
+ const docs = ts.getJSDocCommentsAndTags(ts.getOriginalNode(method));
+ const topLevel = docs[0];
+ const description = ts.getTextOfJSDocComment(topLevel.comment);
+ if (!ts.isJSDoc(topLevel)) {
+ return {};
+ }
+
+ const tagSet = (topLevel.tags || [])
+ .map(tag => [tag.tagName.escapedText as string, ts.getTextOfJSDocComment(tag.comment)] as const)
+ .filter(tag => SupportedTags.includes(tag[0] as keyof typeof TagMapper))
+ .map(tag => [tag[0], TagMapper[tag[0] as keyof typeof TagMapper](tag[1])]);
+
+ tagSet.push(["description", description]);
+ return Object.fromEntries(tagSet) as RouteTags;
}
private static generateRouteStatement(
@@ -125,7 +166,7 @@ export abstract class ChainRouteProcessor {
}
private static getMethod(project: Project, methodDecorator: NornirDecoratorInfo): string {
- const name = project.checker.getTypeAtLocation(methodDecorator.declaration.parent).symbol.name;
+ const name = methodDecorator.symbol.name;
return ChainMethodDecoratorTypeMap[name as keyof typeof ChainMethodDecoratorTypeMap];
}
@@ -141,7 +182,6 @@ export abstract class ChainRouteProcessor {
throw new Error("Handler chain input must be a Nornir class");
}
- // eslint-disable-next-line unicorn/no-useless-undefined
const paramTypeNode = project.checker.typeToTypeNode(paramType, undefined, undefined) as ts.TypeReferenceNode;
const [paramTypeArg] = paramTypeNode.typeArguments || [];
if (!paramTypeArg) {
diff --git a/packages/rest/src/transform/transformers/processors/controller-processor.ts b/packages/rest/src/transform/transformers/processors/controller-processor.ts
index 6fe757b..7fc13ad 100644
--- a/packages/rest/src/transform/transformers/processors/controller-processor.ts
+++ b/packages/rest/src/transform/transformers/processors/controller-processor.ts
@@ -27,7 +27,7 @@ export abstract class ControllerProcessor {
const { otherDecorators } = separateNornirDecorators(project, ts.getDecorators(transformedNode) || []);
- return ts.factory.createClassDeclaration(
+ const recreatedNode = ts.factory.createClassDeclaration(
[...transformedModifiers, ...otherDecorators],
transformedNode.name,
transformedNode.typeParameters,
@@ -37,15 +37,18 @@ export abstract class ControllerProcessor {
...routeMeta.getGeneratedMembers(),
],
);
+
+ ts.setTextRange(recreatedNode, node);
+ ts.setOriginalNode(recreatedNode, node);
+
+ return recreatedNode;
}
private static getArguments(project: Project, nornirDecorators: NornirDecoratorInfo[]): {
basePath: string;
apiId: string;
} {
- const basePathDecorator = nornirDecorators.find((decorator) =>
- project.checker.getTypeAtLocation(decorator.declaration.parent).symbol.name === "Controller"
- );
+ const basePathDecorator = nornirDecorators.find((decorator) => decorator.symbol.name === "Controller");
if (basePathDecorator == undefined) throw new Error("Controller must have a controller decorator");
if (!ts.isCallExpression(basePathDecorator.decorator.expression)) {
throw new Error("Controller decorator is not a call expression");
diff --git a/packages/test/__tests__/src/test.spec.ts b/packages/test/__tests__/src/test.spec.ts
index 05de923..3f79cc4 100644
--- a/packages/test/__tests__/src/test.spec.ts
+++ b/packages/test/__tests__/src/test.spec.ts
@@ -1,4 +1,3 @@
-// eslint-disable-next-line unicorn/no-empty-file
describe("test something", () => {
it.skip("should do something", () => {
expect(true).toBe(true);
diff --git a/packages/test/src/controller.ts b/packages/test/src/controller.ts
index df16f2e..6b214b6 100644
--- a/packages/test/src/controller.ts
+++ b/packages/test/src/controller.ts
@@ -13,14 +13,12 @@ import { assertValid } from "@nrfcloud/ts-json-schema-transformer";
interface RouteGetInput extends HttpRequestEmpty {
headers: {
- // eslint-disable-next-line sonarjs/no-duplicate-string
"content-type": AnyMimeType;
};
}
interface RoutePostInputJSON extends HttpRequest {
headers: {
- // eslint-disable-next-line sonarjs/no-duplicate-string
"content-type": MimeType.ApplicationJson;
};
body: RoutePostBodyInput;
@@ -77,17 +75,18 @@ export declare class Tagged {
*/
export type Nominal = T & Tagged> = (T & Tagged) | E;
-const basePath = "/basepath";
+const overallBase = "/root";
+const basePath = `${overallBase}/basepath`;
+
+/**
+ * This is a controller
+ * @summary This is a summary
+ */
@Controller(basePath)
export class TestController {
- static {
- console.log("hello");
- }
-
/**
- * A simple get route
- * @summary Cool Route
+ * Cool get route
*/
@GetChain("/route")
public getRoute(chain: Nornir) {
@@ -105,6 +104,14 @@ export class TestController {
},
}));
}
+
+ /**
+ * A simple post route
+ * @summary Cool Route
+ * @tags cool, route
+ * @deprecated
+ * @operationId coolRoute
+ */
@PostChain("/route")
public postRoute(chain: Nornir) {
return chain
diff --git a/packages/test/src/controller2.ts b/packages/test/src/controller2.ts
index a91d72b..b08a81c 100644
--- a/packages/test/src/controller2.ts
+++ b/packages/test/src/controller2.ts
@@ -43,6 +43,10 @@ interface RoutePostBodyInput {
const basePath = "/basepath/2";
+/**
+ * This is a second controller
+ * @summary This is a summary
+ */
@Controller(basePath, "test")
export class TestController {
@Provider()
@@ -67,6 +71,10 @@ export class TestController {
}));
}
+ /**
+ * The second simple PUT route.
+ * @summary Put route
+ */
@PutChain("/route")
public postRoute(chain: Nornir): Nornir {
return chain
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 00d31ca..beb93aa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -140,18 +140,27 @@ importers:
ajv:
specifier: ^8.12.0
version: 8.12.0
+ atlassian-openapi:
+ specifier: ^1.0.18
+ version: 1.0.18
+ lodash:
+ specifier: ^4.17.21
+ version: 4.17.21
openapi-types:
specifier: ^12.1.0
version: 12.1.0
trouter:
specifier: ^3.2.1
version: 3.2.1
+ ts-is-present:
+ specifier: ^1.2.2
+ version: 1.2.2
ts-json-schema-generator:
specifier: ^1.4.0
version: 1.4.0
ts-morph:
- specifier: ^19.0.0
- version: 19.0.0
+ specifier: ^20.0.0
+ version: 20.0.0
tsutils:
specifier: ^3.21.0
version: 3.21.0(typescript@5.2.2)
@@ -162,6 +171,9 @@ importers:
'@types/jest':
specifier: ^29.4.0
version: 29.4.0
+ '@types/lodash':
+ specifier: ^4.14.202
+ version: 4.14.202
'@types/node':
specifier: ^18.15.11
version: 18.15.11
@@ -2277,8 +2289,8 @@ packages:
'@sinonjs/commons': 2.0.0
dev: true
- /@ts-morph/common@0.20.0:
- resolution: {integrity: sha512-7uKjByfbPpwuzkstL3L5MQyuXPSKdoNG93Fmi2JoDcTf3pEP731JdRFAduRVkOs8oqxPsXKA+ScrWkdQ8t/I+Q==}
+ /@ts-morph/common@0.21.0:
+ resolution: {integrity: sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==}
dependencies:
fast-glob: 3.2.12
minimatch: 7.4.6
@@ -2377,10 +2389,10 @@ packages:
/@types/lodash.clonedeep@4.5.7:
resolution: {integrity: sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw==}
dependencies:
- '@types/lodash': 4.14.194
+ '@types/lodash': 4.14.202
- /@types/lodash@4.14.194:
- resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==}
+ /@types/lodash@4.14.202:
+ resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==}
/@types/minimist@1.2.2:
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
@@ -2789,6 +2801,13 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /atlassian-openapi@1.0.18:
+ resolution: {integrity: sha512-IXgF/cYD8DW1mYB/ejDm/lKQMNXi2iCsxus2Y0ffZOxfa/SLoz0RuEZ4xu4suSRjtlda7qZDonQ6TAkQPVuQig==}
+ dependencies:
+ jsonpointer: 5.0.1
+ urijs: 1.19.11
+ dev: false
+
/available-typed-arrays@1.0.5:
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
engines: {node: '>= 0.4'}
@@ -5202,6 +5221,11 @@ packages:
resolution: {integrity: sha512-890w2Pjtj0iswAxalRlt2kHthi6HKrXEfZcn+ZNZptv7F3rUGIeDuZo+C+h4vXBHLEsVjJrHeCm35nYeZLzSBQ==}
engines: {node: '>=10.0.0'}
+ /jsonpointer@5.0.1:
+ resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/kind-of@6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
@@ -5293,7 +5317,6 @@ packages:
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
- dev: true
/log-symbols@4.1.0:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
@@ -6569,6 +6592,10 @@ packages:
typescript: 5.2.2
dev: true
+ /ts-is-present@1.2.2:
+ resolution: {integrity: sha512-cA5MPLWGWYXvnlJb4TamUUx858HVHBsxxdy8l7jxODOLDyGYnQOllob2A2jyDghGa5iJHs2gzFNHvwGJ0ZfR8g==}
+ dev: false
+
/ts-json-schema-generator@1.4.0:
resolution: {integrity: sha512-wm8vyihmGgYpxrqRshmYkWGNwEk+sf3xV2rUgxv8Ryeh7bSpMO7pZQOht+2rS002eDkFTxR7EwRPXVzrS0WJTg==}
engines: {node: '>=10.0.0'}
@@ -6582,10 +6609,10 @@ packages:
safe-stable-stringify: 2.4.3
typescript: 5.2.2
- /ts-morph@19.0.0:
- resolution: {integrity: sha512-D6qcpiJdn46tUqV45vr5UGM2dnIEuTGNxVhg0sk5NX11orcouwj6i1bMqZIz2mZTZB1Hcgy7C3oEVhAT+f6mbQ==}
+ /ts-morph@20.0.0:
+ resolution: {integrity: sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==}
dependencies:
- '@ts-morph/common': 0.20.0
+ '@ts-morph/common': 0.21.0
code-block-writer: 12.0.0
dev: false
@@ -6859,6 +6886,10 @@ packages:
dependencies:
punycode: 2.3.0
+ /urijs@1.19.11:
+ resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==}
+ dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
From ad03fb9e3dea455de05759ecef50b33ccc18ee26 Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Mon, 4 Dec 2023 11:44:46 -0800
Subject: [PATCH 02/16] in progress work
---
packages/rest/package.json | 1 +
packages/rest/src/runtime/http-event.mts | 7 +-
.../rest/src/transform/controller-meta.ts | 96 ++++++--
packages/rest/src/transform/transform.ts | 25 ++-
.../processors/chain-route-processor.ts | 30 ++-
packages/test/src/controller.ts | 51 +++--
packages/test/src/controller2.ts | 208 +++++++++---------
pnpm-lock.yaml | 89 +-------
8 files changed, 270 insertions(+), 237 deletions(-)
diff --git a/packages/rest/package.json b/packages/rest/package.json
index 7927889..0862773 100644
--- a/packages/rest/package.json
+++ b/packages/rest/package.json
@@ -8,6 +8,7 @@
"@types/aws-lambda": "^8.10.115",
"ajv": "^8.12.0",
"atlassian-openapi": "^1.0.18",
+ "json-schema-traverse": "^1.0.0",
"lodash": "^4.17.21",
"openapi-types": "^12.1.0",
"trouter": "^3.2.1",
diff --git a/packages/rest/src/runtime/http-event.mts b/packages/rest/src/runtime/http-event.mts
index 198bb17..f7f9d0e 100644
--- a/packages/rest/src/runtime/http-event.mts
+++ b/packages/rest/src/runtime/http-event.mts
@@ -31,9 +31,14 @@ export interface HttpRequest {
readonly body?: unknown;
- readonly pathParams: Record;
+ readonly pathParams: PathParams;
}
+/**
+ * @nornirIgnore
+ */
+type PathParams = Record;
+
export interface HttpRequestEmpty extends HttpRequest {
headers: {
"content-type": AnyMimeType;
diff --git a/packages/rest/src/transform/controller-meta.ts b/packages/rest/src/transform/controller-meta.ts
index 4629b4a..617d868 100644
--- a/packages/rest/src/transform/controller-meta.ts
+++ b/packages/rest/src/transform/controller-meta.ts
@@ -1,9 +1,14 @@
-import { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
+import traverse from "json-schema-traverse";
+import { IJsonSchema, OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
+import * as tsm from "ts-morph";
import ts from "typescript";
import { isErrorResult, merge, MergeInput } from "./openapi-merge";
import { Project } from "./project";
import { FileTransformer } from "./transformers/file-transformer";
+import { JSONSchemaType } from "ajv";
+import { ReferenceType, SubNodeParser, UnionType } from "ts-json-schema-generator";
+
export abstract class OpenApiSpecHolder {
private static specFileMap = new Map();
@@ -195,18 +200,7 @@ export class ControllerMeta {
};
}
- public registerRoute(node: ts.Node, routeInfo: {
- method: string;
- path: string;
- description?: string;
- summary?: string;
- input: ts.Type;
- output: ts.Type;
- filePath: string;
- tags?: string[];
- deprecated?: boolean;
- operationId?: string;
- }) {
+ public registerRoute(node: ts.Node, routeInfo: RouteInfo) {
if (this.project.transformOnly) {
return;
}
@@ -228,10 +222,16 @@ export class ControllerMeta {
// responseInfo: this.buildResponseInfo(index, routeInfo.output),
filePath: routeInfo.filePath,
summary: routeInfo.summary,
+ deprecated: routeInfo.deprecated,
+ operationId: routeInfo.operationId,
+ tags: routeInfo.tags,
+ input: routeInfo.input,
+ output: routeInfo.output,
});
}
private generateRouteSpec(route: RouteInfo): OpenAPIV3_1.Document {
+ this.generatePathParamsSpecs(route);
return {
openapi: "3.0.3",
info: {
@@ -254,9 +254,50 @@ export class ControllerMeta {
},
},
},
+ components: {
+ parameters: {},
+ },
} as OpenAPIV3_1.Document;
}
+ private generatePathParamsSpecs(routeInfo: RouteInfo): OpenAPIV3_1.ParameterObject[] {
+ const wrapped = tsm.createWrappedNode(routeInfo.input, { typeChecker: this.project.checker });
+ const property = wrapped.getType().getProperty("pathParams");
+ if (property == undefined) throw new Error("No pathParams property found");
+ const declarations = property.getDeclarations() as tsm.PropertySignature[];
+ const typeNodes = declarations.map((declaration) => declaration.getTypeNodeOrThrow());
+ const paramDeclaractions = typeNodes.map(typeNode => typeNode.getType())
+ .map(type => type.getProperties())
+ .flat()
+ .reduce((acc, property) => {
+ const name = property.getName();
+ const arr = acc[name] || [];
+ const declarations = property.getDeclarations() as tsm.PropertySignature[];
+ arr.push(...declarations);
+ acc[name] = arr;
+ return acc;
+ }, {} as Record);
+
+ const paramObjectSchema = this.project.schemaGenerator.createSchemaFromNodes(
+ [ts.factory.createUnionTypeNode(typeNodes.map((typeNode) => typeNode.compilerNode))],
+ );
+ const schemas = Object.fromEntries(
+ Object.entries(paramDeclaractions).map(([name, declarations]) => {
+ const typeSymbols = declarations.map((declaration) => declaration.getSymbolOrThrow());
+ const typeRefs = typeSymbols.map(symbol => {
+ const typeRef = ts.factory.createTypeReferenceNode(name, []);
+
+ (typeRef as any).symbol = symbol.compilerSymbol;
+ return typeRef;
+ });
+ const schema = this.project.schemaGenerator.createSchemaFromNodes(typeRefs);
+ return [name, schema] as const;
+ }),
+ );
+
+ return [];
+ }
+
// private buildRequestInfo(routeIndex: RouteIndex, inputType: ts.Type): RequestInfo {
// const paramterData: { [key in ParameterType]: { [name: string]: ParameterMeta } } = {
// path: {},
@@ -436,6 +477,27 @@ export class ControllerMeta {
// }
}
+// Traverses the schema and extracts a unified definition for the property at the given path
+// export function getUnifiedPropertySchema(schema: IJsonSchema, path: string) {
+// // Take a path from the json schema and convert it to a path in validated object
+// const convertJsonSchemaPathToPropertyPath = (path: string) => {
+// return path
+// // replace properties
+// .replace(/\/properties\//g, ".")
+// // replace items and index
+// .replace(/\/items\/(\d+)\//g, "[].")
+// // replace anyOf and index
+// .replace(/\/anyOf\/(\d+)\//g, ".")
+// // replace oneOf and index
+// .replace(/\/oneOf\/(\d+)\//g, ".")
+// // replace allOf and index
+// .replace(/\/allOf\/(\d+)\//g, ".")
+// }
+// const schema
+//
+// traverse()
+// }
+
function deparameterizePath(path: string) {
return path.replaceAll(/:[^/]+/g, ":param");
}
@@ -445,12 +507,12 @@ export interface RouteInfo {
path: string;
description?: string;
summary?: string;
+ input: ts.TypeNode;
+ output: ts.TypeNode;
+ filePath: string;
+ tags?: string[];
deprecated?: boolean;
operationId?: string;
- tags?: string[];
- // requestInfo: RequestInfo;
- // responseInfo: ResponseInfo;
- filePath: string;
}
export type RouteIndex = Pick;
diff --git a/packages/rest/src/transform/transform.ts b/packages/rest/src/transform/transform.ts
index 7e7e2ab..06ec4fa 100644
--- a/packages/rest/src/transform/transform.ts
+++ b/packages/rest/src/transform/transform.ts
@@ -1,4 +1,13 @@
-import { createFormatter, createParser, SchemaGenerator } from "ts-json-schema-generator";
+import {
+ BaseType,
+ Context,
+ createFormatter,
+ createParser,
+ NeverType,
+ ReferenceType,
+ SchemaGenerator,
+ SubNodeParser,
+} from "ts-json-schema-generator";
import ts from "typescript";
import { AJV_DEFAULTS, AJVOptions, Options, Project, SCHEMA_DEFAULTS, SchemaConfig } from "./project.js";
import { FileTransformer } from "./transformers/file-transformer.js";
@@ -6,6 +15,18 @@ import { FileTransformer } from "./transformers/file-transformer.js";
let project: Project;
// let files: string[] = [];
// let openapi: OpenAPIV3.Document;
+
+export class NornirIgnoreParser implements SubNodeParser {
+ supportsNode(node: ts.Node): boolean {
+ const tags = ts.getJSDocTags(node);
+ return tags.some((tag) => tag.tagName.getText() === "nornirIgnore");
+ }
+
+ createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType {
+ return new NeverType();
+ }
+}
+
export function transform(program: ts.Program, options?: Options): ts.TransformerFactory {
const {
loopEnum,
@@ -42,6 +63,8 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme
const nodeParser = createParser(program as unknown as Parameters[0], {
...schemaConfig,
+ }, (prs) => {
+ // prs.addNodeParser(new NornirIgnoreParser());
});
const typeFormatter = createFormatter({
...schemaConfig,
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 a15c3c0..1e1ab22 100644
--- a/packages/rest/src/transform/transformers/processors/chain-route-processor.ts
+++ b/packages/rest/src/transform/transformers/processors/chain-route-processor.ts
@@ -1,4 +1,5 @@
import { schemaToValidator } from "@nrfcloud/ts-json-schema-transformer/utils";
+import { createWrappedNode } from "ts-morph";
import { isTypeReference } from "tsutils";
import ts from "typescript";
import { ControllerMeta } from "../../controller-meta";
@@ -46,8 +47,7 @@ export abstract class ChainRouteProcessor {
// const wrappedNode = createWrappedNode(node, { typeChecker: project.checker }) as MethodDeclaration;
- const inputTypeNode = ChainRouteProcessor.resolveInputType(project, node);
- const inputType = project.checker.getTypeFromTypeNode(inputTypeNode);
+ const { typeNode: inputTypeNode, type: inputType } = ChainRouteProcessor.resolveInputType(project, node);
// const outputType = ChainRouteProcessor.resolveOutputType(project, methodSignature);
@@ -60,10 +60,10 @@ export abstract class ChainRouteProcessor {
controller.registerRoute(node, {
method,
path,
- input: inputType,
+ input: inputTypeNode,
// FIXME: this should be the output type not the input type
- output: inputType,
+ output: inputTypeNode,
description: parsedDocComments.description,
summary: parsedDocComments.summary,
filePath: source.fileName,
@@ -170,25 +170,39 @@ export abstract class ChainRouteProcessor {
return ChainMethodDecoratorTypeMap[name as keyof typeof ChainMethodDecoratorTypeMap];
}
- private static resolveInputType(project: Project, method: ts.MethodDeclaration): ts.TypeNode {
+ private static resolveInputType(
+ project: Project,
+ method: ts.MethodDeclaration,
+ ): { type: ts.Type; typeNode: ts.TypeNode } {
const params = method.parameters;
if (params.length !== 1) {
throw new Error("Handler chain must have 1 parameter");
}
const [param] = params;
- const paramType = project.checker.getTypeAtLocation(param);
+ const paramTypeNode = param.type;
+ if (!paramTypeNode || !ts.isTypeReferenceNode(paramTypeNode)) {
+ throw new Error("Handler chain parameter must have a type");
+ }
+
+ const paramType = project.checker.getTypeFromTypeNode(paramTypeNode);
const paramDeclaration = paramType.symbol?.declarations?.[0];
if (!paramDeclaration || !isNornirNode(paramDeclaration)) {
throw new Error("Handler chain input must be a Nornir class");
}
- const paramTypeNode = project.checker.typeToTypeNode(paramType, undefined, undefined) as ts.TypeReferenceNode;
+ // const paramTypeNode = project.checker.typeToTypeNode(paramType, undefined, undefined) as ts.TypeReferenceNode;
const [paramTypeArg] = paramTypeNode.typeArguments || [];
+
+ const paramTypeArgType = project.checker.getTypeFromTypeNode(paramTypeArg);
+
if (!paramTypeArg) {
throw new Error("Handler chain input must have a type argument");
}
- return paramTypeArg;
+ return {
+ type: paramTypeArgType,
+ typeNode: paramTypeArg,
+ };
}
private static resolveOutputType(project: Project, methodSignature: ts.Signature): ts.Type {
diff --git a/packages/test/src/controller.ts b/packages/test/src/controller.ts
index 6b214b6..bba9e06 100644
--- a/packages/test/src/controller.ts
+++ b/packages/test/src/controller.ts
@@ -25,6 +25,13 @@ interface RoutePostInputJSON extends HttpRequest {
query: {
test: "boolean";
};
+ pathParams: {
+ /**
+ * Very cool property that does a thing
+ * @pattern ^[a-z]+$
+ */
+ reallyCool: "true" | "false";
+ }
}
interface RoutePostInputCSV extends HttpRequest {
@@ -37,8 +44,12 @@ interface RoutePostInputCSV extends HttpRequest {
* @minLength 5
*/
"csv-header": string;
+
};
body: TestStringType;
+ pathParams: {
+ reallyCool: "stuff";
+ }
}
type RoutePostInput = RoutePostInputJSON | RoutePostInputCSV;
@@ -85,25 +96,25 @@ const basePath = `${overallBase}/basepath`;
*/
@Controller(basePath)
export class TestController {
- /**
- * Cool get route
- */
- @GetChain("/route")
- public getRoute(chain: Nornir) {
- return chain
- .use(input => {
- assertValid(input);
- return input;
- })
- .use(input => input.headers["content-type"])
- .use(contentType => ({
- statusCode: HttpStatusCode.Ok,
- body: `Content-Type: ${contentType}`,
- headers: {
- "content-type": MimeType.TextPlain,
- },
- }));
- }
+ // /**
+ // * Cool get route
+ // */
+ // @GetChain("/route")
+ // public getRoute(chain: Nornir) {
+ // return chain
+ // .use(input => {
+ // assertValid(input);
+ // return input;
+ // })
+ // .use(input => input.headers["content-type"])
+ // .use(contentType => ({
+ // statusCode: HttpStatusCode.Ok,
+ // body: `Content-Type: ${contentType}`,
+ // headers: {
+ // "content-type": MimeType.TextPlain,
+ // },
+ // }));
+ // }
/**
* A simple post route
@@ -112,7 +123,7 @@ export class TestController {
* @deprecated
* @operationId coolRoute
*/
- @PostChain("/route")
+ @PostChain("/route/{cool}")
public postRoute(chain: Nornir) {
return chain
.use(contentType => ({
diff --git a/packages/test/src/controller2.ts b/packages/test/src/controller2.ts
index b08a81c..b80703e 100644
--- a/packages/test/src/controller2.ts
+++ b/packages/test/src/controller2.ts
@@ -1,104 +1,104 @@
-import { Nornir } from "@nornir/core";
-import {
- AnyMimeType,
- Controller,
- GetChain,
- HttpRequest,
- HttpRequestEmpty,
- HttpResponse,
- HttpResponseEmpty,
- HttpStatusCode,
- MimeType,
- Provider,
- PutChain,
-} from "@nornir/rest";
-
-interface RouteGetInput extends HttpRequestEmpty {
- headers: GetHeaders;
-}
-interface GetHeaders {
- "content-type": AnyMimeType;
- [key: string]: string;
-}
-
-interface RoutePostInputJSON extends HttpRequest {
- headers: {
- "content-type": MimeType.ApplicationJson;
- };
- body: RoutePostBodyInput;
-}
-
-interface RoutePostInputCSV extends HttpRequest {
- headers: {
- "content-type": MimeType.TextCsv;
- };
- body: string;
-}
-
-type RoutePutInput = RoutePostInputJSON | RoutePostInputCSV;
-
-interface RoutePostBodyInput {
- cool: string;
-}
-
-const basePath = "/basepath/2";
-
-/**
- * This is a second controller
- * @summary This is a summary
- */
-@Controller(basePath, "test")
-export class TestController {
- @Provider()
- public static test() {
- return new TestController();
- }
-
- /**
- * The second simple GET route.
- * @summary Get route
- */
- @GetChain("/route")
- public getRoute(chain: Nornir) {
- return chain
- .use(input => input.headers["content-type"])
- .use(contentType => ({
- statusCode: HttpStatusCode.Ok,
- body: `Content-Type: ${contentType}`,
- headers: {
- "content-type": MimeType.TextPlain,
- },
- }));
- }
-
- /**
- * The second simple PUT route.
- * @summary Put route
- */
- @PutChain("/route")
- public postRoute(chain: Nornir): Nornir {
- return chain
- .use(() => ({
- statusCode: HttpStatusCode.Created,
- headers: {
- "content-type": AnyMimeType,
- },
- }));
- }
-}
-
-type PutResponse = PutSuccessResponse | PutBadRequestResponse;
-
-interface PutSuccessResponse extends HttpResponseEmpty {
- statusCode: HttpStatusCode.Created;
-}
-
-interface PutBadRequestResponse extends HttpResponse {
- statusCode: HttpStatusCode.BadRequest;
- headers: {
- "content-type": MimeType.ApplicationJson;
- };
- body: {
- potato: boolean;
- };
-}
+// import { Nornir } from "@nornir/core";
+// import {
+// AnyMimeType,
+// Controller,
+// GetChain,
+// HttpRequest,
+// HttpRequestEmpty,
+// HttpResponse,
+// HttpResponseEmpty,
+// HttpStatusCode,
+// MimeType,
+// Provider,
+// PutChain,
+// } from "@nornir/rest";
+//
+// interface RouteGetInput extends HttpRequestEmpty {
+// headers: GetHeaders;
+// }
+// interface GetHeaders {
+// "content-type": AnyMimeType;
+// [key: string]: string;
+// }
+//
+// interface RoutePostInputJSON extends HttpRequest {
+// headers: {
+// "content-type": MimeType.ApplicationJson;
+// };
+// body: RoutePostBodyInput;
+// }
+//
+// interface RoutePostInputCSV extends HttpRequest {
+// headers: {
+// "content-type": MimeType.TextCsv;
+// };
+// body: string;
+// }
+//
+// type RoutePutInput = RoutePostInputJSON | RoutePostInputCSV;
+//
+// interface RoutePostBodyInput {
+// cool: string;
+// }
+//
+// const basePath = "/basepath/2";
+//
+// /**
+// * This is a second controller
+// * @summary This is a summary
+// */
+// @Controller(basePath, "test")
+// export class TestController {
+// @Provider()
+// public static test() {
+// return new TestController();
+// }
+//
+// /**
+// * The second simple GET route.
+// * @summary Get route
+// */
+// @GetChain("/route")
+// public getRoute(chain: Nornir) {
+// return chain
+// .use(input => input.headers["content-type"])
+// .use(contentType => ({
+// statusCode: HttpStatusCode.Ok,
+// body: `Content-Type: ${contentType}`,
+// headers: {
+// "content-type": MimeType.TextPlain,
+// },
+// }));
+// }
+//
+// /**
+// * The second simple PUT route.
+// * @summary Put route
+// */
+// @PutChain("/route")
+// public postRoute(chain: Nornir): Nornir {
+// return chain
+// .use(() => ({
+// statusCode: HttpStatusCode.Created,
+// headers: {
+// "content-type": AnyMimeType,
+// },
+// }));
+// }
+// }
+//
+// type PutResponse = PutSuccessResponse | PutBadRequestResponse;
+//
+// interface PutSuccessResponse extends HttpResponseEmpty {
+// statusCode: HttpStatusCode.Created;
+// }
+//
+// interface PutBadRequestResponse extends HttpResponse {
+// statusCode: HttpStatusCode.BadRequest;
+// headers: {
+// "content-type": MimeType.ApplicationJson;
+// };
+// body: {
+// potato: boolean;
+// };
+// }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index beb93aa..63a6beb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -56,12 +56,6 @@ importers:
eslint-plugin-no-secrets:
specifier: ^0.8.9
version: 0.8.9(eslint@8.45.0)
- eslint-plugin-sonarjs:
- specifier: ^0.19.0
- version: 0.19.0(eslint@8.45.0)
- eslint-plugin-unicorn:
- specifier: ^48.0.0
- version: 48.0.0(eslint@8.45.0)
eslint-plugin-workspaces:
specifier: ^0.9.0
version: 0.9.0
@@ -143,6 +137,9 @@ importers:
atlassian-openapi:
specifier: ^1.0.18
version: 1.0.18
+ json-schema-traverse:
+ specifier: ^1.0.0
+ version: 1.0.0
lodash:
specifier: ^4.17.21
version: 4.17.21
@@ -500,11 +497,6 @@ packages:
engines: {node: '>=6.9.0'}
dev: true
- /@babel/helper-validator-identifier@7.22.5:
- resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
- engines: {node: '>=6.9.0'}
- dev: true
-
/@babel/helper-validator-option@7.21.0:
resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==}
engines: {node: '>=6.9.0'}
@@ -3014,11 +3006,6 @@ packages:
ieee754: 1.2.1
dev: true
- /builtin-modules@3.3.0:
- resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
- engines: {node: '>=6'}
- dev: true
-
/call-bind@1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
@@ -3126,13 +3113,6 @@ packages:
resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==}
dev: true
- /clean-regexp@1.0.0:
- resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
- engines: {node: '>=4'}
- dependencies:
- escape-string-regexp: 1.0.5
- dev: true
-
/clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
@@ -3626,15 +3606,6 @@ packages:
eslint: 8.45.0
dev: true
- /eslint-plugin-sonarjs@0.19.0(eslint@8.45.0):
- resolution: {integrity: sha512-6+s5oNk5TFtVlbRxqZN7FIGmjdPCYQKaTzFPmqieCmsU1kBYDzndTeQav0xtQNwZJWu5awWfTGe8Srq9xFOGnw==}
- engines: {node: '>=14'}
- peerDependencies:
- eslint: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
- dependencies:
- eslint: 8.45.0
- dev: true
-
/eslint-plugin-turbo@1.9.3(eslint@8.45.0):
resolution: {integrity: sha512-ZsRtksdzk3v+z5/I/K4E50E4lfZ7oYmLX395gkrUMBz4/spJlYbr+GC8hP9oVNLj9s5Pvnm9rLv/zoj5PVYaVw==}
peerDependencies:
@@ -3643,30 +3614,6 @@ packages:
eslint: 8.45.0
dev: true
- /eslint-plugin-unicorn@48.0.0(eslint@8.45.0):
- resolution: {integrity: sha512-8fk/v3p1ro34JSVDBEmtOq6EEQRpMR0iTir79q69KnXFZ6DJyPkT3RAi+ZoTqhQMdDSpGh8BGR68ne1sP5cnAA==}
- engines: {node: '>=16'}
- peerDependencies:
- eslint: '>=8.44.0'
- dependencies:
- '@babel/helper-validator-identifier': 7.22.5
- '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0)
- ci-info: 3.8.0
- clean-regexp: 1.0.0
- eslint: 8.45.0
- esquery: 1.5.0
- indent-string: 4.0.0
- is-builtin-module: 3.2.1
- jsesc: 3.0.2
- lodash: 4.17.21
- pluralize: 8.0.0
- read-pkg-up: 7.0.1
- regexp-tree: 0.1.27
- regjsparser: 0.10.0
- semver: 7.5.4
- strip-indent: 3.0.0
- dev: true
-
/eslint-plugin-workspaces@0.9.0:
resolution: {integrity: sha512-krMuZ+yZgzwv1oTBfz50oamNVPDIm7CDyot3i1GRKBqMD2oXAwnXHLQWH7ctpV8k6YVrkhcaZhuV9IJxD8OPAQ==}
dependencies:
@@ -4461,13 +4408,6 @@ packages:
has-tostringtag: 1.0.0
dev: true
- /is-builtin-module@3.2.1:
- resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
- engines: {node: '>=6'}
- dependencies:
- builtin-modules: 3.3.0
- dev: true
-
/is-callable@1.2.7:
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
engines: {node: '>= 0.4'}
@@ -5173,12 +5113,6 @@ packages:
hasBin: true
dev: true
- /jsesc@3.0.2:
- resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
- engines: {node: '>=6'}
- hasBin: true
- dev: true
-
/json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
dev: true
@@ -5896,11 +5830,6 @@ packages:
v8flags: 4.0.0
dev: true
- /pluralize@8.0.0:
- resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
- engines: {node: '>=4'}
- dev: true
-
/preferred-pm@3.0.3:
resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==}
engines: {node: '>=10'}
@@ -6045,11 +5974,6 @@ packages:
'@babel/runtime': 7.21.5
dev: true
- /regexp-tree@0.1.27:
- resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
- hasBin: true
- dev: true
-
/regexp.prototype.flags@1.5.0:
resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
engines: {node: '>= 0.4'}
@@ -6076,13 +6000,6 @@ packages:
unicode-match-property-value-ecmascript: 2.1.0
dev: true
- /regjsparser@0.10.0:
- resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==}
- hasBin: true
- dependencies:
- jsesc: 0.5.0
- dev: true
-
/regjsparser@0.9.1:
resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==}
hasBin: true
From d0dbe772b9dbe23b394bbcc7e96af238804fbfdb Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Mon, 4 Dec 2023 17:04:49 -0800
Subject: [PATCH 03/16] path params working!
---
packages/rest/package.json | 1 +
.../rest/src/transform/controller-meta.ts | 130 ++++++++++++------
packages/test/src/controller.ts | 14 +-
pnpm-lock.yaml | 11 +-
4 files changed, 107 insertions(+), 49 deletions(-)
diff --git a/packages/rest/package.json b/packages/rest/package.json
index 0862773..c61b0cb 100644
--- a/packages/rest/package.json
+++ b/packages/rest/package.json
@@ -20,6 +20,7 @@
"devDependencies": {
"@jest/globals": "^29.5.0",
"@types/jest": "^29.4.0",
+ "@types/json-schema": "^7.0.15",
"@types/lodash": "^4.14.202",
"@types/node": "^18.15.11",
"eslint": "^8.45.0",
diff --git a/packages/rest/src/transform/controller-meta.ts b/packages/rest/src/transform/controller-meta.ts
index 617d868..56db98a 100644
--- a/packages/rest/src/transform/controller-meta.ts
+++ b/packages/rest/src/transform/controller-meta.ts
@@ -7,6 +7,7 @@ import { Project } from "./project";
import { FileTransformer } from "./transformers/file-transformer";
import { JSONSchemaType } from "ajv";
+import { JSONSchema7 } from "json-schema";
import { ReferenceType, SubNodeParser, UnionType } from "ts-json-schema-generator";
export abstract class OpenApiSpecHolder {
@@ -251,6 +252,7 @@ export class ControllerMeta {
description: "OK",
},
},
+ parameters: this.generatePathParamsSpecs(route),
},
},
},
@@ -266,36 +268,44 @@ export class ControllerMeta {
if (property == undefined) throw new Error("No pathParams property found");
const declarations = property.getDeclarations() as tsm.PropertySignature[];
const typeNodes = declarations.map((declaration) => declaration.getTypeNodeOrThrow());
- const paramDeclaractions = typeNodes.map(typeNode => typeNode.getType())
- .map(type => type.getProperties())
- .flat()
- .reduce((acc, property) => {
- const name = property.getName();
- const arr = acc[name] || [];
- const declarations = property.getDeclarations() as tsm.PropertySignature[];
- arr.push(...declarations);
- acc[name] = arr;
- return acc;
- }, {} as Record);
const paramObjectSchema = this.project.schemaGenerator.createSchemaFromNodes(
[ts.factory.createUnionTypeNode(typeNodes.map((typeNode) => typeNode.compilerNode))],
);
- const schemas = Object.fromEntries(
- Object.entries(paramDeclaractions).map(([name, declarations]) => {
- const typeSymbols = declarations.map((declaration) => declaration.getSymbolOrThrow());
- const typeRefs = typeSymbols.map(symbol => {
- const typeRef = ts.factory.createTypeReferenceNode(name, []);
-
- (typeRef as any).symbol = symbol.compilerSymbol;
- return typeRef;
- });
- const schema = this.project.schemaGenerator.createSchemaFromNodes(typeRefs);
- return [name, schema] as const;
- }),
- );
- return [];
+ const propertySchemas = getUnifiedPropertySchemas(paramObjectSchema, "/");
+
+ return Object.entries(propertySchemas).map(([name, schema]) => {
+ // Just take the first provided description and example for now
+ const description = schema.schemaSet.find((schema) => schema.description)?.description;
+ let example = (schema.schemaSet.find((schema) => schema.examples))?.examples;
+ if (Array.isArray(example)) {
+ example = example[0];
+ }
+
+ // If every schema is deprecated, then the parameter is deprecated
+ const deprecated = schema.schemaSet.every((schema) => (schema as { deprecated?: boolean }).deprecated);
+
+ const mergedSchema = schema?.schemaSet.length === 1
+ ? schema.schemaSet[0]
+ : {
+ anyOf: schema.schemaSet,
+ };
+
+ mergedSchema.definitions = paramObjectSchema.definitions;
+
+ const paramObject: OpenAPIV3_1.ParameterObject = {
+ name,
+ in: "path",
+ required: schema.required,
+ description,
+ example,
+ deprecated,
+ schema: mergedSchema as OpenAPIV3.NonArraySchemaObject,
+ };
+
+ return paramObject;
+ });
}
// private buildRequestInfo(routeIndex: RouteIndex, inputType: ts.Type): RequestInfo {
@@ -478,25 +488,57 @@ export class ControllerMeta {
}
// Traverses the schema and extracts a unified definition for the property at the given path
-// export function getUnifiedPropertySchema(schema: IJsonSchema, path: string) {
-// // Take a path from the json schema and convert it to a path in validated object
-// const convertJsonSchemaPathToPropertyPath = (path: string) => {
-// return path
-// // replace properties
-// .replace(/\/properties\//g, ".")
-// // replace items and index
-// .replace(/\/items\/(\d+)\//g, "[].")
-// // replace anyOf and index
-// .replace(/\/anyOf\/(\d+)\//g, ".")
-// // replace oneOf and index
-// .replace(/\/oneOf\/(\d+)\//g, ".")
-// // replace allOf and index
-// .replace(/\/allOf\/(\d+)\//g, ".")
-// }
-// const schema
-//
-// traverse()
-// }
+export function getUnifiedPropertySchemas(schema: JSONSchema7, parentPath: string) {
+ // Take a path from the json schema and convert it to a path in validated object
+ const convertJsonSchemaPathIfPropertyPath = (path: string) => {
+ if (path.split("/").at(-2) !== "properties") {
+ return "/";
+ }
+
+ return path
+ // replace properties
+ .replace(/\/properties\//g, "/")
+ // replace items and index
+ .replace(/\/items\/(\d+)\//g, "/")
+ // replace anyOf and index
+ .replace(/\/anyOf\/(\d+)\//g, "/")
+ // replace oneOf and index
+ .replace(/\/oneOf\/(\d+)\//g, "/")
+ // replace allOf and index
+ .replace(/\/allOf\/(\d+)\//g, "/");
+ };
+
+ const isDirectChildPath = (childPath: string, parentPath: string) => {
+ const childPathParts = childPath.split("/").filter(part => part !== "");
+ const parentPathParts = parentPath.split("/").filter(part => part !== "");
+
+ if (childPathParts.length !== parentPathParts.length + 1) {
+ return false;
+ }
+
+ return parentPathParts.every((part, index) => part === childPathParts[index]);
+ };
+
+ const schemas: Record = {};
+
+ traverse(schema, {
+ allKeys: false,
+ cb: {
+ pre: (schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) => {
+ const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr);
+
+ if (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;
+ }
+ },
+ },
+ });
+
+ return schemas;
+}
function deparameterizePath(path: string) {
return path.replaceAll(/:[^/]+/g, ":param");
diff --git a/packages/test/src/controller.ts b/packages/test/src/controller.ts
index bba9e06..0401ce8 100644
--- a/packages/test/src/controller.ts
+++ b/packages/test/src/controller.ts
@@ -29,9 +29,15 @@ interface RoutePostInputJSON extends HttpRequest {
/**
* Very cool property that does a thing
* @pattern ^[a-z]+$
+ * @example "true"
*/
reallyCool: "true" | "false";
- }
+
+ /**
+ * Even cooler property
+ */
+ evenCooler?: number;
+ };
}
interface RoutePostInputCSV extends HttpRequest {
@@ -44,12 +50,14 @@ interface RoutePostInputCSV extends HttpRequest {
* @minLength 5
*/
"csv-header": string;
-
};
body: TestStringType;
pathParams: {
+ /**
+ * @deprecated
+ */
reallyCool: "stuff";
- }
+ };
}
type RoutePostInput = RoutePostInputJSON | RoutePostInputCSV;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 63a6beb..9f0e8c7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -168,6 +168,9 @@ importers:
'@types/jest':
specifier: ^29.4.0
version: 29.4.0
+ '@types/json-schema':
+ specifier: ^7.0.15
+ version: 7.0.15
'@types/lodash':
specifier: ^4.14.202
version: 4.14.202
@@ -249,7 +252,7 @@ packages:
engines: {node: '>= 16'}
dependencies:
'@jsdevtools/ono': 7.1.3
- '@types/json-schema': 7.0.12
+ '@types/json-schema': 7.0.15
'@types/lodash.clonedeep': 4.5.7
js-yaml: 4.1.0
lodash.clonedeep: 4.5.0
@@ -2370,6 +2373,10 @@ packages:
/@types/json-schema@7.0.12:
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
+ dev: true
+
+ /@types/json-schema@7.0.15:
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
/@types/liftoff@4.0.0:
resolution: {integrity: sha512-Ny/PJkO6nxWAQnaet8q/oWz15lrfwvdvBpuY4treB0CSsBO1CG0fVuNLngR3m3bepQLd+E4c3Y3DlC2okpUvPw==}
@@ -6518,7 +6525,7 @@ packages:
engines: {node: '>=10.0.0'}
hasBin: true
dependencies:
- '@types/json-schema': 7.0.12
+ '@types/json-schema': 7.0.15
commander: 11.0.0
glob: 8.1.0
json5: 2.2.3
From 91edb68eba115f1f327f393c61a5373c86c8c8a1 Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Sat, 9 Dec 2023 17:06:59 -0800
Subject: [PATCH 04/16] Working body and responses with content type
discrimination
---
packages/core/package.json | 2 +-
packages/rest/package.json | 5 +-
packages/rest/src/runtime/http-event.mts | 310 +++++++++------
packages/rest/src/runtime/index.mts | 16 +-
packages/rest/src/runtime/route-holder.mts | 4 +-
packages/rest/src/runtime/router.mts | 4 +-
.../rest/src/transform/controller-meta.ts | 368 ++++++------------
.../rest/src/transform/json-schema-utils.ts | 268 +++++++++++++
packages/rest/src/transform/project.ts | 1 +
packages/rest/src/transform/transform.ts | 11 +-
.../transformers/node-transformer.ts | 2 +-
.../processors/chain-route-processor.ts | 44 ++-
packages/test/package.json | 2 +-
packages/test/src/controller.ts | 115 ++++--
packages/test/src/rest.ts | 9 +-
pnpm-lock.yaml | 55 ++-
16 files changed, 746 insertions(+), 470 deletions(-)
create mode 100644 packages/rest/src/transform/json-schema-utils.ts
diff --git a/packages/core/package.json b/packages/core/package.json
index cd7bead..97c06b0 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -5,7 +5,7 @@
"author": "John Conley",
"devDependencies": {
"@jest/globals": "^29.5.0",
- "@nrfcloud/ts-json-schema-transformer": "^1.2.4",
+ "@nrfcloud/ts-json-schema-transformer": "^1.2.5",
"@types/jest": "^29.4.0",
"@types/node": "^18.15.11",
"esbuild": "^0.17.18",
diff --git a/packages/rest/package.json b/packages/rest/package.json
index c61b0cb..a4b69bf 100644
--- a/packages/rest/package.json
+++ b/packages/rest/package.json
@@ -3,8 +3,9 @@
"description": "A nornir library",
"version": "1.3.0",
"dependencies": {
+ "@apidevtools/json-schema-ref-parser": "^11.1.0",
"@nornir/core": "workspace:^",
- "@nrfcloud/ts-json-schema-transformer": "^1.2.4",
+ "@nrfcloud/ts-json-schema-transformer": "^1.2.5",
"@types/aws-lambda": "^8.10.115",
"ajv": "^8.12.0",
"atlassian-openapi": "^1.0.18",
@@ -13,7 +14,7 @@
"openapi-types": "^12.1.0",
"trouter": "^3.2.1",
"ts-is-present": "^1.2.2",
- "ts-json-schema-generator": "^1.4.0",
+ "ts-json-schema-generator": "^1.5.0",
"ts-morph": "^20.0.0",
"tsutils": "^3.21.0"
},
diff --git a/packages/rest/src/runtime/http-event.mts b/packages/rest/src/runtime/http-event.mts
index f7f9d0e..95e08a7 100644
--- a/packages/rest/src/runtime/http-event.mts
+++ b/packages/rest/src/runtime/http-event.mts
@@ -13,35 +13,34 @@ export type UnparsedHttpEvent = Omit & {
rawQuery: string
}
-// export type HttpHeaders = {
-// readonly "content-type": MimeType;
-// } & Record
-
export type HttpHeadersWithContentType = {
- readonly "content-type": MimeType | AnyMimeType
+ readonly "content-type": MimeType
} & HttpHeaders;
+export type HttpHeadersWithoutContentType = {
+ readonly "content-type"?: undefined
+} & HttpHeaders
+
export type HttpHeaders = Record;
export interface HttpRequest {
- readonly headers: HttpHeadersWithContentType;
+ readonly headers: HttpHeadersWithContentType | HttpHeadersWithoutContentType;
- readonly query: Record | Array>;
+ readonly query: QueryParams;
readonly body?: unknown;
readonly pathParams: PathParams;
}
-/**
- * @nornirIgnore
- */
+type QueryParams = Record | Array>;
+
type PathParams = Record;
export interface HttpRequestEmpty extends HttpRequest {
headers: {
- "content-type": AnyMimeType;
+ "content-type": never
}
// body?: undefined;
}
@@ -55,7 +54,7 @@ export interface HttpRequestJSON extends HttpRequest {
export interface HttpResponse {
readonly statusCode: HttpStatusCode;
- readonly headers: HttpHeadersWithContentType;
+ readonly headers: HttpHeadersWithContentType | HttpHeadersWithoutContentType;
readonly body?: unknown;
}
@@ -65,106 +64,197 @@ export interface SerializedHttpResponse extends Omit {
export interface HttpResponseEmpty extends HttpResponse {
headers: {
- "content-type": AnyMimeType;
+ "content-type": never
},
body?: undefined;
}
-export enum HttpStatusCode {
- Continue = "100",
- SwitchingProtocols = "101",
- Processing = "102",
- Ok = "200",
- Created = "201",
- Accepted = "202",
- NonAuthoritativeInformation = "203",
- NoContent = "204",
- ResetContent = "205",
- PartialContent = "206",
- MultiStatus = "207",
- AlreadyReported = "208",
- IMUsed = "226",
- MultipleChoices = "300",
- MovedPermanently = "301",
- Found = "302",
- SeeOther = "303",
- NotModified = "304",
- UseProxy = "305",
- TemporaryRedirect = "307",
- PermanentRedirect = "308",
- BadRequest = "400",
- Unauthorized = "401",
- PaymentRequired = "402",
- Forbidden = "403",
- NotFound = "404",
- MethodNotAllowed = "405",
- NotAcceptable = "406",
- ProxyAuthenticationRequired = "407",
- RequestTimeout = "408",
- Conflict = "409",
- Gone = "410",
- LengthRequired = "411",
- PreconditionFailed = "412",
- PayloadTooLarge = "413",
- RequestURITooLong = "414",
- UnsupportedMediaType = "415",
- RequestedRangeNotSatisfiable = "416",
- ExpectationFailed = "417",
- ImATeapot = "418",
- MisdirectedRequest = "421",
- UnprocessableEntity = "422",
- Locked = "423",
- FailedDependency = "424",
- UpgradeRequired = "426",
- PreconditionRequired = "428",
- TooManyRequests = "429",
- RequestHeaderFieldsTooLarge = "431",
- UnavailableForLegalReasons = "451",
- InternalServerError = "500",
- NotImplemented = "501",
- BadGateway = "502",
- ServiceUnavailable = "503",
- GatewayTimeout = "504",
- HTTPVersionNotSupported = "505",
- VariantAlsoNegotiates = "506",
- InsufficientStorage = "507",
- LoopDetected = "508",
- NotExtended = "510",
-}
+// export enum HttpStatusCode {
+// Continue = "100",
+// SwitchingProtocols = "101",
+// Processing = "102",
+// Ok = "200",
+// Created = "201",
+// Accepted = "202",
+// NonAuthoritativeInformation = "203",
+// NoContent = "204",
+// ResetContent = "205",
+// PartialContent = "206",
+// MultiStatus = "207",
+// AlreadyReported = "208",
+// IMUsed = "226",
+// MultipleChoices = "300",
+// MovedPermanently = "301",
+// Found = "302",
+// SeeOther = "303",
+// NotModified = "304",
+// UseProxy = "305",
+// TemporaryRedirect = "307",
+// PermanentRedirect = "308",
+// BadRequest = "400",
+// Unauthorized = "401",
+// PaymentRequired = "402",
+// Forbidden = "403",
+// NotFound = "404",
+// MethodNotAllowed = "405",
+// NotAcceptable = "406",
+// ProxyAuthenticationRequired = "407",
+// RequestTimeout = "408",
+// Conflict = "409",
+// Gone = "410",
+// LengthRequired = "411",
+// PreconditionFailed = "412",
+// PayloadTooLarge = "413",
+// RequestURITooLong = "414",
+// UnsupportedMediaType = "415",
+// RequestedRangeNotSatisfiable = "416",
+// ExpectationFailed = "417",
+// ImATeapot = "418",
+// MisdirectedRequest = "421",
+// UnprocessableEntity = "422",
+// Locked = "423",
+// FailedDependency = "424",
+// UpgradeRequired = "426",
+// PreconditionRequired = "428",
+// TooManyRequests = "429",
+// RequestHeaderFieldsTooLarge = "431",
+// UnavailableForLegalReasons = "451",
+// InternalServerError = "500",
+// NotImplemented = "501",
+// BadGateway = "502",
+// ServiceUnavailable = "503",
+// GatewayTimeout = "504",
+// HTTPVersionNotSupported = "505",
+// VariantAlsoNegotiates = "506",
+// InsufficientStorage = "507",
+// LoopDetected = "508",
+// NotExtended = "510",
+// }
-export type AnyMimeType = Nominal
-export const AnyMimeType = "*/*" as AnyMimeType;
-
-export enum MimeType {
- None = "",
- ApplicationJson = "application/json",
- ApplicationOctetStream = "application/octet-stream",
- ApplicationPdf = "application/pdf",
- ApplicationXWwwFormUrlencoded = "application/x-www-form-urlencoded",
- ApplicationZip = "application/zip",
- ApplicationGzip = "application/gzip",
- ApplicationBzip = "application/bzip",
- ApplicationBzip2 = "application/bzip2",
- ApplicationLdJson = "application/ld+json",
- FontWoff = "font/woff",
- FontWoff2 = "font/woff2",
- FontTtf = "font/ttf",
- FontOtf = "font/otf",
- AudioMpeg = "audio/mpeg",
- AudioXWav = "audio/x-wav",
- ImageGif = "image/gif",
- ImageJpeg = "image/jpeg",
- ImagePng = "image/png",
- MultipartFormData = "multipart/form-data",
- TextCss = "text/css",
- TextCsv = "text/csv",
- TextHtml = "text/html",
- TextPlain = "text/plain",
- TextXml = "text/xml",
- VideoMpeg = "video/mpeg",
- VideoMp4 = "video/mp4",
- VideoQuicktime = "video/quicktime",
- VideoXMsVideo = "video/x-msvideo",
- VideoXFlv = "video/x-flv",
- VideoWebm = "video/webm",
-}
+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";
+
+
+// export enum MimeType {
+// Any = "*/*",
+// ApplicationJson = "application/json",
+// ApplicationOctetStream = "application/octet-stream",
+// ApplicationPdf = "application/pdf",
+// ApplicationXWwwFormUrlencoded = "application/x-www-form-urlencoded",
+// ApplicationZip = "application/zip",
+// ApplicationGzip = "application/gzip",
+// ApplicationBzip = "application/bzip",
+// ApplicationBzip2 = "application/bzip2",
+// ApplicationLdJson = "application/ld+json",
+// FontWoff = "font/woff",
+// FontWoff2 = "font/woff2",
+// FontTtf = "font/ttf",
+// FontOtf = "font/otf",
+// AudioMpeg = "audio/mpeg",
+// AudioXWav = "audio/x-wav",
+// ImageGif = "image/gif",
+// ImageJpeg = "image/jpeg",
+// ImagePng = "image/png",
+// MultipartFormData = "multipart/form-data",
+// TextCss = "text/css",
+// TextCsv = "text/csv",
+// TextHtml = "text/html",
+// TextPlain = "text/plain",
+// TextXml = "text/xml",
+// VideoMpeg = "video/mpeg",
+// VideoMp4 = "video/mp4",
+// VideoQuicktime = "video/quicktime",
+// VideoXMsVideo = "video/x-msvideo",
+// VideoXFlv = "video/x-flv",
+// VideoWebm = "video/webm",
+// }
+export type MimeType =
+ | "*/*"
+ | "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";
diff --git a/packages/rest/src/runtime/index.mts b/packages/rest/src/runtime/index.mts
index bbe738d..03e7b60 100644
--- a/packages/rest/src/runtime/index.mts
+++ b/packages/rest/src/runtime/index.mts
@@ -1,4 +1,11 @@
import {nornir} from "@nornir/core";
+import {UnparsedHttpEvent} from './http-event.mjs';
+import {normalizeEventHeaders} from "./utils.mjs"
+import {httpEventParser} from './parse.mjs'
+import {httpResponseSerializer} from './serialize.mjs'
+
+import {Router} from './router.mjs';
+import {httpErrorHandler} from "./error.mjs";
export {
GetChain, Controller, PostChain, DeleteChain, HeadChain, OptionsChain, PatchChain, PutChain,
@@ -7,7 +14,7 @@ export {
export {
HttpResponse, HttpRequest, HttpEvent, HttpMethod, HttpRequestEmpty, HttpResponseEmpty,
HttpStatusCode, HttpRequestJSON, HttpHeaders, MimeType,
- UnparsedHttpEvent, SerializedHttpResponse, AnyMimeType
+ UnparsedHttpEvent, SerializedHttpResponse
} from './http-event.mjs';
export {RouteHolder, NornirRestRequestValidationError} from './route-holder.mjs'
export {NornirRestRequestError, NornirRestError, httpErrorHandler, mapError, mapErrorClass} from './error.mjs'
@@ -17,13 +24,6 @@ export {httpResponseSerializer, HttpBodySerializer, HttpBodySerializerMap} from
export {normalizeEventHeaders, normalizeHeaders, getContentType} from "./utils.mjs"
export {Router} from "./router.mjs"
-import {UnparsedHttpEvent} from './http-event.mjs';
-import {normalizeEventHeaders} from "./utils.mjs"
-import {httpEventParser} from './parse.mjs'
-import {httpResponseSerializer} from './serialize.mjs'
-
-import {Router} from './router.mjs';
-import {httpErrorHandler} from "./error.mjs";
export const router = Router.build
export function restChain() {
diff --git a/packages/rest/src/runtime/route-holder.mts b/packages/rest/src/runtime/route-holder.mts
index dd43827..1a01cb5 100644
--- a/packages/rest/src/runtime/route-holder.mts
+++ b/packages/rest/src/runtime/route-holder.mts
@@ -50,10 +50,10 @@ export class NornirRestRequestValidationError exten
toHttpResponse(): HttpResponse {
return {
- statusCode: HttpStatusCode.UnprocessableEntity,
+ statusCode: "422",
body: {errors: this.errors},
headers: {
- 'content-type': MimeType.ApplicationJson,
+ 'content-type': "application/json"
},
}
}
diff --git a/packages/rest/src/runtime/router.mts b/packages/rest/src/runtime/router.mts
index 20b2331..97bed8f 100644
--- a/packages/rest/src/runtime/router.mts
+++ b/packages/rest/src/runtime/router.mts
@@ -90,10 +90,10 @@ export class NornirRouteNotFoundError extends NornirRestRequestError();
@@ -25,6 +31,9 @@ export abstract class OpenApiSpecHolder {
oas: spec,
dispute: {
alwaysApply: true,
+ mergeDispute: true,
+ prefix: "",
+ suffix: "",
},
})) as MergeInput;
@@ -213,26 +222,33 @@ export class ControllerMeta {
throw new Error(`Route already registered: ${index.method} ${index.path}`);
}
- OpenApiSpecHolder.addSpecForFile(this.source, this.generateRouteSpec(routeInfo));
-
- methods.set(index.method, {
+ const modifiedRouteInfo = {
method: routeInfo.method,
path: this.basePath + routeInfo.path.toLowerCase(),
description: routeInfo.description,
// requestInfo: this.buildRequestInfo(index, routeInfo.input),
// responseInfo: this.buildResponseInfo(index, routeInfo.output),
+ outputSchema: routeInfo.outputSchema,
+ inputSchema: routeInfo.inputSchema,
filePath: routeInfo.filePath,
summary: routeInfo.summary,
deprecated: routeInfo.deprecated,
operationId: routeInfo.operationId,
tags: routeInfo.tags,
input: routeInfo.input,
- output: routeInfo.output,
- });
+ };
+
+ OpenApiSpecHolder.addSpecForFile(this.source, this.generateRouteSpec(modifiedRouteInfo));
+
+ methods.set(index.method, modifiedRouteInfo);
}
private generateRouteSpec(route: RouteInfo): OpenAPIV3_1.Document {
- this.generatePathParamsSpecs(route);
+ const inputSchema = moveRefsToAllOf(route.inputSchema);
+ const routeIndex = this.getRouteIndex(route);
+ const dereferencedInputSchema = dereferenceSchema(inputSchema);
+ const outputSchema = moveRefsToAllOf(route.outputSchema);
+ const dereferencedOutputSchema = dereferenceSchema(outputSchema);
return {
openapi: "3.0.3",
info: {
@@ -240,40 +256,35 @@ export class ControllerMeta {
version: "1.0.0",
},
paths: {
- [this.basePath + route.path.toLowerCase()]: {
+ [route.path]: {
[route.method.toLowerCase()]: {
deprecated: route.deprecated,
tags: route.tags,
operationId: route.operationId,
summary: route.summary,
description: route.description,
- responses: {
- 200: {
- description: "OK",
- },
- },
- parameters: this.generatePathParamsSpecs(route),
+ responses: this.generateOutputType(routeIndex, dereferencedOutputSchema),
+ requestBody: this.generateRequestBody(routeIndex, dereferencedInputSchema),
+ parameters: [
+ ...this.generateParametersForSchemaPath(dereferencedInputSchema, "/pathParams", "path"),
+ ...this.generateParametersForSchemaPath(dereferencedInputSchema, "/query", "query"),
+ ...this.generateParametersForSchemaPath(dereferencedInputSchema, "/headers", "header"),
+ ],
},
},
},
components: {
+ schemas: {
+ ...rewriteRefsForOpenApi(inputSchema).definitions,
+ ...rewriteRefsForOpenApi(outputSchema).definitions,
+ },
parameters: {},
},
} as OpenAPIV3_1.Document;
}
- private generatePathParamsSpecs(routeInfo: RouteInfo): OpenAPIV3_1.ParameterObject[] {
- const wrapped = tsm.createWrappedNode(routeInfo.input, { typeChecker: this.project.checker });
- const property = wrapped.getType().getProperty("pathParams");
- if (property == undefined) throw new Error("No pathParams property found");
- const declarations = property.getDeclarations() as tsm.PropertySignature[];
- const typeNodes = declarations.map((declaration) => declaration.getTypeNodeOrThrow());
-
- const paramObjectSchema = this.project.schemaGenerator.createSchemaFromNodes(
- [ts.factory.createUnionTypeNode(typeNodes.map((typeNode) => typeNode.compilerNode))],
- );
-
- const propertySchemas = getUnifiedPropertySchemas(paramObjectSchema, "/");
+ private generateParametersForSchemaPath(inputSchema: JSONSchema7, schemaPath: string, paramType: string) {
+ const propertySchemas = getUnifiedPropertySchemas(inputSchema, schemaPath);
return Object.entries(propertySchemas).map(([name, schema]) => {
// Just take the first provided description and example for now
@@ -284,7 +295,11 @@ export class ControllerMeta {
}
// If every schema is deprecated, then the parameter is deprecated
- const deprecated = schema.schemaSet.every((schema) => (schema as { deprecated?: boolean }).deprecated);
+ const deprecated = schema.schemaSet.every((schema) =>
+ (schema as {
+ deprecated?: boolean;
+ }).deprecated
+ );
const mergedSchema = schema?.schemaSet.length === 1
? schema.schemaSet[0]
@@ -292,252 +307,88 @@ export class ControllerMeta {
anyOf: schema.schemaSet,
};
- mergedSchema.definitions = paramObjectSchema.definitions;
-
const paramObject: OpenAPIV3_1.ParameterObject = {
name,
- in: "path",
+ in: paramType,
required: schema.required,
description,
example,
deprecated,
- schema: mergedSchema as OpenAPIV3.NonArraySchemaObject,
+ schema: rewriteRefsForOpenApi(unresolveRefs(mergedSchema)) as OpenAPIV3.NonArraySchemaObject,
};
return paramObject;
});
}
- // private buildRequestInfo(routeIndex: RouteIndex, inputType: ts.Type): RequestInfo {
- // const paramterData: { [key in ParameterType]: { [name: string]: ParameterMeta } } = {
- // path: {},
- // header: {},
- // query: {},
- // };
- // const body: { [contentType: string]: Metadata } = {};
- //
- // const meta = MetadataFactory.generate(
- // this.project.checker,
- // ControllerMeta.metadataCollection,
- // inputType,
- // { resolve: false, constant: true },
- // );
- // for (const object of meta.objects) {
- // for (const property of object.properties) {
- // const key = MetadataUtils.getSoleLiteral(property.key);
- // if (key != null && !isRequestTypeField(key)) {
- // throw new Error(`Invalid request field: ${key}`);
- // }
- // switch (key) {
- // case "query":
- // this.buildParameterInfo(property.value, "query", paramterData);
- // break;
- // case "pathParams":
- // this.buildParameterInfo(property.value, "path", paramterData);
- // break;
- // case "headers":
- // this.buildParameterInfo(property.value, "header", paramterData);
- // break;
- // }
- // }
- // this.buildBodyInfo(routeIndex, object, body);
- // }
- //
- // return {
- // body,
- // parameters: [
- // ...Object.values(paramterData.path),
- // ...Object.values(paramterData.header),
- // ...Object.values(paramterData.query),
- // ],
- // };
- // }
+ private generateOutputType(routeIndex: RouteIndex, outputSchema: JSONSchema7): OpenAPIV3_1.ResponsesObject {
+ const responses: OpenAPIV3_1.ResponsesObject = {};
+ const statusCodeDiscriminatedSchemas = resolveDiscriminantProperty(outputSchema, "/statusCode");
- // private buildResponseInfo(routeIndex: RouteIndex, outputType: ts.Type): ResponseInfo {
- // const responses: ResponseInfo = {};
- // const meta = MetadataFactory.generate(
- // this.project.checker,
- // ControllerMeta.metadataCollection,
- // outputType,
- // { resolve: false, constant: true },
- // );
- // for (const object of meta.objects) {
- // const statusCodeProp = MetadataUtils.getPropertyByStringIndex(object, "statusCode");
- // if (statusCodeProp == null) {
- // throw new Error("Response must have a statusCode property");
- // }
- // let statusCodes = statusCodeProp.constants.map((c) => c.values).flat().map(v => v.toString());
- // if (HttpStatusCodes.every((c) => statusCodes.includes(c))) {
- // strictError(
- // this.project,
- // new StrictTransformationError(
- // "Response status codes must be literal values",
- // "Defaulting response status code to 200",
- // routeIndex,
- // ),
- // );
- // statusCodes = ["200"];
- // }
- //
- // if (statusCodes.length === 0) {
- // throw new Error("Literal status codes must be specified");
- // }
- //
- // for (const statusCode of statusCodes) {
- // if (responses[statusCode] != null) {
- // throw new Error(`Response already defined for status code ${statusCode}`);
- // }
- // const headerParamHolder: { [key in ParameterType]: { [name: string]: ParameterMeta } } = {
- // path: {},
- // header: {},
- // query: {},
- // };
- // const headerProp = MetadataUtils.getPropertyByStringIndex(object, "headers");
- // if (headerProp != null) {
- // this.buildParameterInfo(headerProp, "header", headerParamHolder);
- // }
- // const result: ResponseInfo[string] = {
- // body: {},
- // headers: Object.values(headerParamHolder.header),
- // };
- // this.buildBodyInfo(routeIndex, object, result.body);
- // responses[statusCode] = result;
- // }
- // }
- // return responses;
- // }
+ if (statusCodeDiscriminatedSchemas == null) {
+ throw new TransformationError("Could not resolve status codes for some responses", routeIndex);
+ }
- // private buildBodyInfo(
- // routeIndex: RouteIndex,
- // object: MetadataObject,
- // bodyTypes: { [contentType: string]: Metadata },
- // ) {
- // let contentType = this.getContentTypeFromObject(object);
- // const bodyType = MetadataUtils.getPropertyByStringIndex(object, "body");
- // if (bodyType == null || (bodyType.empty() && !bodyType.nullable)) {
- // return;
- // }
- // if (contentType == null) {
- // strictError(
- // this.project,
- // new StrictTransformationError(
- // "No content type specified for body",
- // "No content type specified, defaulting to application/json",
- // routeIndex,
- // ),
- // );
- // }
- // contentType = contentType || DEFAULT_CONTENT_TYPE;
- // const existingBody = bodyTypes[contentType];
- // if (existingBody != null) {
- // if (MetadataUtils.equal(existingBody, bodyType)) {
- // return;
- // } else {
- // throw new Error(`Content type ${contentType} already defined`);
- // }
- // } else {
- // bodyTypes[contentType] = bodyType;
- // }
- // }
+ for (const [statusCode, schema] of Object.entries(statusCodeDiscriminatedSchemas)) {
+ const headers = this.generateParametersForSchemaPath(schema, "/headers", "header");
+ const contentTypeDiscriminatedSchemas = resolveDiscriminantProperty(schema, "/headers/content-type");
+ const bodySchema = getUnifiedPropertySchemas(schema, "/")["body"];
+ if (contentTypeDiscriminatedSchemas == null && bodySchema != null) {
+ throw new TransformationError(`Could not resolve content types for "${statusCode}" responses`, routeIndex);
+ }
- // private getContentTypeFromObject(metaObject: MetadataObject): string | null {
- // const headers = MetadataUtils.getPropertyByStringIndex(metaObject, "headers");
- // if (headers == null) {
- // return null;
- // }
- // if (headers.objects.length !== 1) {
- // return null;
- // }
- // const headerObject = headers.objects[0];
- // const contentType = MetadataUtils.getPropertyByStringIndex(headerObject, "content-type");
- // if (contentType == null) {
- // return null;
- // }
- // return MetadataUtils.getSoleLiteral(contentType);
- // }
+ const content = contentTypeDiscriminatedSchemas == null ? undefined : Object.fromEntries(
+ Object.entries(contentTypeDiscriminatedSchemas).map(([contentType, schema]) => {
+ const branchBodySchema = getUnifiedPropertySchemas(schema, "/")["body"];
+ return [
+ contentType,
+ {
+ schema: rewriteRefsForOpenApi(
+ unresolveRefs(joinSchemas(branchBodySchema.schemaSet)),
+ ) as OpenAPIV3.NonArraySchemaObject,
+ },
+ ];
+ }),
+ );
+
+ responses[statusCode] = {
+ description: schema.description || "",
+ headers: Object.fromEntries(headers.map(header => [header.name, header])),
+ content,
+ };
+ }
- // private buildParameterInfo(
- // inputMeta: Metadata,
- // parameterType: ParameterType,
- // parameterData: { [key in ParameterType]: { [name: string]: ParameterMeta } },
- // ) {
- // for (const object of inputMeta.objects) {
- // for (const property of object.properties) {
- // const key = MetadataUtils.getSoleLiteral(property.key);
- // if (key == null) {
- // continue;
- // }
- // const meta = property.value;
- // const existingParameter = parameterData[parameterType][key];
- // if (existingParameter != null) {
- // parameterData[parameterType][key] = {
- // name: key,
- // meta: Metadata.merge(existingParameter.meta, meta),
- // type: parameterType,
- // };
- // } else {
- // parameterData[parameterType][key] = {
- // name: key,
- // meta,
- // type: parameterType,
- // };
- // }
- // }
- // }
- // }
-}
+ return responses;
+ }
-// Traverses the schema and extracts a unified definition for the property at the given path
-export function getUnifiedPropertySchemas(schema: JSONSchema7, parentPath: string) {
- // Take a path from the json schema and convert it to a path in validated object
- const convertJsonSchemaPathIfPropertyPath = (path: string) => {
- if (path.split("/").at(-2) !== "properties") {
- return "/";
+ private generateRequestBody(routeIndex: RouteIndex, inputSchema: JSONSchema7): OpenAPIV3_1.RequestBodyObject | void {
+ const bodySchema = getUnifiedPropertySchemas(inputSchema, "/")["body"];
+ const contentTypeDiscriminatedSchemas = resolveDiscriminantProperty(inputSchema, "/headers/content-type");
+ if (bodySchema == null) {
+ return;
}
- return path
- // replace properties
- .replace(/\/properties\//g, "/")
- // replace items and index
- .replace(/\/items\/(\d+)\//g, "/")
- // replace anyOf and index
- .replace(/\/anyOf\/(\d+)\//g, "/")
- // replace oneOf and index
- .replace(/\/oneOf\/(\d+)\//g, "/")
- // replace allOf and index
- .replace(/\/allOf\/(\d+)\//g, "/");
- };
-
- const isDirectChildPath = (childPath: string, parentPath: string) => {
- const childPathParts = childPath.split("/").filter(part => part !== "");
- const parentPathParts = parentPath.split("/").filter(part => part !== "");
-
- if (childPathParts.length !== parentPathParts.length + 1) {
- return false;
+ if (contentTypeDiscriminatedSchemas == null) {
+ throw new TransformationError("Could not resolve content types for request body", routeIndex);
}
- return parentPathParts.every((part, index) => part === childPathParts[index]);
- };
-
- const schemas: Record = {};
-
- traverse(schema, {
- allKeys: false,
- cb: {
- pre: (schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) => {
- const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr);
-
- if (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;
- }
- },
- },
- });
+ const content = Object.fromEntries(
+ Object.entries(contentTypeDiscriminatedSchemas).map(([contentType, schema]) => {
+ const branchBodySchema = getUnifiedPropertySchemas(schema, "/")["body"];
+ return [
+ contentType,
+ {
+ schema: rewriteRefsForOpenApi(unresolveRefs(joinSchemas(branchBodySchema.schemaSet))),
+ },
+ ];
+ }),
+ );
- return schemas;
+ return {
+ required: bodySchema.required,
+ content,
+ } as OpenAPIV3_1.RequestBodyObject;
+ }
}
function deparameterizePath(path: string) {
@@ -550,7 +401,8 @@ export interface RouteInfo {
description?: string;
summary?: string;
input: ts.TypeNode;
- output: ts.TypeNode;
+ inputSchema: JSONSchema7;
+ outputSchema: JSONSchema7;
filePath: string;
tags?: string[];
deprecated?: boolean;
diff --git a/packages/rest/src/transform/json-schema-utils.ts b/packages/rest/src/transform/json-schema-utils.ts
new file mode 100644
index 0000000..82a777a
--- /dev/null
+++ b/packages/rest/src/transform/json-schema-utils.ts
@@ -0,0 +1,268 @@
+// Traverses the schema and extracts a unified definitions for the properties under the given path
+import $RefParser from "@apidevtools/json-schema-ref-parser";
+import dereference from "@apidevtools/json-schema-ref-parser/dist/lib/dereference.js";
+import { JSONSchema7 } from "json-schema";
+import traverse from "json-schema-traverse";
+import { cloneDeep } from "lodash";
+
+const refParser = new $RefParser();
+
+export function dereferenceSchema(schema: JSONSchema7) {
+ const clonedSchema = cloneDeep(schema);
+ refParser.parse(clonedSchema);
+ refParser.schema = clonedSchema;
+ dereference(refParser, {
+ dereference: {
+ circular: true,
+ onDereference(path: string, value: JSONSchema7) {
+ (value as { "x-resolved-ref": string })["x-resolved-ref"] = path;
+ },
+ },
+ });
+ return clonedSchema;
+}
+
+interface UnifiedPropertySchema {
+ schemaSet: JSONSchema7[];
+ required: boolean;
+}
+
+export function joinSchemas(schemaSet: JSONSchema7[]): JSONSchema7 {
+ if (schemaSet.length === 0) {
+ return {};
+ }
+ if (schemaSet.length === 1) {
+ return schemaSet[0];
+ }
+
+ const joinedSchema: JSONSchema7 = {
+ allOf: schemaSet,
+ };
+
+ return joinedSchema;
+}
+
+export function getUnifiedPropertySchemas(
+ schema: JSONSchema7,
+ parentPath: string,
+ ignoreAdditionalProperties = false,
+): Record {
+ // Take a path from the json schema and convert it to a path in validated object
+
+ const convertJsonSchemaPathIfPropertyPath = (path: string) => {
+ if (path.split("/").at(-2) !== "properties") {
+ return undefined;
+ }
+
+ return path
+ // replace properties
+ .replace(/\/properties\//g, "/")
+ // replace items and index
+ .replace(/\/items\/(\d+)(\/|$)/g, "$2")
+ // replace anyOf and index
+ .replace(/\/anyOf\/(\d+)(\/|$)/g, "$2")
+ // replace oneOf and index
+ .replace(/\/oneOf\/(\d+)(\/|$)/g, "$2")
+ // replace allOf and index
+ .replace(/\/allOf\/(\d+)(\/|$)/g, "$2");
+ };
+
+ const isUnifiedSchemaEmpty = (unifiedSchema: UnifiedPropertySchema) => {
+ return !unifiedSchema.required && (
+ unifiedSchema.schemaSet.length === 0
+ || unifiedSchema.schemaSet.every(schema => isSchemaEmpty(schema))
+ );
+ };
+
+ const isDirectChildPath = (childPath: string, parentPath: string) => {
+ const childPathParts = childPath.split("/").filter(part => part !== "");
+ const parentPathParts = parentPath.split("/").filter(part => part !== "");
+
+ if (childPathParts.length !== parentPathParts.length + 1) {
+ return false;
+ }
+
+ return parentPathParts.every((part, index) => part === childPathParts[index]);
+ };
+
+ const schemas: Record = {};
+ let parentSchemas = 0;
+
+ traverse(schema, {
+ allKeys: false,
+ cb: {
+ pre(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) {
+ const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr);
+ if (convertedPath === parentPath && parentJsonPtr != "") {
+ parentSchemas++;
+ }
+ },
+ post: (schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) => {
+ const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr);
+
+ const convertedParentPath = convertJsonSchemaPathIfPropertyPath(parentJsonPtr || "/");
+
+ 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;
+ }
+ },
+ },
+ });
+
+ return Object.fromEntries(
+ Object.entries(schemas).map(([key, value]) => {
+ return [key, {
+ schemaSet: value.schemaSet,
+ required: value.required ? (parentSchemas) <= value.schemaSet.length : false,
+ }];
+ }).filter(([, value]) => !isUnifiedSchemaEmpty(value as UnifiedPropertySchema)),
+ );
+}
+
+export function unresolveRefs(schema: JSONSchema7) {
+ const clonedSchema = cloneDeep(schema);
+ traverse(clonedSchema, {
+ cb: {
+ pre: (subSchema) => {
+ if (subSchema["x-resolved-ref"]) {
+ const ref = subSchema["x-resolved-ref"];
+ Object.keys(subSchema).forEach(key => delete subSchema[key]);
+ subSchema.$ref = ref;
+ }
+ },
+ },
+ });
+
+ return clonedSchema;
+}
+
+const MOVED_REF_MARKER = Symbol("moved-ref-marker");
+export function moveRefsToAllOf(schema: JSONSchema7, markMovedRef = true) {
+ const clonedSchema = cloneDeep(schema);
+ traverse(clonedSchema, {
+ cb: {
+ pre: (schema) => {
+ if (schema.$ref && !(schema as { [MOVED_REF_MARKER]: unknown })[MOVED_REF_MARKER]) {
+ const allOf = schema.allOf || [];
+ const ref = schema.$ref; // schema.$ref.replace("#/definitions/", "#/components/schemas/");
+ delete schema.$ref;
+ schema.allOf = [
+ ...allOf,
+ {
+ $ref: ref,
+ [MOVED_REF_MARKER]: true,
+ },
+ ];
+ }
+ },
+ },
+ });
+
+ return clonedSchema;
+}
+
+export function rewriteRefsForOpenApi(schema: JSONSchema7) {
+ const clonedSchema = cloneDeep(schema);
+ traverse(clonedSchema, {
+ cb: {
+ pre: (schema) => {
+ if (schema.$ref) {
+ schema.$ref = schema.$ref.replace("#/definitions/", "#/components/schemas/");
+ }
+ },
+ },
+ });
+
+ return clonedSchema;
+}
+
+export function resolveDiscriminantProperty(schema: JSONSchema7, propertyPath: string) {
+ type SchemaBranch = { discriminatorValue: string | number; branchSchema: JSONSchema7 };
+ if (schema.allOf != null && Object.keys(schema.allOf).length === 1) {
+ return resolveDiscriminantProperty(schema.allOf[0] as JSONSchema7, propertyPath);
+ }
+
+ const discriminatorMap: Record = {};
+ const addToDiscriminatorMap = (branch: SchemaBranch): boolean => {
+ if (discriminatorMap[branch.discriminatorValue] != null) {
+ return false;
+ }
+ discriminatorMap[branch.discriminatorValue] = branch.branchSchema;
+ return true;
+ };
+
+ const addManyToDiscriminatorMap = (discriminatorValues: string[], schema: JSONSchema7): boolean => {
+ return discriminatorValues.every(value =>
+ addToDiscriminatorMap({ discriminatorValue: value, branchSchema: schema })
+ );
+ };
+
+ const analyzeBranch = (branchSchema: JSONSchema7, propertyPath: string): void | string[] => {
+ const parentPath = propertyPath.split("/").slice(0, -1).join("/");
+ const propertyName = propertyPath.split("/").at(-1);
+
+ if (propertyName == null) {
+ return;
+ }
+
+ const branchSchemaProperties = getUnifiedPropertySchemas(branchSchema as JSONSchema7, parentPath);
+ const discriminantProperty = branchSchemaProperties[propertyName];
+ if (discriminantProperty == null) {
+ return;
+ }
+ if (!discriminantProperty.required) {
+ return;
+ }
+ const discriminatorValues: string[] = [];
+
+ for (const schema of discriminantProperty.schemaSet) {
+ if (
+ !(schema.type === "string" || schema.type === "number")
+ || (schema.const == null && schema.enum == null)
+ ) {
+ return;
+ }
+
+ if (schema.const != null) {
+ discriminatorValues.push(schema.const as string);
+ }
+ if (schema.enum != null) {
+ discriminatorValues.push(...(schema.enum as string[]));
+ }
+ }
+
+ return discriminatorValues;
+ };
+
+ if (schema.anyOf == null) {
+ const result = analyzeBranch(schema, propertyPath);
+ if (result == null || !addManyToDiscriminatorMap(result, schema)) {
+ return;
+ }
+ return discriminatorMap;
+ }
+
+ for (const branchSchema of schema.anyOf) {
+ const result = analyzeBranch(branchSchema as JSONSchema7, propertyPath);
+ if (result == null || !addManyToDiscriminatorMap(result, branchSchema as JSONSchema7)) {
+ return;
+ }
+ }
+
+ return discriminatorMap;
+}
+
+export function isSchemaEmpty(schema: JSONSchema7) {
+ return Object.keys(schema)
+ .filter(key => !(key.startsWith("x-") || key.startsWith("$")))
+ .length === 0;
+}
diff --git a/packages/rest/src/transform/project.ts b/packages/rest/src/transform/project.ts
index 0a96af7..b474c93 100644
--- a/packages/rest/src/transform/project.ts
+++ b/packages/rest/src/transform/project.ts
@@ -44,6 +44,7 @@ export const SCHEMA_DEFAULTS: Config = {
encodeRefs: true,
additionalProperties: true,
topRef: false,
+ discriminatorType: "open-api",
};
export type Options = AJVOptions & SchemaConfig;
diff --git a/packages/rest/src/transform/transform.ts b/packages/rest/src/transform/transform.ts
index 06ec4fa..4a3a7b9 100644
--- a/packages/rest/src/transform/transform.ts
+++ b/packages/rest/src/transform/transform.ts
@@ -6,6 +6,7 @@ import {
NeverType,
ReferenceType,
SchemaGenerator,
+ StringType,
SubNodeParser,
} from "ts-json-schema-generator";
import ts from "typescript";
@@ -16,14 +17,13 @@ let project: Project;
// let files: string[] = [];
// let openapi: OpenAPIV3.Document;
-export class NornirIgnoreParser implements SubNodeParser {
+export class TemplateExpressionNodeParser implements SubNodeParser {
supportsNode(node: ts.Node): boolean {
- const tags = ts.getJSDocTags(node);
- return tags.some((tag) => tag.tagName.getText() === "nornirIgnore");
+ return ts.isTemplateExpression(node);
}
createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType {
- return new NeverType();
+ return new StringType();
}
}
@@ -46,7 +46,7 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme
...SCHEMA_DEFAULTS,
jsDoc: jsDoc || SCHEMA_DEFAULTS.jsDoc,
strictTuples: strictTuples || SCHEMA_DEFAULTS.strictTuples,
- encodeRefs: encodeRefs || SCHEMA_DEFAULTS.encodeRefs,
+ encodeRefs: false,
additionalProperties: additionalProperties || SCHEMA_DEFAULTS.additionalProperties,
sortProps: sortProps || SCHEMA_DEFAULTS.sortProps,
expose,
@@ -65,6 +65,7 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme
...schemaConfig,
}, (prs) => {
// prs.addNodeParser(new NornirIgnoreParser());
+ prs.addNodeParser(new TemplateExpressionNodeParser());
});
const typeFormatter = createFormatter({
...schemaConfig,
diff --git a/packages/rest/src/transform/transformers/node-transformer.ts b/packages/rest/src/transform/transformers/node-transformer.ts
index adfa6ae..ef42beb 100644
--- a/packages/rest/src/transform/transformers/node-transformer.ts
+++ b/packages/rest/src/transform/transformers/node-transformer.ts
@@ -10,7 +10,7 @@ export abstract class NodeTransformer {
context: ts.TransformationContext,
): ts.Node {
if (ts.isClassDeclaration(node)) {
- return ClassTransformer.transform(project, source, node, context);
+ return ClassTransformer.transform(project, source, ts.getOriginalNode(node) as ts.ClassDeclaration, context);
}
return node;
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 1e1ab22..f42864b 100644
--- a/packages/rest/src/transform/transformers/processors/chain-route-processor.ts
+++ b/packages/rest/src/transform/transformers/processors/chain-route-processor.ts
@@ -1,8 +1,9 @@
import { schemaToValidator } from "@nrfcloud/ts-json-schema-transformer/utils";
-import { createWrappedNode } from "ts-morph";
+import tsp from "ts-morph";
import { isTypeReference } from "tsutils";
import ts from "typescript";
import { ControllerMeta } from "../../controller-meta";
+import { moveRefsToAllOf } from "../../json-schema-utils";
import { getStringLiteralOrConst, isNornirNode, NornirDecoratorInfo, separateNornirDecorators } from "../../lib";
import { Project } from "../../project";
@@ -49,11 +50,13 @@ export abstract class ChainRouteProcessor {
const { typeNode: inputTypeNode, type: inputType } = ChainRouteProcessor.resolveInputType(project, node);
- // const outputType = ChainRouteProcessor.resolveOutputType(project, methodSignature);
+ const outputType = ChainRouteProcessor.resolveOutputType(project, node);
+
+ const outputSchema = project.schemaGenerator.createSchemaFromNodes([outputType.node]);
const inputSchema = project.schemaGenerator.createSchemaFromNodes([inputTypeNode]);
- const inputValidator = schemaToValidator(inputSchema, project.options.validation);
+ const inputValidator = schemaToValidator(moveRefsToAllOf(inputSchema, false), project.options.validation);
const parsedDocComments = ChainRouteProcessor.parseJSDoc(project, node);
@@ -61,9 +64,9 @@ export abstract class ChainRouteProcessor {
method,
path,
input: inputTypeNode,
-
+ inputSchema,
+ outputSchema: outputSchema,
// FIXME: this should be the output type not the input type
- output: inputTypeNode,
description: parsedDocComments.description,
summary: parsedDocComments.summary,
filePath: source.fileName,
@@ -205,20 +208,21 @@ export abstract class ChainRouteProcessor {
};
}
- private static resolveOutputType(project: Project, methodSignature: ts.Signature): ts.Type {
- const returnType = methodSignature.getReturnType();
- const returnTypeDeclaration = returnType.symbol?.declarations?.[0];
- if (!returnTypeDeclaration) {
- throw new Error("Handler chain return type declaration not found");
- }
- if (!isNornirNode(returnTypeDeclaration)) {
- throw new Error("Handler chain return must be a Nornir class");
- }
- if (!isTypeReference(returnType)) {
- throw new Error("Handler chain return must use a type reference");
- }
- const typeArguments = project.checker.getTypeArguments(returnType);
- const [outputType] = typeArguments;
- return outputType;
+ private static resolveOutputType(
+ project: Project,
+ methodDeclaration: ts.MethodDeclaration,
+ ): { type: ts.Type; node: ts.Node } {
+ const wrapped = tsp.createWrappedNode(methodDeclaration, { typeChecker: project.checker });
+ const type = wrapped.getReturnType();
+ const typeArgs = type.getTypeArguments();
+
+ const [, outputType] = typeArgs;
+ const symbol = outputType.getSymbol();
+ const declaration = symbol?.getDeclarations()?.[0];
+ return {
+ type: outputType.compilerType,
+ node: declaration?.compilerNode
+ || project.checker.typeToTypeNode(outputType.compilerType, undefined, undefined) as ts.TypeReferenceNode,
+ };
}
}
diff --git a/packages/test/package.json b/packages/test/package.json
index 0068033..75446c4 100644
--- a/packages/test/package.json
+++ b/packages/test/package.json
@@ -8,7 +8,7 @@
},
"devDependencies": {
"@jest/globals": "^29.5.0",
- "@nrfcloud/ts-json-schema-transformer": "^1.2.4",
+ "@nrfcloud/ts-json-schema-transformer": "^1.2.5",
"@types/aws-lambda": "^8.10.115",
"@types/jest": "^29.4.0",
"@types/node": "^18.15.11",
diff --git a/packages/test/src/controller.ts b/packages/test/src/controller.ts
index 0401ce8..87ab427 100644
--- a/packages/test/src/controller.ts
+++ b/packages/test/src/controller.ts
@@ -1,10 +1,10 @@
import { Nornir } from "@nornir/core";
import {
- AnyMimeType,
Controller,
GetChain,
type HttpRequest,
HttpRequestEmpty,
+ HttpResponse,
HttpStatusCode,
MimeType,
PostChain,
@@ -12,15 +12,21 @@ import {
import { assertValid } from "@nrfcloud/ts-json-schema-transformer";
interface RouteGetInput extends HttpRequestEmpty {
- headers: {
- "content-type": AnyMimeType;
+ pathParams: {
+ /**
+ * @pattern ^[a-z]+$
+ */
+ cool: TestStringType;
};
}
interface RoutePostInputJSON extends HttpRequest {
headers: {
- "content-type": MimeType.ApplicationJson;
- };
+ "content-type": "application/json";
+ } | { "content-type": "text/plain" };
+ /**
+ * @contentMediaType application/json
+ */
body: RoutePostBodyInput;
query: {
test: "boolean";
@@ -42,7 +48,7 @@ interface RoutePostInputJSON extends HttpRequest {
interface RoutePostInputCSV extends HttpRequest {
headers: {
- "content-type": MimeType.TextCsv;
+ "content-type": "text/csv";
/**
* This is a CSV header
* @example "cool,cool2"
@@ -51,16 +57,56 @@ interface RoutePostInputCSV extends HttpRequest {
*/
"csv-header": string;
};
+ /**
+ * @contentMediaType text/csv
+ */
body: TestStringType;
pathParams: {
/**
* @deprecated
*/
- reallyCool: "stuff";
+ reallyCool: TestStringType;
};
}
-type RoutePostInput = RoutePostInputJSON | RoutePostInputCSV;
+export type RoutePostInput = RoutePostInputCSV | RoutePostInputJSONAlias;
+
+export type RoutePostInputJSONAlias = RoutePostInputJSON;
+
+/**
+ * This is a comment
+ */
+export interface RouteGetOutputSuccess extends HttpResponse {
+ /**
+ * This is a property
+ */
+ statusCode: "200" | "201";
+ body: {
+ bleep: string;
+ bloop: number;
+ };
+ headers: {
+ "content-type": "application/json";
+ };
+}
+
+/**
+ * This is a comment on RouteGetOutputError
+ */
+export interface RouteGetOutputError extends HttpResponse {
+ statusCode: "400";
+ body: {
+ message: string;
+ };
+ headers: {
+ "content-type": "application/json";
+ };
+}
+
+/**
+ * Output of the GET route
+ */
+export type RouteGetOutput = RouteGetOutputSuccess | RouteGetOutputError;
/**
* this is a comment
@@ -78,7 +124,7 @@ interface RoutePostBodyInput {
* @pattern ^[a-z]+$
* @minLength 5
*/
-type TestStringType = Nominal;
+export type TestStringType = Nominal;
export declare class Tagged {
protected _nominal_: N;
@@ -104,30 +150,33 @@ const basePath = `${overallBase}/basepath`;
*/
@Controller(basePath)
export class TestController {
- // /**
- // * Cool get route
- // */
- // @GetChain("/route")
- // public getRoute(chain: Nornir) {
- // return chain
- // .use(input => {
- // assertValid(input);
- // return input;
- // })
- // .use(input => input.headers["content-type"])
- // .use(contentType => ({
- // statusCode: HttpStatusCode.Ok,
- // body: `Content-Type: ${contentType}`,
- // headers: {
- // "content-type": MimeType.TextPlain,
- // },
- // }));
- // }
+ /**
+ * Cool get route
+ */
+ @GetChain("/route/{cool}")
+ public getRoute(chain: Nornir) {
+ return chain
+ .use(input => {
+ assertValid(input);
+ return input;
+ })
+ .use(input => input.headers?.toString())
+ .use(contentType => ({
+ statusCode: "200" as const,
+ body: {
+ bleep: "bloop",
+ bloop: 5,
+ },
+ headers: {
+ "content-type": "application/json" as const,
+ } as const,
+ } as RouteGetOutput));
+ }
/**
* A simple post route
* @summary Cool Route
- * @tags cool, route
+ * @tags cool
* @deprecated
* @operationId coolRoute
*/
@@ -135,11 +184,9 @@ export class TestController {
public postRoute(chain: Nornir) {
return chain
.use(contentType => ({
- statusCode: HttpStatusCode.Ok,
- body: `Content-Type: ${contentType}`,
- headers: {
- "content-type": MimeType.TextPlain,
- },
+ statusCode: "200" as const,
+ // body: `Content-Type: ${contentType}`,
+ headers: {},
}));
}
}
diff --git a/packages/test/src/rest.ts b/packages/test/src/rest.ts
index 8a2e88e..a3c8f82 100644
--- a/packages/test/src/rest.ts
+++ b/packages/test/src/rest.ts
@@ -1,6 +1,5 @@
import nornir from "@nornir/core";
import {
- AnyMimeType,
ApiGatewayProxyV2,
httpErrorHandler,
httpEventParser,
@@ -38,14 +37,12 @@ const frameworkChain = nornir()
.use(router())
.useResult(httpErrorHandler([
mapErrorClass(TestError, (err) => ({
- statusCode: HttpStatusCode.InternalServerError,
- headers: {
- "content-type": AnyMimeType,
- },
+ statusCode: "500",
+ headers: {},
})),
]))
.use(httpResponseSerializer({
- [MimeType.ApplicationZip]: () => Buffer.from(""),
+ ["application/bzip"]: () => Buffer.from(""),
}));
export const handler: APIGatewayProxyHandlerV2 = nornir()
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9f0e8c7..52b298b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -96,8 +96,8 @@ importers:
specifier: ^29.5.0
version: 29.5.0
'@nrfcloud/ts-json-schema-transformer':
- specifier: ^1.2.4
- version: 1.2.4(typescript@5.2.2)
+ specifier: ^1.2.5
+ version: 1.2.5(typescript@5.2.2)
'@types/jest':
specifier: ^29.4.0
version: 29.4.0
@@ -122,12 +122,15 @@ importers:
packages/rest:
dependencies:
+ '@apidevtools/json-schema-ref-parser':
+ specifier: ^11.1.0
+ version: 11.1.0
'@nornir/core':
specifier: workspace:^
version: link:../core
'@nrfcloud/ts-json-schema-transformer':
- specifier: ^1.2.4
- version: 1.2.4(typescript@5.2.2)
+ specifier: ^1.2.5
+ version: 1.2.5(typescript@5.2.2)
'@types/aws-lambda':
specifier: ^8.10.115
version: 8.10.115
@@ -153,8 +156,8 @@ importers:
specifier: ^1.2.2
version: 1.2.2
ts-json-schema-generator:
- specifier: ^1.4.0
- version: 1.4.0
+ specifier: ^1.5.0
+ version: 1.5.0
ts-morph:
specifier: ^20.0.0
version: 20.0.0
@@ -205,8 +208,8 @@ importers:
specifier: ^29.5.0
version: 29.5.0
'@nrfcloud/ts-json-schema-transformer':
- specifier: ^1.2.4
- version: 1.2.4(typescript@5.2.2)
+ specifier: ^1.2.5
+ version: 1.2.5(typescript@5.2.2)
'@types/aws-lambda':
specifier: ^8.10.115
version: 8.10.115
@@ -257,6 +260,17 @@ packages:
js-yaml: 4.1.0
lodash.clonedeep: 4.5.0
+ /@apidevtools/json-schema-ref-parser@11.1.0:
+ resolution: {integrity: sha512-g/VW9ZQEFJAOwAyUb8JFf7MLiLy2uEB4rU270rGzDwICxnxMlPy0O11KVePSgS36K1NI29gSlK84n5INGhd4Ag==}
+ engines: {node: '>= 16'}
+ dependencies:
+ '@jsdevtools/ono': 7.1.3
+ '@types/json-schema': 7.0.15
+ '@types/lodash.clonedeep': 4.5.7
+ js-yaml: 4.1.0
+ lodash.clonedeep: 4.5.0
+ dev: false
+
/@babel/code-frame@7.21.4:
resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==}
engines: {node: '>=6.9.0'}
@@ -2248,8 +2262,8 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
- /@nrfcloud/ts-json-schema-transformer@1.2.4(typescript@5.2.2):
- resolution: {integrity: sha512-JyZkblORtJCRzE2wOYQC3pIeXjkxg/irNYtC7ufcErCPBB9NHzvqMoAaIPu0bZJdxCLvLKL4qTh7NYpm1gg/Gg==}
+ /@nrfcloud/ts-json-schema-transformer@1.2.5(typescript@5.2.2):
+ resolution: {integrity: sha512-AL1ZYkwtusprB4Hsf5CuySaRT0hPUhVA8rWb58FS13FcZd9DKi8mKg6s09Jmy7Ersud0Q5WnkxHvEM+ltz8tqA==}
engines: {node: '>=18.0.0'}
peerDependencies:
typescript: '>=5'
@@ -2258,7 +2272,7 @@ packages:
ajv: 8.12.0
esbuild: 0.17.18
json-schema-faker: /@jfconley/json-schema-faker@0.5.0-rcv.48
- ts-json-schema-generator: 1.4.0
+ ts-json-schema-generator: 1.5.0
typescript: 5.2.2
/@parcel/source-map@2.1.1:
@@ -2371,10 +2385,6 @@ packages:
pretty-format: 29.5.0
dev: true
- /@types/json-schema@7.0.12:
- resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
- dev: true
-
/@types/json-schema@7.0.15:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -2583,7 +2593,7 @@ packages:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0)
- '@types/json-schema': 7.0.12
+ '@types/json-schema': 7.0.15
'@types/semver': 7.5.0
'@typescript-eslint/scope-manager': 5.59.2
'@typescript-eslint/types': 5.59.2
@@ -2603,7 +2613,7 @@ packages:
eslint: ^7.0.0 || ^8.0.0
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0)
- '@types/json-schema': 7.0.12
+ '@types/json-schema': 7.0.15
'@types/semver': 7.5.0
'@typescript-eslint/scope-manager': 6.2.0
'@typescript-eslint/types': 6.2.0
@@ -6520,8 +6530,8 @@ packages:
resolution: {integrity: sha512-cA5MPLWGWYXvnlJb4TamUUx858HVHBsxxdy8l7jxODOLDyGYnQOllob2A2jyDghGa5iJHs2gzFNHvwGJ0ZfR8g==}
dev: false
- /ts-json-schema-generator@1.4.0:
- resolution: {integrity: sha512-wm8vyihmGgYpxrqRshmYkWGNwEk+sf3xV2rUgxv8Ryeh7bSpMO7pZQOht+2rS002eDkFTxR7EwRPXVzrS0WJTg==}
+ /ts-json-schema-generator@1.5.0:
+ resolution: {integrity: sha512-RkiaJ6YxGc5EWVPfyHxszTmpGxX8HC2XBvcFlAl1zcvpOG4tjjh+eXioStXJQYTvr9MoK8zCOWzAUlko3K0DiA==}
engines: {node: '>=10.0.0'}
hasBin: true
dependencies:
@@ -6531,7 +6541,7 @@ packages:
json5: 2.2.3
normalize-path: 3.0.0
safe-stable-stringify: 2.4.3
- typescript: 5.2.2
+ typescript: 5.3.3
/ts-morph@20.0.0:
resolution: {integrity: sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==}
@@ -6723,6 +6733,11 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ /typescript@5.3.3:
+ resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
/ufo@1.1.2:
resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==}
dev: true
From 6d2d8d94eafefb2bf4a6be59b129ce5dfe066b0a Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Sun, 10 Dec 2023 12:52:10 -0800
Subject: [PATCH 05/16] support examples and descriptions
---
packages/rest/src/runtime/http-event.mts | 8 +-
.../rest/src/transform/controller-meta.ts | 30 ++-
.../rest/src/transform/json-schema-utils.ts | 37 +++-
.../transformers/file-transformer.ts | 1 -
packages/test/src/controller.ts | 9 +-
packages/test/src/controller2.ts | 197 +++++++++---------
6 files changed, 156 insertions(+), 126 deletions(-)
diff --git a/packages/rest/src/runtime/http-event.mts b/packages/rest/src/runtime/http-event.mts
index 95e08a7..5bde8d7 100644
--- a/packages/rest/src/runtime/http-event.mts
+++ b/packages/rest/src/runtime/http-event.mts
@@ -39,10 +39,7 @@ type QueryParams = Record | Ar
type PathParams = Record;
export interface HttpRequestEmpty extends HttpRequest {
- headers: {
- "content-type": never
- }
- // body?: undefined;
+ body?: undefined;
}
export interface HttpRequestJSON extends HttpRequest {
@@ -63,9 +60,6 @@ export interface SerializedHttpResponse extends Omit {
}
export interface HttpResponseEmpty extends HttpResponse {
- headers: {
- "content-type": never
- },
body?: undefined;
}
diff --git a/packages/rest/src/transform/controller-meta.ts b/packages/rest/src/transform/controller-meta.ts
index 86e01bf..a2229c3 100644
--- a/packages/rest/src/transform/controller-meta.ts
+++ b/packages/rest/src/transform/controller-meta.ts
@@ -4,6 +4,8 @@ import ts from "typescript";
import { TransformationError } from "./error";
import {
dereferenceSchema,
+ getFirstExample,
+ getSchemaOrAllOf,
getUnifiedPropertySchemas,
isSchemaEmpty,
joinSchemas,
@@ -339,20 +341,23 @@ export class ControllerMeta {
const content = contentTypeDiscriminatedSchemas == null ? undefined : Object.fromEntries(
Object.entries(contentTypeDiscriminatedSchemas).map(([contentType, schema]) => {
- const branchBodySchema = getUnifiedPropertySchemas(schema, "/")["body"];
+ const branchBodySchemaSet = getUnifiedPropertySchemas(schema, "/")["body"];
+ const branchBodySchema = rewriteRefsForOpenApi(
+ unresolveRefs(joinSchemas(branchBodySchemaSet.schemaSet)),
+ );
+ const example = getFirstExample(branchBodySchema);
return [
contentType,
{
- schema: rewriteRefsForOpenApi(
- unresolveRefs(joinSchemas(branchBodySchema.schemaSet)),
- ) as OpenAPIV3.NonArraySchemaObject,
+ ...(example == null ? {} : { example }),
+ schema: branchBodySchema as OpenAPIV3.NonArraySchemaObject,
},
];
}),
);
responses[statusCode] = {
- description: schema.description || "",
+ description: getSchemaOrAllOf(schema).description || "",
headers: Object.fromEntries(headers.map(header => [header.name, header])),
content,
};
@@ -374,17 +379,28 @@ export class ControllerMeta {
const content = Object.fromEntries(
Object.entries(contentTypeDiscriminatedSchemas).map(([contentType, schema]) => {
- const branchBodySchema = getUnifiedPropertySchemas(schema, "/")["body"];
+ const branchBodySchemaSet = getUnifiedPropertySchemas(schema, "/")["body"];
+ const branchBodySchema = rewriteRefsForOpenApi(unresolveRefs(joinSchemas(branchBodySchemaSet.schemaSet)));
+ const example = getFirstExample(branchBodySchema);
+
return [
contentType,
{
- schema: rewriteRefsForOpenApi(unresolveRefs(joinSchemas(branchBodySchema.schemaSet))),
+ ...(example == null ? {} : { example }),
+ schema: branchBodySchema,
},
];
}),
);
+ // If there is exactly one branch, then we can use the description from that branch.
+ // Otherwise, don't use a description.
+ const description = Object.keys(content).length === 1
+ ? getSchemaOrAllOf(Object.values(content)[0].schema).description
+ : undefined;
+
return {
+ description,
required: bodySchema.required,
content,
} as OpenAPIV3_1.RequestBodyObject;
diff --git a/packages/rest/src/transform/json-schema-utils.ts b/packages/rest/src/transform/json-schema-utils.ts
index 82a777a..e6b5a64 100644
--- a/packages/rest/src/transform/json-schema-utils.ts
+++ b/packages/rest/src/transform/json-schema-utils.ts
@@ -4,6 +4,7 @@ import dereference from "@apidevtools/json-schema-ref-parser/dist/lib/dereferenc
import { JSONSchema7 } from "json-schema";
import traverse from "json-schema-traverse";
import { cloneDeep } from "lodash";
+import { OpenAPIV3_1 } from "openapi-types";
const refParser = new $RefParser();
@@ -45,7 +46,6 @@ export function joinSchemas(schemaSet: JSONSchema7[]): JSONSchema7 {
export function getUnifiedPropertySchemas(
schema: JSONSchema7,
parentPath: string,
- ignoreAdditionalProperties = false,
): Record {
// Take a path from the json schema and convert it to a path in validated object
@@ -185,6 +185,25 @@ export function rewriteRefsForOpenApi(schema: JSONSchema7) {
return clonedSchema;
}
+export function getSchemaOrAllOf(schema: JSONSchema7): JSONSchema7 {
+ if (schema.allOf != null && schema.allOf.length === 1) {
+ return schema.allOf[0] as JSONSchema7;
+ }
+
+ return schema;
+}
+
+export function getFirstExample(schema: JSONSchema7): unknown {
+ schema = getSchemaOrAllOf(schema);
+ if ((schema as { example?: string }).example != null) {
+ return (schema as { example?: string }).example;
+ }
+ if (schema.examples != null) {
+ return Array.isArray(schema.examples) ? schema.examples[0] : schema.examples;
+ }
+ return undefined;
+}
+
export function resolveDiscriminantProperty(schema: JSONSchema7, propertyPath: string) {
type SchemaBranch = { discriminatorValue: string | number; branchSchema: JSONSchema7 };
if (schema.allOf != null && Object.keys(schema.allOf).length === 1) {
@@ -261,8 +280,16 @@ export function resolveDiscriminantProperty(schema: JSONSchema7, propertyPath: s
return discriminatorMap;
}
-export function isSchemaEmpty(schema: JSONSchema7) {
- return Object.keys(schema)
- .filter(key => !(key.startsWith("x-") || key.startsWith("$")))
- .length === 0;
+export function isSchemaEmpty(schema: JSONSchema7): boolean {
+ const keys = Object.keys(schema);
+ for (const key of keys) {
+ if (key.startsWith("x-") || key.startsWith("$")) {
+ continue;
+ }
+ if (key === "not" && isSchemaEmpty(schema.not as JSONSchema7)) {
+ continue;
+ }
+ return false;
+ }
+ return true;
}
diff --git a/packages/rest/src/transform/transformers/file-transformer.ts b/packages/rest/src/transform/transformers/file-transformer.ts
index 3ce73f2..8a0cbd1 100644
--- a/packages/rest/src/transform/transformers/file-transformer.ts
+++ b/packages/rest/src/transform/transformers/file-transformer.ts
@@ -40,7 +40,6 @@ export abstract class FileTransformer {
);
const mergedSpec = OpenApiSpecHolder.getSpecForFile(file);
- console.log("Merged Spec:", JSON.stringify(mergedSpec, null, 2));
compilerHost.writeFile(schemaFileName, JSON.stringify(mergedSpec, null, 2), false, undefined, []);
diff --git a/packages/test/src/controller.ts b/packages/test/src/controller.ts
index 87ab427..9997e5c 100644
--- a/packages/test/src/controller.ts
+++ b/packages/test/src/controller.ts
@@ -25,7 +25,8 @@ interface RoutePostInputJSON extends HttpRequest {
"content-type": "application/json";
} | { "content-type": "text/plain" };
/**
- * @contentMediaType application/json
+ * A cool json input
+ * @example { "cool": "stuff" }
*/
body: RoutePostBodyInput;
query: {
@@ -58,7 +59,8 @@ interface RoutePostInputCSV extends HttpRequest {
"csv-header": string;
};
/**
- * @contentMediaType text/csv
+ * This is a CSV body
+ * @example "cool,cool2"
*/
body: TestStringType;
pathParams: {
@@ -95,6 +97,9 @@ export interface RouteGetOutputSuccess extends HttpResponse {
*/
export interface RouteGetOutputError extends HttpResponse {
statusCode: "400";
+ /**
+ * @example { "message": "Bad Request"}
+ */
body: {
message: string;
};
diff --git a/packages/test/src/controller2.ts b/packages/test/src/controller2.ts
index b80703e..67a883b 100644
--- a/packages/test/src/controller2.ts
+++ b/packages/test/src/controller2.ts
@@ -1,104 +1,93 @@
-// import { Nornir } from "@nornir/core";
-// import {
-// AnyMimeType,
-// Controller,
-// GetChain,
-// HttpRequest,
-// HttpRequestEmpty,
-// HttpResponse,
-// HttpResponseEmpty,
-// HttpStatusCode,
-// MimeType,
-// Provider,
-// PutChain,
-// } from "@nornir/rest";
-//
-// interface RouteGetInput extends HttpRequestEmpty {
-// headers: GetHeaders;
-// }
-// interface GetHeaders {
-// "content-type": AnyMimeType;
-// [key: string]: string;
-// }
-//
-// interface RoutePostInputJSON extends HttpRequest {
-// headers: {
-// "content-type": MimeType.ApplicationJson;
-// };
-// body: RoutePostBodyInput;
-// }
-//
-// interface RoutePostInputCSV extends HttpRequest {
-// headers: {
-// "content-type": MimeType.TextCsv;
-// };
-// body: string;
-// }
-//
-// type RoutePutInput = RoutePostInputJSON | RoutePostInputCSV;
-//
-// interface RoutePostBodyInput {
-// cool: string;
-// }
-//
-// const basePath = "/basepath/2";
-//
-// /**
-// * This is a second controller
-// * @summary This is a summary
-// */
-// @Controller(basePath, "test")
-// export class TestController {
-// @Provider()
-// public static test() {
-// return new TestController();
-// }
-//
-// /**
-// * The second simple GET route.
-// * @summary Get route
-// */
-// @GetChain("/route")
-// public getRoute(chain: Nornir) {
-// return chain
-// .use(input => input.headers["content-type"])
-// .use(contentType => ({
-// statusCode: HttpStatusCode.Ok,
-// body: `Content-Type: ${contentType}`,
-// headers: {
-// "content-type": MimeType.TextPlain,
-// },
-// }));
-// }
-//
-// /**
-// * The second simple PUT route.
-// * @summary Put route
-// */
-// @PutChain("/route")
-// public postRoute(chain: Nornir): Nornir {
-// return chain
-// .use(() => ({
-// statusCode: HttpStatusCode.Created,
-// headers: {
-// "content-type": AnyMimeType,
-// },
-// }));
-// }
-// }
-//
-// type PutResponse = PutSuccessResponse | PutBadRequestResponse;
-//
-// interface PutSuccessResponse extends HttpResponseEmpty {
-// statusCode: HttpStatusCode.Created;
-// }
-//
-// interface PutBadRequestResponse extends HttpResponse {
-// statusCode: HttpStatusCode.BadRequest;
-// headers: {
-// "content-type": MimeType.ApplicationJson;
-// };
-// body: {
-// potato: boolean;
-// };
-// }
+import { Nornir } from "@nornir/core";
+import {
+ Controller,
+ GetChain,
+ HttpRequest,
+ HttpRequestEmpty,
+ HttpResponse,
+ HttpResponseEmpty,
+ Provider,
+ PutChain,
+} from "@nornir/rest";
+
+interface RouteGetInput extends HttpRequestEmpty {
+}
+
+interface RoutePostInputJSON extends HttpRequest {
+ headers: {
+ "content-type": "application/json";
+ };
+ body: RoutePostBodyInput;
+}
+
+interface RoutePostInputCSV extends HttpRequest {
+ headers: {
+ "content-type": "text/csv";
+ };
+ body: string;
+}
+
+type RoutePutInput = RoutePostInputJSON | RoutePostInputCSV;
+
+interface RoutePostBodyInput {
+ cool: string;
+}
+
+const basePath = "/basepath/2";
+
+/**
+ * This is a second controller
+ * @summary This is a summary
+ */
+@Controller(basePath, "test")
+export class TestController {
+ @Provider()
+ public static test() {
+ return new TestController();
+ }
+
+ /**
+ * The second simple GET route.
+ * @summary Get route
+ */
+ @GetChain("/route")
+ public getRoute(chain: Nornir) {
+ return chain
+ .use(contentType => ({
+ statusCode: "200",
+ body: `Content-Type: ${contentType}`,
+ headers: {
+ "content-type": "text/plain",
+ },
+ } as const));
+ }
+
+ /**
+ * The second simple PUT route.
+ * @summary Put route
+ */
+ @PutChain("/route")
+ public postRoute(chain: Nornir): Nornir {
+ return chain
+ .use(() => ({
+ statusCode: "201",
+ headers: {},
+ }));
+ }
+}
+
+type PutResponse = PutSuccessResponse | PutBadRequestResponse;
+
+interface PutSuccessResponse extends HttpResponseEmpty {
+ statusCode: "201";
+}
+
+interface PutBadRequestResponse extends HttpResponse {
+ statusCode: "422";
+ headers: {
+ "content-type": "application/json";
+ };
+ body: {
+ potato: boolean;
+ };
+}
From 01d17ae997087fa37d9031f7cc5d508258566a79 Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Mon, 11 Dec 2023 14:55:16 -0800
Subject: [PATCH 06/16] simple spec collector
---
packages/rest/package.json | 9 +-
packages/rest/src/cli/cli.ts | 42 +
packages/rest/src/cli/lib/collect.ts | 30 +
packages/rest/src/cli/lib/ts-utils.ts | 25 +
.../rest/src/transform/controller-meta.ts | 7 +-
packages/test/base.openapi.json | 8 +
packages/test/openapi.json | 753 ++++++++++++++++++
pnpm-lock.yaml | 345 +++++++-
8 files changed, 1184 insertions(+), 35 deletions(-)
create mode 100644 packages/rest/src/cli/cli.ts
create mode 100644 packages/rest/src/cli/lib/collect.ts
create mode 100644 packages/rest/src/cli/lib/ts-utils.ts
create mode 100644 packages/test/base.openapi.json
create mode 100644 packages/test/openapi.json
diff --git a/packages/rest/package.json b/packages/rest/package.json
index a4b69bf..5d56a2e 100644
--- a/packages/rest/package.json
+++ b/packages/rest/package.json
@@ -2,6 +2,9 @@
"name": "@nornir/rest",
"description": "A nornir library",
"version": "1.3.0",
+ "bin": {
+ "nornir-oas": "./dist/cli/cli.js"
+ },
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.1.0",
"@nornir/core": "workspace:^",
@@ -9,14 +12,17 @@
"@types/aws-lambda": "^8.10.115",
"ajv": "^8.12.0",
"atlassian-openapi": "^1.0.18",
+ "glob": "^10.3.10",
"json-schema-traverse": "^1.0.0",
"lodash": "^4.17.21",
+ "openapi-diff": "^0.23.6",
"openapi-types": "^12.1.0",
"trouter": "^3.2.1",
"ts-is-present": "^1.2.2",
"ts-json-schema-generator": "^1.5.0",
"ts-morph": "^20.0.0",
- "tsutils": "^3.21.0"
+ "tsutils": "^3.21.0",
+ "yargs": "^17.7.2"
},
"devDependencies": {
"@jest/globals": "^29.5.0",
@@ -24,6 +30,7 @@
"@types/json-schema": "^7.0.15",
"@types/lodash": "^4.14.202",
"@types/node": "^18.15.11",
+ "@types/yargs": "^17.0.32",
"eslint": "^8.45.0",
"jest": "^29.5.0",
"ts-patch": "^3.0.2",
diff --git a/packages/rest/src/cli/cli.ts b/packages/rest/src/cli/cli.ts
new file mode 100644
index 0000000..a43130a
--- /dev/null
+++ b/packages/rest/src/cli/cli.ts
@@ -0,0 +1,42 @@
+import { readFileSync, writeFileSync } from "fs";
+import { merge } from "lodash";
+import path from "path";
+import yargs from "yargs";
+import { isErrorResult } from "../transform/openapi-merge";
+import { getMergedSpec, getSpecFiles } from "./lib/collect";
+import { resolveTsConfigOutdir } from "./lib/ts-utils";
+
+yargs
+ .scriptName("nornir-oas")
+ .command("collect", "Build OpenAPI spec from collected spec files", args =>
+ args
+ .option("output", {
+ default: path.join(process.cwd(), "openapi.json"),
+ alias: "o",
+ type: "string",
+ description: "Output file for the generated OpenAPI spec",
+ })
+ .option("scanDirectory", {
+ default: resolveTsConfigOutdir() ?? path.join(process.cwd(), "dist"),
+ type: "string",
+ alias: "s",
+ description:
+ "Directory to scan for spec files. This is probably the directory where your compiled typescript files are.",
+ })
+ .option("overrideSpec", {
+ type: "string",
+ alias: "b",
+ description: "Path to an openapi JSON file to deeply merge with collected specification",
+ }), args => {
+ const mergedSpec = getMergedSpec(args.scanDirectory);
+ if (isErrorResult(mergedSpec)) {
+ throw new Error("Failed to merge spec files");
+ }
+ let spec = mergedSpec.output;
+ if (args.overrideSpec) {
+ const overrideSpec = JSON.parse(readFileSync(args.overrideSpec, "utf-8"));
+ spec = merge(spec, overrideSpec);
+ }
+
+ writeFileSync(args.output, JSON.stringify(spec, null, 2));
+ }).strictCommands().demandCommand().parse();
diff --git a/packages/rest/src/cli/lib/collect.ts b/packages/rest/src/cli/lib/collect.ts
new file mode 100644
index 0000000..af5c176
--- /dev/null
+++ b/packages/rest/src/cli/lib/collect.ts
@@ -0,0 +1,30 @@
+import { Swagger } from "atlassian-openapi";
+import { readFileSync } from "fs";
+import { sync } from "glob";
+import { OpenAPIV3_1 } from "openapi-types";
+import { merge } from "../../transform/openapi-merge/index.js";
+import SwaggerV3 = Swagger.SwaggerV3;
+
+export function getSpecFiles(scanDir: string) {
+ return sync(`${scanDir}/**/*.nornir.oas.json`);
+}
+
+export function readSpecFiles(paths: string[]) {
+ return paths.map(path => {
+ return JSON.parse(readFileSync(path, "utf-8")) as OpenAPIV3_1.Document;
+ });
+}
+
+export function getMergedSpec(scanDir: string) {
+ const files = getSpecFiles(scanDir);
+ const specs = readSpecFiles(files);
+ return merge(specs.map(spec => ({
+ dispute: {
+ // mergeDispute: true,
+ // alwaysApply: true,
+ // prefix: "",
+ // suffix: ""
+ },
+ oas: spec as SwaggerV3,
+ })));
+}
diff --git a/packages/rest/src/cli/lib/ts-utils.ts b/packages/rest/src/cli/lib/ts-utils.ts
new file mode 100644
index 0000000..74b1321
--- /dev/null
+++ b/packages/rest/src/cli/lib/ts-utils.ts
@@ -0,0 +1,25 @@
+import fs from "node:fs";
+import { dirname, resolve } from "node:path";
+import ts from "typescript";
+
+export function resolveTsConfigOutdir(searchPath = process.cwd(), configName = "tsconfig.json") {
+ const path = ts.findConfigFile(searchPath, ts.sys.fileExists, configName);
+ if (path == undefined) {
+ return;
+ }
+ const config = ts.readConfigFile(path, ts.sys.readFile);
+ if (config.error) {
+ return;
+ }
+ const compilerOptions = config.config as { compilerOptions: ts.CompilerOptions };
+
+ const pathDir = dirname(path);
+
+ const outDir = compilerOptions.compilerOptions.outDir;
+
+ if (outDir == undefined) {
+ return undefined;
+ }
+
+ return resolve(pathDir, outDir);
+}
diff --git a/packages/rest/src/transform/controller-meta.ts b/packages/rest/src/transform/controller-meta.ts
index a2229c3..3306fd4 100644
--- a/packages/rest/src/transform/controller-meta.ts
+++ b/packages/rest/src/transform/controller-meta.ts
@@ -31,12 +31,7 @@ export abstract class OpenApiSpecHolder {
const mergeInputs: MergeInput = (this.specFileMap.get(file.fileName) || [])
.map((spec) => ({
oas: spec,
- dispute: {
- alwaysApply: true,
- mergeDispute: true,
- prefix: "",
- suffix: "",
- },
+ dispute: {},
})) as MergeInput;
const merged = merge(mergeInputs);
diff --git a/packages/test/base.openapi.json b/packages/test/base.openapi.json
new file mode 100644
index 0000000..d7df19d
--- /dev/null
+++ b/packages/test/base.openapi.json
@@ -0,0 +1,8 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "description": "A test api",
+ "version": "1.0.0",
+ "title": "Test API"
+ }
+}
diff --git a/packages/test/openapi.json b/packages/test/openapi.json
new file mode 100644
index 0000000..d501cb6
--- /dev/null
+++ b/packages/test/openapi.json
@@ -0,0 +1,753 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Test API",
+ "version": "1.0.0",
+ "description": "A test api"
+ },
+ "paths": {
+ "/basepath/2/route": {
+ "get": {
+ "summary": "Get route",
+ "description": "The second simple GET route.",
+ "responses": {
+ "200": {
+ "description": "",
+ "headers": {
+ "content-type": {
+ "name": "content-type",
+ "in": "header",
+ "required": true,
+ "deprecated": false,
+ "schema": {
+ "type": "string",
+ "const": "text/plain"
+ }
+ }
+ },
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "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.",
+ "responses": {
+ "201": {
+ "description": "",
+ "headers": {
+ "content-type": {
+ "name": "content-type",
+ "in": "header",
+ "required": false,
+ "deprecated": false,
+ "schema": {
+ "anyOf": [
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/MimeType"
+ }
+ ]
+ },
+ {
+ "not": {}
+ }
+ ]
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "",
+ "headers": {
+ "content-type": {
+ "name": "content-type",
+ "in": "header",
+ "required": true,
+ "deprecated": false,
+ "schema": {
+ "type": "string",
+ "const": "application/json"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "potato": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "potato"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "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"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ "/root/basepath/route/{cool}": {
+ "get": {
+ "description": "Cool get route",
+ "responses": {
+ "200": {
+ "description": "This is a comment",
+ "headers": {
+ "content-type": {
+ "name": "content-type",
+ "in": "header",
+ "required": true,
+ "deprecated": false,
+ "schema": {
+ "type": "string",
+ "const": "application/json"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "bleep": {
+ "type": "string"
+ },
+ "bloop": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "bleep",
+ "bloop"
+ ]
+ }
+ }
+ }
+ },
+ "201": {
+ "description": "This is a comment",
+ "headers": {
+ "content-type": {
+ "name": "content-type",
+ "in": "header",
+ "required": true,
+ "deprecated": false,
+ "schema": {
+ "type": "string",
+ "const": "application/json"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "bleep": {
+ "type": "string"
+ },
+ "bloop": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "bleep",
+ "bloop"
+ ]
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "This is a comment on RouteGetOutputError",
+ "headers": {
+ "content-type": {
+ "name": "content-type",
+ "in": "header",
+ "required": true,
+ "deprecated": false,
+ "schema": {
+ "type": "string",
+ "const": "application/json"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "example": {
+ "message": "Bad Request"
+ },
+ "schema": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "examples": [
+ {
+ "message": "Bad Request"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "cool",
+ "in": "path",
+ "required": true,
+ "deprecated": false,
+ "schema": {
+ "pattern": "^[a-z]+$",
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/TestStringType"
+ }
+ ]
+ }
+ },
+ {
+ "name": "content-type",
+ "in": "header",
+ "required": false,
+ "deprecated": false,
+ "schema": {
+ "anyOf": [
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/MimeType"
+ }
+ ]
+ },
+ {
+ "not": {}
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "post": {
+ "deprecated": true,
+ "tags": [
+ "cool"
+ ],
+ "operationId": "coolRoute",
+ "summary": "Cool Route",
+ "description": "A simple post route",
+ "responses": {
+ "200": {
+ "description": "",
+ "headers": {}
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "text/csv": {
+ "schema": {
+ "description": "This is a CSV body",
+ "examples": [
+ "cool,cool2"
+ ],
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/TestStringType"
+ }
+ ]
+ }
+ },
+ "application/json": {
+ "example": {
+ "cool": "stuff"
+ },
+ "schema": {
+ "type": "object",
+ "properties": {
+ "cool": {
+ "type": "string",
+ "description": "This is a cool property",
+ "minLength": 5
+ }
+ },
+ "required": [
+ "cool"
+ ],
+ "description": "A cool json input",
+ "examples": [
+ {
+ "cool": "stuff"
+ }
+ ]
+ }
+ },
+ "text/plain": {
+ "example": {
+ "cool": "stuff"
+ },
+ "schema": {
+ "type": "object",
+ "properties": {
+ "cool": {
+ "type": "string",
+ "description": "This is a cool property",
+ "minLength": 5
+ }
+ },
+ "required": [
+ "cool"
+ ],
+ "description": "A cool json input",
+ "examples": [
+ {
+ "cool": "stuff"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "reallyCool",
+ "in": "path",
+ "required": true,
+ "description": "Very cool property that does a thing",
+ "example": "true",
+ "deprecated": false,
+ "schema": {
+ "anyOf": [
+ {
+ "deprecated": true,
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/TestStringType"
+ }
+ ]
+ },
+ {
+ "type": "string",
+ "enum": [
+ "true",
+ "false"
+ ],
+ "description": "Very cool property that does a thing",
+ "examples": [
+ "true"
+ ],
+ "pattern": "^[a-z]+$"
+ }
+ ]
+ }
+ },
+ {
+ "name": "evenCooler",
+ "in": "path",
+ "required": false,
+ "description": "Even cooler property",
+ "deprecated": false,
+ "schema": {
+ "type": "number",
+ "description": "Even cooler property"
+ }
+ },
+ {
+ "name": "test",
+ "in": "query",
+ "required": false,
+ "deprecated": false,
+ "schema": {
+ "type": "string",
+ "const": "boolean"
+ }
+ },
+ {
+ "name": "content-type",
+ "in": "header",
+ "required": true,
+ "deprecated": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "text/csv"
+ },
+ {
+ "type": "string",
+ "const": "application/json"
+ },
+ {
+ "type": "string",
+ "const": "text/plain"
+ }
+ ]
+ }
+ },
+ {
+ "name": "csv-header",
+ "in": "header",
+ "required": false,
+ "description": "This is a CSV header",
+ "example": "cool,cool2",
+ "deprecated": false,
+ "schema": {
+ "type": "string",
+ "description": "This is a CSV header",
+ "examples": [
+ "cool,cool2"
+ ],
+ "pattern": "^[a-z]+,[a-z]+$",
+ "minLength": 5
+ }
+ }
+ ]
+ }
+ }
+ },
+ "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",
+ "properties": {
+ "content-type": {
+ "not": {}
+ }
+ }
+ },
+ "TestStringType": {
+ "description": "Amazing string",
+ "pattern": "^[a-z]+$",
+ "minLength": 5,
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/Nominal"
+ }
+ ]
+ },
+ "Nominal": {
+ "type": [
+ "string"
+ ],
+ "description": "Constructs a nominal type of type `T`. Useful to prevent any value of type `T` from being used or modified in places it shouldn't (think `id`s)."
+ },
+ "RouteGetOutputSuccess": {
+ "type": "object",
+ "properties": {
+ "statusCode": {
+ "type": "string",
+ "enum": [
+ "200",
+ "201"
+ ],
+ "description": "This is a property"
+ },
+ "headers": {
+ "type": "object",
+ "properties": {
+ "content-type": {
+ "type": "string",
+ "const": "application/json"
+ }
+ },
+ "required": [
+ "content-type"
+ ]
+ },
+ "body": {
+ "type": "object",
+ "properties": {
+ "bleep": {
+ "type": "string"
+ },
+ "bloop": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "bleep",
+ "bloop"
+ ]
+ }
+ },
+ "required": [
+ "body",
+ "headers",
+ "statusCode"
+ ],
+ "description": "This is a comment"
+ },
+ "RouteGetOutputError": {
+ "type": "object",
+ "properties": {
+ "statusCode": {
+ "type": "string",
+ "const": "400"
+ },
+ "headers": {
+ "type": "object",
+ "properties": {
+ "content-type": {
+ "type": "string",
+ "const": "application/json"
+ }
+ },
+ "required": [
+ "content-type"
+ ]
+ },
+ "body": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "examples": [
+ {
+ "message": "Bad Request"
+ }
+ ]
+ }
+ },
+ "required": [
+ "body",
+ "headers",
+ "statusCode"
+ ],
+ "description": "This is a comment on RouteGetOutputError"
+ },
+ "RoutePostInputJSONAlias": {
+ "type": "object",
+ "properties": {
+ "headers": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "content-type": {
+ "type": "string",
+ "const": "application/json"
+ }
+ },
+ "required": [
+ "content-type"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "content-type": {
+ "type": "string",
+ "const": "text/plain"
+ }
+ },
+ "required": [
+ "content-type"
+ ]
+ }
+ ]
+ },
+ "query": {
+ "type": "object",
+ "properties": {
+ "test": {
+ "type": "string",
+ "const": "boolean"
+ }
+ },
+ "required": [
+ "test"
+ ]
+ },
+ "body": {
+ "type": "object",
+ "properties": {
+ "cool": {
+ "type": "string",
+ "description": "This is a cool property",
+ "minLength": 5
+ }
+ },
+ "required": [
+ "cool"
+ ],
+ "description": "A cool json input",
+ "examples": [
+ {
+ "cool": "stuff"
+ }
+ ]
+ },
+ "pathParams": {
+ "type": "object",
+ "properties": {
+ "reallyCool": {
+ "type": "string",
+ "enum": [
+ "true",
+ "false"
+ ],
+ "description": "Very cool property that does a thing",
+ "examples": [
+ "true"
+ ],
+ "pattern": "^[a-z]+$"
+ },
+ "evenCooler": {
+ "type": "number",
+ "description": "Even cooler property"
+ }
+ },
+ "required": [
+ "reallyCool"
+ ]
+ }
+ },
+ "required": [
+ "body",
+ "headers",
+ "pathParams",
+ "query"
+ ]
+ }
+ },
+ "parameters": {}
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 52b298b..8971096 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -140,12 +140,18 @@ importers:
atlassian-openapi:
specifier: ^1.0.18
version: 1.0.18
+ glob:
+ specifier: ^10.3.10
+ version: 10.3.10
json-schema-traverse:
specifier: ^1.0.0
version: 1.0.0
lodash:
specifier: ^4.17.21
version: 4.17.21
+ openapi-diff:
+ specifier: ^0.23.6
+ version: 0.23.6(openapi-types@12.1.0)
openapi-types:
specifier: ^12.1.0
version: 12.1.0
@@ -164,6 +170,9 @@ importers:
tsutils:
specifier: ^3.21.0
version: 3.21.0(typescript@5.2.2)
+ yargs:
+ specifier: ^17.7.2
+ version: 17.7.2
devDependencies:
'@jest/globals':
specifier: ^29.5.0
@@ -180,6 +189,9 @@ importers:
'@types/node':
specifier: ^18.15.11
version: 18.15.11
+ '@types/yargs':
+ specifier: ^17.0.32
+ version: 17.0.32
eslint:
specifier: ^8.45.0
version: 8.45.0
@@ -271,6 +283,47 @@ packages:
lodash.clonedeep: 4.5.0
dev: false
+ /@apidevtools/json-schema-ref-parser@9.0.9:
+ resolution: {integrity: sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==}
+ dependencies:
+ '@jsdevtools/ono': 7.1.3
+ '@types/json-schema': 7.0.15
+ call-me-maybe: 1.0.2
+ js-yaml: 4.1.0
+ dev: false
+
+ /@apidevtools/json-schema-ref-parser@9.1.2:
+ resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==}
+ dependencies:
+ '@jsdevtools/ono': 7.1.3
+ '@types/json-schema': 7.0.15
+ call-me-maybe: 1.0.2
+ js-yaml: 4.1.0
+ dev: false
+
+ /@apidevtools/openapi-schemas@2.1.0:
+ resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /@apidevtools/swagger-methods@3.0.2:
+ resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==}
+ dev: false
+
+ /@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.0):
+ resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==}
+ peerDependencies:
+ openapi-types: '>=7'
+ dependencies:
+ '@apidevtools/json-schema-ref-parser': 9.1.2
+ '@apidevtools/openapi-schemas': 2.1.0
+ '@apidevtools/swagger-methods': 3.0.2
+ '@jsdevtools/ono': 7.1.3
+ call-me-maybe: 1.0.2
+ openapi-types: 12.1.0
+ z-schema: 5.0.5
+ dev: false
+
/@babel/code-frame@7.21.4:
resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==}
engines: {node: '>=6.9.0'}
@@ -1946,6 +1999,18 @@ packages:
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
dev: true
+ /@isaacs/cliui@8.0.2:
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: /string-width@4.2.3
+ strip-ansi: 7.0.1
+ strip-ansi-cjs: /strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: /wrap-ansi@7.0.0
+ dev: false
+
/@istanbuljs/load-nyc-config@1.1.0:
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
engines: {node: '>=8'}
@@ -2171,7 +2236,7 @@ packages:
'@types/istanbul-lib-coverage': 2.0.4
'@types/istanbul-reports': 3.0.1
'@types/node': 18.15.11
- '@types/yargs': 17.0.24
+ '@types/yargs': 17.0.32
chalk: 4.1.2
dev: true
@@ -2282,6 +2347,13 @@ packages:
detect-libc: 1.0.3
dev: true
+ /@pkgjs/parseargs@0.11.0:
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
/@sinclair/typebox@0.25.24:
resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==}
dev: true
@@ -2441,8 +2513,8 @@ packages:
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
dev: true
- /@types/yargs@17.0.24:
- resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==}
+ /@types/yargs@17.0.32:
+ resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==}
dependencies:
'@types/yargs-parser': 21.0.0
dev: true
@@ -2718,12 +2790,10 @@ packages:
/ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
- dev: true
/ansi-regex@6.0.1:
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
engines: {node: '>=12'}
- dev: true
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
@@ -2737,13 +2807,17 @@ packages:
engines: {node: '>=8'}
dependencies:
color-convert: 2.0.1
- dev: true
/ansi-styles@5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
dev: true
+ /ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+ dev: false
+
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -2810,6 +2884,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /assert-plus@1.0.0:
+ resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
+ engines: {node: '>=0.8'}
+ dev: false
+
/atlassian-openapi@1.0.18:
resolution: {integrity: sha512-IXgF/cYD8DW1mYB/ejDm/lKQMNXi2iCsxus2Y0ffZOxfa/SLoz0RuEZ4xu4suSRjtlda7qZDonQ6TAkQPVuQig==}
dependencies:
@@ -2822,6 +2901,14 @@ packages:
engines: {node: '>= 0.4'}
dev: true
+ /axios@0.24.0:
+ resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==}
+ dependencies:
+ follow-redirects: 1.15.3
+ transitivePeerDependencies:
+ - debug
+ dev: false
+
/babel-jest@29.5.0(@babel/core@7.21.4):
resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -3030,6 +3117,10 @@ packages:
get-intrinsic: 1.2.1
dev: true
+ /call-me-maybe@1.0.2:
+ resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
+ dev: false
+
/callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -3174,7 +3265,6 @@ packages:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
- dev: true
/clone@1.0.4:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
@@ -3205,7 +3295,6 @@ packages:
engines: {node: '>=7.0.0'}
dependencies:
color-name: 1.1.4
- dev: true
/color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
@@ -3213,7 +3302,6 @@ packages:
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
- dev: true
/commander@10.0.0:
resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==}
@@ -3224,6 +3312,23 @@ packages:
resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==}
engines: {node: '>=16'}
+ /commander@7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+ dev: false
+
+ /commander@8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+ dev: false
+
+ /commander@9.5.0:
+ resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
+ engines: {node: ^12.20.0 || >=14}
+ requiresBuild: true
+ dev: false
+ optional: true
+
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
@@ -3250,6 +3355,10 @@ packages:
browserslist: 4.21.5
dev: true
+ /core-util-is@1.0.2:
+ resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
+ dev: false
+
/cosmiconfig@8.0.0:
resolution: {integrity: sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==}
engines: {node: '>=14'}
@@ -3275,7 +3384,6 @@ packages:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
- dev: true
/csv-generate@3.4.3:
resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==}
@@ -3428,6 +3536,10 @@ packages:
- supports-color
dev: true
+ /eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ dev: false
+
/electron-to-chromium@1.4.380:
resolution: {integrity: sha512-XKGdI4pWM78eLH2cbXJHiBnWUwFSzZM7XujsB6stDiGu9AeSqziedP6amNLpJzE3i0rLTcfAwdCTs5ecP5yeSg==}
dev: true
@@ -3439,7 +3551,10 @@ packages:
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
- dev: true
+
+ /emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ dev: false
/enquirer@2.3.6:
resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
@@ -3555,7 +3670,6 @@ packages:
/escalade@3.1.1:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
engines: {node: '>=6'}
- dev: true
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
@@ -3821,6 +3935,11 @@ packages:
tmp: 0.0.33
dev: true
+ /extsprintf@1.4.1:
+ resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==}
+ engines: {'0': node >=0.6.0}
+ dev: false
+
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -3948,6 +4067,16 @@ packages:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
dev: true
+ /follow-redirects@1.15.3:
+ resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+ dev: false
+
/for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
dependencies:
@@ -3966,6 +4095,14 @@ packages:
for-in: 1.0.2
dev: true
+ /foreground-child@3.1.1:
+ resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
+ engines: {node: '>=14'}
+ dependencies:
+ cross-spawn: 7.0.3
+ signal-exit: 4.1.0
+ dev: false
+
/fs-extra@10.1.0:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'}
@@ -4039,7 +4176,6 @@ packages:
/get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
- dev: true
/get-intrinsic@1.2.1:
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
@@ -4081,6 +4217,18 @@ packages:
is-glob: 4.0.3
dev: true
+ /glob@10.3.10:
+ resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+ dependencies:
+ foreground-child: 3.1.1
+ jackspeak: 2.3.6
+ minimatch: 9.0.3
+ minipass: 7.0.4
+ path-scurry: 1.10.1
+ dev: false
+
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
dependencies:
@@ -4463,7 +4611,6 @@ packages:
/is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
- dev: true
/is-generator-fn@2.1.0:
resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==}
@@ -4622,7 +4769,6 @@ packages:
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
- dev: true
/isobject@3.0.1:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
@@ -4675,6 +4821,15 @@ packages:
istanbul-lib-report: 3.0.0
dev: true
+ /jackspeak@2.3.6:
+ resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+ dev: false
+
/jest-changed-files@29.5.0:
resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -5134,6 +5289,31 @@ packages:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
dev: true
+ /json-schema-diff@0.17.1:
+ resolution: {integrity: sha512-bBflcH+NRM/bKbw2G0WIh0ltCZb3PCyruTdopx3hZaXSHKM1+F7ILfDzyl9CxbLAS40/6EhkBYQUMFBefhBkgg==}
+ engines: {node: '>=6.14.1'}
+ hasBin: true
+ dependencies:
+ ajv: 8.12.0
+ commander: 7.2.0
+ json-schema-ref-parser: 9.0.9
+ json-schema-spec-types: 0.1.2
+ lodash: 4.17.21
+ verror: 1.10.1
+ dev: false
+
+ /json-schema-ref-parser@9.0.9:
+ resolution: {integrity: sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q==}
+ engines: {node: '>=10'}
+ deprecated: Please switch to @apidevtools/json-schema-ref-parser
+ dependencies:
+ '@apidevtools/json-schema-ref-parser': 9.0.9
+ dev: false
+
+ /json-schema-spec-types@0.1.2:
+ resolution: {integrity: sha512-MDl8fA8ONckmQOm2+eXKJaFJNvxk7eGin+XFofNjS3q3PRKSoEvgMVb0ehOpCAYkUiLoMiqdU7obV7AmzAmyLw==}
+ dev: false
+
/json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
dev: true
@@ -5172,6 +5352,11 @@ packages:
resolution: {integrity: sha512-890w2Pjtj0iswAxalRlt2kHthi6HKrXEfZcn+ZNZptv7F3rUGIeDuZo+C+h4vXBHLEsVjJrHeCm35nYeZLzSBQ==}
engines: {node: '>=10.0.0'}
+ /jsonpointer@4.1.0:
+ resolution: {integrity: sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/jsonpointer@5.0.1:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'}
@@ -5256,7 +5441,10 @@ packages:
/lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
- dev: true
+
+ /lodash.isequal@4.5.0:
+ resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
+ dev: false
/lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -5291,6 +5479,11 @@ packages:
tslib: 2.5.0
dev: true
+ /lru-cache@10.1.0:
+ resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==}
+ engines: {node: 14 || >=16.14}
+ dev: false
+
/lru-cache@4.1.5:
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
dependencies:
@@ -5414,6 +5607,13 @@ packages:
brace-expansion: 2.0.1
dev: false
+ /minimatch@9.0.3:
+ resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ brace-expansion: 2.0.1
+ dev: false
+
/minimist-options@4.1.0:
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
engines: {node: '>= 6'}
@@ -5427,6 +5627,11 @@ packages:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: true
+ /minipass@7.0.4:
+ resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dev: false
+
/mixme@0.5.9:
resolution: {integrity: sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==}
engines: {node: '>= 8.0.0'}
@@ -5592,10 +5797,35 @@ packages:
is-wsl: 2.2.0
dev: true
+ /openapi-diff@0.23.6(openapi-types@12.1.0):
+ resolution: {integrity: sha512-ukUueb7BnumShfwpuwYs0ncE/WWLggph1L6IN5En02sEkaN0RrUFp/a0WgE/NNRm5S2JX/dvemsdUNrKPqiw5Q==}
+ engines: {node: '>=6.11.4'}
+ hasBin: true
+ dependencies:
+ axios: 0.24.0
+ commander: 8.3.0
+ js-yaml: 4.1.0
+ json-schema-diff: 0.17.1
+ jsonpointer: 4.1.0
+ lodash: 4.17.21
+ openapi3-ts: 2.0.2
+ swagger-parser: 10.0.3(openapi-types@12.1.0)
+ verror: 1.10.1
+ transitivePeerDependencies:
+ - debug
+ - openapi-types
+ dev: false
+
/openapi-types@12.1.0:
resolution: {integrity: sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA==}
dev: false
+ /openapi3-ts@2.0.2:
+ resolution: {integrity: sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==}
+ dependencies:
+ yaml: 1.10.2
+ dev: false
+
/optionator@0.9.3:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
engines: {node: '>= 0.8.0'}
@@ -5768,7 +5998,6 @@ packages:
/path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
- dev: true
/path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -5786,6 +6015,14 @@ packages:
path-root-regex: 0.1.2
dev: true
+ /path-scurry@1.10.1:
+ resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ lru-cache: 10.1.0
+ minipass: 7.0.4
+ dev: false
+
/path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
@@ -6027,7 +6264,6 @@ packages:
/require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
- dev: true
/require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
@@ -6199,7 +6435,6 @@ packages:
engines: {node: '>=8'}
dependencies:
shebang-regex: 3.0.0
- dev: true
/shebang-regex@1.0.0:
resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
@@ -6209,7 +6444,6 @@ packages:
/shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
- dev: true
/side-channel@1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
@@ -6223,6 +6457,11 @@ packages:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true
+ /signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+ dev: false
+
/sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
dev: true
@@ -6337,7 +6576,15 @@ packages:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
- dev: true
+
+ /string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.0.1
+ dev: false
/string.prototype.trim@1.2.7:
resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==}
@@ -6375,14 +6622,12 @@ packages:
engines: {node: '>=8'}
dependencies:
ansi-regex: 5.0.1
- dev: true
/strip-ansi@7.0.1:
resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==}
engines: {node: '>=12'}
dependencies:
ansi-regex: 6.0.1
- dev: true
/strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
@@ -6437,6 +6682,15 @@ packages:
engines: {node: '>= 0.4'}
dev: true
+ /swagger-parser@10.0.3(openapi-types@12.1.0):
+ resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==}
+ engines: {node: '>=10'}
+ dependencies:
+ '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.0)
+ transitivePeerDependencies:
+ - openapi-types
+ dev: false
+
/syncpack@9.8.4:
resolution: {integrity: sha512-i81rO+dHuJ2dO8YQq6SCExcyN0x9ZVTY7cVPn8pWjS5Dml0A8uM0cOaneOludFesdrLXMZUA/uEWa74ddBgkPQ==}
engines: {node: '>=14'}
@@ -6859,6 +7113,20 @@ packages:
spdx-expression-parse: 3.0.1
dev: true
+ /validator@13.11.0:
+ resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==}
+ engines: {node: '>= 0.10'}
+ dev: false
+
+ /verror@1.10.1:
+ resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==}
+ engines: {node: '>=0.6.0'}
+ dependencies:
+ assert-plus: 1.0.0
+ core-util-is: 1.0.2
+ extsprintf: 1.4.1
+ dev: false
+
/walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
dependencies:
@@ -6917,7 +7185,6 @@ packages:
hasBin: true
dependencies:
isexe: 2.0.0
- dev: true
/wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
@@ -6939,7 +7206,15 @@ packages:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
- dev: true
+
+ /wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.0.1
+ dev: false
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
@@ -6963,7 +7238,6 @@ packages:
/y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
- dev: true
/yallist@2.1.2:
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
@@ -6977,6 +7251,11 @@ packages:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: true
+ /yaml@1.10.2:
+ resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
+ engines: {node: '>= 6'}
+ dev: false
+
/yaml@2.3.1:
resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==}
engines: {node: '>= 14'}
@@ -6993,7 +7272,6 @@ packages:
/yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
- dev: true
/yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
@@ -7023,7 +7301,6 @@ packages:
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
- dev: true
/yauzl@2.10.0:
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
@@ -7037,6 +7314,18 @@ packages:
engines: {node: '>=10'}
dev: true
+ /z-schema@5.0.5:
+ resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==}
+ engines: {node: '>=8.0.0'}
+ hasBin: true
+ dependencies:
+ lodash.get: 4.4.2
+ lodash.isequal: 4.5.0
+ validator: 13.11.0
+ optionalDependencies:
+ commander: 9.5.0
+ dev: false
+
/zod@3.20.6:
resolution: {integrity: sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==}
dev: true
From 5bcacad69af80b04c206517d7ae2ebfe1d6624a6 Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Mon, 11 Dec 2023 16:17:53 -0800
Subject: [PATCH 07/16] docs(changeset): OpenAPI 3 spec generation
---
.changeset/witty-teachers-smoke.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/witty-teachers-smoke.md
diff --git a/.changeset/witty-teachers-smoke.md b/.changeset/witty-teachers-smoke.md
new file mode 100644
index 0000000..748dff0
--- /dev/null
+++ b/.changeset/witty-teachers-smoke.md
@@ -0,0 +1,5 @@
+---
+"@nornir/rest": major
+---
+
+OpenAPI 3 spec generation
From 71de82fac3e66515545a2b4fbafdbdf8b85ba6d7 Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Mon, 11 Dec 2023 16:26:40 -0800
Subject: [PATCH 08/16] fix merge conflicts
---
packages/rest/src/runtime/parse.mts | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/packages/rest/src/runtime/parse.mts b/packages/rest/src/runtime/parse.mts
index de0c646..19667bf 100644
--- a/packages/rest/src/runtime/parse.mts
+++ b/packages/rest/src/runtime/parse.mts
@@ -1,9 +1,8 @@
-import {HttpEvent, HttpResponse, HttpStatusCode, MimeType, UnparsedHttpEvent} from "./http-event.mjs";
+import {HttpEvent, HttpResponse, MimeType, UnparsedHttpEvent} from "./http-event.mjs";
import querystring from "node:querystring";
import {getContentType} from "./utils.mjs";
import {NornirRestError} from "./error.mjs";
-import {AttachmentRegistry} from "@nornir/core";
export type HttpBodyParser = (body: Buffer) => unknown
@@ -17,9 +16,9 @@ export class NornirRestParseError extends NornirRestError {
public toHttpResponse(): HttpResponse {
return {
- statusCode: HttpStatusCode.UnprocessableEntity,
+ statusCode: "422",
headers: {
- "content-type": MimeType.TextPlain,
+ "content-type": "text/plain",
},
body: this.message
}
From 2c92f15dfda3de502a4c42fcbb62a8a13e38e57a Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Tue, 12 Dec 2023 14:20:10 -0800
Subject: [PATCH 09/16] fixes for spec generation
---
packages/rest/__tests__/src/routing.spec.mts | 45 ++++----
packages/rest/src/cli/cli.ts | 2 +-
packages/rest/src/cli/lib/ts-utils.ts | 1 -
packages/rest/src/runtime/decorators.mts | 1 -
packages/rest/src/runtime/http-event.mts | 106 +-----------------
packages/rest/src/runtime/route-holder.mts | 2 +-
packages/rest/src/runtime/router.mts | 10 +-
.../rest/src/transform/controller-meta.ts | 43 +++----
.../rest/src/transform/json-schema-utils.ts | 14 +--
packages/rest/src/transform/project.ts | 2 +-
packages/rest/src/transform/transform.ts | 43 +++++--
.../controller-method-transformer.ts | 11 +-
.../transformers/file-transformer.ts | 1 -
.../processors/chain-route-processor.ts | 12 +-
packages/test/__tests__/tsconfig.json | 2 +-
packages/test/src/controller.ts | 29 ++---
packages/test/src/rest.ts | 6 +-
17 files changed, 115 insertions(+), 215 deletions(-)
diff --git a/packages/rest/__tests__/src/routing.spec.mts b/packages/rest/__tests__/src/routing.spec.mts
index d5addc6..42be9a8 100644
--- a/packages/rest/__tests__/src/routing.spec.mts
+++ b/packages/rest/__tests__/src/routing.spec.mts
@@ -1,11 +1,9 @@
import {
- AnyMimeType,
Controller,
GetChain,
HttpEvent,
HttpRequest,
- HttpStatusCode,
- MimeType,
+ HttpRequestEmpty,
normalizeEventHeaders,
NornirRestRequestValidationError,
PostChain,
@@ -15,13 +13,13 @@ import {nornir, Nornir} from "@nornir/core";
import {describe} from "@jest/globals";
import {NornirRouteNotFoundError} from "../../dist/runtime/router.mjs";
-interface RouteGetInput extends HttpRequest {
+interface RouteGetInput extends HttpRequestEmpty {
}
interface RoutePostInputJSON extends HttpRequest {
headers: {
- "content-type": MimeType.ApplicationJson;
+ "content-type": "application/json";
};
body: RoutePostBodyInput;
query: {
@@ -31,7 +29,7 @@ interface RoutePostInputJSON extends HttpRequest {
interface RoutePostInputCSV extends HttpRequest {
headers: {
- "content-type": MimeType.TextCsv;
+ "content-type": "text/csv";
/**
* This is a CSV header
* @example "cool,cool2"
@@ -84,24 +82,24 @@ class TestController {
return chain
.use(console.log)
.use(() => ({
- statusCode: HttpStatusCode.Ok,
+ statusCode: "200",
body: `cool`,
headers: {
- "content-type": MimeType.TextPlain,
+ "content-type": "text/plain"
},
- }));
+ } as const));
}
@GetChain("/route2")
public getEmptyRoute(chain: Nornir) {
return chain
- .use(() => ({
- statusCode: HttpStatusCode.Ok,
- body: undefined,
- headers: {
- "content-type": AnyMimeType
- },
- }));
+ .use(() => {
+ return {
+ statusCode: "200" as const,
+ body: undefined,
+ headers: {},
+ }
+ });
}
@PostChain("/route")
@@ -109,12 +107,12 @@ class TestController {
return chain
.use(input => input.headers["content-type"])
.use(contentType => ({
- statusCode: HttpStatusCode.Ok,
+ statusCode: "200",
body: `Content-Type: ${contentType}`,
headers: {
- "content-type": MimeType.TextPlain,
+ "content-type": "text/plain"
},
- }));
+ } as const));
}
}
@@ -133,7 +131,7 @@ describe("REST tests", () => {
headers: {},
query: {}
});
- expect(response.statusCode).toEqual(HttpStatusCode.Ok);
+ expect(response.statusCode).toEqual("200");
expect(response.body).toBe("cool");
expect(response.headers["content-type"]).toBe("text/plain");
})
@@ -153,7 +151,7 @@ describe("REST tests", () => {
}
});
expect(response).toEqual({
- statusCode: HttpStatusCode.Ok,
+ statusCode: "200",
body: "Content-Type: application/json",
headers: {
"content-type": "text/plain"
@@ -169,10 +167,9 @@ describe("REST tests", () => {
query: {}
});
expect(response).toEqual({
- statusCode: HttpStatusCode.Ok,
+ statusCode: "200",
body: undefined,
headers: {
- "content-type": AnyMimeType
}
})
})
@@ -195,7 +192,7 @@ describe("REST tests", () => {
method: "POST",
path: "/basepath/route",
headers: {
- "content-type": MimeType.ApplicationJson,
+ "content-type": "application/json",
},
body: {
cool: "cool"
diff --git a/packages/rest/src/cli/cli.ts b/packages/rest/src/cli/cli.ts
index a43130a..392e937 100644
--- a/packages/rest/src/cli/cli.ts
+++ b/packages/rest/src/cli/cli.ts
@@ -3,7 +3,7 @@ import { merge } from "lodash";
import path from "path";
import yargs from "yargs";
import { isErrorResult } from "../transform/openapi-merge";
-import { getMergedSpec, getSpecFiles } from "./lib/collect";
+import { getMergedSpec } from "./lib/collect";
import { resolveTsConfigOutdir } from "./lib/ts-utils";
yargs
diff --git a/packages/rest/src/cli/lib/ts-utils.ts b/packages/rest/src/cli/lib/ts-utils.ts
index 74b1321..f743775 100644
--- a/packages/rest/src/cli/lib/ts-utils.ts
+++ b/packages/rest/src/cli/lib/ts-utils.ts
@@ -1,4 +1,3 @@
-import fs from "node:fs";
import { dirname, resolve } from "node:path";
import ts from "typescript";
diff --git a/packages/rest/src/runtime/decorators.mts b/packages/rest/src/runtime/decorators.mts
index 01b9525..4126b5f 100644
--- a/packages/rest/src/runtime/decorators.mts
+++ b/packages/rest/src/runtime/decorators.mts
@@ -96,6 +96,5 @@ export function Provider() {
}
}
-type Exact = IfEquals;
type IfEquals = (() => G extends T ? 1 : 2) extends (() => G extends U ? 1 : 2) ? Y
: N;
diff --git a/packages/rest/src/runtime/http-event.mts b/packages/rest/src/runtime/http-event.mts
index 5bde8d7..e907e10 100644
--- a/packages/rest/src/runtime/http-event.mts
+++ b/packages/rest/src/runtime/http-event.mts
@@ -1,5 +1,3 @@
-import {Nominal} from "./utils.mjs";
-
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
export type HttpEvent = Omit & {
@@ -22,10 +20,10 @@ export type HttpHeadersWithoutContentType = {
} & HttpHeaders
-export type HttpHeaders = Record;
+export type HttpHeaders = Record;
export interface HttpRequest {
- readonly headers: HttpHeadersWithContentType | HttpHeadersWithoutContentType;
+ readonly headers: HttpHeadersWithoutContentType | HttpHeadersWithContentType;
readonly query: QueryParams;
@@ -39,7 +37,8 @@ type QueryParams = Record | Ar
type PathParams = Record;
export interface HttpRequestEmpty extends HttpRequest {
- body?: undefined;
+ headers: HttpHeadersWithoutContentType
+ body?: undefined
}
export interface HttpRequestJSON extends HttpRequest {
@@ -60,70 +59,9 @@ export interface SerializedHttpResponse extends Omit {
}
export interface HttpResponseEmpty extends HttpResponse {
- body?: undefined;
+ readonly body?: undefined
}
-// export enum HttpStatusCode {
-// Continue = "100",
-// SwitchingProtocols = "101",
-// Processing = "102",
-// Ok = "200",
-// Created = "201",
-// Accepted = "202",
-// NonAuthoritativeInformation = "203",
-// NoContent = "204",
-// ResetContent = "205",
-// PartialContent = "206",
-// MultiStatus = "207",
-// AlreadyReported = "208",
-// IMUsed = "226",
-// MultipleChoices = "300",
-// MovedPermanently = "301",
-// Found = "302",
-// SeeOther = "303",
-// NotModified = "304",
-// UseProxy = "305",
-// TemporaryRedirect = "307",
-// PermanentRedirect = "308",
-// BadRequest = "400",
-// Unauthorized = "401",
-// PaymentRequired = "402",
-// Forbidden = "403",
-// NotFound = "404",
-// MethodNotAllowed = "405",
-// NotAcceptable = "406",
-// ProxyAuthenticationRequired = "407",
-// RequestTimeout = "408",
-// Conflict = "409",
-// Gone = "410",
-// LengthRequired = "411",
-// PreconditionFailed = "412",
-// PayloadTooLarge = "413",
-// RequestURITooLong = "414",
-// UnsupportedMediaType = "415",
-// RequestedRangeNotSatisfiable = "416",
-// ExpectationFailed = "417",
-// ImATeapot = "418",
-// MisdirectedRequest = "421",
-// UnprocessableEntity = "422",
-// Locked = "423",
-// FailedDependency = "424",
-// UpgradeRequired = "426",
-// PreconditionRequired = "428",
-// TooManyRequests = "429",
-// RequestHeaderFieldsTooLarge = "431",
-// UnavailableForLegalReasons = "451",
-// InternalServerError = "500",
-// NotImplemented = "501",
-// BadGateway = "502",
-// ServiceUnavailable = "503",
-// GatewayTimeout = "504",
-// HTTPVersionNotSupported = "505",
-// VariantAlsoNegotiates = "506",
-// InsufficientStorage = "507",
-// LoopDetected = "508",
-// NotExtended = "510",
-// }
export type HttpStatusCode =
| "100"
@@ -186,40 +124,6 @@ export type HttpStatusCode =
| "508"
| "510";
-
-// export enum MimeType {
-// Any = "*/*",
-// ApplicationJson = "application/json",
-// ApplicationOctetStream = "application/octet-stream",
-// ApplicationPdf = "application/pdf",
-// ApplicationXWwwFormUrlencoded = "application/x-www-form-urlencoded",
-// ApplicationZip = "application/zip",
-// ApplicationGzip = "application/gzip",
-// ApplicationBzip = "application/bzip",
-// ApplicationBzip2 = "application/bzip2",
-// ApplicationLdJson = "application/ld+json",
-// FontWoff = "font/woff",
-// FontWoff2 = "font/woff2",
-// FontTtf = "font/ttf",
-// FontOtf = "font/otf",
-// AudioMpeg = "audio/mpeg",
-// AudioXWav = "audio/x-wav",
-// ImageGif = "image/gif",
-// ImageJpeg = "image/jpeg",
-// ImagePng = "image/png",
-// MultipartFormData = "multipart/form-data",
-// TextCss = "text/css",
-// TextCsv = "text/csv",
-// TextHtml = "text/html",
-// TextPlain = "text/plain",
-// TextXml = "text/xml",
-// VideoMpeg = "video/mpeg",
-// VideoMp4 = "video/mp4",
-// VideoQuicktime = "video/quicktime",
-// VideoXMsVideo = "video/x-msvideo",
-// VideoXFlv = "video/x-flv",
-// VideoWebm = "video/webm",
-// }
export type MimeType =
| "*/*"
| "application/json"
diff --git a/packages/rest/src/runtime/route-holder.mts b/packages/rest/src/runtime/route-holder.mts
index 1a01cb5..a46806d 100644
--- a/packages/rest/src/runtime/route-holder.mts
+++ b/packages/rest/src/runtime/route-holder.mts
@@ -1,4 +1,4 @@
-import {HttpMethod, HttpRequest, HttpResponse, HttpStatusCode, MimeType} from './http-event.mjs';
+import {HttpMethod, HttpRequest, HttpResponse} from './http-event.mjs';
import {Nornir} from '@nornir/core';
import {NornirRestRequestError} from './error.mjs';
import {type ErrorObject, type ValidateFunction} from 'ajv'
diff --git a/packages/rest/src/runtime/router.mts b/packages/rest/src/runtime/router.mts
index e3ea5f4..2a3a30c 100644
--- a/packages/rest/src/runtime/router.mts
+++ b/packages/rest/src/runtime/router.mts
@@ -1,14 +1,6 @@
import Trouter from 'trouter';
import {RouteBuilder, RouteHolder} from './route-holder.mjs';
-import {
- HttpEvent,
- HttpHeadersWithContentType,
- HttpMethod,
- HttpRequest,
- HttpResponse,
- HttpStatusCode,
- MimeType
-} from './http-event.mjs';
+import {HttpEvent, HttpHeadersWithContentType, HttpMethod, HttpRequest, HttpResponse} from './http-event.mjs';
import {AttachmentRegistry, Nornir, Result} from '@nornir/core';
import {NornirRestRequestError} from "./error.mjs";
diff --git a/packages/rest/src/transform/controller-meta.ts b/packages/rest/src/transform/controller-meta.ts
index 3306fd4..d756dbf 100644
--- a/packages/rest/src/transform/controller-meta.ts
+++ b/packages/rest/src/transform/controller-meta.ts
@@ -7,7 +7,6 @@ import {
getFirstExample,
getSchemaOrAllOf,
getUnifiedPropertySchemas,
- isSchemaEmpty,
joinSchemas,
moveRefsToAllOf,
resolveDiscriminantProperty,
@@ -34,6 +33,16 @@ export abstract class OpenApiSpecHolder {
dispute: {},
})) as MergeInput;
+ if (mergeInputs.length === 0) {
+ return {
+ openapi: "3.0.3",
+ info: {
+ title: "Nornir API",
+ version: "1.0.0",
+ },
+ components: {},
+ };
+ }
const merged = merge(mergeInputs);
if (isErrorResult(merged)) {
throw new Error(merged.message);
@@ -52,18 +61,6 @@ export class ControllerMeta {
public readonly initializationStatements: ts.Statement[] = [];
private instanceProviderExpression: ts.Expression;
- // public static getRoutes(): RouteInfo[] {
- // const methods = ControllerMeta.routes.values();
- // const routes = Array.from(methods).map((method) => Array.from(method.values()));
- // return routes.flat();
- // }
-
- // public static getAndClearRoutes(): RouteInfo[] {
- // const routes = this.getRoutes();
- // this.routes.clear();
- // return routes;
- // }
-
public static clearCache() {
this.cache.clear();
}
@@ -95,14 +92,6 @@ export class ControllerMeta {
return this.cache.get(name);
}
- public static getAssert(route: ts.ClassDeclaration): ControllerMeta {
- const meta = this.get(route);
- if (!meta) {
- throw new Error("Route not found: " + route.getText());
- }
- return meta;
- }
-
private constructor(
private readonly project: Project,
public readonly source: ts.SourceFile,
@@ -337,10 +326,12 @@ export class ControllerMeta {
const content = contentTypeDiscriminatedSchemas == null ? undefined : Object.fromEntries(
Object.entries(contentTypeDiscriminatedSchemas).map(([contentType, schema]) => {
const branchBodySchemaSet = getUnifiedPropertySchemas(schema, "/")["body"];
- const branchBodySchema = rewriteRefsForOpenApi(
- unresolveRefs(joinSchemas(branchBodySchemaSet.schemaSet)),
- );
- const example = getFirstExample(branchBodySchema);
+ const branchBodySchema = branchBodySchemaSet != null
+ ? rewriteRefsForOpenApi(
+ unresolveRefs(joinSchemas(branchBodySchemaSet.schemaSet)),
+ )
+ : undefined;
+ const example = branchBodySchema != null ? getFirstExample(branchBodySchema) : undefined;
return [
contentType,
{
@@ -364,7 +355,7 @@ export class ControllerMeta {
private generateRequestBody(routeIndex: RouteIndex, inputSchema: JSONSchema7): OpenAPIV3_1.RequestBodyObject | void {
const bodySchema = getUnifiedPropertySchemas(inputSchema, "/")["body"];
const contentTypeDiscriminatedSchemas = resolveDiscriminantProperty(inputSchema, "/headers/content-type");
- if (bodySchema == null) {
+ if (bodySchema == null || (bodySchema.schemaSet.length === 1)) {
return;
}
diff --git a/packages/rest/src/transform/json-schema-utils.ts b/packages/rest/src/transform/json-schema-utils.ts
index e6b5a64..bdbaabc 100644
--- a/packages/rest/src/transform/json-schema-utils.ts
+++ b/packages/rest/src/transform/json-schema-utils.ts
@@ -4,7 +4,6 @@ import dereference from "@apidevtools/json-schema-ref-parser/dist/lib/dereferenc
import { JSONSchema7 } from "json-schema";
import traverse from "json-schema-traverse";
import { cloneDeep } from "lodash";
-import { OpenAPIV3_1 } from "openapi-types";
const refParser = new $RefParser();
@@ -36,11 +35,9 @@ export function joinSchemas(schemaSet: JSONSchema7[]): JSONSchema7 {
return schemaSet[0];
}
- const joinedSchema: JSONSchema7 = {
+ return {
allOf: schemaSet,
};
-
- return joinedSchema;
}
export function getUnifiedPropertySchemas(
@@ -94,7 +91,7 @@ export function getUnifiedPropertySchemas(
traverse(schema, {
allKeys: false,
cb: {
- pre(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) {
+ pre(schema, jsonPtr, rootSchema, parentJsonPtr) {
const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr);
if (convertedPath === parentPath && parentJsonPtr != "") {
parentSchemas++;
@@ -103,8 +100,6 @@ export function getUnifiedPropertySchemas(
post: (schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) => {
const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr);
- const convertedParentPath = convertJsonSchemaPathIfPropertyPath(parentJsonPtr || "/");
-
if (convertedPath != null && isDirectChildPath(convertedPath, parentPath)) {
const schemaSet = schemas[keyIndex || ""] || {
schemaSet: [],
@@ -146,7 +141,7 @@ export function unresolveRefs(schema: JSONSchema7) {
}
const MOVED_REF_MARKER = Symbol("moved-ref-marker");
-export function moveRefsToAllOf(schema: JSONSchema7, markMovedRef = true) {
+export function moveRefsToAllOf(schema: JSONSchema7) {
const clonedSchema = cloneDeep(schema);
traverse(clonedSchema, {
cb: {
@@ -286,9 +281,6 @@ export function isSchemaEmpty(schema: JSONSchema7): boolean {
if (key.startsWith("x-") || key.startsWith("$")) {
continue;
}
- if (key === "not" && isSchemaEmpty(schema.not as JSONSchema7)) {
- continue;
- }
return false;
}
return true;
diff --git a/packages/rest/src/transform/project.ts b/packages/rest/src/transform/project.ts
index b474c93..db45c26 100644
--- a/packages/rest/src/transform/project.ts
+++ b/packages/rest/src/transform/project.ts
@@ -41,7 +41,7 @@ export const SCHEMA_DEFAULTS: Config = {
jsDoc: "extended",
sortProps: true,
strictTuples: false,
- encodeRefs: true,
+ encodeRefs: false,
additionalProperties: true,
topRef: false,
discriminatorType: "open-api",
diff --git a/packages/rest/src/transform/transform.ts b/packages/rest/src/transform/transform.ts
index 4a3a7b9..db72ef2 100644
--- a/packages/rest/src/transform/transform.ts
+++ b/packages/rest/src/transform/transform.ts
@@ -1,38 +1,59 @@
import {
BaseType,
- Context,
createFormatter,
createParser,
- NeverType,
- ReferenceType,
+ Definition,
SchemaGenerator,
StringType,
SubNodeParser,
+ SubTypeFormatter,
+ UndefinedType,
} from "ts-json-schema-generator";
import ts from "typescript";
import { AJV_DEFAULTS, AJVOptions, Options, Project, SCHEMA_DEFAULTS, SchemaConfig } from "./project.js";
import { FileTransformer } from "./transformers/file-transformer.js";
let project: Project;
-// let files: string[] = [];
-// let openapi: OpenAPIV3.Document;
export class TemplateExpressionNodeParser implements SubNodeParser {
supportsNode(node: ts.Node): boolean {
return ts.isTemplateExpression(node);
}
- createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType {
+ 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;
+ }
+
+ getChildren(): BaseType[] {
+ return [];
+ }
+
+ getDefinition(): Definition {
+ return {};
+ }
+}
+
export function transform(program: ts.Program, options?: Options): ts.TransformerFactory {
const {
loopEnum,
loopRequired,
additionalProperties,
- encodeRefs,
strictTuples,
jsDoc,
removeAdditional,
@@ -46,7 +67,6 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme
...SCHEMA_DEFAULTS,
jsDoc: jsDoc || SCHEMA_DEFAULTS.jsDoc,
strictTuples: strictTuples || SCHEMA_DEFAULTS.strictTuples,
- encodeRefs: false,
additionalProperties: additionalProperties || SCHEMA_DEFAULTS.additionalProperties,
sortProps: sortProps || SCHEMA_DEFAULTS.sortProps,
expose,
@@ -66,9 +86,12 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme
}, (prs) => {
// prs.addNodeParser(new NornirIgnoreParser());
prs.addNodeParser(new TemplateExpressionNodeParser());
+ prs.addNodeParser(new UndefinedIdentifierParser());
});
const typeFormatter = createFormatter({
...schemaConfig,
+ }, frm => {
+ frm.addTypeFormatter(new UndefinedFormatter());
});
const schemaGenerator = new SchemaGenerator(
@@ -82,6 +105,8 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme
? ts.createIncrementalCompilerHost(program.getCompilerOptions())
: ts.createCompilerHost(program.getCompilerOptions());
+ const configPath = program.getCompilerOptions().configFilePath as string;
+
const parseConfigHost: ts.ParseConfigFileHost = {
useCaseSensitiveFileNames: true,
fileExists: fileName => compilerHost!.fileExists(fileName),
@@ -110,7 +135,7 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme
typeFormatter,
compilerHost,
parsedCommandLine: ts.getParsedCommandLineOfConfigFile(
- ts.findConfigFile(process.cwd(), ts.sys.fileExists, "tsconfig.json") as string,
+ configPath,
program.getCompilerOptions(),
parseConfigHost,
)!,
diff --git a/packages/rest/src/transform/transformers/controller-method-transformer.ts b/packages/rest/src/transform/transformers/controller-method-transformer.ts
index 47c7bec..16f993b 100644
--- a/packages/rest/src/transform/transformers/controller-method-transformer.ts
+++ b/packages/rest/src/transform/transformers/controller-method-transformer.ts
@@ -1,5 +1,6 @@
import ts from "typescript";
import { ControllerMeta } from "../controller-meta";
+import { TransformationError } from "../error";
import { NornirDecoratorInfo, separateNornirDecorators } from "../lib";
import { Project } from "../project";
import { ChainMethodDecoratorTypes, ChainRouteProcessor } from "./processors/chain-route-processor";
@@ -28,7 +29,15 @@ export abstract class ControllerMethodTransformer {
if (!method) return node;
- return METHOD_DECORATOR_PROCESSORS[method](methodDecorator, project, source, node, controller);
+ try {
+ return METHOD_DECORATOR_PROCESSORS[method](methodDecorator, project, source, node, controller);
+ } catch (e) {
+ console.error(e);
+ if (e instanceof TransformationError) {
+ throw e;
+ }
+ return node;
+ }
}
public static transformControllerMethods(
diff --git a/packages/rest/src/transform/transformers/file-transformer.ts b/packages/rest/src/transform/transformers/file-transformer.ts
index 8a0cbd1..fe70cb6 100644
--- a/packages/rest/src/transform/transformers/file-transformer.ts
+++ b/packages/rest/src/transform/transformers/file-transformer.ts
@@ -1,5 +1,4 @@
import { rmSync } from "fs";
-import { parseJsDocOfNode } from "tsutils";
import ts from "typescript";
import { ControllerMeta, OpenApiSpecHolder } from "../controller-meta";
import { TransformationError } from "../error";
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 f42864b..89911dd 100644
--- a/packages/rest/src/transform/transformers/processors/chain-route-processor.ts
+++ b/packages/rest/src/transform/transformers/processors/chain-route-processor.ts
@@ -1,6 +1,5 @@
import { schemaToValidator } from "@nrfcloud/ts-json-schema-transformer/utils";
import tsp from "ts-morph";
-import { isTypeReference } from "tsutils";
import ts from "typescript";
import { ControllerMeta } from "../../controller-meta";
import { moveRefsToAllOf } from "../../json-schema-utils";
@@ -48,7 +47,7 @@ export abstract class ChainRouteProcessor {
// const wrappedNode = createWrappedNode(node, { typeChecker: project.checker }) as MethodDeclaration;
- const { typeNode: inputTypeNode, type: inputType } = ChainRouteProcessor.resolveInputType(project, node);
+ const { typeNode: inputTypeNode } = ChainRouteProcessor.resolveInputType(project, node);
const outputType = ChainRouteProcessor.resolveOutputType(project, node);
@@ -56,7 +55,7 @@ export abstract class ChainRouteProcessor {
const inputSchema = project.schemaGenerator.createSchemaFromNodes([inputTypeNode]);
- const inputValidator = schemaToValidator(moveRefsToAllOf(inputSchema, false), project.options.validation);
+ const inputValidator = schemaToValidator(moveRefsToAllOf(inputSchema), project.options.validation);
const parsedDocComments = ChainRouteProcessor.parseJSDoc(project, node);
@@ -103,9 +102,12 @@ export abstract class ChainRouteProcessor {
return recreatedNode;
}
- private static parseJSDoc(project: Project, method: ts.MethodDeclaration): RouteTags {
+ private static parseJSDoc(_project: Project, method: ts.MethodDeclaration): RouteTags {
const docs = ts.getJSDocCommentsAndTags(ts.getOriginalNode(method));
const topLevel = docs[0];
+ if (!topLevel) {
+ return {};
+ }
const description = ts.getTextOfJSDocComment(topLevel.comment);
if (!ts.isJSDoc(topLevel)) {
return {};
@@ -168,7 +170,7 @@ export abstract class ChainRouteProcessor {
return path;
}
- private static getMethod(project: Project, methodDecorator: NornirDecoratorInfo): string {
+ private static getMethod(_project: Project, methodDecorator: NornirDecoratorInfo): string {
const name = methodDecorator.symbol.name;
return ChainMethodDecoratorTypeMap[name as keyof typeof ChainMethodDecoratorTypeMap];
}
diff --git a/packages/test/__tests__/tsconfig.json b/packages/test/__tests__/tsconfig.json
index 6aefe25..bda034e 100644
--- a/packages/test/__tests__/tsconfig.json
+++ b/packages/test/__tests__/tsconfig.json
@@ -2,7 +2,7 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"pretty": true,
-
+ "incremental": false,
"baseUrl": ".",
"outDir": "dist",
"rootDir": "src",
diff --git a/packages/test/src/controller.ts b/packages/test/src/controller.ts
index 9997e5c..51c383d 100644
--- a/packages/test/src/controller.ts
+++ b/packages/test/src/controller.ts
@@ -1,14 +1,5 @@
import { Nornir } from "@nornir/core";
-import {
- Controller,
- GetChain,
- type HttpRequest,
- HttpRequestEmpty,
- HttpResponse,
- HttpStatusCode,
- MimeType,
- PostChain,
-} from "@nornir/rest";
+import { Controller, GetChain, type HttpRequest, HttpRequestEmpty, HttpResponse, PostChain } from "@nornir/rest";
import { assertValid } from "@nrfcloud/ts-json-schema-transformer";
interface RouteGetInput extends HttpRequestEmpty {
@@ -97,12 +88,13 @@ export interface RouteGetOutputSuccess extends HttpResponse {
*/
export interface RouteGetOutputError extends HttpResponse {
statusCode: "400";
- /**
- * @example { "message": "Bad Request"}
- */
- body: {
- message: string;
- };
+ // /**
+ // * @example { "message": "Bad Request"}
+ // */
+ // body: {
+ // message: string;
+ // };
+ body: undefined;
headers: {
"content-type": "application/json";
};
@@ -166,7 +158,7 @@ export class TestController {
return input;
})
.use(input => input.headers?.toString())
- .use(contentType => ({
+ .use(_contentType => ({
statusCode: "200" as const,
body: {
bleep: "bloop",
@@ -188,8 +180,9 @@ export class TestController {
@PostChain("/route/{cool}")
public postRoute(chain: Nornir) {
return chain
- .use(contentType => ({
+ .use(_contentType => ({
statusCode: "200" as const,
+ body: undefined,
// body: `Content-Type: ${contentType}`,
headers: {},
}));
diff --git a/packages/test/src/rest.ts b/packages/test/src/rest.ts
index a3c8f82..58c1906 100644
--- a/packages/test/src/rest.ts
+++ b/packages/test/src/rest.ts
@@ -4,9 +4,7 @@ import {
httpErrorHandler,
httpEventParser,
httpResponseSerializer,
- HttpStatusCode,
mapErrorClass,
- MimeType,
normalizeEventHeaders,
router,
startLocalServer,
@@ -32,11 +30,11 @@ export class TestError implements NodeJS.ErrnoException {
const frameworkChain = nornir()
.use(normalizeEventHeaders)
.use(httpEventParser({
- "text/csv": body => ({ cool: "stuff" }),
+ "text/csv": _body => ({ cool: "stuff" }),
}))
.use(router())
.useResult(httpErrorHandler([
- mapErrorClass(TestError, (err) => ({
+ mapErrorClass(TestError, (_err) => ({
statusCode: "500",
headers: {},
})),
From 8771d52741ea4b3405012bae97274933bfc50385 Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Fri, 15 Dec 2023 12:32:33 -0800
Subject: [PATCH 10/16] update to typescript 5.3
---
package.json | 2 +-
packages/core/package.json | 4 +-
packages/rest/package.json | 8 +-
packages/test/package.json | 4 +-
pnpm-lock.yaml | 382 +++++++++++++++++++++++++++++--------
5 files changed, 315 insertions(+), 85 deletions(-)
diff --git a/package.json b/package.json
index f3abbe8..ee86fea 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
"syncpack": "^9.8.4",
"ts-patch": "^3.1.1",
"turbo": "^1.9.2",
- "typescript": "^5.2.2"
+ "typescript": "^5.3.3"
},
"engines": {
"node": ">=18.0.0",
diff --git a/packages/core/package.json b/packages/core/package.json
index e01b08d..0521159 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -5,14 +5,14 @@
"author": "John Conley",
"devDependencies": {
"@jest/globals": "^29.5.0",
- "@nrfcloud/ts-json-schema-transformer": "^1.2.5",
+ "@nrfcloud/ts-json-schema-transformer": "^1.3.0",
"@types/jest": "^29.4.0",
"@types/node": "^18.15.11",
"esbuild": "^0.17.18",
"eslint": "^8.45.0",
"jest": "^29.5.0",
"ts-patch": "^3.1.1",
- "typescript": "^5.2.2"
+ "typescript": "^5.3.3"
},
"engines": {
"node": ">=18.0.0",
diff --git a/packages/rest/package.json b/packages/rest/package.json
index 1da6198..4650620 100644
--- a/packages/rest/package.json
+++ b/packages/rest/package.json
@@ -8,7 +8,7 @@
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.1.0",
"@nornir/core": "workspace:^",
- "@nrfcloud/ts-json-schema-transformer": "^1.2.5",
+ "@nrfcloud/ts-json-schema-transformer": "^1.3.0",
"@types/aws-lambda": "^8.10.115",
"ajv": "^8.12.0",
"atlassian-openapi": "^1.0.18",
@@ -20,7 +20,7 @@
"trouter": "^3.2.1",
"ts-is-present": "^1.2.2",
"ts-json-schema-generator": "^1.5.0",
- "ts-morph": "^20.0.0",
+ "ts-morph": "^21.0.1",
"tsutils": "^3.21.0",
"yargs": "^17.7.2"
},
@@ -34,7 +34,7 @@
"eslint": "^8.45.0",
"jest": "^29.5.0",
"ts-patch": "^3.1.1",
- "typescript": "^5.2.2"
+ "typescript": "^5.3.3"
},
"engines": {
"node": ">=18.0.0",
@@ -84,6 +84,8 @@
"test": "pnpm tests"
},
"tsp": {
+ "name": "@nornir/rest",
+ "transform": "./dist/transform/transform.js",
"tscOptions": {
"parseAllJsDoc": true
}
diff --git a/packages/test/package.json b/packages/test/package.json
index 780173e..509b568 100644
--- a/packages/test/package.json
+++ b/packages/test/package.json
@@ -8,7 +8,7 @@
},
"devDependencies": {
"@jest/globals": "^29.5.0",
- "@nrfcloud/ts-json-schema-transformer": "^1.2.5",
+ "@nrfcloud/ts-json-schema-transformer": "^1.3.0",
"@types/aws-lambda": "^8.10.115",
"@types/jest": "^29.4.0",
"@types/node": "^18.15.11",
@@ -16,7 +16,7 @@
"eslint": "^8.45.0",
"jest": "^29.5.0",
"ts-patch": "^3.1.1",
- "typescript": "^5.2.2"
+ "typescript": "^5.3.3"
},
"engines": {
"node": ">=18.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d744e10..f788bbf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -31,10 +31,10 @@ importers:
version: 18.15.11
'@typescript-eslint/eslint-plugin':
specifier: ^6.2.0
- version: 6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.2.2)
+ version: 6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.3.3)
'@typescript-eslint/parser':
specifier: ^6.2.0
- version: 6.2.0(eslint@8.45.0)(typescript@5.2.2)
+ version: 6.2.0(eslint@8.45.0)(typescript@5.3.3)
dprint:
specifier: ^0.34.5
version: 0.34.5
@@ -52,7 +52,7 @@ importers:
version: 3.2.0(eslint@8.45.0)
eslint-plugin-jest:
specifier: ^27.2.3
- version: 27.2.3(@typescript-eslint/eslint-plugin@6.2.0)(eslint@8.45.0)(jest@29.5.0)(typescript@5.2.2)
+ version: 27.2.3(@typescript-eslint/eslint-plugin@6.2.0)(eslint@8.45.0)(jest@29.5.0)(typescript@5.3.3)
eslint-plugin-no-secrets:
specifier: ^0.8.9
version: 0.8.9(eslint@8.45.0)
@@ -87,8 +87,8 @@ importers:
specifier: ^1.9.2
version: 1.9.2
typescript:
- specifier: ^5.2.2
- version: 5.2.2
+ specifier: ^5.3.3
+ version: 5.3.3
packages/core:
devDependencies:
@@ -96,8 +96,8 @@ importers:
specifier: ^29.5.0
version: 29.5.0
'@nrfcloud/ts-json-schema-transformer':
- specifier: ^1.2.5
- version: 1.2.5(typescript@5.2.2)
+ specifier: ^1.3.0
+ version: 1.3.0(typescript@5.3.3)
'@types/jest':
specifier: ^29.4.0
version: 29.4.0
@@ -117,8 +117,8 @@ importers:
specifier: ^3.1.1
version: 3.1.1
typescript:
- specifier: ^5.2.2
- version: 5.2.2
+ specifier: ^5.3.3
+ version: 5.3.3
packages/rest:
dependencies:
@@ -129,8 +129,8 @@ importers:
specifier: workspace:^
version: link:../core
'@nrfcloud/ts-json-schema-transformer':
- specifier: ^1.2.5
- version: 1.2.5(typescript@5.2.2)
+ specifier: ^1.3.0
+ version: 1.3.0(typescript@5.3.3)
'@types/aws-lambda':
specifier: ^8.10.115
version: 8.10.115
@@ -165,11 +165,11 @@ importers:
specifier: ^1.5.0
version: 1.5.0
ts-morph:
- specifier: ^20.0.0
- version: 20.0.0
+ specifier: ^21.0.1
+ version: 21.0.1
tsutils:
specifier: ^3.21.0
- version: 3.21.0(typescript@5.2.2)
+ version: 3.21.0(typescript@5.3.3)
yargs:
specifier: ^17.7.2
version: 17.7.2
@@ -202,8 +202,8 @@ importers:
specifier: ^3.1.1
version: 3.1.1
typescript:
- specifier: ^5.2.2
- version: 5.2.2
+ specifier: ^5.3.3
+ version: 5.3.3
packages/scripts: {}
@@ -220,8 +220,8 @@ importers:
specifier: ^29.5.0
version: 29.5.0
'@nrfcloud/ts-json-schema-transformer':
- specifier: ^1.2.5
- version: 1.2.5(typescript@5.2.2)
+ specifier: ^1.3.0
+ version: 1.3.0(typescript@5.3.3)
'@types/aws-lambda':
specifier: ^8.10.115
version: 8.10.115
@@ -244,8 +244,8 @@ importers:
specifier: ^3.1.1
version: 3.1.1
typescript:
- specifier: ^5.2.2
- version: 5.2.2
+ specifier: ^5.3.3
+ version: 5.3.3
packages:
@@ -1755,6 +1755,15 @@ packages:
cpu: [arm64]
os: [android]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/android-arm64@0.19.9:
+ resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
optional: true
/@esbuild/android-arm@0.17.18:
@@ -1763,6 +1772,15 @@ packages:
cpu: [arm]
os: [android]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/android-arm@0.19.9:
+ resolution: {integrity: sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
optional: true
/@esbuild/android-x64@0.17.18:
@@ -1771,6 +1789,15 @@ packages:
cpu: [x64]
os: [android]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/android-x64@0.19.9:
+ resolution: {integrity: sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
optional: true
/@esbuild/darwin-arm64@0.17.18:
@@ -1779,6 +1806,15 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/darwin-arm64@0.19.9:
+ resolution: {integrity: sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
optional: true
/@esbuild/darwin-x64@0.17.18:
@@ -1787,6 +1823,15 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/darwin-x64@0.19.9:
+ resolution: {integrity: sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
optional: true
/@esbuild/freebsd-arm64@0.17.18:
@@ -1795,6 +1840,15 @@ packages:
cpu: [arm64]
os: [freebsd]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/freebsd-arm64@0.19.9:
+ resolution: {integrity: sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
optional: true
/@esbuild/freebsd-x64@0.17.18:
@@ -1803,6 +1857,15 @@ packages:
cpu: [x64]
os: [freebsd]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/freebsd-x64@0.19.9:
+ resolution: {integrity: sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
optional: true
/@esbuild/linux-arm64@0.17.18:
@@ -1811,6 +1874,15 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-arm64@0.19.9:
+ resolution: {integrity: sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
optional: true
/@esbuild/linux-arm@0.17.18:
@@ -1819,6 +1891,15 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-arm@0.19.9:
+ resolution: {integrity: sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
optional: true
/@esbuild/linux-ia32@0.17.18:
@@ -1827,6 +1908,15 @@ packages:
cpu: [ia32]
os: [linux]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-ia32@0.19.9:
+ resolution: {integrity: sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
optional: true
/@esbuild/linux-loong64@0.17.18:
@@ -1835,6 +1925,15 @@ packages:
cpu: [loong64]
os: [linux]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-loong64@0.19.9:
+ resolution: {integrity: sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+ requiresBuild: true
optional: true
/@esbuild/linux-mips64el@0.17.18:
@@ -1843,6 +1942,15 @@ packages:
cpu: [mips64el]
os: [linux]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-mips64el@0.19.9:
+ resolution: {integrity: sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
optional: true
/@esbuild/linux-ppc64@0.17.18:
@@ -1851,6 +1959,15 @@ packages:
cpu: [ppc64]
os: [linux]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-ppc64@0.19.9:
+ resolution: {integrity: sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
optional: true
/@esbuild/linux-riscv64@0.17.18:
@@ -1859,6 +1976,15 @@ packages:
cpu: [riscv64]
os: [linux]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-riscv64@0.19.9:
+ resolution: {integrity: sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
optional: true
/@esbuild/linux-s390x@0.17.18:
@@ -1867,6 +1993,15 @@ packages:
cpu: [s390x]
os: [linux]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-s390x@0.19.9:
+ resolution: {integrity: sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
optional: true
/@esbuild/linux-x64@0.17.18:
@@ -1875,6 +2010,15 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-x64@0.19.9:
+ resolution: {integrity: sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
optional: true
/@esbuild/netbsd-x64@0.17.18:
@@ -1883,6 +2027,15 @@ packages:
cpu: [x64]
os: [netbsd]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/netbsd-x64@0.19.9:
+ resolution: {integrity: sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
optional: true
/@esbuild/openbsd-x64@0.17.18:
@@ -1891,6 +2044,15 @@ packages:
cpu: [x64]
os: [openbsd]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/openbsd-x64@0.19.9:
+ resolution: {integrity: sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
optional: true
/@esbuild/sunos-x64@0.17.18:
@@ -1899,6 +2061,15 @@ packages:
cpu: [x64]
os: [sunos]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/sunos-x64@0.19.9:
+ resolution: {integrity: sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
optional: true
/@esbuild/win32-arm64@0.17.18:
@@ -1907,6 +2078,15 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-arm64@0.19.9:
+ resolution: {integrity: sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
optional: true
/@esbuild/win32-ia32@0.17.18:
@@ -1915,6 +2095,15 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-ia32@0.19.9:
+ resolution: {integrity: sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
optional: true
/@esbuild/win32-x64@0.17.18:
@@ -1923,6 +2112,15 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-x64@0.19.9:
+ resolution: {integrity: sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
optional: true
/@eslint-community/eslint-utils@4.4.0(eslint@8.45.0):
@@ -2327,18 +2525,18 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
- /@nrfcloud/ts-json-schema-transformer@1.2.5(typescript@5.2.2):
- resolution: {integrity: sha512-AL1ZYkwtusprB4Hsf5CuySaRT0hPUhVA8rWb58FS13FcZd9DKi8mKg6s09Jmy7Ersud0Q5WnkxHvEM+ltz8tqA==}
+ /@nrfcloud/ts-json-schema-transformer@1.3.0(typescript@5.3.3):
+ resolution: {integrity: sha512-b/WjwnLSYtikoADzb6gNPLM4BVELePplR2xOKEYTvj4ozGITlX6RaHVc70SXm4GwyX+riagL3dW2MmFBeM6NLw==}
engines: {node: '>=18.0.0'}
peerDependencies:
typescript: '>=5'
dependencies:
'@apidevtools/json-schema-ref-parser': 10.1.0
ajv: 8.12.0
- esbuild: 0.17.18
+ esbuild: 0.19.9
json-schema-faker: /@jfconley/json-schema-faker@0.5.0-rcv.48
ts-json-schema-generator: 1.5.0
- typescript: 5.2.2
+ typescript: 5.3.3
/@parcel/source-map@2.1.1:
resolution: {integrity: sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==}
@@ -2370,12 +2568,12 @@ packages:
'@sinonjs/commons': 2.0.0
dev: true
- /@ts-morph/common@0.21.0:
- resolution: {integrity: sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==}
+ /@ts-morph/common@0.22.0:
+ resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==}
dependencies:
- fast-glob: 3.2.12
- minimatch: 7.4.6
- mkdirp: 2.1.6
+ fast-glob: 3.3.2
+ minimatch: 9.0.3
+ mkdirp: 3.0.1
path-browserify: 1.0.1
dev: false
@@ -2519,7 +2717,7 @@ packages:
'@types/yargs-parser': 21.0.0
dev: true
- /@typescript-eslint/eslint-plugin@6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.2.2):
+ /@typescript-eslint/eslint-plugin@6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.3.3):
resolution: {integrity: sha512-rClGrMuyS/3j0ETa1Ui7s6GkLhfZGKZL3ZrChLeAiACBE/tRc1wq8SNZESUuluxhLj9FkUefRs2l6bCIArWBiQ==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -2531,10 +2729,10 @@ packages:
optional: true
dependencies:
'@eslint-community/regexpp': 4.5.1
- '@typescript-eslint/parser': 6.2.0(eslint@8.45.0)(typescript@5.2.2)
+ '@typescript-eslint/parser': 6.2.0(eslint@8.45.0)(typescript@5.3.3)
'@typescript-eslint/scope-manager': 6.2.0
- '@typescript-eslint/type-utils': 6.2.0(eslint@8.45.0)(typescript@5.2.2)
- '@typescript-eslint/utils': 6.2.0(eslint@8.45.0)(typescript@5.2.2)
+ '@typescript-eslint/type-utils': 6.2.0(eslint@8.45.0)(typescript@5.3.3)
+ '@typescript-eslint/utils': 6.2.0(eslint@8.45.0)(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 6.2.0
debug: 4.3.4
eslint: 8.45.0
@@ -2543,13 +2741,13 @@ packages:
natural-compare: 1.4.0
natural-compare-lite: 1.4.0
semver: 7.5.4
- ts-api-utils: 1.0.1(typescript@5.2.2)
- typescript: 5.2.2
+ ts-api-utils: 1.0.1(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/parser@6.2.0(eslint@8.45.0)(typescript@5.2.2):
+ /@typescript-eslint/parser@6.2.0(eslint@8.45.0)(typescript@5.3.3):
resolution: {integrity: sha512-igVYOqtiK/UsvKAmmloQAruAdUHihsOCvplJpplPZ+3h4aDkC/UKZZNKgB6h93ayuYLuEymU3h8nF1xMRbh37g==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -2561,11 +2759,11 @@ packages:
dependencies:
'@typescript-eslint/scope-manager': 6.2.0
'@typescript-eslint/types': 6.2.0
- '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.2.2)
+ '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 6.2.0
debug: 4.3.4
eslint: 8.45.0
- typescript: 5.2.2
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
@@ -2586,7 +2784,7 @@ packages:
'@typescript-eslint/visitor-keys': 6.2.0
dev: true
- /@typescript-eslint/type-utils@6.2.0(eslint@8.45.0)(typescript@5.2.2):
+ /@typescript-eslint/type-utils@6.2.0(eslint@8.45.0)(typescript@5.3.3):
resolution: {integrity: sha512-DnGZuNU2JN3AYwddYIqrVkYW0uUQdv0AY+kz2M25euVNlujcN2u+rJgfJsBFlUEzBB6OQkUqSZPyuTLf2bP5mw==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -2596,12 +2794,12 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.2.2)
- '@typescript-eslint/utils': 6.2.0(eslint@8.45.0)(typescript@5.2.2)
+ '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.3.3)
+ '@typescript-eslint/utils': 6.2.0(eslint@8.45.0)(typescript@5.3.3)
debug: 4.3.4
eslint: 8.45.0
- ts-api-utils: 1.0.1(typescript@5.2.2)
- typescript: 5.2.2
+ ts-api-utils: 1.0.1(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
@@ -2616,7 +2814,7 @@ packages:
engines: {node: ^16.0.0 || >=18.0.0}
dev: true
- /@typescript-eslint/typescript-estree@5.59.2(typescript@5.2.2):
+ /@typescript-eslint/typescript-estree@5.59.2(typescript@5.3.3):
resolution: {integrity: sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -2631,13 +2829,13 @@ packages:
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
- tsutils: 3.21.0(typescript@5.2.2)
- typescript: 5.2.2
+ tsutils: 3.21.0(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/typescript-estree@6.2.0(typescript@5.2.2):
+ /@typescript-eslint/typescript-estree@6.2.0(typescript@5.3.3):
resolution: {integrity: sha512-Mts6+3HQMSM+LZCglsc2yMIny37IhUgp1Qe8yJUYVyO6rHP7/vN0vajKu3JvHCBIy8TSiKddJ/Zwu80jhnGj1w==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -2652,13 +2850,13 @@ packages:
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
- ts-api-utils: 1.0.1(typescript@5.2.2)
- typescript: 5.2.2
+ ts-api-utils: 1.0.1(typescript@5.3.3)
+ typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/utils@5.59.2(eslint@8.45.0)(typescript@5.2.2):
+ /@typescript-eslint/utils@5.59.2(eslint@8.45.0)(typescript@5.3.3):
resolution: {integrity: sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -2669,7 +2867,7 @@ packages:
'@types/semver': 7.5.0
'@typescript-eslint/scope-manager': 5.59.2
'@typescript-eslint/types': 5.59.2
- '@typescript-eslint/typescript-estree': 5.59.2(typescript@5.2.2)
+ '@typescript-eslint/typescript-estree': 5.59.2(typescript@5.3.3)
eslint: 8.45.0
eslint-scope: 5.1.1
semver: 7.5.4
@@ -2678,7 +2876,7 @@ packages:
- typescript
dev: true
- /@typescript-eslint/utils@6.2.0(eslint@8.45.0)(typescript@5.2.2):
+ /@typescript-eslint/utils@6.2.0(eslint@8.45.0)(typescript@5.3.3):
resolution: {integrity: sha512-RCFrC1lXiX1qEZN8LmLrxYRhOkElEsPKTVSNout8DMzf8PeWoQG7Rxz2SadpJa3VSh5oYKGwt7j7X/VRg+Y3OQ==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -2689,7 +2887,7 @@ packages:
'@types/semver': 7.5.0
'@typescript-eslint/scope-manager': 6.2.0
'@typescript-eslint/types': 6.2.0
- '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.2.2)
+ '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.3.3)
eslint: 8.45.0
semver: 7.5.4
transitivePeerDependencies:
@@ -3666,6 +3864,36 @@ packages:
'@esbuild/win32-arm64': 0.17.18
'@esbuild/win32-ia32': 0.17.18
'@esbuild/win32-x64': 0.17.18
+ dev: true
+
+ /esbuild@0.19.9:
+ resolution: {integrity: sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg==}
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@esbuild/android-arm': 0.19.9
+ '@esbuild/android-arm64': 0.19.9
+ '@esbuild/android-x64': 0.19.9
+ '@esbuild/darwin-arm64': 0.19.9
+ '@esbuild/darwin-x64': 0.19.9
+ '@esbuild/freebsd-arm64': 0.19.9
+ '@esbuild/freebsd-x64': 0.19.9
+ '@esbuild/linux-arm': 0.19.9
+ '@esbuild/linux-arm64': 0.19.9
+ '@esbuild/linux-ia32': 0.19.9
+ '@esbuild/linux-loong64': 0.19.9
+ '@esbuild/linux-mips64el': 0.19.9
+ '@esbuild/linux-ppc64': 0.19.9
+ '@esbuild/linux-riscv64': 0.19.9
+ '@esbuild/linux-s390x': 0.19.9
+ '@esbuild/linux-x64': 0.19.9
+ '@esbuild/netbsd-x64': 0.19.9
+ '@esbuild/openbsd-x64': 0.19.9
+ '@esbuild/sunos-x64': 0.19.9
+ '@esbuild/win32-arm64': 0.19.9
+ '@esbuild/win32-ia32': 0.19.9
+ '@esbuild/win32-x64': 0.19.9
/escalade@3.1.1:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
@@ -3706,7 +3934,7 @@ packages:
ignore: 5.2.4
dev: true
- /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.2.0)(eslint@8.45.0)(jest@29.5.0)(typescript@5.2.2):
+ /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.2.0)(eslint@8.45.0)(jest@29.5.0)(typescript@5.3.3):
resolution: {integrity: sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
@@ -3719,8 +3947,8 @@ packages:
jest:
optional: true
dependencies:
- '@typescript-eslint/eslint-plugin': 6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.2.2)
- '@typescript-eslint/utils': 5.59.2(eslint@8.45.0)(typescript@5.2.2)
+ '@typescript-eslint/eslint-plugin': 6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.3.3)
+ '@typescript-eslint/utils': 5.59.2(eslint@8.45.0)(typescript@5.3.3)
eslint: 8.45.0
jest: 29.5.0(@types/node@18.15.11)
transitivePeerDependencies:
@@ -3952,6 +4180,18 @@ packages:
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.5
+ dev: true
+
+ /fast-glob@3.3.2:
+ resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
+ engines: {node: '>=8.6.0'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.5
+ dev: false
/fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
@@ -5600,13 +5840,6 @@ packages:
brace-expansion: 2.0.1
dev: true
- /minimatch@7.4.6:
- resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==}
- engines: {node: '>=10'}
- dependencies:
- brace-expansion: 2.0.1
- dev: false
-
/minimatch@9.0.3:
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -5643,8 +5876,8 @@ packages:
hasBin: true
dev: true
- /mkdirp@2.1.6:
- resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==}
+ /mkdirp@3.0.1:
+ resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
engines: {node: '>=10'}
hasBin: true
dev: false
@@ -6771,13 +7004,13 @@ packages:
regexparam: 1.3.0
dev: false
- /ts-api-utils@1.0.1(typescript@5.2.2):
+ /ts-api-utils@1.0.1(typescript@5.3.3):
resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==}
engines: {node: '>=16.13.0'}
peerDependencies:
typescript: '>=4.2.0'
dependencies:
- typescript: 5.2.2
+ typescript: 5.3.3
dev: true
/ts-is-present@1.2.2:
@@ -6797,10 +7030,10 @@ packages:
safe-stable-stringify: 2.4.3
typescript: 5.3.3
- /ts-morph@20.0.0:
- resolution: {integrity: sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==}
+ /ts-morph@21.0.1:
+ resolution: {integrity: sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==}
dependencies:
- '@ts-morph/common': 0.21.0
+ '@ts-morph/common': 0.22.0
code-block-writer: 12.0.0
dev: false
@@ -6823,14 +7056,14 @@ packages:
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
dev: true
- /tsutils@3.21.0(typescript@5.2.2):
+ /tsutils@3.21.0(typescript@5.3.3):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
peerDependencies:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
dependencies:
tslib: 1.14.1
- typescript: 5.2.2
+ typescript: 5.3.3
/tty-table@4.2.1:
resolution: {integrity: sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==}
@@ -6982,11 +7215,6 @@ packages:
is-typed-array: 1.1.12
dev: true
- /typescript@5.2.2:
- resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
- engines: {node: '>=14.17'}
- hasBin: true
-
/typescript@5.3.3:
resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
engines: {node: '>=14.17'}
From bf5e1862c842aa17ebb1f0416df0d13336d87eb0 Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Fri, 15 Dec 2023 13:30:44 -0800
Subject: [PATCH 11/16] bring back mime type and status code enum
---
packages/rest/src/runtime/http-event.mts | 249 +++++++++++++--------
packages/rest/src/runtime/parse.mts | 6 +-
packages/rest/src/runtime/route-holder.mts | 6 +-
packages/rest/src/runtime/router.mts | 14 +-
packages/test/src/controller.ts | 29 ++-
packages/test/src/rest.ts | 3 +-
6 files changed, 193 insertions(+), 114 deletions(-)
diff --git a/packages/rest/src/runtime/http-event.mts b/packages/rest/src/runtime/http-event.mts
index e907e10..2c7cf5b 100644
--- a/packages/rest/src/runtime/http-event.mts
+++ b/packages/rest/src/runtime/http-event.mts
@@ -62,97 +62,158 @@ export interface HttpResponseEmpty extends HttpResponse {
readonly body?: undefined
}
-
-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";
-
-export type MimeType =
- | "*/*"
- | "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";
+export enum HttpStatusCode {
+ Continue = "100",
+ SwitchingProtocols = "101",
+ Processing = "102",
+ Ok = "200",
+ Created = "201",
+ Accepted = "202",
+ NonAuthoritativeInformation = "203",
+ NoContent = "204",
+ ResetContent = "205",
+ PartialContent = "206",
+ MultiStatus = "207",
+ AlreadyReported = "208",
+ IMUsed = "226",
+ MultipleChoices = "300",
+ MovedPermanently = "301",
+ Found = "302",
+ SeeOther = "303",
+ NotModified = "304",
+ UseProxy = "305",
+ TemporaryRedirect = "307",
+ PermanentRedirect = "308",
+ BadRequest = "400",
+ Unauthorized = "401",
+ PaymentRequired = "402",
+ Forbidden = "403",
+ NotFound = "404",
+ MethodNotAllowed = "405",
+ NotAcceptable = "406",
+ ProxyAuthenticationRequired = "407",
+ RequestTimeout = "408",
+ Conflict = "409",
+ Gone = "410",
+ LengthRequired = "411",
+ PreconditionFailed = "412",
+ PayloadTooLarge = "413",
+ RequestURITooLong = "414",
+ UnsupportedMediaType = "415",
+ RequestedRangeNotSatisfiable = "416",
+ ExpectationFailed = "417",
+ ImATeapot = "418",
+ MisdirectedRequest = "421",
+ UnprocessableEntity = "422",
+ Locked = "423",
+ FailedDependency = "424",
+ UpgradeRequired = "426",
+ PreconditionRequired = "428",
+ TooManyRequests = "429",
+ RequestHeaderFieldsTooLarge = "431",
+ UnavailableForLegalReasons = "451",
+ InternalServerError = "500",
+ NotImplemented = "501",
+ BadGateway = "502",
+ ServiceUnavailable = "503",
+ GatewayTimeout = "504",
+ HTTPVersionNotSupported = "505",
+ VariantAlsoNegotiates = "506",
+ InsufficientStorage = "507",
+ 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";
+
+export enum MimeType {
+ ApplicationJson = "application/json",
+ ApplicationOctetStream = "application/octet-stream",
+ ApplicationPdf = "application/pdf",
+ ApplicationXWwwFormUrlencoded = "application/x-www-form-urlencoded",
+ ApplicationZip = "application/zip",
+ ApplicationGzip = "application/gzip",
+ ApplicationBzip = "application/bzip",
+ ApplicationBzip2 = "application/bzip2",
+ ApplicationLdJson = "application/ld+json",
+ FontWoff = "font/woff",
+ FontWoff2 = "font/woff2",
+ FontTtf = "font/ttf",
+ FontOtf = "font/otf",
+ AudioMpeg = "audio/mpeg",
+ AudioXWav = "audio/x-wav",
+ ImageGif = "image/gif",
+ ImageJpeg = "image/jpeg",
+ ImagePng = "image/png",
+ MultipartFormData = "multipart/form-data",
+ TextCss = "text/css",
+ TextCsv = "text/csv",
+ TextHtml = "text/html",
+ TextPlain = "text/plain",
+ TextXml = "text/xml",
+ VideoMpeg = "video/mpeg",
+ VideoMp4 = "video/mp4",
+ VideoQuicktime = "video/quicktime",
+ VideoXMsVideo = "video/x-msvideo",
+ VideoXFlv = "video/x-flv",
+ VideoWebm = "video/webm",
+}
diff --git a/packages/rest/src/runtime/parse.mts b/packages/rest/src/runtime/parse.mts
index 19667bf..8832f2d 100644
--- a/packages/rest/src/runtime/parse.mts
+++ b/packages/rest/src/runtime/parse.mts
@@ -1,4 +1,4 @@
-import {HttpEvent, HttpResponse, MimeType, UnparsedHttpEvent} from "./http-event.mjs";
+import {HttpEvent, HttpResponse, HttpStatusCode, MimeType, UnparsedHttpEvent} from "./http-event.mjs";
import querystring from "node:querystring";
import {getContentType} from "./utils.mjs";
@@ -16,9 +16,9 @@ export class NornirRestParseError extends NornirRestError {
public toHttpResponse(): HttpResponse {
return {
- statusCode: "422",
+ statusCode: HttpStatusCode.UnprocessableEntity,
headers: {
- "content-type": "text/plain",
+ "content-type": MimeType.ApplicationJson,
},
body: this.message
}
diff --git a/packages/rest/src/runtime/route-holder.mts b/packages/rest/src/runtime/route-holder.mts
index a46806d..cafcc9e 100644
--- a/packages/rest/src/runtime/route-holder.mts
+++ b/packages/rest/src/runtime/route-holder.mts
@@ -1,4 +1,4 @@
-import {HttpMethod, HttpRequest, HttpResponse} from './http-event.mjs';
+import {HttpMethod, HttpRequest, HttpResponse, HttpStatusCode, MimeType} from './http-event.mjs';
import {Nornir} from '@nornir/core';
import {NornirRestRequestError} from './error.mjs';
import {type ErrorObject, type ValidateFunction} from 'ajv'
@@ -50,10 +50,10 @@ export class NornirRestRequestValidationError exten
toHttpResponse(): HttpResponse {
return {
- statusCode: "422",
+ statusCode: HttpStatusCode.UnprocessableEntity,
body: {errors: this.errors},
headers: {
- 'content-type': "application/json"
+ 'content-type': MimeType.ApplicationJson
},
}
}
diff --git a/packages/rest/src/runtime/router.mts b/packages/rest/src/runtime/router.mts
index 2a3a30c..b7e0be9 100644
--- a/packages/rest/src/runtime/router.mts
+++ b/packages/rest/src/runtime/router.mts
@@ -1,6 +1,14 @@
import Trouter from 'trouter';
import {RouteBuilder, RouteHolder} from './route-holder.mjs';
-import {HttpEvent, HttpHeadersWithContentType, HttpMethod, HttpRequest, HttpResponse} from './http-event.mjs';
+import {
+ HttpEvent,
+ HttpHeadersWithContentType,
+ HttpMethod,
+ HttpRequest,
+ HttpResponse,
+ HttpStatusCode,
+ MimeType
+} from './http-event.mjs';
import {AttachmentRegistry, Nornir, Result} from '@nornir/core';
import {NornirRestRequestError} from "./error.mjs";
@@ -82,10 +90,10 @@ export class NornirRouteNotFoundError extends NornirRestRequestError,
- ): Nornir }> {
+ ): Nornir }> {
return chain
.use(_contentType => ({
- statusCode: "200" as const,
+ statusCode: HttpStatusCode.Ok,
body: undefined,
// body: `Content-Type: ${contentType}`,
headers: {},
diff --git a/packages/test/src/rest.ts b/packages/test/src/rest.ts
index 58c1906..a976c39 100644
--- a/packages/test/src/rest.ts
+++ b/packages/test/src/rest.ts
@@ -4,6 +4,7 @@ import {
httpErrorHandler,
httpEventParser,
httpResponseSerializer,
+ HttpStatusCode,
mapErrorClass,
normalizeEventHeaders,
router,
@@ -35,7 +36,7 @@ const frameworkChain = nornir()
.use(router())
.useResult(httpErrorHandler([
mapErrorClass(TestError, (_err) => ({
- statusCode: "500",
+ statusCode: HttpStatusCode.BadRequest,
headers: {},
})),
]))
From 650141672fa7da65f514a05a2e26730b8b96cf76 Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Fri, 15 Dec 2023 13:37:19 -0800
Subject: [PATCH 12/16] fix tests
---
packages/rest/__tests__/src/routing.spec.mts | 28 +++++++++++---------
1 file changed, 15 insertions(+), 13 deletions(-)
diff --git a/packages/rest/__tests__/src/routing.spec.mts b/packages/rest/__tests__/src/routing.spec.mts
index 42be9a8..cfa1993 100644
--- a/packages/rest/__tests__/src/routing.spec.mts
+++ b/packages/rest/__tests__/src/routing.spec.mts
@@ -7,7 +7,9 @@ import {
normalizeEventHeaders,
NornirRestRequestValidationError,
PostChain,
- router
+ router,
+ MimeType,
+ HttpStatusCode
} from "../../dist/runtime/index.mjs";
import {nornir, Nornir} from "@nornir/core";
import {describe} from "@jest/globals";
@@ -19,7 +21,7 @@ interface RouteGetInput extends HttpRequestEmpty {
interface RoutePostInputJSON extends HttpRequest {
headers: {
- "content-type": "application/json";
+ "content-type": MimeType.ApplicationJson;
};
body: RoutePostBodyInput;
query: {
@@ -29,7 +31,7 @@ interface RoutePostInputJSON extends HttpRequest {
interface RoutePostInputCSV extends HttpRequest {
headers: {
- "content-type": "text/csv";
+ "content-type": MimeType.TextCsv;
/**
* This is a CSV header
* @example "cool,cool2"
@@ -78,24 +80,24 @@ class TestController {
* @summary Cool Route
*/
@GetChain("/route")
- public getRoute(chain: Nornir) {
+ public getRoute(chain: Nornir): Nornir {
return chain
.use(console.log)
.use(() => ({
- statusCode: "200",
+ statusCode: HttpStatusCode.Ok,
body: `cool`,
headers: {
- "content-type": "text/plain"
+ "content-type": MimeType.TextPlain
},
- } as const));
+ }));
}
@GetChain("/route2")
- public getEmptyRoute(chain: Nornir) {
+ public getEmptyRoute(chain: Nornir): Nornir}> {
return chain
.use(() => {
return {
- statusCode: "200" as const,
+ statusCode: HttpStatusCode.Ok,
body: undefined,
headers: {},
}
@@ -103,14 +105,14 @@ class TestController {
}
@PostChain("/route")
- public postRoute(chain: Nornir) {
+ public postRoute(chain: Nornir): Nornir {
return chain
.use(input => input.headers["content-type"])
.use(contentType => ({
- statusCode: "200",
+ statusCode: HttpStatusCode.Ok,
body: `Content-Type: ${contentType}`,
headers: {
- "content-type": "text/plain"
+ "content-type": MimeType.TextPlain
},
} as const));
}
@@ -192,7 +194,7 @@ describe("REST tests", () => {
method: "POST",
path: "/basepath/route",
headers: {
- "content-type": "application/json",
+ "content-type": MimeType.ApplicationJson,
},
body: {
cool: "cool"
From 57c216d94bfc6352a4d0e5f37920710381b2ae47 Mon Sep 17 00:00:00 2001
From: John Conley <8932043+jfrconley@users.noreply.github.com>
Date: Sat, 16 Dec 2023 18:05:02 -0800
Subject: [PATCH 13/16] lots of bug fixes
---
packages/rest/src/runtime/decorators.mts | 26 +-
packages/rest/src/runtime/http-event.mts | 71 +----
packages/rest/src/runtime/index.mts | 2 +-
packages/rest/src/runtime/serialize.mts | 12 +-
.../rest/src/transform/controller-meta.ts | 17 +-
packages/rest/src/transform/error.ts | 16 +-
.../rest/src/transform/json-schema-utils.ts | 84 ++++--
packages/rest/src/transform/project.ts | 65 +++-
packages/rest/src/transform/transform.ts | 39 +--
.../controller-method-transformer.ts | 1 +
.../processors/chain-route-processor.ts | 32 +-
packages/test/openapi.json | 282 +++---------------
packages/test/src/controller.ts | 6 +-
packages/test/src/rest.ts | 3 +
14 files changed, 270 insertions(+), 386 deletions(-)
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