Skip to content

Commit

Permalink
Enhance IMigrateRoute structure.
Browse files Browse the repository at this point in the history
  • Loading branch information
samchon committed Jul 11, 2024
1 parent 6088df3 commit 1bc51b0
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 79 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Here is the entire list of differences between OpenAPI v3.1 and emended OpenApi.
- Merge `OpenApiV3_1.IJsonSchema.IRecursiveReference` to `OpenApi.IJsonSchema.IReference`
- Merge `OpenApiV3_1.IJsonSchema.IAllOf` to `OpenApi.IJsonSchema.IObject`

Additionally, `@samchon/openapi` provides [`IMigrateDocument`](https://github.com/samchon/openapi/blob/master/src/IMigrateDocument.ts) for OpenAPI generators.
Additionally, `@samchon/openapi` provides [`IMigrateDocument`](https://github.com/samchon/openapi/blob/master/src/IMigrateDocument.ts) for OpenAPI generators. If you're developing TypeScript, [`@nestia/editor`](https://nestia.io/docs/editor) would be the best project utilizing the [`IMigrateDocument`](https://github.com/samchon/openapi/blob/master/src/IMigrateDocument.ts) for the OpenAPI SDK generation. Otherwise, you wanna utilize OpenAPI document for OpenAI function calling, [`@wrtnio/openai-function-schema`](https://github.com/wrtnio/openai-function-schema/) has been prepared for you.



Expand Down
5 changes: 3 additions & 2 deletions examples/v3.1/shopping.json
Original file line number Diff line number Diff line change
Expand Up @@ -5930,12 +5930,13 @@
"type": "string",
"format": "uuid"
},
"description": "Target sale's ",
"title": "Target sale's ID",
"description": "Target sale's ID to update.\nNote that, you have to specify only the sale ID of your own.",
"required": true
}
],
"requestBody": {
"description": "New information of the sale",
"description": "New information of the sale.\nNote that, your input data would entirely modify the sale, so that have to be careful if you only want the partial updating.",
"content": {
"application/json": {
"schema": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@samchon/openapi",
"version": "0.3.4",
"version": "0.4.0",
"description": "OpenAPI definitions and converters for 'typia' and 'nestia'.",
"main": "./lib/index.js",
"module": "./lib/index.mjs",
Expand Down
24 changes: 18 additions & 6 deletions src/IMigrateRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,12 @@ export namespace IMigrateRoute {
schema: Schema;

/**
* Description comment for the path parameter.
* Original parameter info from the OpenAPI document.
*
* The `parameter` is a function returning the original
* {@link OpenApi.IOperation.IParameter} from the {@link OpenAPI} document.
*/
description?: string;
parameter: () => OpenApi.IOperation.IParameter<Schema>;
}

/**
Expand All @@ -219,6 +222,8 @@ export namespace IMigrateRoute {
* Metadata of headers data type.
*/
schema: Schema;
title: () => string | undefined;
description: () => string | undefined;
}

/**
Expand All @@ -230,6 +235,8 @@ export namespace IMigrateRoute {
name: string;
key: string;
schema: Schema;
title: () => string | undefined;
description: () => string | undefined;
}

/**
Expand Down Expand Up @@ -262,6 +269,11 @@ export namespace IMigrateRoute {
*/
schema: Schema;

/**
* Description comment for the request/response body.
*/
description: () => string | undefined;

/**
* Whether the body is encrypted or not.
*/
Expand All @@ -275,13 +287,13 @@ export namespace IMigrateRoute {
Schema extends OpenApi.IJsonSchema = OpenApi.IJsonSchema,
> {
/**
* Description comment for the exception.
* Metadata of response body data type.
*/
description?: string;
schema: Schema;

/**
* Metadata of response body data type.
* Description comment for the exception.
*/
schema: Schema;
response: () => OpenApi.IOperation.IResponse<Schema>;
}
}
194 changes: 125 additions & 69 deletions src/internal/MigrateRouteConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,21 @@ export namespace MigrateRouteConverter {
OpenApiTypeChecker.isString(p.schema) ||
OpenApiTypeChecker.isArray(p.schema),
);
if (objects.length === 1 && primitives.length === 0) return objects[0];
const out = (elem: {
schema: OpenApi.IJsonSchema;
title?: string;
description?: string;
}) =>
({
...elem,
name: type,
key: type,
title: () => elem.title,
description: () => elem.description,
}) satisfies IMigrateRoute.IHeaders;

if (objects.length === 1 && primitives.length === 0)
return out(parameters[0]);
else if (objects.length > 1) {
failures.push(`${type} typed parameters must be only one object type`);
return false;
Expand Down Expand Up @@ -127,37 +141,39 @@ export namespace MigrateRouteConverter {
];
return parameters.length === 0
? null
: emplaceReference({
document: props.document,
name:
StringUtil.pascal(`I/Api/${props.path}`) +
"." +
StringUtil.pascal(`${props.method}/${type}`),
schema: {
type: "object",
properties: Object.fromEntries([
...new Map<string, OpenApi.IJsonSchema>(
entire
.map((o) =>
Object.entries(o.properties ?? {}).map(
([name, schema]) =>
[
name,
{
...schema,
description:
schema.description ?? schema.description,
} as OpenApi.IJsonSchema,
] as const,
),
)
.flat(),
),
]),
required: [
...new Set(entire.map((o) => o.required ?? []).flat()),
],
},
: out({
schema: emplaceReference({
document: props.document,
name:
StringUtil.pascal(`I/Api/${props.path}`) +
"." +
StringUtil.pascal(`${props.method}/${type}`),
schema: {
type: "object",
properties: Object.fromEntries([
...new Map<string, OpenApi.IJsonSchema>(
entire
.map((o) =>
Object.entries(o.properties ?? {}).map(
([name, schema]) =>
[
name,
{
...schema,
description:
schema.description ?? schema.description,
} as OpenApi.IJsonSchema,
] as const,
),
)
.flat(),
),
]),
required: [
...new Set(entire.map((o) => o.required ?? []).flat()),
],
},
}),
});
});

Expand Down Expand Up @@ -200,18 +216,29 @@ export namespace MigrateRouteConverter {
);
if (failures.length) return failures;

const parameters: IMigrateRoute.IParameter[] = (
props.operation.parameters ?? []
)
.filter((p) => p.in === "path")
.map((p, i) => ({
// FILL KEY NAME IF NOT EXISTsS
name: parameterNames[i],
key: (() => {
let key: string = StringUtil.normalize(parameterNames[i]);
if (Escaper.variable(key)) return key;
while (true) {
key = "_" + key;
if (!parameterNames.some((s) => s === key)) return key;
}
})(),
schema: p.schema,
parameter: () => p,
}));
return {
method: props.method,
path: props.path,
emendedPath: props.emendedPath,
accessor: ["@lazy"],
headers: headers
? {
name: "headers",
key: "headers",
schema: headers,
}
: null,
parameters: (props.operation.parameters ?? [])
.filter((p) => p.in === "path")
.map((p, i) => ({
Expand All @@ -225,68 +252,93 @@ export namespace MigrateRouteConverter {
if (!parameterNames.some((s) => s === key)) return key;
}
})(),
schema: {
...p!.schema,
description: p!.schema.description ?? p!.description,
},
schema: p.schema,
parameter: () => p,
})),
query: query
? {
name: "query",
key: "query",
schema: query,
}
: null,
headers: headers || null,
query: query || null,
body: body as IMigrateRoute.IBody | null,
success: success as IMigrateRoute.IBody | null,
exceptions: Object.fromEntries(
Object.entries(props.operation.responses ?? {})
.filter(
([key]) => key !== "200" && key !== "201" && key !== "default",
)
.map(([key, value]) => [
key,
.map(([status, response]) => [
status,
{
description: value.description,
schema: value.content?.["application/json"]?.schema ?? {},
schema: response.content?.["application/json"]?.schema ?? {},
response: () => response,
},
]),
),
comment: () => writeDescription(props.operation),
comment: () =>
writeRouteComment({
operation: props.operation,
parameters,
query: query || null,
body: body || null,
}),
operation: () => props.operation,
};
};

const writeDescription = (original: OpenApi.IOperation): string => {
const writeRouteComment = (props: {
operation: OpenApi.IOperation;
parameters: IMigrateRoute.IParameter[];
query: IMigrateRoute.IQuery | null;
body: IMigrateRoute.IBody | null;
}): string => {
const commentTags: string[] = [];
const add = (text: string) => {
if (commentTags.every((line) => line !== text)) commentTags.push(text);
};

let description: string = original.description ?? "";
if (original.summary) {
const emended: string = original.summary.endsWith(".")
? original.summary
: original.summary + ".";
if (!!description.length && !description.startsWith(original.summary))
let description: string = props.operation.description ?? "";
if (props.operation.summary) {
const emended: string = props.operation.summary.endsWith(".")
? props.operation.summary
: props.operation.summary + ".";
if (
!!description.length &&
!description.startsWith(props.operation.summary)
)
description = `${emended}\n${description}`;
}
for (const p of original.parameters ?? [])
if (p.description) add(`@param ${p.name} ${p.description}`);
if (original.requestBody?.description)
add(`@param body ${original.requestBody.description}`);
for (const security of original.security ?? [])
description = description
.split("\n")
.map((s) => s.trim())
.join("\n");

for (const p of props.parameters ?? []) {
const param = p.parameter();
if (param.description || param.title) {
const text: string = (param.description ?? param.title)!;
add(`@param ${p.name} ${writeIndented(text, p.name.length + 8)}`);
}
}
if (props.body?.description()?.length)
add(`@param body ${writeIndented(props.body.description()!, 12)}`);
for (const security of props.operation.security ?? [])
for (const [name, scopes] of Object.entries(security))
add(`@security ${[name, ...scopes].join("")}`);
if (original.tags) original.tags.forEach((name) => add(`@tag ${name}`));
if (original.deprecated) add("@deprecated");
if (props.operation.tags)
props.operation.tags.forEach((name) => add(`@tag ${name}`));
if (props.operation.deprecated) add("@deprecated");
return description.length
? commentTags.length
? `${description}\n\n${commentTags.join("\n")}`
: description
: commentTags.join("\n");
};

const writeIndented = (text: string, spaces: number): string =>
text
.split("\n")
.map((s) => s.trim())
.map((s, i) => (i === 0 ? s : `${" ".repeat(spaces)}${s}`))
.join("\n");

const emplaceBodySchema =
(from: "request" | "response") =>
(
Expand Down Expand Up @@ -322,6 +374,7 @@ export namespace MigrateRouteConverter {
: emplacer(schema)
: {},
"x-nestia-encrypted": meta["x-nestia-encrypted"],
description: () => meta.description,
};
}

Expand All @@ -339,6 +392,7 @@ export namespace MigrateRouteConverter {
? schema
: emplacer(schema)
: {},
description: () => meta.description,
};
}

Expand All @@ -349,6 +403,7 @@ export namespace MigrateRouteConverter {
name: "body",
key: "body",
schema: { type: "string" },
description: () => meta.description,
};

if (from === "request") {
Expand All @@ -366,6 +421,7 @@ export namespace MigrateRouteConverter {
? schema
: emplacer(schema)
: {},
description: () => meta.description,
};
}
}
Expand Down
Loading

0 comments on commit 1bc51b0

Please sign in to comment.