diff --git a/common/changes/@typespec/compiler/feature-op-returns-docs_2023-09-19-16-32.json b/common/changes/@typespec/compiler/feature-op-returns-docs_2023-09-19-16-32.json new file mode 100644 index 0000000000..18268f1f6c --- /dev/null +++ b/common/changes/@typespec/compiler/feature-op-returns-docs_2023-09-19-16-32.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@typespec/compiler", + "comment": "Add support for `@returns` and `@errors` doc comment tags. `@returns`(or `@returnsDoc` decorator) can be used to describe the success return types of an operation. `@errors`(or `@errorsDoc` decorator) can be used to describe the error return types of an operation.", + "type": "none" + } + ], + "packageName": "@typespec/compiler" +} diff --git a/common/changes/@typespec/http/feature-op-returns-docs_2023-09-19-16-32.json b/common/changes/@typespec/http/feature-op-returns-docs_2023-09-19-16-32.json new file mode 100644 index 0000000000..931a1d2bb5 --- /dev/null +++ b/common/changes/@typespec/http/feature-op-returns-docs_2023-09-19-16-32.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@typespec/http", + "comment": "Add support for `@returns` and `@errors` doc comment tags.", + "type": "none" + } + ], + "packageName": "@typespec/http" +} \ No newline at end of file diff --git a/common/changes/@typespec/openapi3/feature-op-returns-docs_2023-09-19-16-32.json b/common/changes/@typespec/openapi3/feature-op-returns-docs_2023-09-19-16-32.json new file mode 100644 index 0000000000..ee001dfd4c --- /dev/null +++ b/common/changes/@typespec/openapi3/feature-op-returns-docs_2023-09-19-16-32.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@typespec/openapi3", + "comment": "Add support for `@returns` and `@errors` doc comment tags.", + "type": "none" + } + ], + "packageName": "@typespec/openapi3" +} \ No newline at end of file diff --git a/docs/standard-library/built-in-decorators.md b/docs/standard-library/built-in-decorators.md index 7fe661ff2b..a6dcb9ade1 100644 --- a/docs/standard-library/built-in-decorators.md +++ b/docs/standard-library/built-in-decorators.md @@ -158,6 +158,32 @@ message: string; ``` +### `@errorsDoc` {#@errorsDoc} + +Attach a documentation string to describe the error return types of an operation. +If an operation returns a union of success and errors it only describe the errors. See `@errorsDoc` for success documentation. + +```typespec +@errorsDoc(doc: valueof string) +``` + +#### Target + +`Operation` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| doc | `valueof scalar string` | Documentation string | + +#### Examples + +```typespec +@errorsDoc("Returns doc") +op get(): Pet | NotFound; +``` + + ### `@format` {#@format} Specify a known data format hint for this string type. For example `uuid`, `uri`, etc. @@ -631,6 +657,32 @@ expireAt: int32; ``` +### `@returnsDoc` {#@returnsDoc} + +Attach a documentation string to describe the successful return types of an operation. +If an operation returns a union of success and errors it only describe the success. See `@errorsDoc` for error documentation. + +```typespec +@returnsDoc(doc: valueof string) +``` + +#### Target + +`Operation` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| doc | `valueof scalar string` | Documentation string | + +#### Examples + +```typespec +@returnsDoc("Returns doc") +op get(): Pet | NotFound; +``` + + ### `@returnTypeVisibility` {#@returnTypeVisibility} Sets which visibilities apply to the return type for the given operation. diff --git a/packages/compiler/lib/decorators.tsp b/packages/compiler/lib/decorators.tsp index 9d8a9845e1..231e1531b6 100644 --- a/packages/compiler/lib/decorators.tsp +++ b/packages/compiler/lib/decorators.tsp @@ -29,6 +29,32 @@ extern dec summary(target: unknown, summary: valueof string); */ extern dec doc(target: unknown, doc: valueof string, formatArgs?: {}); +/** + * Attach a documentation string to describe the successful return types of an operation. + * If an operation returns a union of success and errors it only describe the success. See `@errorsDoc` for error documentation. + * @param doc Documentation string + * + * @example + * ```typespec + * @returnsDoc("Returns doc") + * op get(): Pet | NotFound; + * ``` + */ +extern dec returnsDoc(target: Operation, doc: valueof string); + +/** + * Attach a documentation string to describe the error return types of an operation. + * If an operation returns a union of success and errors it only describe the errors. See `@errorsDoc` for success documentation. + * @param doc Documentation string + * + * @example + * ```typespec + * @errorsDoc("Returns doc") + * op get(): Pet | NotFound; + * ``` + */ +extern dec errorsDoc(target: Operation, doc: valueof string); + /** * Mark this type as deprecated. * diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 5ce1db7908..246c478d04 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3179,10 +3179,7 @@ export function createChecker(program: Program): Checker { ) { const doc = extractParamDoc(prop.parent.parent.parent, type.name); if (doc) { - type.decorators.unshift({ - decorator: $docFromComment, - args: [{ value: createLiteralType(doc), jsValue: doc }], - }); + type.decorators.unshift(createDocFromCommentDecorator("self", doc)); } } finishType(type); @@ -3193,6 +3190,16 @@ export function createChecker(program: Program): Checker { return type; } + function createDocFromCommentDecorator(key: "self" | "returns" | "errors", doc: string) { + return { + decorator: $docFromComment, + args: [ + { value: createLiteralType(key), jsValue: key }, + { value: createLiteralType(doc), jsValue: doc }, + ], + }; + } + function isValueType(type: Type): boolean { if (type === nullType) { return true; @@ -3439,10 +3446,16 @@ export function createChecker(program: Program): Checker { // Doc comment should always be the first decorator in case an explicit @doc must override it. const docComment = extractMainDoc(targetType); if (docComment) { - decorators.unshift({ - decorator: $docFromComment, - args: [{ value: createLiteralType(docComment), jsValue: docComment }], - }); + decorators.unshift(createDocFromCommentDecorator("self", docComment)); + } + if (targetType.kind === "Operation") { + const returnTypesDocs = extractReturnsDocs(targetType); + if (returnTypesDocs.returns) { + decorators.unshift(createDocFromCommentDecorator("returns", returnTypesDocs.returns)); + } + if (returnTypesDocs.errors) { + decorators.unshift(createDocFromCommentDecorator("errors", returnTypesDocs.errors)); + } } return decorators; } @@ -5827,6 +5840,30 @@ function extractMainDoc(type: Type): string | undefined { return trimmed === "" ? undefined : trimmed; } +function extractReturnsDocs(type: Type): { + returns: string | undefined; + errors: string | undefined; +} { + const result: { returns: string | undefined; errors: string | undefined } = { + returns: undefined, + errors: undefined, + }; + if (type.node?.docs === undefined) { + return result; + } + for (const doc of type.node.docs) { + for (const tag of doc.tags) { + if (tag.kind === SyntaxKind.DocReturnsTag) { + result.returns = getDocContent(tag.content); + } + if (tag.kind === SyntaxKind.DocErrorsTag) { + result.errors = getDocContent(tag.content); + } + } + } + return result; +} + function extractParamDoc(node: OperationStatementNode, paramName: string): string | undefined { if (node.docs === undefined) { return undefined; diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 10e9d50a53..be2f84ee8c 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -27,6 +27,7 @@ import { DirectiveArgument, DirectiveExpressionNode, DocContent, + DocErrorsTagNode, DocNode, DocParamTagNode, DocReturnsTagNode, @@ -2399,7 +2400,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa } type ParamLikeTag = DocTemplateTagNode | DocParamTagNode; - type SimpleTag = DocReturnsTagNode | DocUnknownTagNode; + type SimpleTag = DocReturnsTagNode | DocErrorsTagNode | DocUnknownTagNode; function parseDocTag(): DocTag { const pos = tokenPos(); @@ -2413,6 +2414,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case "return": case "returns": return parseDocSimpleTag(pos, tagName, SyntaxKind.DocReturnsTag); + case "errors": + return parseDocSimpleTag(pos, tagName, SyntaxKind.DocErrorsTag); default: return parseDocSimpleTag(pos, tagName, SyntaxKind.DocUnknownTag); } @@ -3127,6 +3130,7 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined visitNode(cb, node.tagName) || visitNode(cb, node.paramName) || visitEach(cb, node.content) ); case SyntaxKind.DocReturnsTag: + case SyntaxKind.DocErrorsTag: case SyntaxKind.DocUnknownTag: return visitNode(cb, node.tagName) || visitEach(cb, node.content); diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 110f8121b1..524b7225c7 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -752,6 +752,7 @@ export enum SyntaxKind { DocText, DocParamTag, DocReturnsTag, + DocErrorsTag, DocTemplateTag, DocUnknownTag, Projection, @@ -1558,7 +1559,12 @@ export interface DocTagBaseNode extends BaseNode { readonly content: readonly DocContent[]; } -export type DocTag = DocReturnsTagNode | DocParamTagNode | DocTemplateTagNode | DocUnknownTagNode; +export type DocTag = + | DocReturnsTagNode + | DocErrorsTagNode + | DocParamTagNode + | DocTemplateTagNode + | DocUnknownTagNode; export type DocContent = DocTextNode; export interface DocTextNode extends BaseNode { @@ -1570,6 +1576,10 @@ export interface DocReturnsTagNode extends DocTagBaseNode { readonly kind: SyntaxKind.DocReturnsTag; } +export interface DocErrorsTagNode extends DocTagBaseNode { + readonly kind: SyntaxKind.DocErrorsTag; +} + export interface DocParamTagNode extends DocTagBaseNode { readonly kind: SyntaxKind.DocParamTag; readonly paramName: IdentifierNode; diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 0f5926d717..6f4cd7b385 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -347,6 +347,7 @@ export function printNode( case SyntaxKind.DocParamTag: case SyntaxKind.DocTemplateTag: case SyntaxKind.DocReturnsTag: + case SyntaxKind.DocErrorsTag: case SyntaxKind.DocUnknownTag: // https://github.com/microsoft/typespec/issues/1319 Tracks pretty-printing doc comments. compilerAssert( diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index bbefc0c198..fbfa380b8e 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -77,6 +77,10 @@ export function getSummary(program: Program, type: Type): string | undefined { } const docsKey = createStateSymbol("docs"); +const returnsDocsKey = createStateSymbol("returnsDocs"); +const errorsDocsKey = createStateSymbol("errorDocs"); +type DocTarget = "self" | "returns" | "errors"; + export interface DocData { /** * Doc value. @@ -88,7 +92,7 @@ export interface DocData { * - `@doc` means the `@doc` decorator was used * - `comment` means it was set from a `/** comment * /` */ - source: "@doc" | "comment"; + source: "decorator" | "comment"; } /** * @doc attaches a documentation string. Works great with multi-line string literals. @@ -103,19 +107,50 @@ export function $doc(context: DecoratorContext, target: Type, text: string, sour if (sourceObject) { text = replaceTemplatedStringFromProperties(text, sourceObject); } - setDocData(context.program, target, { value: text, source: "@doc" }); + setDocData(context.program, target, "self", { value: text, source: "decorator" }); } /** * @internal to be used to set the `@doc` from doc comment. */ -export function $docFromComment(context: DecoratorContext, target: Type, text: string) { - setDocData(context.program, target, { value: text, source: "comment" }); +export function $docFromComment( + context: DecoratorContext, + target: Type, + key: DocTarget, + text: string +) { + setDocData(context.program, target, key, { value: text, source: "comment" }); +} + +function getDocKey(target: DocTarget): symbol { + switch (target) { + case "self": + return docsKey; + case "returns": + return returnsDocsKey; + case "errors": + return errorsDocsKey; + } } -function setDocData(program: Program, target: Type, data: DocData) { - program.stateMap(docsKey).set(target, data); +function setDocData(program: Program, target: Type, key: DocTarget, data: DocData) { + program.stateMap(getDocKey(key)).set(target, data); +} + +/** + * Get the documentation information for the given type. In most cases you probably just want to use {@link getDoc} + * @param program Program + * @param target Type + * @returns Doc data with source information. + */ +export function getDocDataInternal( + program: Program, + target: Type, + key: DocTarget +): DocData | undefined { + return program.stateMap(getDocKey(key)).get(target); } + /** * Get the documentation information for the given type. In most cases you probably just want to use {@link getDoc} * @param program Program @@ -123,7 +158,7 @@ function setDocData(program: Program, target: Type, data: DocData) { * @returns Doc data with source information. */ export function getDocData(program: Program, target: Type): DocData | undefined { - return program.stateMap(docsKey).get(target); + return getDocDataInternal(program, target, "self"); } /** @@ -133,7 +168,57 @@ export function getDocData(program: Program, target: Type): DocData | undefined * @returns Documentation value */ export function getDoc(program: Program, target: Type): string | undefined { - return getDocData(program, target)?.value; + return getDocDataInternal(program, target, "self")?.value; +} + +export function $returnsDoc(context: DecoratorContext, target: Operation, text: string) { + validateDecoratorUniqueOnNode(context, target, $doc); + setDocData(context.program, target, "returns", { value: text, source: "decorator" }); +} + +/** + * Get the documentation information for the return success types of an operation. In most cases you probably just want to use {@link getReturnsDoc} + * @param program Program + * @param target Type + * @returns Doc data with source information. + */ +export function getReturnsDocData(program: Program, target: Operation): DocData | undefined { + return getDocDataInternal(program, target, "returns"); +} + +/** + * Get the documentation string for the return success types of an operation. + * @param program Program + * @param target Type + * @returns Documentation value + */ +export function getReturnsDoc(program: Program, target: Operation): string | undefined { + return getDocDataInternal(program, target, "returns")?.value; +} + +export function $errorsDoc(context: DecoratorContext, target: Operation, text: string) { + validateDecoratorUniqueOnNode(context, target, $doc); + setDocData(context.program, target, "errors", { value: text, source: "decorator" }); +} + +/** + * Get the documentation information for the return errors types of an operation. In most cases you probably just want to use {@link getErrorsDoc} + * @param program Program + * @param target Type + * @returns Doc data with source information. + */ +export function getErrorsDocData(program: Program, target: Operation): DocData | undefined { + return getDocDataInternal(program, target, "errors"); +} + +/** + * Get the documentation string for the return errors types of an operation. + * @param program Program + * @param target Type + * @returns Documentation value + */ +export function getErrorsDoc(program: Program, target: Operation): string | undefined { + return getDocDataInternal(program, target, "errors")?.value; } export function $inspectType(program: Program, target: Type, text: string) { diff --git a/packages/compiler/src/server/type-details.ts b/packages/compiler/src/server/type-details.ts index 91315186a8..0d9c43fdde 100644 --- a/packages/compiler/src/server/type-details.ts +++ b/packages/compiler/src/server/type-details.ts @@ -65,7 +65,7 @@ function getSymbolDocumentation(program: Program, symbol: Sym) { const type = symbol.type ?? program.checker.getTypeForNode(symbol.declarations[0]); const apiDocs = getDocData(program, type); // The doc comment is already included above we don't want to duplicate - if (apiDocs && apiDocs.source === "@doc") { + if (apiDocs && apiDocs.source === "comment") { docs.push(apiDocs.value); } diff --git a/packages/compiler/test/checker/doc-comment.test.ts b/packages/compiler/test/checker/doc-comment.test.ts index 09914a24ba..d359610617 100644 --- a/packages/compiler/test/checker/doc-comment.test.ts +++ b/packages/compiler/test/checker/doc-comment.test.ts @@ -1,6 +1,6 @@ import { ok, strictEqual } from "assert"; import { Model, Operation } from "../../src/core/index.js"; -import { getDoc } from "../../src/lib/decorators.js"; +import { getDoc, getErrorsDoc, getReturnsDoc } from "../../src/lib/decorators.js"; import { BasicTestRunner, createTestRunner } from "../../src/testing/index.js"; describe("compiler: checker: doc comments", () => { @@ -142,6 +142,94 @@ describe("compiler: checker: doc comments", () => { }); }); + describe("@returns", () => { + it("set the returnsDoc on an operation", async () => { + const { test } = (await runner.compile(` + + /** + * @returns A string + */ + @test op test(): string; + `)) as { test: Operation }; + + strictEqual(getReturnsDoc(runner.program, test), "A string"); + }); + + it("@returnsDoc decorator override the doc comment", async () => { + const { test } = (await runner.compile(` + + /** + * @returns A string + */ + @returnsDoc("Another string") + @test op test(): string; + `)) as { test: Operation }; + + strictEqual(getReturnsDoc(runner.program, test), "Another string"); + }); + + it("doc comment on op is override the base comment", async () => { + const { test } = (await runner.compile(` + + /** + * @returns A string + */ + op base(): string; + + /** + * @returns Another string + */ + @test op test(): string; + `)) as { test: Operation }; + + strictEqual(getReturnsDoc(runner.program, test), "Another string"); + }); + }); + + describe("@errors", () => { + it("set the errorsDoc on an operation", async () => { + const { test } = (await runner.compile(` + + /** + * @errors A string + */ + @test op test(): string; + `)) as { test: Operation }; + + strictEqual(getErrorsDoc(runner.program, test), "A string"); + }); + + it("@errorsDoc decorator override the doc comment", async () => { + const { test } = (await runner.compile(` + + /** + * @errors A string + */ + @errorsDoc("Another string") + @test op test(): string; + `)) as { test: Operation }; + + strictEqual(getErrorsDoc(runner.program, test), "Another string"); + }); + + it("doc comment on op is override the base comment", async () => { + const { test } = (await runner.compile(` + + /** + * @errors A string + */ + op base(): string; + + /** + * @errors Another string + */ + @test op test(): string; + `)) as { test: Operation }; + + strictEqual(getErrorsDoc(runner.program, test), "Another string"); + }); + }); + it("using @param in doc comment of operation applies doc on the parameters", async () => { const { addUser } = (await runner.compile(` diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index e3914c4257..6ab18c5ae3 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -3,11 +3,13 @@ import { Model, Operation, Scalar, getVisibility, isSecret } from "../../src/ind import { getDoc, getEncode, + getErrorsDoc, getFriendlyName, getKeyName, getKnownValues, getOverloadedOperation, getOverloads, + getReturnsDoc, isErrorModel, } from "../../src/lib/decorators.js"; import { BasicTestRunner, createTestRunner, expectDiagnostics } from "../../src/testing/index.js"; @@ -139,6 +141,60 @@ describe("compiler: built-in decorators", () => { }); }); + describe("@returnsDoc", () => { + it("applies @returnsDoc on operation", async () => { + const { test } = (await runner.compile( + ` + @test + @returnsDoc("A string") + op test(): string; + ` + )) as { test: Operation }; + + strictEqual(getReturnsDoc(runner.program, test), "A string"); + }); + + it("emit diagnostic if doc is not a string", async () => { + const diagnostics = await runner.diagnose(` + @test + @returnsDoc(123) + op test(): string; + `); + + expectDiagnostics(diagnostics, { + code: "invalid-argument", + message: `Argument '123' is not assignable to parameter of type 'valueof string'`, + }); + }); + }); + + describe("@errorsDoc", () => { + it("applies @errorsDoc on operation", async () => { + const { test } = (await runner.compile( + ` + @test + @errorsDoc("An error") + op test(): string; + ` + )) as { test: Operation }; + + strictEqual(getErrorsDoc(runner.program, test), "An error"); + }); + + it("emit diagnostic if doc is not a string", async () => { + const diagnostics = await runner.diagnose(` + @test + @errorsDoc(123) + op test(): string; + `); + + expectDiagnostics(diagnostics, { + code: "invalid-argument", + message: `Argument '123' is not assignable to parameter of type 'valueof string'`, + }); + }); + }); + describe("@friendlyName", () => { it("applies @friendlyName on model", async () => { const { A, B, C } = await runner.compile(` diff --git a/packages/http/src/responses.ts b/packages/http/src/responses.ts index c78e901fae..796071ae2e 100644 --- a/packages/http/src/responses.ts +++ b/packages/http/src/responses.ts @@ -3,6 +3,8 @@ import { Diagnostic, DiagnosticCollector, getDoc, + getErrorsDoc, + getReturnsDoc, isArrayModelType, isErrorModel, isNullType, @@ -43,10 +45,10 @@ export function getResponsesForOperation( // TODO how should we treat this? https://github.com/microsoft/typespec/issues/356 continue; } - processResponseType(program, diagnostics, responses, option.type); + processResponseType(program, diagnostics, operation, responses, option.type); } } else { - processResponseType(program, diagnostics, responses, responseType); + processResponseType(program, diagnostics, operation, responses, responseType); } return diagnostics.wrap(Object.values(responses)); @@ -55,6 +57,7 @@ export function getResponsesForOperation( function processResponseType( program: Program, diagnostics: DiagnosticCollector, + operation: Operation, responses: Record, responseType: Type ) { @@ -96,7 +99,7 @@ function processResponseType( const response: HttpOperationResponse = responses[statusCode] ?? { statusCode, type: responseType, - description: getResponseDescription(program, responseType, statusCode, bodyType), + description: getResponseDescription(program, operation, responseType, statusCode, bodyType), responses: [], }; @@ -223,6 +226,7 @@ function getResponseBody( function getResponseDescription( program: Program, + operation: Operation, responseType: Type, statusCode: string, bodyType: Type | undefined @@ -241,5 +245,12 @@ function getResponseDescription( } } + const desc = isErrorModel(program, responseType) + ? getErrorsDoc(program, operation) + : getReturnsDoc(program, operation); + if (desc) { + return desc; + } + return getStatusCodeDescription(statusCode); } diff --git a/packages/http/test/response-descriptions.test.ts b/packages/http/test/response-descriptions.test.ts new file mode 100644 index 0000000000..cd88321115 --- /dev/null +++ b/packages/http/test/response-descriptions.test.ts @@ -0,0 +1,68 @@ +import { expectDiagnosticEmpty } from "@typespec/compiler/testing"; +import { strictEqual } from "assert"; +import { getOperationsWithServiceNamespace } from "./test-host.js"; + +describe("http: response descriptions", () => { + async function getHttpOp(code: string) { + const [ops, diagnostics] = await getOperationsWithServiceNamespace(code); + expectDiagnosticEmpty(diagnostics); + strictEqual(ops.length, 1); + return ops[0]; + } + + it("use a default message by status code if not specified", async () => { + const op = await getHttpOp( + ` + op read(): {@statusCode _: 200, content: string}; + ` + ); + strictEqual(op.responses[0].description, "The request has succeeded."); + }); + + it("@returns set doc for all success responses", async () => { + const op = await getHttpOp( + ` + @error model Error {} + @returnsDoc("A string") + op read(): { @statusCode _: 200, content: string } | { @statusCode _: 201, content: string } | Error; + ` + ); + strictEqual(op.responses[0].description, "A string"); + strictEqual(op.responses[1].description, "A string"); + strictEqual(op.responses[2].description, undefined); + }); + + it("@errors set doc for all success responses", async () => { + const op = await getHttpOp( + ` + @error model Error {} + @errorsDoc("Generic error") + op read(): { @statusCode _: 200, content: string } | { @statusCode _: 201, content: string } | Error; + ` + ); + strictEqual(op.responses[0].description, "The request has succeeded."); + strictEqual( + op.responses[1].description, + "The request has succeeded and a new resource has been created as a result." + ); + strictEqual(op.responses[2].description, "Generic error"); + }); + + it("@doc explicitly on a response override the operation returns doc", async () => { + const op = await getHttpOp( + ` + @error model Error {} + @error @doc("Not found model") model NotFound {@statusCode _: 404} + @errorsDoc("Generic error") + op read(): { @statusCode _: 200, content: string } | { @statusCode _: 201, content: string } | Error | NotFound; + ` + ); + strictEqual(op.responses[0].description, "The request has succeeded."); + strictEqual( + op.responses[1].description, + "The request has succeeded and a new resource has been created as a result." + ); + strictEqual(op.responses[2].description, "Not found model"); + strictEqual(op.responses[3].description, "Generic error"); + }); +}); diff --git a/packages/openapi3/test/response-descriptions.test.ts b/packages/openapi3/test/response-descriptions.test.ts new file mode 100644 index 0000000000..f20fd0e97a --- /dev/null +++ b/packages/openapi3/test/response-descriptions.test.ts @@ -0,0 +1,63 @@ +import { strictEqual } from "assert"; +import { openApiFor } from "./test-host.js"; + +describe("openapi3: response descriptions", () => { + it("use a default message by status code if not specified", async () => { + const res = await openApiFor( + ` + op read(): {@statusCode _: 200, content: string}; + ` + ); + strictEqual(res.paths["/"].get.responses["200"].description, "The request has succeeded."); + }); + + it("@returns set doc for all success responses", async () => { + const res = await openApiFor( + ` + @error model Error {} + @returnsDoc("A string") + op read(): { @statusCode _: 200, content: string } | { @statusCode _: 201, content: string } | Error; + ` + ); + strictEqual(res.paths["/"].get.responses["200"].description, "A string"); + strictEqual(res.paths["/"].get.responses["201"].description, "A string"); + strictEqual( + res.paths["/"].get.responses["default"].description, + "An unexpected error response." + ); + }); + + it("@errors set doc for all success responses", async () => { + const res = await openApiFor( + ` + @error model Error {} + @errorsDoc("Generic error") + op read(): { @statusCode _: 200, content: string } | { @statusCode _: 201, content: string } | Error; + ` + ); + strictEqual(res.paths["/"].get.responses["200"].description, "The request has succeeded."); + strictEqual( + res.paths["/"].get.responses["201"].description, + "The request has succeeded and a new resource has been created as a result." + ); + strictEqual(res.paths["/"].get.responses["default"].description, "Generic error"); + }); + + it("@doc explicitly on a response override the operation returns doc", async () => { + const res = await openApiFor( + ` + @error model Error {} + @error @doc("Not found model") model NotFound {@statusCode _: 404} + @errorsDoc("Generic error") + op read(): { @statusCode _: 200, content: string } | { @statusCode _: 201, content: string } | Error | NotFound; + ` + ); + strictEqual(res.paths["/"].get.responses["200"].description, "The request has succeeded."); + strictEqual( + res.paths["/"].get.responses["201"].description, + "The request has succeeded and a new resource has been created as a result." + ); + strictEqual(res.paths["/"].get.responses["404"].description, "Not found model"); + strictEqual(res.paths["/"].get.responses["default"].description, "Generic error"); + }); +});