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}
|
### `@format` {#@format}
|
||||||
|
|
||||||
Specify a known data format hint for this string type. For example `uuid`, `uri`, etc.
|
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}
|
### `@returnTypeVisibility` {#@returnTypeVisibility}
|
||||||
|
|
||||||
Sets which visibilities apply to the return type for the given operation.
|
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?: {});
|
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.
|
* Mark this type as deprecated.
|
||||||
*
|
*
|
||||||
|
|
|
@ -3179,10 +3179,7 @@ export function createChecker(program: Program): Checker {
|
||||||
) {
|
) {
|
||||||
const doc = extractParamDoc(prop.parent.parent.parent, type.name);
|
const doc = extractParamDoc(prop.parent.parent.parent, type.name);
|
||||||
if (doc) {
|
if (doc) {
|
||||||
type.decorators.unshift({
|
type.decorators.unshift(createDocFromCommentDecorator("self", doc));
|
||||||
decorator: $docFromComment,
|
|
||||||
args: [{ value: createLiteralType(doc), jsValue: doc }],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finishType(type);
|
finishType(type);
|
||||||
|
@ -3193,6 +3190,16 @@ export function createChecker(program: Program): Checker {
|
||||||
return type;
|
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 {
|
function isValueType(type: Type): boolean {
|
||||||
if (type === nullType) {
|
if (type === nullType) {
|
||||||
return true;
|
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.
|
// Doc comment should always be the first decorator in case an explicit @doc must override it.
|
||||||
const docComment = extractMainDoc(targetType);
|
const docComment = extractMainDoc(targetType);
|
||||||
if (docComment) {
|
if (docComment) {
|
||||||
decorators.unshift({
|
decorators.unshift(createDocFromCommentDecorator("self", docComment));
|
||||||
decorator: $docFromComment,
|
}
|
||||||
args: [{ value: createLiteralType(docComment), jsValue: 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;
|
return decorators;
|
||||||
}
|
}
|
||||||
|
@ -5827,6 +5840,30 @@ function extractMainDoc(type: Type): string | undefined {
|
||||||
return trimmed === "" ? undefined : trimmed;
|
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 {
|
function extractParamDoc(node: OperationStatementNode, paramName: string): string | undefined {
|
||||||
if (node.docs === undefined) {
|
if (node.docs === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {
|
||||||
DirectiveArgument,
|
DirectiveArgument,
|
||||||
DirectiveExpressionNode,
|
DirectiveExpressionNode,
|
||||||
DocContent,
|
DocContent,
|
||||||
|
DocErrorsTagNode,
|
||||||
DocNode,
|
DocNode,
|
||||||
DocParamTagNode,
|
DocParamTagNode,
|
||||||
DocReturnsTagNode,
|
DocReturnsTagNode,
|
||||||
|
@ -2399,7 +2400,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParamLikeTag = DocTemplateTagNode | DocParamTagNode;
|
type ParamLikeTag = DocTemplateTagNode | DocParamTagNode;
|
||||||
type SimpleTag = DocReturnsTagNode | DocUnknownTagNode;
|
type SimpleTag = DocReturnsTagNode | DocErrorsTagNode | DocUnknownTagNode;
|
||||||
|
|
||||||
function parseDocTag(): DocTag {
|
function parseDocTag(): DocTag {
|
||||||
const pos = tokenPos();
|
const pos = tokenPos();
|
||||||
|
@ -2413,6 +2414,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
|
||||||
case "return":
|
case "return":
|
||||||
case "returns":
|
case "returns":
|
||||||
return parseDocSimpleTag(pos, tagName, SyntaxKind.DocReturnsTag);
|
return parseDocSimpleTag(pos, tagName, SyntaxKind.DocReturnsTag);
|
||||||
|
case "errors":
|
||||||
|
return parseDocSimpleTag(pos, tagName, SyntaxKind.DocErrorsTag);
|
||||||
default:
|
default:
|
||||||
return parseDocSimpleTag(pos, tagName, SyntaxKind.DocUnknownTag);
|
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)
|
visitNode(cb, node.tagName) || visitNode(cb, node.paramName) || visitEach(cb, node.content)
|
||||||
);
|
);
|
||||||
case SyntaxKind.DocReturnsTag:
|
case SyntaxKind.DocReturnsTag:
|
||||||
|
case SyntaxKind.DocErrorsTag:
|
||||||
case SyntaxKind.DocUnknownTag:
|
case SyntaxKind.DocUnknownTag:
|
||||||
return visitNode(cb, node.tagName) || visitEach(cb, node.content);
|
return visitNode(cb, node.tagName) || visitEach(cb, node.content);
|
||||||
|
|
||||||
|
|
|
@ -752,6 +752,7 @@ export enum SyntaxKind {
|
||||||
DocText,
|
DocText,
|
||||||
DocParamTag,
|
DocParamTag,
|
||||||
DocReturnsTag,
|
DocReturnsTag,
|
||||||
|
DocErrorsTag,
|
||||||
DocTemplateTag,
|
DocTemplateTag,
|
||||||
DocUnknownTag,
|
DocUnknownTag,
|
||||||
Projection,
|
Projection,
|
||||||
|
@ -1558,7 +1559,12 @@ export interface DocTagBaseNode extends BaseNode {
|
||||||
readonly content: readonly DocContent[];
|
readonly content: readonly DocContent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocTag = DocReturnsTagNode | DocParamTagNode | DocTemplateTagNode | DocUnknownTagNode;
|
export type DocTag =
|
||||||
|
| DocReturnsTagNode
|
||||||
|
| DocErrorsTagNode
|
||||||
|
| DocParamTagNode
|
||||||
|
| DocTemplateTagNode
|
||||||
|
| DocUnknownTagNode;
|
||||||
export type DocContent = DocTextNode;
|
export type DocContent = DocTextNode;
|
||||||
|
|
||||||
export interface DocTextNode extends BaseNode {
|
export interface DocTextNode extends BaseNode {
|
||||||
|
@ -1570,6 +1576,10 @@ export interface DocReturnsTagNode extends DocTagBaseNode {
|
||||||
readonly kind: SyntaxKind.DocReturnsTag;
|
readonly kind: SyntaxKind.DocReturnsTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DocErrorsTagNode extends DocTagBaseNode {
|
||||||
|
readonly kind: SyntaxKind.DocErrorsTag;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DocParamTagNode extends DocTagBaseNode {
|
export interface DocParamTagNode extends DocTagBaseNode {
|
||||||
readonly kind: SyntaxKind.DocParamTag;
|
readonly kind: SyntaxKind.DocParamTag;
|
||||||
readonly paramName: IdentifierNode;
|
readonly paramName: IdentifierNode;
|
||||||
|
|
|
@ -347,6 +347,7 @@ export function printNode(
|
||||||
case SyntaxKind.DocParamTag:
|
case SyntaxKind.DocParamTag:
|
||||||
case SyntaxKind.DocTemplateTag:
|
case SyntaxKind.DocTemplateTag:
|
||||||
case SyntaxKind.DocReturnsTag:
|
case SyntaxKind.DocReturnsTag:
|
||||||
|
case SyntaxKind.DocErrorsTag:
|
||||||
case SyntaxKind.DocUnknownTag:
|
case SyntaxKind.DocUnknownTag:
|
||||||
// https://github.com/microsoft/typespec/issues/1319 Tracks pretty-printing doc comments.
|
// https://github.com/microsoft/typespec/issues/1319 Tracks pretty-printing doc comments.
|
||||||
compilerAssert(
|
compilerAssert(
|
||||||
|
|
|
@ -77,6 +77,10 @@ export function getSummary(program: Program, type: Type): string | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
const docsKey = createStateSymbol("docs");
|
const docsKey = createStateSymbol("docs");
|
||||||
|
const returnsDocsKey = createStateSymbol("returnsDocs");
|
||||||
|
const errorsDocsKey = createStateSymbol("errorDocs");
|
||||||
|
type DocTarget = "self" | "returns" | "errors";
|
||||||
|
|
||||||
export interface DocData {
|
export interface DocData {
|
||||||
/**
|
/**
|
||||||
* Doc value.
|
* Doc value.
|
||||||
|
@ -88,7 +92,7 @@ export interface DocData {
|
||||||
* - `@doc` means the `@doc` decorator was used
|
* - `@doc` means the `@doc` decorator was used
|
||||||
* - `comment` means it was set from a `/** comment * /`
|
* - `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.
|
* @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) {
|
if (sourceObject) {
|
||||||
text = replaceTemplatedStringFromProperties(text, 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.
|
* @internal to be used to set the `@doc` from doc comment.
|
||||||
*/
|
*/
|
||||||
export function $docFromComment(context: DecoratorContext, target: Type, text: string) {
|
export function $docFromComment(
|
||||||
setDocData(context.program, target, { value: text, source: "comment" });
|
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) {
|
function getDocKey(target: DocTarget): symbol {
|
||||||
program.stateMap(docsKey).set(target, data);
|
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}
|
* Get the documentation information for the given type. In most cases you probably just want to use {@link getDoc}
|
||||||
* @param program Program
|
* @param program Program
|
||||||
|
@ -123,7 +158,7 @@ function setDocData(program: Program, target: Type, data: DocData) {
|
||||||
* @returns Doc data with source information.
|
* @returns Doc data with source information.
|
||||||
*/
|
*/
|
||||||
export function getDocData(program: Program, target: Type): DocData | undefined {
|
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
|
* @returns Documentation value
|
||||||
*/
|
*/
|
||||||
export function getDoc(program: Program, target: Type): string | undefined {
|
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) {
|
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 type = symbol.type ?? program.checker.getTypeForNode(symbol.declarations[0]);
|
||||||
const apiDocs = getDocData(program, type);
|
const apiDocs = getDocData(program, type);
|
||||||
// The doc comment is already included above we don't want to duplicate
|
// 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);
|
docs.push(apiDocs.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ok, strictEqual } from "assert";
|
import { ok, strictEqual } from "assert";
|
||||||
import { Model, Operation } from "../../src/core/index.js";
|
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";
|
import { BasicTestRunner, createTestRunner } from "../../src/testing/index.js";
|
||||||
|
|
||||||
describe("compiler: checker: doc comments", () => {
|
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 () => {
|
it("using @param in doc comment of operation applies doc on the parameters", async () => {
|
||||||
const { addUser } = (await runner.compile(`
|
const { addUser } = (await runner.compile(`
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,13 @@ import { Model, Operation, Scalar, getVisibility, isSecret } from "../../src/ind
|
||||||
import {
|
import {
|
||||||
getDoc,
|
getDoc,
|
||||||
getEncode,
|
getEncode,
|
||||||
|
getErrorsDoc,
|
||||||
getFriendlyName,
|
getFriendlyName,
|
||||||
getKeyName,
|
getKeyName,
|
||||||
getKnownValues,
|
getKnownValues,
|
||||||
getOverloadedOperation,
|
getOverloadedOperation,
|
||||||
getOverloads,
|
getOverloads,
|
||||||
|
getReturnsDoc,
|
||||||
isErrorModel,
|
isErrorModel,
|
||||||
} from "../../src/lib/decorators.js";
|
} from "../../src/lib/decorators.js";
|
||||||
import { BasicTestRunner, createTestRunner, expectDiagnostics } from "../../src/testing/index.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", () => {
|
describe("@friendlyName", () => {
|
||||||
it("applies @friendlyName on model", async () => {
|
it("applies @friendlyName on model", async () => {
|
||||||
const { A, B, C } = await runner.compile(`
|
const { A, B, C } = await runner.compile(`
|
||||||
|
|
|
@ -3,6 +3,8 @@ import {
|
||||||
Diagnostic,
|
Diagnostic,
|
||||||
DiagnosticCollector,
|
DiagnosticCollector,
|
||||||
getDoc,
|
getDoc,
|
||||||
|
getErrorsDoc,
|
||||||
|
getReturnsDoc,
|
||||||
isArrayModelType,
|
isArrayModelType,
|
||||||
isErrorModel,
|
isErrorModel,
|
||||||
isNullType,
|
isNullType,
|
||||||
|
@ -43,10 +45,10 @@ export function getResponsesForOperation(
|
||||||
// TODO how should we treat this? https://github.com/microsoft/typespec/issues/356
|
// TODO how should we treat this? https://github.com/microsoft/typespec/issues/356
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
processResponseType(program, diagnostics, responses, option.type);
|
processResponseType(program, diagnostics, operation, responses, option.type);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
processResponseType(program, diagnostics, responses, responseType);
|
processResponseType(program, diagnostics, operation, responses, responseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
return diagnostics.wrap(Object.values(responses));
|
return diagnostics.wrap(Object.values(responses));
|
||||||
|
@ -55,6 +57,7 @@ export function getResponsesForOperation(
|
||||||
function processResponseType(
|
function processResponseType(
|
||||||
program: Program,
|
program: Program,
|
||||||
diagnostics: DiagnosticCollector,
|
diagnostics: DiagnosticCollector,
|
||||||
|
operation: Operation,
|
||||||
responses: Record<string, HttpOperationResponse>,
|
responses: Record<string, HttpOperationResponse>,
|
||||||
responseType: Type
|
responseType: Type
|
||||||
) {
|
) {
|
||||||
|
@ -96,7 +99,7 @@ function processResponseType(
|
||||||
const response: HttpOperationResponse = responses[statusCode] ?? {
|
const response: HttpOperationResponse = responses[statusCode] ?? {
|
||||||
statusCode,
|
statusCode,
|
||||||
type: responseType,
|
type: responseType,
|
||||||
description: getResponseDescription(program, responseType, statusCode, bodyType),
|
description: getResponseDescription(program, operation, responseType, statusCode, bodyType),
|
||||||
responses: [],
|
responses: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -223,6 +226,7 @@ function getResponseBody(
|
||||||
|
|
||||||
function getResponseDescription(
|
function getResponseDescription(
|
||||||
program: Program,
|
program: Program,
|
||||||
|
operation: Operation,
|
||||||
responseType: Type,
|
responseType: Type,
|
||||||
statusCode: string,
|
statusCode: string,
|
||||||
bodyType: Type | undefined
|
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);
|
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");
|
||||||
|
});
|
||||||
|
});
|
Загрузка…
Ссылка в новой задаче