Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .chronus/changes/openapi-license-identifier-2026-7-17-21-15-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
changeKind: feature
packages:
- "@typespec/openapi"
---

Add support for the OpenAPI `info.license.identifier` field (an SPDX license expression) on the `License` model used with `@info`. The `identifier` and `url` fields are mutually exclusive; specifying both now results in a diagnostic.

```tsp
@info(#{
license: #{
name: "Apache 2.0",
identifier: "Apache-2.0",
},
})
```
1 change: 1 addition & 0 deletions packages/openapi/generated-defs/TypeSpec.OpenAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface Contact {
export interface License {
readonly [key: string]: unknown;
readonly name: string;
readonly identifier?: string;
readonly url?: string;
}

Expand Down
9 changes: 8 additions & 1 deletion packages/openapi/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,14 @@ 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. */
/**
* An [SPDX](https://spdx.org/licenses/) license expression for the API. The `identifier` field is mutually exclusive of the `url` field.
*/
identifier?: string;

/**
* A URL to the license used for the API. MUST be in the format of a URL. The `url` field is mutually exclusive of the `identifier` field.
*/
url?: url;

...Record<unknown>;
Expand Down
10 changes: 10 additions & 0 deletions packages/openapi/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,16 @@ export const $info: InfoDecorator = (
return;
}
}

// Validate that license `url` and `identifier` are mutually exclusive
if (data.license?.url && data.license?.identifier) {
reportDiagnostic(context.program, {
code: "license-url-and-identifier",
target: context.getArgumentTarget(0)!,
});
return;
}

setInfo(context.program, entity, data);
};

Expand Down
6 changes: 6 additions & 0 deletions packages/openapi/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`${"property"}: ${"value"} is not a valid URL.`,
},
},
"license-url-and-identifier": {
severity: "error",
messages: {
default: `License "url" and "identifier" are mutually exclusive. Specify only one of them.`,
},
},
"duplicate-tag": {
severity: "error",
messages: {
Expand Down
9 changes: 8 additions & 1 deletion packages/openapi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@ 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. */
/**
* An SPDX license expression for the API. Mutually exclusive of the `url` field.
*/
identifier?: string;

/**
* A URL to the license used for the API. MUST be in the format of a URL. Mutually exclusive of the `identifier` field.
*/
url?: string;
}

Expand Down
42 changes: 42 additions & 0 deletions packages/openapi/test/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,48 @@ describe("@info", () => {
});
});

it("set license identifier", async () => {
const { program, Service } = await Tester.compile(t.code`
@info(#{
title: "My API",
version: "1.0.0",
license: #{
name: "Apache 2.0",
identifier: "Apache-2.0"
},
})
namespace ${t.namespace("Service")} {}
`);

deepStrictEqual(getInfo(program, Service), {
title: "My API",
version: "1.0.0",
license: {
name: "Apache 2.0",
identifier: "Apache-2.0",
},
});
});

it("emit diagnostic if license has both url and identifier", async () => {
const diagnostics = await Tester.diagnose(`
@info(#{
title: "My API",
version: "1.0.0",
license: #{
name: "Apache 2.0",
identifier: "Apache-2.0",
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
},
})
namespace Service {}
`);

expectDiagnostics(diagnostics, {
code: "@typespec/openapi/license-url-and-identifier",
});
});

it("resolveInfo() merge with data from @service and @summary", async () => {
const { program, Service } = await Tester.compile(t.code`
@service(#{
Expand Down
21 changes: 21 additions & 0 deletions packages/openapi3/test/info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,25 @@ worksFor(supportedVersions, ({ openApiFor }) => {
},
});
});

it("set the license identifier with @info decorator", async () => {
const res = await openApiFor(
`
@service
@info(#{
license: #{
name: "Apache 2.0",
identifier: "Apache-2.0"
},
})
namespace Foo {
op test(): string;
}
`,
);
deepStrictEqual(res.info.license, {
name: "Apache 2.0",
identifier: "Apache-2.0",
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ model TypeSpec.OpenAPI.License

#### Properties

| Name | Type | Description |
| ---- | --------- | ---------------------------------------------------------------------- |
| name | `string` | The license name used for the API. |
| url? | `url` | A URL to the license used for the API. MUST be in the format of a URL. |
| | `unknown` | Additional properties |
| Name | Type | Description |
| ----------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| name | `string` | The license name used for the API. |
| identifier? | `string` | An [SPDX](https://spdx.org/licenses/) license expression for the API. The `identifier` field is mutually exclusive of the `url` field. |
| url? | `url` | A URL to the license used for the API. MUST be in the format of a URL. The `url` field is mutually exclusive of the `identifier` field. |
| | `unknown` | Additional properties |

### `TagMetadata` {#TypeSpec.OpenAPI.TagMetadata}

Expand Down
Loading