Feature: Add support for `@returns` and `@errors` doc comment tags (#2436)
fix #2384
This commit is contained in:
Родитель
8415d52842
Коммит
9a2a1bf771
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/http",
|
||||
"comment": "Add support for `@returns` and `@errors` doc comment tags.",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/http"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/openapi3",
|
||||
"comment": "Add support for `@returns` and `@errors` doc comment tags.",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/openapi3"
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<T>(node: Node, cb: NodeCallback<T>): 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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 setDocData(program: Program, target: Type, data: DocData) {
|
||||
program.stateMap(docsKey).set(target, data);
|
||||
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, 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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(`
|
||||
|
||||
|
|
|
@ -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(`
|
||||
|
|
|
@ -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<string, HttpOperationResponse>,
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
|
@ -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");
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче