Skip to content

Commit

Permalink
Feature: Add new @info decorator (#2169)
Browse files Browse the repository at this point in the history
fix [#980](#980)

Add new `@info` decorator providing the ability to specify the
additional fields from openapi info object.

---------

Co-authored-by: Brian Terlson <brian.terlson@microsoft.com>
  • Loading branch information
timotheeguerin and bterlson authored Jul 12, 2023
1 parent 09c2561 commit 5e53967
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/openapi",
"comment": "Add new `@info` decorator providing the ability to specify the additional fields from openapi info object.",
"type": "none"
}
],
"packageName": "@typespec/openapi"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/openapi3",
"comment": "Add support for `@info` decorator providing the ability to specify the additional fields from openapi info object.",
"type": "none"
}
],
"packageName": "@typespec/openapi3"
}
33 changes: 33 additions & 0 deletions docs/standard-library/openapi/reference/data-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: "Data types"
toc_min_heading_level: 2
toc_max_heading_level: 3
---

# Data types

## OpenAPI

### `AdditionalInfo` {#OpenAPI.AdditionalInfo}

Additional information for the OpenAPI document.

```typespec
model OpenAPI.AdditionalInfo
```

### `Contact` {#OpenAPI.Contact}

Contact information for the exposed API.

```typespec
model OpenAPI.Contact
```

### `License` {#OpenAPI.License}

License information for the exposed API.

```typespec
model OpenAPI.License
```
19 changes: 19 additions & 0 deletions docs/standard-library/openapi/reference/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ Specify the OpenAPI `externalDocs` property for this type.
op listPets(): Pet[];
```

### `@info` {#@OpenAPI.info}

Specify OpenAPI additional information.
The service `title` and `version` are already specified using `@service`.

```typespec
@OpenAPI.info(additionalInfo: OpenAPI.AdditionalInfo)
```

#### Target

`Namespace`

#### Parameters

| Name | Type | Description |
| -------------- | ------------------------------ | ---------------------- |
| additionalInfo | `model OpenAPI.AdditionalInfo` | Additional information |

### `@operationId` {#@OpenAPI.operationId}

Specify the OpenAPI `operationId` property for this operation.
Expand Down
7 changes: 7 additions & 0 deletions docs/standard-library/openapi/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,11 @@ npm install --save-peer @typespec/openapi
- [`@defaultResponse`](./decorators.md#@OpenAPI.defaultResponse)
- [`@extension`](./decorators.md#@OpenAPI.extension)
- [`@externalDocs`](./decorators.md#@OpenAPI.externalDocs)
- [`@info`](./decorators.md#@OpenAPI.info)
- [`@operationId`](./decorators.md#@OpenAPI.operationId)

### Models

- [`AdditionalInfo`](./data-types.md#OpenAPI.AdditionalInfo)
- [`Contact`](./data-types.md#OpenAPI.Contact)
- [`License`](./data-types.md#OpenAPI.License)
40 changes: 40 additions & 0 deletions packages/openapi/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,43 @@ extern dec defaultResponse(target: Model);
* ```
*/
extern dec externalDocs(target: unknown, url: valueof string, description?: valueof string);

/** Additional information for the OpenAPI document. */
model AdditionalInfo {
/** A URL to the Terms of Service for the API. MUST be in the format of a URL. */
termsOfService?: url;

/** The contact information for the exposed API. */
contact?: Contact;

/** The license information for the exposed API. */
license?: License;
}

/** Contact information for the exposed API. */
model Contact {
/** The identifying name of the contact person/organization. */
name?: string;

/** The URL pointing to the contact information. MUST be in the format of a URL. */
url?: url;

/** The email address of the contact person/organization. MUST be in the format of an email address. */
email?: string;
}

/** License information for the exposed API. */
model License {
/** The license name used for the API. */
name: string;

/** A URL to the license used for the API. MUST be in the format of a URL. */
url?: url;
}

/**
* Specify OpenAPI additional information.
* The service `title` and `version` are already specified using `@service`.
* @param additionalInfo Additional information
*/
extern dec info(target: Namespace, additionalInfo: AdditionalInfo);
14 changes: 13 additions & 1 deletion packages/openapi/src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
DecoratorContext,
Model,
Namespace,
Operation,
Program,
Type,
Expand All @@ -9,7 +10,7 @@ import {
} from "@typespec/compiler";
import { setStatusCode } from "@typespec/http";
import { createStateSymbol, reportDiagnostic } from "./lib.js";
import { ExtensionKey } from "./types.js";
import { AdditionalInfo, ExtensionKey } from "./types.js";

export const namespace = "OpenAPI";

Expand Down Expand Up @@ -123,3 +124,14 @@ export function $externalDocs(
export function getExternalDocs(program: Program, entity: Type): ExternalDocs | undefined {
return program.stateMap(externalDocsKey).get(entity);
}

const infoKey = createStateSymbol("info");
export function $info(context: DecoratorContext, entity: Namespace, model: Model) {
const [data, diagnostics] = typespecTypeToJson(model, context.getArgumentTarget(0)!);
context.program.reportDiagnostics(diagnostics);
context.program.stateMap(infoKey).set(entity, data);
}

export function getInfo(program: Program, entity: Namespace): AdditionalInfo | undefined {
return program.stateMap(infoKey).get(entity);
}
33 changes: 33 additions & 0 deletions packages/openapi/src/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
export type ExtensionKey = `x-${string}`;

/**
* OpenAPI additional information
*/
export interface AdditionalInfo {
/** A URL to the Terms of Service for the API. MUST be in the format of a URL. */
termsOfService?: string;

/** The contact information for the exposed API. */
contact?: Contact;

/** The license information for the exposed API. */
license?: License;
}

export interface Contact {
/** The identifying name of the contact person/organization. */
name?: string;

/** The URL pointing to the contact information. MUST be in the format of a URL. */
url?: string;

/** The email address of the contact person/organization. MUST be in the format of an email address. */
email?: string;
}

export interface License {
/** The license name used for the API. */
name: string;

/** A URL to the license used for the API. MUST be in the format of a URL. */
url?: string;
}
60 changes: 59 additions & 1 deletion packages/openapi/test/decorators.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Namespace } from "@typespec/compiler";
import { BasicTestRunner, expectDiagnostics } from "@typespec/compiler/testing";
import { deepStrictEqual } from "assert";
import { getExtensions, getExternalDocs } from "../src/decorators.js";
import { getExtensions, getExternalDocs, getInfo } from "../src/decorators.js";
import { createOpenAPITestRunner } from "./test-host.js";

describe("openapi: decorators", () => {
Expand Down Expand Up @@ -146,4 +147,61 @@ describe("openapi: decorators", () => {
});
});
});

describe("@info", () => {
it("emit diagnostic if use on non namespace", async () => {
const diagnostics = await runner.diagnose(`
@info({})
model Foo {}
`);

expectDiagnostics(diagnostics, {
code: "decorator-wrong-target",
message: "Cannot apply @info decorator to Foo since it is not assignable to Namespace",
});
});

it("emit diagnostic if info parameter is not an object", async () => {
const diagnostics = await runner.diagnose(`
@info(123)
namespace Service {}
`);

expectDiagnostics(diagnostics, {
code: "invalid-argument",
message: "Argument '123' is not assignable to parameter of type 'OpenAPI.AdditionalInfo'",
});
});

it("set all properties", async () => {
const { Service } = (await runner.compile(`
@info({
termsOfService: "http://example.com/terms/",
contact: {
name: "API Support",
url: "http://www.example.com/support",
email: "support@example.com"
},
license: {
name: "Apache 2.0",
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
},
})
@test namespace Service {}
`)) as { Service: Namespace };

deepStrictEqual(getInfo(runner.program, Service), {
termsOfService: "http://example.com/terms/",
contact: {
name: "API Support",
url: "http://www.example.com/support",
email: "support@example.com",
},
license: {
name: "Apache 2.0",
url: "http://www.apache.org/licenses/LICENSE-2.0.html",
},
});
});
});
});
2 changes: 2 additions & 0 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import {
checkDuplicateTypeName,
getExtensions,
getExternalDocs,
getInfo,
getOpenAPITypeName,
getParameterKey,
isReadonlyProperty,
Expand Down Expand Up @@ -262,6 +263,7 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt
title: service.title ?? "(title)",
version: version ?? service.version ?? "0000-00-00",
description: getDoc(program, service.type),
...getInfo(program, service.type),
},
externalDocs: getExternalDocs(program, service.type),
tags: [],
Expand Down
37 changes: 37 additions & 0 deletions packages/openapi3/test/info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,41 @@ describe("openapi3: info", () => {
description: "more info",
});
});

it("set the additional information with @info decorator", async () => {
const res = await openApiFor(
`
@service
@info({
termsOfService: "http://example.com/terms/",
contact: {
name: "API Support",
url: "http://www.example.com/support",
email: "support@example.com"
},
license: {
name: "Apache 2.0",
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
},
})
namespace Foo {
op test(): string;
}
`
);
deepStrictEqual(res.info, {
title: "(title)",
version: "0000-00-00",
termsOfService: "http://example.com/terms/",
contact: {
name: "API Support",
url: "http://www.example.com/support",
email: "support@example.com",
},
license: {
name: "Apache 2.0",
url: "http://www.apache.org/licenses/LICENSE-2.0.html",
},
});
});
});

0 comments on commit 5e53967

Please sign in to comment.