tsoa icon indicating copy to clipboard operation
tsoa copied to clipboard

"." in method docs example will break rendering

Open pquerner opened this issue 1 year ago • 7 comments

I have a endpoint which consumes a JSON body with a optional callback_url property, which is of type string. If this property is set, we will HTTP POST to this URL.

Setting up examples with

    /**
    * Get status for order id
    *
    * @param orderId
    * @isString orderId
    * @param body Accepts external callback url<br>
    * which will receive the response from our API as POST request<br><br>
    * Currently only accessible URLs are tested. If your URL needs authentication, please contact us.
    * @example body {
    *  "session": "example",
    *  "callback_url": "https:\/\/external.url.com\/api\/post\/stuff\/here"
    * }
    * @example body {
    *  "session": "example"
    * }
    */

I have also tried the URL string unescaped, or without slashes (except for the protocol). A string such as a will render it just fine, so I suspect something with the URL string itself, although I am not sure which part is responsible (protocol, slash escapes, ...)

Sorting

  • I'm submitting a ...

    • [x] bug report
    • [ ] feature request
    • [ ] support request
  • I confirm that I

    • [x] used the search to make sure that a similar issue hasn't already been submit

Expected Behavior

Example 1 should be displayed / generated without problems.

Current Behavior

Example is generated as image

while missing the header Example 1 and the actual example. When changing the selectbox option to any other type, and back to the Example 1 (empty string), this is shown:

image

Using a string for callback_url such as a will output this:

image

Code:

    * @example body {
    *  "session": "example",
    *  "callback_url": "a"
    * }

Context (Environment)

Version of the library: 4.1.0 Version of NodeJS: v18.6.0

Swagger yml generated:

{
	"components": {
		"examples": {},
		"headers": {},
		"parameters": {},
		"requestBodies": {},
		"responses": {},
		"schemas": {
			"RequestError": {
				"properties": {
					"success": {
						"type": "boolean",
						"enum": [
							false
						],
						"nullable": false
					},
					"error": {
						"type": "number",
						"enum": [
							500
						],
						"nullable": false
					},
					"details": {
						"properties": {},
						"additionalProperties": {
							"properties": {
								"value": {
									"type": "string"
								},
								"message": {
									"type": "string"
								}
							},
							"type": "object"
						},
						"type": "object"
					}
				},
				"required": [
					"success",
					"error"
				],
				"type": "object",
				"additionalProperties": false
			},
			"UnauthorizedError": {
				"properties": {
					"success": {
						"type": "boolean",
						"enum": [
							false
						],
						"nullable": false
					},
					"error": {
						"type": "number",
						"enum": [
							401
						],
						"nullable": false
					}
				},
				"required": [
					"success",
					"error"
				],
				"type": "object",
				"additionalProperties": false
			},
			"InternalError": {
				"properties": {
					"success": {
						"type": "boolean",
						"enum": [
							false
						],
						"nullable": false
					},
					"error": {
						"type": "number",
						"enum": [
							500
						],
						"nullable": false
					}
				},
				"required": [
					"success",
					"error"
				],
				"type": "object",
				"additionalProperties": false
			},
			"StatusGetRequest": {
				"properties": {
					"session": {
						"type": "string"
					},
					"callback_url": {
						"type": "string"
					}
				},
				"required": [
					"session"
				],
				"type": "object",
				"additionalProperties": false
			}
		},
		"securitySchemes": {
			"api_key": {
				"type": "apiKey",
				"name": "x-api-key",
				"in": "header"
			}
		}
	},
	"info": {
		"title": "xxx",
		"version": "1.0.0",
		"contact": {
			"name": "xx ",
			"url": "xxx"
		}
	},
	"openapi": "3.0.0",
	"paths": {
		"/status/get/order/{orderId}": {
			"post": {
				"operationId": "GetStatusForOrder",
				"responses": {
					"200": {
						"description": "",
						"content": {
							"application/json": {
								"schema": {}
							}
						}
					},
					"400": {
						"description": "",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/RequestError"
								}
							}
						}
					},
					"401": {
						"description": "",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/UnauthorizedError"
								}
							}
						}
					},
					"500": {
						"description": "",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/InternalError"
								}
							}
						}
					}
				},
				"description": "Get status for order id",
				"security": [
					{
						"api_key": []
					}
				],
				"parameters": [
					{
						"in": "path",
						"name": "orderId",
						"required": true,
						"schema": {
							"type": "string"
						}
					}
				],
				"requestBody": {
					"description": "Accepts external callback url<br>\nwhich will receive the response from our API as POST request<br><br>\nCurrently only accessible URLs are tested. If your URL needs authentication, please contact us.",
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/StatusGetRequest",
								"description": "Accepts external callback url<br>\nwhich will receive the response from our API as POST request<br><br>\nCurrently only accessible URLs are tested. If your URL needs authentication, please contact us."
							},
							"examples": {
								"": {
									"value": {
										"session": "example",
										"callback_url": "https://external.url.com/api/post/stuff/here"
									}
								},
								"Example 1": {
									"value": {
										"session": "example"
									}
								}
							}
						}
					}
				}
			}
		}
	},
	"servers": [
		{
			"url": "/"
		}
	]
}

This part of the example is empty string:

"examples": {
								"": {
									"value": {
										"session": "example",
										"callback_url": "https://external.url.com/api/post/stuff/here"
									}
								},
								"Example 1": {
									"value": {
										"session": "example"
									}
								}
							}

pquerner avatar Jul 20 '22 08:07 pquerner

Hello there pquerner 👋

Thank you for opening your very first issue in this project.

We will try to get back to you as soon as we can.👀

github-actions[bot] avatar Jul 20 '22 08:07 github-actions[bot]

I think I tracked it down to not "URLs" but points ".".

ie, this fails:

@example couponCode "ABC.DEF"
@example couponCode "https://google.com"

and this doesnt fail

@example couponCode "ABCDEF" 
@example couponCode "https://googlecom"

I have also tested this behaviour against the latest version 4.1.1.

pquerner avatar Jul 28 '22 08:07 pquerner

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days

github-actions[bot] avatar Aug 28 '22 00:08 github-actions[bot]

I am guessing a faulty regexp somewhere (interpret "." literally?), but I couldnt find any in the library. Probably in some external, but I had no luck there either.

pquerner avatar Aug 31 '22 00:08 pquerner

We mostly rely on the AST that TS provides, there is a similar issue. https://github.com/lukeautry/tsoa/blob/master/packages/cli/src/utils/jsDocUtils.ts

If you wanna debug it, start around here and see where it goes off the rails

WoH avatar Aug 31 '22 09:08 WoH

I believe its this code:

https://github.com/lukeautry/tsoa/blob/v4.1.2/packages/cli/src/metadataGeneration/parameterGenerator.ts#L354-L360

      const isExample = (tag.tagName.text === 'example' || tag.tagName.escapedText === 'example') && !!tag.comment && comment?.startsWith(parameterName);
      const hasExampleLabel = (comment?.indexOf('.') || -1) > 0;

      if (isExample) {
        // custom example label is delimited by first '.' and the rest will all be included as example label
        exampleLabels.push(hasExampleLabel ? comment?.split(' ')[0].split('.').slice(1).join('.') : undefined);
      }
    /**
     * Some docs
     *
     * @example body {
     *  "session": "example",
     *  "callback_url": "https://googlecom"
     * }
     * @example body {
     *  "session": "example"
     * }
     */

produces

isExample: true,
hasExampleLabel: false,
comment: 'body {"session": "example","callback_url": "https://googlecom"}',
exampleLabels: "[null]"

while

/**
     * Some docs
     *
     * @example body {
     *  "session": "example",
     *  "callback_url": "https://google.com"
     * }
     * @example body {
     *  "session": "example"
     * }
     */

produces

isExample: true,
hasExampleLabel: true,
comment: 'body {"session": "example","callback_url": "https://google.com"}',
exampleLabels: [""]

An empty string is pushed into the exampleLabels array, while the example without the dot pushes undefined into it > resulting in a "Example <Counter>" output and the other a "<empty string>" output.

Maybe something like this can be used for request examples

@example body {
 "session": "example",
 "callback_url": "https://googlecom"
} (Test)

where Test would become the example label

or better yet have a proper RequestExample decorator so it doesnt have to parse the doc comments ?

Feature request was already seen in #1107

pquerner avatar Sep 14 '22 09:09 pquerner

Ok this was a tough nut to crack and I don't believe its documented anywhere (but in the code).

This works:

/**
     * Some docs
     *
     * @example body.Test {
     *  "session": "example",
     *  "callback_url": "https://google.com"
     * }
     * @example body {
     *  "session": "example"
     * }
     */

image

This honestly needs to be made simpler, no? :D

// Edit While this breaks again:

/**
     * Some docs
     *
     * @example body.Test {
     *  "session": "pe.ter",
     *  "callback_url": "https://google.com"
     * }
     * @example body {
     *  "session": "example"
     * }
     * @example body {
     *  "session": "asd.f"
     * }
     */
Error: Minified React error #185; visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

(when its just asdf its fine again)..

🤷

This also works

/**
     * Some docs
     *
     * @example body.Test {
     *  "session": "pe.ter",
     *  "callback_url": "https://google.com"
     * }
     * @example body.Test2 {
     *  "session": "example"
     * }
     * @example body.Test3 {
     *  "session": "asd.f"
     * }
     */

Idk.. its time for a proper RequestExample :D this is insane

//Edit This also doesnt like spaces as example label. The response example labels can do that aswell.

pquerner avatar Sep 14 '22 14:09 pquerner

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days

github-actions[bot] avatar Oct 15 '22 00:10 github-actions[bot]

I'd like to work on this. Hopefully I dont get stuck, but I am sure WoH will be here to help me out if needed? :)

//edit

OK I need a little push right from the start. I dont even know where to properly start. I thought I could fetch some knowledge from https://github.com/lukeautry/tsoa/pull/1309/files or https://github.com/lukeautry/tsoa/pull/1123/files but I dont know where the "generator" stuff happens (for the swagger.json).

So far I only have this decorator which I can import in a test project, but it doesnt actually do anything (of course).

I thought I could even watch older git commits of smaller stuff, but even that seems out of reach since they are too old and the projects way shifted away from this "style".

I wanted the decorator to behave like that:

interface ISessionRequest {
    session: string
}

@RequestExample({
        "session": "example"
    }, 'body', 'Label')
public async debug(
        @Body() body: ISessionRequest
    ): Promise<IResponse> {
        return Promise.resolve({
            foo: 'bar'
        });
    }

Thought of linking the interface into that aswell? Like Example decorator

@RequestExample<ISessionRequest>({
        "session": "asd"
    }, 'Label')

Which would make the "body" thingy obsolete I reckon.

pquerner avatar Oct 18 '22 18:10 pquerner

Indeed this was caused by a bad hasExampleLabel check. I have opened a PR to fix this with your example as test cases. The hasExampleLabel now will only check if there is an example and only look at the dot in location part but not in the content.

davidqqq avatar Nov 05 '22 11:11 davidqqq