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 all 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
8 changes: 8 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,8 @@
---
changeKind: feature
packages:
- "@typespec/openapi"
- "@typespec/openapi3"
---

@extension decorator supports multipleOf, uniqueItems, maxProperties, and minProperties, apply to properties in the Schema Object.
44 changes: 40 additions & 4 deletions packages/openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-` |
| 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;
```

###### A schema can ensure that each of the items in an array is unique.

```typespec
model Foo {
@extension("uniqueItems", true)
x: unknown[];
}
```

###### Numbers can be restricted to a multiple of a given number, using the multipleOf keyword. It may be set to any positive number.

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

###### The number of properties on an object can be restricted using the maxProperties keyword.

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

###### The number of properties on an object can be restricted using the minProperties keyword.

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

#### `@externalDocs`

Specify the OpenAPI `externalDocs` property for this type.
Expand Down
30 changes: 29 additions & 1 deletion packages/openapi/generated-defs/TypeSpec.OpenAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,42 @@ 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-`
* @param value Extension value.
* @example
* ```typespec
* @extension("x-custom", "My value")
* @extension("x-pageable", {nextLink: "x-next-link"})
* op read(): string;
* ```
* @example A schema can ensure that each of the items in an array is unique.
* ```typespec
* model Foo {
* @extension("uniqueItems", true)
* x: unknown[];
* };
* ```
* @example Numbers can be restricted to a multiple of a given number, using the multipleOf keyword. It may be set to any positive number.
* ```typespec
* model Foo {
* @extension("multipleOf", 1)
* x: int32;
* };
* ```
* @example The number of properties on an object can be restricted using the maxProperties keyword.
* ```typespec
* model Foo {
* @extension("maxProperties", 1)
* x: int32;
* };
* ```
* @example The number of properties on an object can be restricted using the minProperties keyword.
* ```typespec
* model Foo {
* @extension("minProperties", 1)
* x: int32;
* };
* ```
*/
export type ExtensionDecorator = (
context: DecoratorContext,
Expand Down
35 changes: 33 additions & 2 deletions packages/openapi/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,47 @@ 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-`
* @param value Extension value.
*
* @example
*
* ```typespec
* @extension("x-custom", "My value")
* @extension("x-pageable", {nextLink: "x-next-link"})
* op read(): string;
* ```
*
* @example A schema can ensure that each of the items in an array is unique.
* ```typespec
* model Foo {
* @extension("uniqueItems", true)
* x: unknown[];
* };
* ```
*
* @example Numbers can be restricted to a multiple of a given number, using the multipleOf keyword. It may be set to any positive number.
* ```typespec
* model Foo {
* @extension("multipleOf", 1)
* x: int32;
* };
* ```
*
* @example The number of properties on an object can be restricted using the maxProperties keyword.
* ```typespec
* model Foo {
* @extension("maxProperties", 1)
* x: int32;
* };
* ```
*
* @example The number of properties on an object can be restricted using the minProperties keyword.
* ```typespec
* model Foo {
* @extension("minProperties", 1)
* x: int32;
* };
* ```
*/
extern dec extension(target: unknown, key: valueof string, value: unknown);

Expand Down
134 changes: 129 additions & 5 deletions packages/openapi/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getDoc,
getService,
getSummary,
isArrayModelType,
Model,
Namespace,
Operation,
Expand All @@ -25,7 +26,8 @@ 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 schemaExtensions = ["minProperties", "maxProperties", "uniqueItems", "multipleOf"];

const operationIdsKey = createStateSymbol("operationIds");
/**
Expand Down Expand Up @@ -58,19 +60,110 @@ export const $extension: ExtensionDecorator = (
extensionName: string,
value: TypeSpecValue,
) => {
// Convert the TypeSpec value to JSON and collect any diagnostics
const [data, diagnostics] = typespecTypeToJson(value, entity);
const isModelProperty = entity.kind === "ModelProperty";
const isMode = entity.kind === "Model";
const isScalar = entity.kind === "Scalar";

// Handle the "uniqueItems" extension
if (extensionName === "uniqueItems") {
// Check invalid target
if (
!(
isModelProperty &&
entity.type.kind === "Model" &&
isArrayModelType(context.program, entity.type)
)
) {
// Report diagnostic if the target is invalid for "uniqueItems"
reportDiagnostic(context.program, {
code: "invalid-extension-target",
messageId: "uniqueItems",
format: { paramName: entity.kind },
target: entity,
});
return;
}

// Check invalid data, Report diagnostic if the extension value is not a boolean
if (data !== true && data !== false) {
reportDiagnostic(context.program, {
code: "invalid-extension-value",
messageId: "uniqueItems",
format: { extensionName: extensionName },
target: entity,
});
return;
}
// Set the extension for "uniqueItems"
setExtension(context.program, entity, extensionName, data);
return;
}

// Handle other schema extensions
if (schemaExtensions.includes(extensionName) && extensionName !== "uniqueItems") {
skywing918 marked this conversation as resolved.
Show resolved Hide resolved
// Handle the "multipleOf" extension
const isNumber = (name: string) => name !== "string" && name !== "boolean";
if (extensionName === "multipleOf") {
if (
isMode ||
(isScalar && !isNumber(entity.name)) ||
(isModelProperty && !(entity.type.kind === "Scalar" && isNumber(entity.type.name)))
) {
reportDiagnostic(context.program, {
code: "invalid-extension-target",
messageId: "multipleOf",
format: { paramName: entity.kind },
target: entity,
});
return;
}
} else {
// Handle the "minProperties/maxProperties" extension
if (!isMode) {
reportDiagnostic(context.program, {
code: "invalid-extension-target",
format: { paramName: entity.kind },
target: entity,
});
return;
}
}

// Check invalid data, Report diagnostic if the extension value is not a number
if (isNaN(Number(data))) {
reportDiagnostic(context.program, {
code: "invalid-extension-value",
format: { extensionName: extensionName },
target: entity,
});
return;
}
// Set the extension for the schema extension
setExtension(context.program, entity, extensionName as SchemaExtensionKey, Number(data));
return;
}

// Check if the extensionName is a valid OpenAPI extension
if (!isOpenAPIExtensionKey(extensionName)) {
reportDiagnostic(context.program, {
code: "invalid-extension-key",
messageId: "decorator",
format: { value: extensionName },
target: entity,
});
return;
}

const [data, diagnostics] = typespecTypeToJson(value, entity);
// Report diagnostics if invalid data is found
if (diagnostics.length > 0) {
context.program.reportDiagnostics(diagnostics);
return;
}
setExtension(context.program, entity, extensionName as ExtensionKey, data);

// Set the extension for valid OpenAPI extensions
setExtension(context.program, entity, extensionName, data);
};

/**
Expand All @@ -97,7 +190,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 @@ -112,7 +205,38 @@ export function setExtension(
* @param entity Type
*/
export function getExtensions(program: Program, entity: Type): ReadonlyMap<ExtensionKey, any> {
return program.stateMap(openApiExtensionKey).get(entity) ?? new Map<ExtensionKey, any>();
const allExtensions = program.stateMap(openApiExtensionKey).get(entity);
return allExtensions
? filterSchemaExtensions<ExtensionKey>(allExtensions, false)
: new Map<ExtensionKey, any>();
}

/**
* Get schema extensions set for the given type.
* @param program Program
* @param entity Type
*/
export function getSchemaExtensions(
program: Program,
entity: Type,
): ReadonlyMap<SchemaExtensionKey, any> {
const allExtensions = program.stateMap(openApiExtensionKey).get(entity);
return allExtensions
? filterSchemaExtensions<SchemaExtensionKey>(allExtensions, true)
: new Map<SchemaExtensionKey, any>();
}

function filterSchemaExtensions<T>(
extensions: Map<ExtensionKey | SchemaExtensionKey, any>,
isSchema: boolean,
): ReadonlyMap<T, any> {
const result = new Map<T, any>();
for (const [key, value] of extensions) {
if (schemaExtensions.includes(key) === isSchema) {
result.set(key as T, value);
}
}
return result;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/openapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
getExternalDocs,
getInfo,
getOperationId,
getSchemaExtensions,
getTagsMetadata,
isDefaultResponse,
resolveInfo,
Expand Down
16 changes: 16 additions & 0 deletions packages/openapi/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ 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"}'`,
},
},
"invalid-extension-value": {
severity: "error",
messages: {
default: paramMessage`'${"extensionName"}' must number.'`,
uniqueItems: paramMessage`${"extensionName"}' must boolean.`,
},
},
"invalid-extension-target": {
severity: "error",
messages: {
default: paramMessage`'minProperties/maxProperties' can only apply to model, but ${"paramName"} is not`,
uniqueItems: paramMessage`'uniqueItems' can only be apply to properties that are arrays, but ${"paramName"} is not`,
multipleOf: paramMessage`'multipleOf' can only be apply to properties that are number, but ${"paramName"} is not`,
},
},
"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
Loading
Loading