Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate @service version and allow @OpenAPI.info to take all properties allowed by openapi #2902

Merged
Merged
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
Loading