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

Missing decorators.. #5173

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
95883a2
initial
Nov 21, 2024
0d97bb9
update
Nov 21, 2024
f81f9e0
Merge branch 'microsoft:main' into MissingDecorators
skywing918 Nov 22, 2024
5591d33
update
Nov 22, 2024
58e1c02
Merge branch 'main' into MissingDecorators
skywing918 Nov 22, 2024
5059191
up
Nov 25, 2024
0d80ef2
Merge branch 'main' into MissingDecorators
skywing918 Nov 25, 2024
3cbf773
Merge branch 'main' into MissingDecorators
skywing918 Nov 26, 2024
495b6e5
Merge branch 'main' into MissingDecorators
skywing918 Nov 28, 2024
fae84ce
Merge branch 'main' into MissingDecorators
skywing918 Nov 28, 2024
f1d755e
Merge branch 'main' into MissingDecorators
skywing918 Nov 29, 2024
92099b2
Merge branch 'main' into MissingDecorators
skywing918 Dec 1, 2024
17e2834
Merge branch 'main' into MissingDecorators
skywing918 Dec 2, 2024
037dc16
change the defind and message
Dec 2, 2024
a86ab09
refact get for decorator
Dec 2, 2024
c21d9ff
Merge branch 'main' into MissingDecorators
skywing918 Dec 3, 2024
1bfe9bb
up
Dec 3, 2024
4c33177
Merge branch 'main' into MissingDecorators
skywing918 Dec 4, 2024
62055a0
up
Dec 4, 2024
0974afd
Merge branch 'MissingDecorators' of https://github.com/skywing918/typ…
Dec 4, 2024
eef3a42
up
Dec 4, 2024
d158a6f
Merge branch 'main' into MissingDecorators
skywing918 Dec 5, 2024
d1fc719
Merge branch 'microsoft:main' into MissingDecorators
skywing918 Dec 6, 2024
756d9ce
check target for minProperties/maxProperties/multipleOf
Dec 6, 2024
cef7ff9
Merge branch 'main' into MissingDecorators
skywing918 Dec 6, 2024
c853c31
up
Dec 6, 2024
90ad04d
Merge branch 'main' into MissingDecorators
skywing918 Dec 6, 2024
7223ea6
add warning if set minProperties/maxProperties on components.parameters
Dec 6, 2024
83d473e
Merge branch 'main' into MissingDecorators
skywing918 Dec 7, 2024
5d95393
Merge branch 'main' into MissingDecorators
skywing918 Dec 9, 2024
d58c960
Merge branch 'main' into MissingDecorators
skywing918 Dec 9, 2024
0f36c61
Merge branch 'main' into MissingDecorators
skywing918 Dec 10, 2024
d706f92
Merge branch 'main' into MissingDecorators
skywing918 Dec 11, 2024
c10252d
Merge branch 'main' into MissingDecorators
skywing918 Dec 11, 2024
3bc31c3
Merge branch 'main' into MissingDecorators
skywing918 Dec 12, 2024
ce5e664
Merge branch 'main' into MissingDecorators
skywing918 Dec 12, 2024
411ab37
Merge branch 'main' into MissingDecorators
skywing918 Dec 13, 2024
4e64cee
Merge branch 'main' into MissingDecorators
skywing918 Dec 16, 2024
dbd01f1
Merge branch 'main' into MissingDecorators
skywing918 Dec 24, 2024
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
7 changes: 7 additions & 0 deletions .chronus/changes/MissingDecorators-2024-10-22-15-39-24.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/openapi"
---

@extension decorator supports multipleOf, uniqueItems, maxProperties, and minProperties
7 changes: 7 additions & 0 deletions .chronus/changes/MissingDecorators-2024-10-22-15-44-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
changeKind: feature
packages:
- "@typespec/openapi3"
---

@extension decorator supports keywords: multipleOf, uniqueItems, maxProperties, and minProperties,apply to properties in the Schema Object.
46 changes: 41 additions & 5 deletions packages/openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ op listPets(): Pet[] | PetStoreResponse;
Attach some custom data to the OpenAPI element generated from this type.

```typespec
@TypeSpec.OpenAPI.extension(key: valueof string, value: unknown)
@TypeSpec.OpenAPI.extension(key: valueof string, value?: unknown)
```

##### Target
Expand All @@ -59,10 +59,10 @@ Attach some custom data to the OpenAPI element generated from this type.

##### Parameters

| Name | Type | Description |
| ----- | ---------------- | ----------------------------------- |
| key | `valueof string` | Extension key. Must start with `x-` |
| value | `unknown` | Extension value. |
| Name | Type | Description |
| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------- |
| key | `valueof string` | minProperties/maxProperties/uniqueItems/multipleOf or Extension key. the extension key must start with `x-` |
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
| value | `unknown` | Extension value. |

##### Examples

Expand All @@ -77,6 +77,42 @@ Attach some custom data to the OpenAPI element generated from this type.
op read(): string;
```

###### Specify that every item in the array must be unique.

```typespec
model Foo {
@extension("uniqueItems")
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
x: unknown[];
}
```

###### Specify that the numeric type must be a multiple of some numeric value.

```typespec
model Foo {
@extension("multipleOf", 1)
x: int32;
}
```

###### Specify the maximum number of properties this object can have.

```typespec
model Foo {
@extension("maxProperties", 1)
x: int32;
}
```

###### Specify the minimum number of properties this object can have.

```typespec
model Foo {
@extension("minProperties", 1)
x: int32;
}
```

#### `@externalDocs`

Specify the OpenAPI `externalDocs` property for this type.
Expand Down
32 changes: 30 additions & 2 deletions packages/openapi/generated-defs/TypeSpec.OpenAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,48 @@ export type OperationIdDecorator = (
/**
* Attach some custom data to the OpenAPI element generated from this type.
*
* @param key Extension key. Must start with `x-`
* @param key minProperties/maxProperties/uniqueItems/multipleOf or Extension key. the extension key must start with `x-`
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
* @param value Extension value.
* @example
* ```typespec
* @extension("x-custom", "My value")
* @extension("x-pageable", {nextLink: "x-next-link"})
* op read(): string;
* ```
* @example Specify that every item in the array must be unique.
* ```typespec
* model Foo {
* @extension("uniqueItems")
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
* x: unknown[];
* };
* ```
* @example Specify that the numeric type must be a multiple of some numeric value.
* ```typespec
* model Foo {
* @extension("multipleOf", 1)
* x: int32;
* };
* ```
* @example Specify the maximum number of properties this object can have.
* ```typespec
* model Foo {
* @extension("maxProperties", 1)
* x: int32;
* };
* ```
* @example Specify the minimum number of properties this object can have.
* ```typespec
* model Foo {
* @extension("minProperties", 1)
* x: int32;
* };
* ```
*/
export type ExtensionDecorator = (
context: DecoratorContext,
target: Type,
key: string,
value: Type,
value?: Type,
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
) => void;

/**
Expand Down
37 changes: 34 additions & 3 deletions packages/openapi/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,49 @@ extern dec operationId(target: Operation, operationId: valueof string);
/**
* Attach some custom data to the OpenAPI element generated from this type.
*
* @param key Extension key. Must start with `x-`
* @param key minProperties/maxProperties/uniqueItems/multipleOf or Extension key. the extension key must start with `x-`
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
* @param value Extension value.
*
* @example
*
* ```typespec
* @extension("x-custom", "My value")
* @extension("x-pageable", {nextLink: "x-next-link"})
* op read(): string;
* ```
*
* @example Specify that every item in the array must be unique.
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
* ```typespec
* model Foo {
* @extension("uniqueItems")
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
* x: unknown[];
* };
* ```
*
* @example Specify that the numeric type must be a multiple of some numeric value.
* ```typespec
* model Foo {
* @extension("multipleOf", 1)
* x: int32;
* };
* ```
*
* @example Specify the maximum number of properties this object can have.
* ```typespec
* model Foo {
* @extension("maxProperties", 1)
* x: int32;
* };
* ```
*
* @example Specify the minimum number of properties this object can have.
* ```typespec
* model Foo {
* @extension("minProperties", 1)
* x: int32;
* };
* ```
*/
extern dec extension(target: unknown, key: valueof string, value: unknown);
extern dec extension(target: unknown, key: valueof string, value?: unknown);

/**
* Specify that this model is to be treated as the OpenAPI `default` response.
Expand Down
78 changes: 69 additions & 9 deletions packages/openapi/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from "../generated-defs/TypeSpec.OpenAPI.js";
import { isOpenAPIExtensionKey, validateAdditionalInfoModel, validateIsUri } from "./helpers.js";
import { createStateSymbol, OpenAPIKeys, reportDiagnostic } from "./lib.js";
import { AdditionalInfo, ExtensionKey, ExternalDocs } from "./types.js";
import { AdditionalInfo, ExtensionKey, ExternalDocs, SchemaExtensionKey } from "./types.js";

const operationIdsKey = createStateSymbol("operationIds");
/**
Expand Down Expand Up @@ -56,21 +56,78 @@ export const $extension: ExtensionDecorator = (
context: DecoratorContext,
entity: Type,
extensionName: string,
value: TypeSpecValue,
value?: TypeSpecValue,
) => {
if (!isOpenAPIExtensionKey(extensionName)) {
const validExtensions = ["minProperties", "maxProperties", "uniqueItems", "multipleOf"];
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
const isModelProperty = entity.kind === "ModelProperty";

if (
!(validExtensions.includes(extensionName) && isModelProperty) &&
!isOpenAPIExtensionKey(extensionName)
) {
reportDiagnostic(context.program, {
code: "invalid-extension-key",
messageId: "decorator",
format: { value: extensionName },
target: entity,
});
return;
}

if (extensionName !== "uniqueItems" && value === undefined) {
reportDiagnostic(context.program, {
code: "missing-extension-value",
format: { extension: extensionName },
target: entity,
});
return;
}

const [data, diagnostics] = typespecTypeToJson(value, entity);
if (diagnostics.length > 0) {
context.program.reportDiagnostics(diagnostics);
let inputData: any = true;
if (value !== undefined) {
const [data, diagnostics] = typespecTypeToJson(value, entity);
const numberExtensions = ["minProperties", "maxProperties", "multipleOf"];
if (numberExtensions.includes(extensionName) && isNaN(Number(data))) {
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
reportDiagnostic(context.program, {
code: "invalid-extension-value",
format: { extensionName: extensionName },
target: entity,
});
return;
}

if (extensionName === "uniqueItems" && data !== true && data !== false) {
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
reportDiagnostic(context.program, {
code: "invalid-extension-value",
messageId: "uniqueItems",
format: { extensionName: extensionName },
target: entity,
});
return;
}

switch (extensionName) {
case "minProperties":
case "maxProperties":
case "multipleOf":
inputData = Number(data);
break;
case "uniqueItems":
inputData = data === true ? true : false;
break;
default:
if (diagnostics.length > 0) {
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
context.program.reportDiagnostics(diagnostics);
}
inputData = data;
}
}
setExtension(context.program, entity, extensionName as ExtensionKey, data);
setExtension(
context.program,
entity,
extensionName as ExtensionKey | SchemaExtensionKey,
inputData,
);
};

/**
Expand All @@ -97,7 +154,7 @@ export function setInfo(
export function setExtension(
program: Program,
entity: Type,
extensionName: ExtensionKey,
extensionName: ExtensionKey | SchemaExtensionKey,
data: unknown,
) {
const openApiExtensions = program.stateMap(openApiExtensionKey);
Expand All @@ -111,7 +168,10 @@ export function setExtension(
* @param program Program
* @param entity Type
*/
export function getExtensions(program: Program, entity: Type): ReadonlyMap<ExtensionKey, any> {
export function getExtensions(
program: Program,
entity: Type,
): ReadonlyMap<ExtensionKey | SchemaExtensionKey, any> {
return program.stateMap(openApiExtensionKey).get(entity) ?? new Map<ExtensionKey, any>();
}

Expand Down
14 changes: 14 additions & 0 deletions packages/openapi/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ export const $lib = createTypeSpecLibrary({
severity: "error",
messages: {
default: paramMessage`OpenAPI extension must start with 'x-' but was '${"value"}'`,
decorator: paramMessage`extension decorator only support minProperties/maxProperties/uniqueItems/multipleOf/'x-' but was '${"value"}'`,
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
},
},
"missing-extension-value": {
severity: "error",
messages: {
default: paramMessage`extension should have a value for '${"extension"}'`,
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
},
},
"invalid-extension-value": {
severity: "error",
messages: {
default: paramMessage`'${"extensionName"}' must number.'`,
uniqueItems: paramMessage`${"extensionName"}' must boolean.`,
},
},
"duplicate-type-name": {
Expand Down
1 change: 1 addition & 0 deletions packages/openapi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
export type ExtensionKey = `x-${string}`;

export type SchemaExtensionKey = "minProperties" | "maxProperties" | "uniqueItems" | "multipleOf";
/**
* OpenAPI additional information
*/
Expand Down
38 changes: 37 additions & 1 deletion packages/openapi/test/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,42 @@ describe("openapi: decorators", () => {
});

describe("@extension", () => {
it.each([
["minProperties", 1],
["maxProperties", 1],
["uniqueItems", true],
["multipleOf", 1],
])("apply extension on model prop with %s", async (key, value) => {
const { prop } = await runner.compile(`
model Foo {
@extension("${key}", ${value})
@test
prop: string
}
`);

deepStrictEqual(Object.fromEntries(getExtensions(runner.program, prop)), {
[key]: value,
});
});

it.each(["minProperties", "maxProperties", "uniqueItems", "multipleOf"])(
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
"%s, emit diagnostics when passing invalid extension value",
async (key) => {
const diagnostics = await runner.diagnose(`
model Foo {
@extension("${key}", "string")
@test
prop: string
}
`);

expectDiagnostics(diagnostics, {
code: "@typespec/openapi/invalid-extension-value",
});
},
);

it("apply extension on model", async () => {
const { Foo } = await runner.compile(`
@extension("x-custom", "Bar")
Expand Down Expand Up @@ -99,7 +135,7 @@ describe("openapi: decorators", () => {

expectDiagnostics(diagnostics, {
code: "@typespec/openapi/invalid-extension-key",
message: `OpenAPI extension must start with 'x-' but was 'foo'`,
message: `extension decorator only support minProperties/maxProperties/uniqueItems/multipleOf/'x-' but was 'foo'`,
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
Expand Down
Loading
Loading