tsoa
tsoa copied to clipboard
"." in method docs example will break rendering
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
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:
Using a string for callback_url
such as a
will output this:
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"
}
}
}
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.👀
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.
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
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.
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
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
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"
* }
*/
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.
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
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.
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.