Skip to content

Commit

Permalink
New decorator @SwaggerExample().
Browse files Browse the repository at this point in the history
```typescript
import core from "@nestia/core";
import { Controller } from "@nestjs/common";
import typia, { tags } from "typia";
import { v4 } from "uuid";

import { IBbsArticle } from "@api/lib/structures/IBbsArticle";

@controller("bbs/articles")
export class BbsArticlesController {
  /**
   * Create an article.
   *
   * @param request Request object from express. Must be disappeared in SDK
   * @param input Content to store
   * @returns Newly archived article
   *
   * @author Samchon
   * @warning This is an fake API
   */
  @core.SwaggerExample.Response(typia.random<IBbsArticle>())
  @core.TypedRoute.Post()
  public async create(
    @core.SwaggerExample.Parameter(typia.random<IBbsArticle.ICreate>())
    @core.SwaggerExample.Parameter("x", typia.random<IBbsArticle.ICreate>())
    @core.SwaggerExample.Parameter("y", typia.random<IBbsArticle.ICreate>())
    @core.SwaggerExample.Parameter("z", typia.random<IBbsArticle.ICreate>())
    @core.TypedBody()
    input: IBbsArticle.ICreate,
  ): Promise<IBbsArticle> {
    const output: IBbsArticle = {
      ...typia.random<IBbsArticle>(),
      ...input,
    };
    return output;
  }

  @core.SwaggerExample.Response(typia.random<IBbsArticle>())
  @core.SwaggerExample.Response("a", typia.random<IBbsArticle>())
  @core.SwaggerExample.Response("b", typia.random<IBbsArticle>())
  @core.TypedRoute.Put(":id")
  public async update(
    @core.SwaggerExample.Parameter(v4())
    @core.TypedParam("id")
    id: string & tags.Format<"uuid">,
    @core.SwaggerExample.Parameter(typia.random<IBbsArticle.IUpdate>())
    @core.TypedBody()
    input: IBbsArticle.IUpdate,
  ): Promise<IBbsArticle> {
    return {
      ...typia.random<IBbsArticle>(),
      ...input,
      id,
    };
  }
}
```
  • Loading branch information
samchon committed Jul 12, 2024
1 parent 4106018 commit 03b394d
Show file tree
Hide file tree
Showing 30 changed files with 1,013 additions and 16 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@nestia/station",
"version": "3.7.0-dev.20240711",
"version": "3.7.0-dev.20240712",
"description": "Nestia station",
"scripts": {
"build": "node build/index.js",
Expand Down
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/core",
"version": "3.7.0-dev.20240711",
"version": "3.7.0-dev.20240712",
"description": "Super-fast validation decorators of NestJS",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -36,7 +36,7 @@
},
"homepage": "https://nestia.io",
"dependencies": {
"@nestia/fetcher": "^3.7.0-dev.20240711",
"@nestia/fetcher": "^3.7.0-dev.20240712",
"@nestjs/common": ">=7.0.1",
"@nestjs/core": ">=7.0.1",
"@samchon/openapi": "^0.3.4",
Expand All @@ -53,7 +53,7 @@
"ws": "^7.5.3"
},
"peerDependencies": {
"@nestia/fetcher": ">=3.7.0-dev.20240711",
"@nestia/fetcher": ">=3.7.0-dev.20240712",
"@nestjs/common": ">=7.0.1",
"@nestjs/core": ">=7.0.1",
"reflect-metadata": ">=0.1.12",
Expand Down
100 changes: 100 additions & 0 deletions packages/core/src/decorators/SwaggerExample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
export namespace SwaggerExample {
export function Response<T>(value: T): MethodDecorator;
export function Response<T>(key: string, value: T): MethodDecorator;
export function Response(...args: any[]): MethodDecorator {
return function SwaggerExampleResponse(
_target: Object,
_propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<any>,
): TypedPropertyDescriptor<any> {
emplaceValue(emplaceOfResponse(descriptor))(args);
return descriptor;
};
}

export function Parameter<T>(value: T): ParameterDecorator;
export function Parameter<T>(key: string, value: T): ParameterDecorator;
export function Parameter(...args: any[]): ParameterDecorator {
return function SwaggerExampleParameter(
target: Object,
propertyKey: string | symbol | undefined,
index: number,
): void {
emplaceValue(emplaceOfParameter(target, propertyKey ?? "", index))(args);
};
}

export interface IData<T> {
examples?: Record<string, T>;
example?: T;
index?: number;
}
}

const emplaceValue =
<T>(data: SwaggerExample.IData<T>) =>
(args: any[]) => {
if (args.length === 1) data.example = args[0];
else {
const key: string = args[0];
const value: T = args[1];
data.examples ??= {};
data.examples[key] = value;
}
};

const emplaceOfResponse = <T>(
descriptor: TypedPropertyDescriptor<any>,
): SwaggerExample.IData<T> => {
const oldbie: SwaggerExample.IData<T> | undefined = Reflect.getMetadata(
"nestia/SwaggerExample/Response",
descriptor.value,
);
if (oldbie !== undefined) return oldbie;
const newbie: SwaggerExample.IData<T> = {};
Reflect.defineMetadata(
"nestia/SwaggerExample/Response",
newbie,
descriptor.value,
);
return newbie;
};

const emplaceOfParameter = (
target: Object,
propertyKey: string | symbol,
index: number,
): SwaggerExample.IData<any> => {
const array: SwaggerExample.IData<any>[] = emplaceArrayOfParameters(
target,
propertyKey,
);
const oldibe: SwaggerExample.IData<any> | undefined = array.find(
(e) => e.index === index,
);
if (oldibe !== undefined) return oldibe;

const data: SwaggerExample.IData<any> = { index };
array.push(data);
return data;
};

const emplaceArrayOfParameters = (
target: Object,
propertyKey: string | symbol,
): SwaggerExample.IData<any>[] => {
const array: SwaggerExample.IData<any>[] | undefined = Reflect.getMetadata(
"nestia/SwaggerExample/Parameters",
target,
propertyKey,
);
if (array !== undefined) return array;
const newbie: SwaggerExample.IData<any>[] = [];
Reflect.defineMetadata(
"nestia/SwaggerExample/Parameters",
newbie,
target,
propertyKey,
);
return newbie;
};
2 changes: 1 addition & 1 deletion packages/core/src/decorators/WebSocketRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function WebSocketRoute(
_target: Object,
_propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<any>,
) {
): TypedPropertyDescriptor<any> {
Reflect.defineMetadata(
"nestia/WebSocketRoute",
{
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from "./decorators/EncryptedBody";
export * from "./decorators/EncryptedModule";
export * from "./decorators/PlainBody";
export * from "./decorators/SwaggerCustomizer";
export * from "./decorators/SwaggerExample";

export * from "./adaptors/WebSocketAdaptor";
export * from "./decorators/WebSocketRoute";
2 changes: 1 addition & 1 deletion packages/fetcher/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/fetcher",
"version": "3.7.0-dev.20240711",
"version": "3.7.0-dev.20240712",
"description": "Fetcher library of Nestia SDK",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down
10 changes: 5 additions & 5 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/sdk",
"version": "3.7.0-dev.20240711",
"version": "3.7.0-dev.20240712",
"description": "Nestia SDK and Swagger generator",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -32,8 +32,8 @@
},
"homepage": "https://nestia.io",
"dependencies": {
"@nestia/fetcher": "^3.7.0-dev.20240711",
"@nestia/core": "^3.7.0-dev.20240711",
"@nestia/fetcher": "^3.7.0-dev.20240712",
"@nestia/core": "^3.7.0-dev.20240712",
"@samchon/openapi": "^0.3.4",
"cli": "^1.0.1",
"get-function-location": "^2.0.0",
Expand All @@ -47,8 +47,8 @@
"typia": "^6.4.3"
},
"peerDependencies": {
"@nestia/fetcher": ">=3.7.0-dev.20240711",
"@nestia/core": ">=3.7.0-dev.20240711",
"@nestia/fetcher": ">=3.7.0-dev.20240712",
"@nestia/core": ">=3.7.0-dev.20240712",
"@nestjs/common": ">=7.0.1",
"@nestjs/core": ">=7.0.1",
"reflect-metadata": ">=0.1.12",
Expand Down
19 changes: 19 additions & 0 deletions packages/sdk/src/analyses/ReflectHttpOperationAnalyzer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SwaggerExample } from "@nestia/core";
import {
HEADERS_METADATA,
HTTP_CODE_METADATA,
Expand Down Expand Up @@ -65,6 +66,19 @@ export namespace ReflectHttpOperationAnalyzer {
_Analyze_http_parameter(...tuple);
if (child !== null) output.push(child);
}
const examples: SwaggerExample.IData<any>[] =
Reflect.getMetadata(
"nestia/SwaggerExample/Parameters",
props.controller.prototype,
props.name,
) ?? [];
for (const p of output) {
const e = examples.find((elem) => elem.index === p.index);
if (e !== undefined) {
p.example = e.example;
p.examples = e.examples;
}
}
return output.sort((x, y) => x.index - y.index);
})();

Expand All @@ -82,6 +96,8 @@ export namespace ReflectHttpOperationAnalyzer {
}

// DO CONSTRUCT
const example: SwaggerExample.IData<any> | undefined =
Reflect.getMetadata("nestia/SwaggerExample/Response", props.function);
const meta: IReflectHttpOperation = {
protocol: "http",
function: props.function,
Expand Down Expand Up @@ -122,6 +138,9 @@ export namespace ReflectHttpOperationAnalyzer {
[]),
]),
],
...(example
? { example: example.example, examples: example.examples }
: {}),
};

// VALIDATE PATH ARGUMENTS
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/analyses/TypedHttpOperationAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ export namespace TypedHttpOperationAnalyzer {
),
security,
exceptions,
example: props.operation.example,
examples: props.operation.examples,
};

// CONFIGURE PATHS
Expand Down
12 changes: 12 additions & 0 deletions packages/sdk/src/generates/internal/SwaggerSchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ export namespace SwaggerSchemaGenerator {
: {
[route.output.contentType]: {
schema: coalesce(props)(result),
...{
example: route.example,
examples: route.examples,
},
},
},
...(props.config.additional === true
Expand Down Expand Up @@ -205,6 +209,10 @@ export namespace SwaggerSchemaGenerator {
content: {
[contentType]: {
schema,
...{
example: param.example,
examples: param.examples,
},
},
},
required: true,
Expand Down Expand Up @@ -365,6 +373,10 @@ export namespace SwaggerSchemaGenerator {
jsDocTags: p.jsDocTags,
kind: "title",
}),
...{
example: p.example,
examples: p.examples,
},
};
};

Expand Down
12 changes: 12 additions & 0 deletions packages/sdk/src/structures/IReflectHttpOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface IReflectHttpOperation {
number | "2XX" | "3XX" | "4XX" | "5XX",
IReflectHttpOperation.IException
>;
example?: any;
examples?: Record<string, any>;
swaggerTags: string[];
}
export namespace IReflectHttpOperation {
Expand All @@ -34,20 +36,26 @@ export namespace IReflectHttpOperation {
index: number;
name: string;
field: string | undefined;
example?: any;
examples?: Record<string, any>;
}
export interface IHeadersParameter {
custom: true;
category: "headers";
index: number;
name: string;
field: string | undefined;
example?: any;
examples?: Record<string, any>;
}
export interface IQueryParameter {
custom: true;
category: "query";
index: number;
name: string;
field: string | undefined;
example?: any;
examples?: Record<string, any>;
}
export interface IBodyParameter {
custom: true;
Expand All @@ -61,13 +69,17 @@ export namespace IReflectHttpOperation {
| "application/x-www-form-urlencoded"
| "multipart/form-data"
| "text/plain";
example?: any;
examples?: Record<string, any>;
}
export interface IPathParameter {
custom: true;
category: "param";
index: number;
name: string;
field: string | undefined;
example?: any;
examples?: Record<string, any>;
}

export interface IException {
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/structures/ITypedHttpRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface ITypedHttpRoute {
number | "2XX" | "3XX" | "4XX" | "5XX",
ITypedHttpRoute.IOutput
>;
example?: any;
examples?: Record<string, any>;
swaggerTags: string[];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
ApiBasicAuth,
ApiBearerAuth,
ApiOAuth2,
ApiSecurity,
} from "@nestjs/swagger";
import typia from "typia";

Expand Down
16 changes: 16 additions & 0 deletions test/features/swagger-example/nestia.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { INestiaConfig } from "@nestia/sdk";

export const NESTIA_CONFIG: INestiaConfig = {
input: ["src/controllers"],
output: "src/api",
swagger: {
output: "swagger.json",
beautify: true,
security: {
bearer: {
type: "apiKey",
},
},
},
};
export default NESTIA_CONFIG;
27 changes: 27 additions & 0 deletions test/features/swagger-example/src/Backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import core from "@nestia/core";
import { INestApplication } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";

export class Backend {
private application_?: INestApplication;

public async open(): Promise<void> {
this.application_ = await NestFactory.create(
await core.EncryptedModule.dynamic(__dirname + "/controllers", {
key: "A".repeat(32),
iv: "B".repeat(16),
}),
{ logger: false },
);
await this.application_.listen(37_000);
}

public async close(): Promise<void> {
if (this.application_ === undefined) return;

const app = this.application_;
await app.close();

delete this.application_;
}
}
1 change: 1 addition & 0 deletions test/features/swagger-example/src/api/HttpError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HttpError } from "@nestia/fetcher";
1 change: 1 addition & 0 deletions test/features/swagger-example/src/api/IConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { IConnection } from "@nestia/fetcher";
1 change: 1 addition & 0 deletions test/features/swagger-example/src/api/Primitive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { Primitive } from "@nestia/fetcher";
Loading

0 comments on commit 03b394d

Please sign in to comment.