Skip to content

Commit

Permalink
Deprecate @service version and allow @OpenAPI.info to take all pr…
Browse files Browse the repository at this point in the history
…operties allowed by openapi (#2902)

fix [#2821](#2821)

## Deprecate `@service({version`

Using this property will emit a deprecation warning

## Cover everything with `@OpenAPI.info`

Makes sure all the properties allowed on the `info` object of openapi
specification can also be provided in `@info`. The properties will
either override other ways of specifying those previously or be the only
way.
- `@info({description` would override `@doc` on service namespace
- `@info({summary` would override `@summary` on service namespace
- `@info({title` would override `@service({title}` on service namespace
  • Loading branch information
timotheeguerin authored Feb 28, 2024
1 parent 9d8cfb0 commit 717af64
Show file tree
Hide file tree
Showing 51 changed files with 179 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: deprecation
packages:
- "@typespec/compiler"
---

Deprecate `@service` version property. If wanting to describe a service versioning you can use the `@typespec/versioning` library. If wanting to describe the project version you can use the package.json version. For OpenAPI generation. the `@OpenAPI.info` nows decorator allows providing the document version.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/openapi"
- "@typespec/openapi3"
---

Add support for all properties of openapi `info` object on the `@info` decorator
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: internal
packages:
- "@typespec/versioning"
---
14 changes: 11 additions & 3 deletions packages/compiler/src/lib/service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { validateDecoratorUniqueOnNode } from "../core/decorator-utils.js";
import { getTypeName } from "../core/index.js";
import { getTypeName, reportDeprecated } from "../core/index.js";
import { reportDiagnostic } from "../core/messages.js";
import { Program } from "../core/program.js";
import { DecoratorContext, Model, Namespace } from "../core/types.js";

export interface ServiceDetails {
title?: string;
/** @deprecated Service version is deprecated. If wanting to describe a service versioning you can use the `@typespec/versioning` library. If wanting to describe the project version you can use the package.json version */
version?: string;
}

Expand Down Expand Up @@ -68,7 +69,7 @@ export function $service(context: DecoratorContext, target: Namespace, options?:

const serviceDetails: ServiceDetails = {};
const title = options?.properties.get("title")?.type;
const version = options?.properties.get("version")?.type;
const versionProp = options?.properties.get("version");
if (title) {
if (title.kind === "String") {
serviceDetails.title = title.value;
Expand All @@ -80,8 +81,15 @@ export function $service(context: DecoratorContext, target: Namespace, options?:
});
}
}
if (version) {
if (versionProp) {
const version = versionProp.type;
reportDeprecated(
context.program,
"version: property is deprecated in @service. If wanting to describe a service versioning you can use the `@typespec/versioning` library. If wanting to describe the project version you can use the package.json version.",
versionProp
);
if (version.kind === "String") {
// eslint-disable-next-line deprecation/deprecation
serviceDetails.version = version.value;
} else {
reportDiagnostic(context.program, {
Expand Down
10 changes: 8 additions & 2 deletions packages/compiler/test/decorators/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ describe("compiler: service", () => {

it("customize service version", async () => {
const { S } = await runner.compile(`
@test @service({version: "1.2.3"}) namespace S {}
@test @service({
#suppress "deprecated" "test"
version: "1.2.3"
}) namespace S {}
`);

Expand All @@ -91,7 +94,10 @@ describe("compiler: service", () => {

it("emit diagnostic if service version is not a string", async () => {
const diagnostics = await runner.diagnose(`
@test @service({version: 123}) namespace S {}
@test @service({
#suppress "deprecated" "test"
version: 123
}) namespace S {}
`);

expectDiagnostics(diagnostics, {
Expand Down
9 changes: 9 additions & 0 deletions packages/openapi/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ extern dec externalDocs(target: unknown, url: valueof string, description?: valu

/** Additional information for the OpenAPI document. */
model AdditionalInfo {
/** The title of the API. Overrides the `@service` title. */
title?: string;

/** A short summary of the API. Overrides the `@summary` provided on the service namespace. */
summary?: string;

/** The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API implementation version). */
version?: string;

/** A URL to the Terms of Service for the API. MUST be in the format of a URL. */
termsOfService?: url;

Expand Down
29 changes: 28 additions & 1 deletion packages/openapi/src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {
DecoratorContext,
getDoc,
getService,
getSummary,
Model,
Namespace,
Operation,
Expand Down Expand Up @@ -128,11 +131,35 @@ export function getExternalDocs(program: Program, entity: Type): ExternalDocs |

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

export function getInfo(program: Program, entity: Namespace): AdditionalInfo | undefined {
return program.stateMap(infoKey).get(entity);
}

/** Resolve the info entry by merging data specified with `@service`, `@summary` and `@info`. */
export function resolveInfo(program: Program, entity: Namespace): AdditionalInfo | undefined {
const info = getInfo(program, entity);
const service = getService(program, entity);
return omitUndefined({
...info,
title: info?.title ?? service?.title,
// eslint-disable-next-line deprecation/deprecation
version: info?.version ?? service?.version,
summary: info?.summary ?? getSummary(program, entity),
description: info?.description ?? getDoc(program, entity),
});
}

function omitUndefined<T extends Record<string, unknown>>(data: T): T {
return Object.fromEntries(Object.entries(data).filter(([k, v]) => v !== undefined)) as any;
}
12 changes: 12 additions & 0 deletions packages/openapi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ export type ExtensionKey = `x-${string}`;
* OpenAPI additional information
*/
export interface AdditionalInfo {
/** The title of the API. Overrides the `@service` title. */
title?: string;

/** A short summary of the API. Overrides the `@summary` provided on the service namespace. */
summary?: string;

/** A description of the API. Overrides the `@doc` provided on the service namespace. */
description?: string;

/** The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API implementation version). */
version?: string;

/** A URL to the Terms of Service for the API. MUST be in the format of a URL. */
termsOfService?: string;

Expand Down
40 changes: 39 additions & 1 deletion packages/openapi/test/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Namespace } from "@typespec/compiler";
import { BasicTestRunner, expectDiagnostics } from "@typespec/compiler/testing";
import { deepStrictEqual } from "assert";
import { beforeEach, describe, it } from "vitest";
import { getExtensions, getExternalDocs, getInfo } from "../src/decorators.js";
import { getExtensions, getExternalDocs, getInfo, resolveInfo } from "../src/decorators.js";
import { createOpenAPITestRunner } from "./test-host.js";

describe("openapi: decorators", () => {
Expand Down Expand Up @@ -178,6 +178,9 @@ describe("openapi: decorators", () => {
it("set all properties", async () => {
const { Service } = (await runner.compile(`
@info({
title: "My API",
version: "1.0.0",
summary: "My API summary",
termsOfService: "http://example.com/terms/",
contact: {
name: "API Support",
Expand All @@ -193,6 +196,9 @@ describe("openapi: decorators", () => {
`)) as { Service: Namespace };

deepStrictEqual(getInfo(runner.program, Service), {
title: "My API",
version: "1.0.0",
summary: "My API summary",
termsOfService: "http://example.com/terms/",
contact: {
name: "API Support",
Expand All @@ -205,5 +211,37 @@ describe("openapi: decorators", () => {
},
});
});

it("resolveInfo() merge with data from @service and @summary", async () => {
const { Service } = (await runner.compile(`
@service({
title: "Service API",
#suppress "deprecated" "Test"
version: "2.0.0"
})
@summary("My summary")
@info({
version: "1.0.0",
termsOfService: "http://example.com/terms/",
})
@test namespace Service {}
`)) as { Service: Namespace };

deepStrictEqual(resolveInfo(runner.program, Service), {
title: "Service API",
version: "1.0.0",
summary: "My summary",
termsOfService: "http://example.com/terms/",
});
});

it("resolveInfo() returns empty object if nothing is provided", async () => {
const { Service } = (await runner.compile(`
@test namespace Service {}
`)) as { Service: Namespace };

deepStrictEqual(resolveInfo(runner.program, Service), {});
});
});
});
10 changes: 5 additions & 5 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ import {
import {
getExtensions,
getExternalDocs,
getInfo,
getOpenAPITypeName,
getParameterKey,
isDefaultResponse,
isReadonlyProperty,
resolveInfo,
resolveOperationId,
shouldInline,
} from "@typespec/openapi";
Expand Down Expand Up @@ -304,13 +304,13 @@ function createOAPIEmitter(
const securitySchemes = getOpenAPISecuritySchemes(allHttpAuthentications);
const security = getOpenAPISecurity(defaultAuth);

const info = resolveInfo(program, service.type);
root = {
openapi: "3.0.0",
info: {
title: service.title ?? "(title)",
version: version ?? service.version ?? "0000-00-00",
description: getDoc(program, service.type),
...getInfo(program, service.type),
title: "(title)",
...info,
version: version ?? info?.version ?? "0.0.0",
},
externalDocs: getExternalDocs(program, service.type),
tags: [],
Expand Down
7 changes: 5 additions & 2 deletions packages/openapi3/test/info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ describe("openapi3: info", () => {
it("set the service version with @service", async () => {
const res = await openApiFor(
`
@service({version: "1.2.3-test"})
@service({
#suppress "deprecated" "For test"
version: "1.2.3-test"
})
namespace Foo {
op test(): string;
}
Expand Down Expand Up @@ -78,7 +81,7 @@ describe("openapi3: info", () => {
);
deepStrictEqual(res.info, {
title: "(title)",
version: "0000-00-00",
version: "0.0.0",
termsOfService: "http://example.com/terms/",
contact: {
name: "API Support",
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi3/test/output-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("openapi3: output file", () => {
` "openapi": "3.0.0",`,
` "info": {`,
` "title": "(title)",`,
` "version": "0000-00-00"`,
` "version": "0.0.0"`,
` },`,
` "tags": [],`,
` "paths": {},`,
Expand All @@ -28,7 +28,7 @@ describe("openapi3: output file", () => {
`openapi: 3.0.0`,
`info:`,
` title: (title)`,
` version: 0000-00-00`,
` version: 0.0.0`,
`tags: []`,
`paths: {}`,
`components: {}`,
Expand Down
1 change: 0 additions & 1 deletion packages/samples/specs/multiple-types-union/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import "@typespec/openapi";

@service({
title: "Pet Store Service",
version: "2021-03-25",
})
namespace PetStore;
using TypeSpec.Http;
Expand Down
1 change: 0 additions & 1 deletion packages/samples/specs/petstore/petstore.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ using TypeSpec.Http;

@service({
title: "Pet Store Service",
version: "2021-03-25",
})
@doc("This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.")
namespace PetStore;
Expand Down
1 change: 0 additions & 1 deletion packages/samples/specs/rest/petstore/petstore.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import "@typespec/openapi";

@service({
title: "Pet Store Service",
version: "2021-03-25",
})
namespace PetStore;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Authenticated service with interface override
version: 0000-00-00
version: 0.0.0
tags: []
paths:
/one:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Authenticated service with method override
version: 0000-00-00
version: 0.0.0
tags: []
paths:
/one:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Authenticated service
version: 0000-00-00
version: 0.0.0
tags: []
paths:
/:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Binary sample
version: 0000-00-00
version: 0.0.0
tags: []
paths:
/test/base64:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Documentation sample
version: 0000-00-00
version: 0.0.0
tags: []
paths:
/foo/DefaultDescriptions:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
openapi: 3.0.0
info:
title: Sample showcasing encoded names
version: 0000-00-00
description: |-
This example showcase providing a different name over the wire.
In this example the `WithEncodedNames` model has a `notBefore` property that should get serialized as `nbf` when serialized as json.
version: 0.0.0
tags: []
paths:
/:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: (title)
version: 0000-00-00
version: 0.0.0
tags: []
paths:
/:
Expand Down
Loading

0 comments on commit 717af64

Please sign in to comment.