diff --git a/src/transform/schema.ts b/src/transform/schema.ts index 9f73190e1..a2d63e93b 100644 --- a/src/transform/schema.ts +++ b/src/transform/schema.ts @@ -1,5 +1,14 @@ import { GlobalContext } from "../types"; -import { comment, nodeType, tsArrayOf, tsIntersectionOf, tsPartial, tsReadonly, tsTupleOf, tsUnionOf } from "../utils"; +import { + prepareComment, + nodeType, + tsArrayOf, + tsIntersectionOf, + tsPartial, + tsReadonly, + tsTupleOf, + tsUnionOf, +} from "../utils"; interface TransformSchemaObjOptions extends GlobalContext { required: Set; @@ -18,11 +27,9 @@ export function transformSchemaObjMap(obj: Record, options: Transfo for (const k of Object.keys(obj)) { const v = obj[k]; - // 1. JSDoc comment (goes above property) - let schemaComment = ""; - if (v.deprecated) schemaComment += `@deprecated `; - if (v.description) schemaComment += v.description; - if (schemaComment) output += comment(schemaComment); + // 1. Add comment in jsdoc notation + const comment = prepareComment(v); + if (comment) output += comment; // 2. name (with “?” if optional property) const readonly = tsReadonly(options.immutableTypes); diff --git a/src/utils.ts b/src/utils.ts index 845084137..132da0ee7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,42 @@ import { OpenAPI2, OpenAPI3, ReferenceObject } from "./types"; +type CommentObject = { + title?: string; // not jsdoc + format?: string; // not jsdoc + deprecated?: boolean; // jsdoc without value + description?: string; // jsdoc with value + default?: string; // jsdoc with value + example?: string; // jsdoc with value +}; + +/** + * Preparing comments from fields + * @see {comment} for output examples + * @returns void if not comments or jsdoc format comment string + */ +export function prepareComment(v: CommentObject): string | void { + const commentsArray: Array = []; + + // * Not JSDOC tags: [title, format] + if (v.title) commentsArray.push(`${v.title} `); + if (v.format) commentsArray.push(`Format: ${v.format} `); + + // * JSDOC tags without value + // 'Deprecated' without value + if (v.deprecated) commentsArray.push(`@deprecated `); + + // * JSDOC tags with value + const supportedJsDocTags: Array = ["description", "default", "example"]; + for (let index = 0; index < supportedJsDocTags.length; index++) { + const field = supportedJsDocTags[index]; + if (v[field]) commentsArray.push(`@${field} ${v[field]} `); + } + + if (!commentsArray.length) return; + + return comment(commentsArray.join("\n")); +} + export function comment(text: string): string { const commentText = text.trim().replace(/\*\//g, "*\\/"); diff --git a/tests/v3/expected/jsdoc.ts b/tests/v3/expected/jsdoc.ts new file mode 100644 index 000000000..b11e684c5 --- /dev/null +++ b/tests/v3/expected/jsdoc.ts @@ -0,0 +1,150 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/contacts": { + get: operations["getContacts"]; + }; + "/contacts/{userUid}": { + get: operations["getContactInfo"]; + }; + "/contacts/{userUid}/icon": { + get: operations["getContactIcon"]; + }; + "/contacts/me": { + get: operations["getMyInfo"]; + }; + "/contacts/me/icon": { + get: operations["getMyIcon"]; + delete: operations["deleteMyIcon"]; + }; +} + +export interface components { + schemas: { + /** Parent of most important objects */ + BaseEntity: { + /** + * Format: nanoid + * @deprecated + * @description Test description with deprecated + * @example njbusD52k6YoRG346tPgD + */ + uid?: string; + /** + * Format: date-time + * @description It's date example + * @example 1999-03-31 15:00:00.000 + */ + created_at?: string; + /** + * Format: date-time + * @example 2020-07-10 10:10:00.000 + */ + updated_at?: string; + deleted?: boolean; + }; + /** Image for preview */ + Image: { + /** @example https://shantichat.com/data/V1StGXR8_Z5jdHi6B-myT/white-rabbit.png */ + url: string; + /** @example 128 */ + width: unknown; + /** @example 128 */ + height: unknown; + /** @example LEHV6nWB2yk8pyo0adR*.7kCMdnj */ + blurhash?: string; + }; + /** User object */ + User: components["schemas"]["BaseEntity"] & { + /** @example Thomas A. Anderson */ + name?: string; + /** + * @default test + * @example The One + */ + description?: string; + icon?: components["schemas"]["Image"]; + /** @example America/Chicago */ + timezone?: string; + /** + * Format: date-time + * @example 2020-07-10 15:00:00.000 + */ + last_online_at?: string; + }; + }; +} + +export interface operations { + getContacts: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["User"][]; + }; + }; + }; + }; + getContactInfo: { + parameters: { + path: { + userUid: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["User"]; + }; + }; + }; + }; + getContactIcon: { + parameters: { + path: { + userUid: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["Image"]; + }; + }; + }; + }; + getMyInfo: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["User"]; + }; + }; + }; + }; + getMyIcon: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["Image"]; + }; + }; + }; + }; + deleteMyIcon: { + responses: { + /** OK */ + 200: unknown; + }; + }; +} + +export interface external {} diff --git a/tests/v3/specs/jsdoc.json b/tests/v3/specs/jsdoc.json new file mode 100644 index 000000000..bc050e7fa --- /dev/null +++ b/tests/v3/specs/jsdoc.json @@ -0,0 +1,251 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "API", + "description": "REST API prototype (unstalbe, still in active development)", + "version": "0.1.0" + }, + "servers": [ + { + "url": "https://testurl.com/api/v1" + } + ], + "paths": { + "/contacts": { + "get": { + "summary": "Returns list of my contacts", + "description": "", + "operationId": "getContacts", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + }, + "/contacts/{userUid}": { + "get": { + "summary": "Returns contact by UID", + "description": "", + "operationId": "getContactInfo", + "parameters": [ + { + "name": "userUid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "nanoid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + }, + "/contacts/{userUid}/icon": { + "get": { + "summary": "Returns contact icon in full resolution", + "description": "", + "operationId": "getContactIcon", + "parameters": [ + { + "name": "userUid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "nanoid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + } + } + } + }, + "/contacts/me": { + "get": { + "summary": "Returns self contact", + "description": "", + "operationId": "getMyInfo", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + }, + "/contacts/me/icon": { + "get": { + "summary": "Returns self icon in full resolution", + "description": "", + "operationId": "getMyIcon", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Image" + } + } + } + } + } + }, + "delete": { + "summary": "Remove self icon", + "description": "", + "operationId": "deleteMyIcon", + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "BaseEntity": { + "title": "Parent of most important objects", + "type": "object", + "properties": { + "uid": { + "type": "string", + "format": "nanoid", + "example": "njbusD52k6YoRG346tPgD", + "readOnly": true, + "deprecated": true, + "description": "Test description with deprecated" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "1999-03-31 15:00:00.000", + "description": "It's date example", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "example": "2020-07-10 10:10:00.000", + "readOnly": true + }, + "deleted": { + "type": "boolean", + "example": false, + "allowEmptyValue": true, + "readOnly": true + } + } + }, + "Image": { + "title": "Image for preview", + "type": "object", + "properties": { + "url": { + "type": "string", + "example": "https://shantichat.com/data/V1StGXR8_Z5jdHi6B-myT/white-rabbit.png", + "readOnly": true + }, + "width": { + "type": "int", + "example": 128, + "readOnly": true + }, + "height": { + "type": "int", + "example": 128, + "readOnly": true + }, + "blurhash": { + "type": "string", + "example": "LEHV6nWB2yk8pyo0adR*.7kCMdnj", + "allowEmptyValue": true, + "readOnly": true + } + }, + "required": ["url", "width", "height"] + }, + "User": { + "title": "User object", + "allOf": [ + { + "$ref": "#/components/schemas/BaseEntity" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Thomas A. Anderson", + "required": true + }, + "description": { + "type": "string", + "example": "The One", + "allowEmptyValue": true, + "required": true, + "default": "test" + }, + "icon": { + "$ref": "#/components/schemas/Image" + }, + "timezone": { + "type": "string", + "example": "America/Chicago", + "allowEmptyValue": true + }, + "last_online_at": { + "type": "string", + "format": "date-time", + "example": "2020-07-10 15:00:00.000", + "allowEmptyValue": true, + "readOnly": true + } + } + } + ] + } + } + } + } + \ No newline at end of file