diff --git a/.chronus/changes/improve-armid-doc-2024-6-26-10-21-28.md b/.chronus/changes/improve-armid-doc-2024-6-26-10-21-28.md new file mode 100644 index 0000000000..e33df47351 --- /dev/null +++ b/.chronus/changes/improve-armid-doc-2024-6-26-10-21-28.md @@ -0,0 +1,6 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-azure-core" +--- + diff --git a/.chronus/changes/separate_package_creation-2024-6-18-17-1-40.md b/.chronus/changes/separate_package_creation-2024-6-18-17-1-40.md deleted file mode 100644 index 084a032315..0000000000 --- a/.chronus/changes/separate_package_creation-2024-6-18-17-1-40.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -changeKind: feature -packages: - - "@azure-tools/typespec-autorest-canonical" - - "@azure-tools/typespec-autorest" - - "@azure-tools/typespec-client-generator-core" ---- - -expose createTcgcContext, which is the minimal context object that handles scope \ No newline at end of file diff --git a/core b/core index 39d4c60da5..eb245109c0 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 39d4c60da5778ac470ea3dd01d7bbf9967e4d661 +Subproject commit eb245109c05e98e10fdd75c48040d33c432a6a98 diff --git a/cspell.yaml b/cspell.yaml index f9c8299c50..b00c53d92c 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -27,7 +27,6 @@ enableFiletypes: - typespec words: - allof - - mobo - apim - apos - armId @@ -35,6 +34,7 @@ words: - Bazs - byos - clsx + - contosowidgetmanager - DMSS - Donezo - dynatrace @@ -49,12 +49,14 @@ words: - LINUXNEXTVMIMAGE - LINUXOS - LINUXVMIMAGE + - locationyaml - logz - LRO - lropaging - Lucene - MACVMIMAGE - mgmt + - mobo - msazure - msdata - mylocation @@ -64,8 +66,8 @@ words: - oncophenotype - PAYG - prismjs - - pytest - psscriptanalyzer + - pytest - qnas - regionality - Reranker @@ -74,6 +76,5 @@ words: - SERVICERP - tcgc - userrp + - vnet - WINDOWSVMIMAGE - - contosowidgetmanager - - locationyaml diff --git a/docs/howtos/DataPlane Generation - DPG/07tcgcTypes.mdx b/docs/howtos/DataPlane Generation - DPG/07tcgcTypes.mdx index 1f383e22c3..80d97c713c 100644 --- a/docs/howtos/DataPlane Generation - DPG/07tcgcTypes.mdx +++ b/docs/howtos/DataPlane Generation - DPG/07tcgcTypes.mdx @@ -1219,7 +1219,9 @@ interface SdkTypeBase { // created by tcgc. Those won't have an original type __raw?: Type; kind: string; - deprecations?: string; + deprecation?: string; + description?: string; + details?: string; } ``` @@ -1236,6 +1238,9 @@ There is a one-to-one mapping between the TypeSpec scalar kinds and the `SdkBuil export interface SdkBuiltInType extends SdkTypeBase { kind: SdkBuiltInKinds; encode: string; + name: string; + baseType?: SdkBuiltInType; + crossLanguageDefinitionId: string; } ``` @@ -1243,9 +1248,12 @@ export interface SdkBuiltInType extends SdkTypeBase { ```ts interface SdkDatetimeTypeBase extends SdkTypeBase { + name: string; + baseType?: SdkDateTimeType; encode: DateTimeKnownEncoding; // what we send over the wire. Often it's string wireType: SdkBuiltInType; + crossLanguageDefinitionId: string; } interface SdkUtcDatetimeType extends SdkDatetimeTypeBase { @@ -1262,9 +1270,12 @@ interface SdkOffsetDatetimeType extends SdkDatetimeTypeBase { ```ts interface SdkDurationType extends SdkTypeBase { kind: "duration"; + name: string; + baseType?: SdkDurationType; encode: DurationKnownEncoding; // What we send over the wire. It's usually either a string or a float wireType: SdkBuiltInType; + crossLanguageDefinitionId: string; } ``` @@ -1273,8 +1284,9 @@ interface SdkDurationType extends SdkTypeBase { ```ts interface SdkArrayType extends SdkTypeBase { kind: "array"; + name: string; valueType: SdkType; - nullableValues: boolean; + crossLanguageDefinitionId: string; } ``` @@ -1285,7 +1297,6 @@ interface SdkDictionaryType extends SdkTypeBase { kind: "dict"; keyType: SdkType; // currently can only be string valueType: SdkType; - nullableValues: boolean; } ``` @@ -1296,17 +1307,16 @@ export interface SdkEnumType extends SdkTypeBase { kind: "enum"; name: string; // Determines whether the name was generated or not - generatedName: boolean; + isGeneratedName: boolean; valueType: SdkBuiltInType; values: SdkEnumValueType[]; isFixed: boolean; - description?: string; - details?: string; isFlags: boolean; usage: UsageFlags; access: AccessFlags; crossLanguageDefinitionId: string; apiVersions: string[]; + isUnionAsEnum: boolean; } ``` @@ -1318,9 +1328,7 @@ export interface SdkEnumValueType extends SdkTypeBase { name: string; value: string | number; enumType: SdkEnumType; - valueType: SdkType; - description?: string; - details?: string; + valueType: SdkBuiltInType; } ``` @@ -1331,6 +1339,8 @@ export interface SdkConstantType extends SdkTypeBase { kind: "constant"; value: string | number | boolean | null; valueType: SdkBuiltInType; + name: string; + isGeneratedName: boolean; } ``` @@ -1340,9 +1350,10 @@ export interface SdkConstantType extends SdkTypeBase { export interface SdkUnionType extends SdkTypeBase { name: string; // determines if the union name was generated or not - generatedName: boolean; + isGeneratedName: boolean; kind: "union"; values: SdkType[]; + crossLanguageDefinitionId: string; } ``` @@ -1353,13 +1364,9 @@ export interface SdkModelType extends SdkTypeBase { kind: "model"; // purposely can also be header / query params for fidelity with TypeSpec properties: SdkModelPropertyType[]; - isFormDataType: boolean; - isError: boolean; // we will always have a name. generatedName determines if it's generated or not. name: string; - generatedName: string; - description?: string; - details?: string; + isGeneratedName: boolean; access: AccessFlags; usage: UsageFlags; additionalProperties?: SdkType; diff --git a/docs/howtos/DataPlane Generation - DPG/08methodInputs.mdx b/docs/howtos/DataPlane Generation - DPG/08methodInputs.mdx index 72489f1649..4d9d3f73ba 100644 --- a/docs/howtos/DataPlane Generation - DPG/08methodInputs.mdx +++ b/docs/howtos/DataPlane Generation - DPG/08methodInputs.mdx @@ -81,6 +81,13 @@ public User get(); ```go +type ClientGetOptions struct { +} + +type ClientGetResponse struct { +} + +func (client *Client) Get(ctx context.Context, options *ClientGetOptions) (ClientGetResponse, error) ``` @@ -171,7 +178,13 @@ public void post(User user); ```go +type ClientPostOptions struct { +} + +type ClientPostResponse struct { +} +func (client *Client) Post(ctx context.Context, user User, options *ClientPostOptions) (ClientPostResponse, error) ``` @@ -181,9 +194,9 @@ public void post(User user); Please use the _spread_ feature with caution. -- The anonymous model to be spread into operation should have less than 6 settable properties. See [simple methods](https://azure.github.io/azure-sdk/dotnet_introduction.html#dotnet-parameters). -- The anonymous model should be stable across api-versions. Adding an optional property across api-versions could result in one additional method overload in SDK client. -- The anonymous model should not be used in [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386). +- The model to be spread should have less than 6 settable properties. See [simple methods](https://azure.github.io/azure-sdk/dotnet_introduction.html#dotnet-parameters). +- The model to be spread should be stable across api-versions. Adding an optional property across api-versions could result in one additional method overload in SDK client. +- The model to be spread should not be used in [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386). ### Alias @@ -269,13 +282,20 @@ public void upload(String firstName, String lastName); ```go +type ClientUploadOptions struct { +} + +type ClientUploadResponse struct { +} + +func (client *Client) Upload(ctx context.Context, firstName string, lastName string, options *ClientUploadOptions) (ClientUploadResponse, error) ``` -### Alias with HTTP Parameters +### Alias with @header/@query/@path properties @@ -294,8 +314,10 @@ op upload(...User): void; +For Python, we will also generate the overloads described in the Http Post section, but omitting for brevity + ```python -def upload(id: str, first_name: str, last_name: str) -> None: +def upload(self, id: str, first_name: str, last_name: str, *, content_type: str = "application/json") -> None: ... ``` @@ -366,7 +388,13 @@ public void upload(String id, String firstName, String lastName); ```go +type ClientUploadOptions struct { +} +type ClientUploadResponse struct { +} + +func (client *Client) Upload(ctx context.Context, id string, firstName string, lastName string, options *ClientUploadOptions) (ClientUploadResponse, error) ``` @@ -392,7 +420,7 @@ op upload(...User): void; For Python, we will also generate the overloads described in the Http Post section, but omitting for brevity ```python -def upload(self, user: [User, JSON, IO[bytes]], *, content_type: str = "application/json") -> None: +def upload(self, first_name: str, last_name: str, *, content_type: str = "application/json") -> None: ... ``` @@ -402,7 +430,7 @@ def upload(self, user: [User, JSON, IO[bytes]], *, content_type: str = "applicat ```csharp public partial class User { - public User(string firstName, string lastName){} + public User(string firstName, string lastName) { } public string FirstName { get; } public string LastName { get; } } @@ -411,7 +439,7 @@ public virtual async Task UploadAsync(RequestContent content, RequestC public virtual Response Upload(RequestContent content, RequestContext context = null) //convenience method public virtual async Task UploadAsync(User user, CancellationToken cancellationToken = default) -public virtual Response Upload(User user, CancellationToken cancellationToken = default) +public virtual Response Upload(string firstName, string lastName, CancellationToken cancellationToken = default) ``` @@ -426,7 +454,10 @@ export type DemoServiceContext = Client & { (path: "/users"): { post( options: { - body: User; + body: { + firstName: string; + lastName: string; + }; } & RequestParameters ): StreamableMethod; }; @@ -434,11 +465,15 @@ export type DemoServiceContext = Client & { }; // Modular Api Layer -export async function upload(body: User, options: UploadOptionalParams): Promise; +export async function upload( + firstName: string, + lastName: string, + options: UploadOptionalParams +): Promise; // Modular classical client layer export class DemoServiceClient { - upload(body: User, options: UploadOptionalParams): Promise; + upload(firstName: string, lastName: string, options: UploadOptionalParams): Promise; } ``` @@ -446,29 +481,26 @@ export class DemoServiceClient { ```java -// Model class -@Immutable -public final class User implements JsonSerializable { - public User(String firstName, String lastName); - public String getFirstName(); - public String getLastName(); -} - -// Client API -public void upload(User user); +public void upload(String firstName, String lastName); ``` ```go +type ClientUploadOptions struct { +} + +type ClientUploadResponse struct { +} +func (client *Client) Upload(ctx context.Context, firstName string, lastName string, options *ClientUploadOptions) (ClientUploadResponse, error) ``` -### Model with `@body` Property +### Model with `@body` property @@ -493,7 +525,7 @@ op upload(...UserRequest): void; For Python, we will also generate the overloads described in the Http Post section, but omitting for brevity ```python -def upload(body: [User, JSON, IO[bytes]], **kwargs: Any) -> None: +def upload(self, body: [User, JSON, IO[bytes]], *, content_type: str = "application/json") -> None: ... ``` @@ -564,13 +596,24 @@ public void upload(User user); ```go +type User struct { + firstName *string + lastName *string +} + +type ClientUploadOptions struct { +} +type ClientUploadResponse struct { +} + +func (client *Client) Upload(ctx context.Context, user User, options *ClientUploadOptions) (ClientUploadResponse, error) ``` -### Model with Decorated Properties +### Model with @header/@query/@path properties @@ -594,7 +637,7 @@ For Python, we will also generate the overloads described in the Http Post secti ```python -def get_blob_properties(name: str, *, test_header: string, **kwargs: Any) -> None: +def get_blob_properties(self, name: str, *, test_header: string, content_type: str = "application/json") -> None: ... ``` @@ -657,13 +700,19 @@ public void getBlobProperties(String name, String testHeader); ```go +type ClientGetBlobPropertiesOptions struct { +} +type ClientGetBlobPropertiesResponse struct { +} + +func (client *Client) GetBlobProperties(ctx context.Context, name string, testHeader string, options *ClientGetBlobPropertiesOptions) (ClientGetBlobPropertiesResponse, error) ``` -### Model with Decorated and non-Decorated Properties +### Model mixed with normal and @header/@query/@path properties @@ -687,7 +736,7 @@ For Python, we will also generate the overloads described in the Http Post secti class Schema: schema: bytes -def register(body: [Schema, JSON, IO[bytes]], **kwargs: Any) -> None: +def register(self, body: [Schema, JSON, IO[bytes]], *, content_type: str = "application/json") -> None: ... ``` @@ -724,7 +773,7 @@ export type DemoServiceContext = Client & { "content-type": "application/json"; } & RawHttpHeaders; body: { - schema: string | Uint8Array | ReadableStream | NodeJS.ReadableStream; + schema: string; }; } & RequestParameters ): StreamableMethod; @@ -734,7 +783,7 @@ export type DemoServiceContext = Client & { // Modular model export interface Schema { - schema: string | Uint8Array | ReadableStream | NodeJS.ReadableStream; + schema: string; } // Modular api layer @@ -768,7 +817,301 @@ public void register(Schema schema); ```go +type ClientRegisterOptions struct { +} + +type ClientRegisterResponse struct { +} + +func (client *Client) Register(ctx context.Context, schema []byte, options *ClientRegisterOptions) (ClientRegisterResponse, error) +``` + + + + +### Using Azure.Core.ResourceOperations template + +Resource create and update operations are not impacted by spread since they all have explicit defined body parameter. +Only resource action operations are impacted by spread. + +If the action parameter is a model, then the model will be spread. + + + + +```typespec +@resource("widgets") +model Widget { + @key("widgetName") + name: string; +} + +model RepairInfo { + problem: string; + contact: string; +} + +model RepairResult { + reason: string; + info: string; +} + +alias Operations = Azure.Core.ResourceOperations<{}>; + +op scheduleRepairs is Operations.ResourceAction; +``` + + + + +For Python, we will also generate the overloads described in the Http Post section, but omitting for brevity + +```python +class RepairInfo: + problem: str + contact: str + +class RepairResult: + reason: str + info: str + +def scheduleRepairs(self, widget_name: str, problem: str, contact: str, *, content_type: str = "application/json") -> RepairResult: + ... +``` + + + + +```csharp + +``` + + + + +```typescript +// from user experience perspective + +export interface RepairInfo { + problem: string; + contact: string; +} + +export type WidgetServiceContext = Client & { + path: { + ( + path: "/widgets/{widgetName}:scheduleRepairs", + widgetName: string + ): { + post( + options: { + body: RepairInfo; + } & RequestParameters + ): StreamableMethod; + }; + }; +}; + +// Modular api layer +export async function scheduleRepairs( + context: Client, + widgetName: string, + problem: string, + contact: string, + options: ScheduleRepairsOptionalParams = { requestOptions: {} } +): Promise; + +// Modular classical client layer +export class WidgetServiceClient { + scheduleRepairs( + widgetName: string, + problem: string, + contact: string, + options: ScheduleRepairsOptionalParams = { requestOptions: {} } + ): Promise; +} +``` + + + + +```java +public RepairResult scheduleRepairs(String widgetName, String problem, String contact); +``` + + + + +```go +type ClientScheduleRepairsOptions struct { +} + +type RepairResult struct { + reason *string + info *string +} + +type ClientScheduleRepairsResponse struct { + RepairResult +} + +func (client *Client) ScheduleRepairs(ctx context.Context, widgetName string, problem string, contact string, options *ClientScheduleRepairsOptions) (ClientScheduleRepairsResponse, error) +``` + + + + +If you want to keep the model, you could use a wrapper to explicit set the body to prevent spread. + + + + +```typespec +alias BodyParameter< + T, + TName extends valueof string = "body", + TDoc extends valueof string = "Body parameter." +> = { + @doc(TDoc) + @friendlyName(TName) + @bodyRoot + body: T; +}; + +@resource("widgets") +model Widget { + @key("widgetName") + name: string; +} + +model RepairInfo { + problem: string; + contact: string; +} + +model RepairResult { + reason: string; + info: string; +} + +alias Operations = Azure.Core.ResourceOperations<{}>; + +op scheduleRepairs is Operations.ResourceAction, RepairResult>; +``` + + + + +For Python, we will also generate the overloads described in the Http Post section, but omitting for brevity + +```python +class RepairInfo: + problem: str + contact: str + +class RepairResult: + reason: str + info: str + +def scheduleRepairs(self, body: [Schema, JSON, IO[bytes]], *, content_type: str = "application/json") -> RepairResult: + ... +``` + + + + +```csharp + +``` + + + + +```typescript +// from user experience perspective + +export interface RepairInfo { + problem: string; + contact: string; +} + +export type WidgetServiceContext = Client & { + path: { + ( + path: "/widgets/{widgetName}:scheduleRepairs", + widgetName: string + ): { + post( + options: { + body: RepairInfo; + } & RequestParameters + ): StreamableMethod; + }; + }; +}; + +// Modular api layer +export async function scheduleRepairs( + context: Client, + widgetName: string, + body: RepairInfo, + options: ScheduleRepairsOptionalParams = { requestOptions: {} } +): Promise; + +// Modular classical client layer +export class WidgetServiceClient { + scheduleRepairs( + widgetName: string, + body: RepairInfo, + options: ScheduleRepairsOptionalParams = { requestOptions: {} } + ): Promise; +} +``` + + + + +```java +// Model class +@Immutable +public final class RepairInfo implements JsonSerializable { + public RepairInfo(String problem, String contact); + public String getProblem(); + public String getContact(); +} + +@Immutable +public final class RepairResult implements JsonSerializable { + public String getReason(); + public String getInfo(); +} + +// Client API +public RepairResult scheduleRepairs(String widgetName, RepairInfo body); +``` + + + + +```go +type RepairInfo struct { + problem *string + contact *string +} + +type ClientScheduleRepairsOptions struct { +} + +type RepairResult struct { + reason *string + info *string +} + +type ClientScheduleRepairsResponse struct { + RepairResult +} +func (client *Client) ScheduleRepairs(ctx context.Context, widgetName string, body RepairInfo, options *ClientScheduleRepairsOptions) (ClientScheduleRepairsResponse, error) ``` diff --git a/docs/libraries/azure-core/reference/data-types.md b/docs/libraries/azure-core/reference/data-types.md index df2ff96993..12d9ca1719 100644 --- a/docs/libraries/azure-core/reference/data-types.md +++ b/docs/libraries/azure-core/reference/data-types.md @@ -510,15 +510,35 @@ union Azure.Core.RepeatabilityResult A type definition that refers the id to an Azure Resource Manager resource. -Sample usage: -otherArmId: ResourceIdentifier; -networkId: ResourceIdentifier<[{type:"\\Microsoft.Network\\vnet"}]> -vmIds: ResourceIdentifier<[{type:"\\Microsoft.Compute\\vm", scopes["*"]}]> - ```typespec scalar Azure.Core.armResourceIdentifier ``` +#### Examples + +```tsp +model MyModel { + otherArmId: armResourceIdentifier; + networkId: armResourceIdentifier<[ + { + type: "Microsoft.Network/vnet"; + } + ]>; + vmIds: armResourceIdentifier<[ + { + type: "Microsoft.Compute/vm"; + scopes: ["*"]; + } + ]>; + scoped: armResourceIdentifier<[ + { + type: "Microsoft.Compute/vm"; + scopes: ["tenant", "resourceGroup"]; + } + ]>; +} +``` + ### `azureLocation` {#Azure.Core.azureLocation} Represents an Azure geography region where supported resource providers live. diff --git a/docs/libraries/azure-core/reference/interfaces.md b/docs/libraries/azure-core/reference/interfaces.md index f3a8f10fb0..d0bfbeb104 100644 --- a/docs/libraries/azure-core/reference/interfaces.md +++ b/docs/libraries/azure-core/reference/interfaces.md @@ -338,6 +338,10 @@ op Azure.Core.LongRunningResourceCollectionAction(apiVersion: string): Azure.Cor ### `LongRunningResourceCreateOrReplace` {#Azure.Core.LongRunningResourceCreateOrReplace} +:::warning +**Deprecated**: Use `LongRunningResourceCreateOrReplace` from a `ResourceOperations` interface instance. +::: + DEPRECATED: Use `LongRunningResourceCreateOrReplace` from a `ResourceOperations` interface instance. This can be done by instantiating your own version with the traits you want `alias Operations = Azure.Core.ResourceOperations;`. See https://azure.github.io/typespec-azure/docs/getstarted/azure-core/step05#defining-the-operation-interface for details on how to use. @@ -357,6 +361,10 @@ op Azure.Core.LongRunningResourceCreateOrReplace(apiVersion: string, resource: R ### `LongRunningResourceCreateOrUpdate` {#Azure.Core.LongRunningResourceCreateOrUpdate} +:::warning +**Deprecated**: Use `LongRunningResourceCreateOrUpdate` from a `ResourceOperations` interface instance. +::: + DEPRECATED: Use `LongRunningResourceCreateOrUpdate` from a `ResourceOperations` interface instance. This can be done by instantiating your own version with the traits you want `alias Operations = Azure.Core.ResourceOperations;`. See https://azure.github.io/typespec-azure/docs/getstarted/azure-core/step05#defining-the-operation-interface for details on how to use. @@ -376,6 +384,10 @@ op Azure.Core.LongRunningResourceCreateOrUpdate(apiVersion: string, contentType: ### `LongRunningResourceCreateWithServiceProvidedName` {#Azure.Core.LongRunningResourceCreateWithServiceProvidedName} +:::warning +**Deprecated**: Use `LongRunningResourceCreateWithServiceProvidedName` from a `ResourceOperations` interface instance. +::: + DEPRECATED: Use `LongRunningResourceCreateWithServiceProvidedName` from a `ResourceOperations` interface instance. This can be done by instantiating your own version with the traits you want `alias Operations = Azure.Core.ResourceOperations;`. See https://azure.github.io/typespec-azure/docs/getstarted/azure-core/step05#defining-the-operation-interface for details on how to use. @@ -476,6 +488,10 @@ op Azure.Core.ResourceCollectionAction(apiVersion: string): {} | Azure.Core.Foun ### `ResourceCreateOrReplace` {#Azure.Core.ResourceCreateOrReplace} +:::warning +**Deprecated**: Use `ResourceCreateOrReplace` from a `ResourceOperations` interface instance. +::: + DEPRECATED: Use `ResourceCreateOrReplace` from a `ResourceOperations` interface instance. This can be done by instantiating your own version with the traits you want `alias Operations = Azure.Core.ResourceOperations;`. See https://azure.github.io/typespec-azure/docs/getstarted/azure-core/step05#defining-the-operation-interface for details on how to use. @@ -495,6 +511,10 @@ op Azure.Core.ResourceCreateOrReplace(apiVersion: string, resource: Resource): { ### `ResourceCreateOrUpdate` {#Azure.Core.ResourceCreateOrUpdate} +:::warning +**Deprecated**: Use `LongRunningResourceCreateOrReplace` from a `ResourceOperations` interface instance. +::: + DEPRECATED: Use `ResourceCreateOrUpdate` from a `ResourceOperations` interface instance. This can be done by instantiating your own version with the traits you want `alias Operations = Azure.Core.ResourceOperations;`. See https://azure.github.io/typespec-azure/docs/getstarted/azure-core/step05#defining-the-operation-interface for details on how to use. @@ -514,6 +534,10 @@ op Azure.Core.ResourceCreateOrUpdate(apiVersion: string, contentType: "applicati ### `ResourceCreateWithServiceProvidedName` {#Azure.Core.ResourceCreateWithServiceProvidedName} +:::warning +**Deprecated**: Use `ResourceCreateWithServiceProvidedName` from a `ResourceOperations` interface instance. +::: + DEPRECATED: Use `ResourceCreateWithServiceProvidedName` from a `ResourceOperations` interface instance. This can be done by instantiating your own version with the traits you want `alias Operations = Azure.Core.ResourceOperations;`. See https://azure.github.io/typespec-azure/docs/getstarted/azure-core/step05#defining-the-operation-interface for details on how to use. @@ -590,6 +614,10 @@ op Azure.Core.ResourceRead(apiVersion: string): {} | Azure.Core.Foundations.Erro ### `ResourceUpdate` {#Azure.Core.ResourceUpdate} +:::warning +**Deprecated**: Use `ResourceUpdate` from a `ResourceOperations` interface instance. +::: + DEPRECATED: Use `ResourceUpdate` from a `ResourceOperations` interface instance. This can be done by instantiating your own version with the traits you want `alias Operations = Azure.Core.ResourceOperations;`. See https://azure.github.io/typespec-azure/docs/getstarted/azure-core/step05#defining-the-operation-interface for details on how to use. diff --git a/docs/libraries/azure-resource-manager/reference/data-types.md b/docs/libraries/azure-resource-manager/reference/data-types.md index d67893a7b1..769f6b7d37 100644 --- a/docs/libraries/azure-resource-manager/reference/data-types.md +++ b/docs/libraries/azure-resource-manager/reference/data-types.md @@ -1694,6 +1694,10 @@ model Azure.ResourceManager.CommonTypes.TrackedResource ### `UserAssignedIdentities` {#Azure.ResourceManager.CommonTypes.UserAssignedIdentities} +:::warning +**Deprecated**: Do not use this model. Instead, use Record directly. Using this model will result in a different client SDK when generated from TypeSpec compared to the Swagger. +::: + The set of user assigned identities associated with the resource. The userAssignedIdentities dictionary keys will be ARM resource ids in the form: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}. The dictionary values can be empty objects ({}) in requests. ```typespec diff --git a/docs/libraries/azure-resource-manager/reference/interfaces.md b/docs/libraries/azure-resource-manager/reference/interfaces.md index 9b424d61f0..0fc27525a0 100644 --- a/docs/libraries/azure-resource-manager/reference/interfaces.md +++ b/docs/libraries/azure-resource-manager/reference/interfaces.md @@ -247,6 +247,10 @@ op Azure.ResourceManager.ResourceCreateSync.createOrUpdate(provider: "Microsoft. ### `ResourceDeleteAsync` {#Azure.ResourceManager.ResourceDeleteAsync} +:::warning +**Deprecated**: This should be deprecated in a future release +::: + ```typespec interface Azure.ResourceManager.ResourceDeleteAsync ``` @@ -390,6 +394,10 @@ op Azure.ResourceManager.ResourceListBySubscription.listBySubscription(apiVersio ### `ResourceOperations` {#Azure.ResourceManager.ResourceOperations} +:::warning +**Deprecated**: Use Azure.ResourceManager.TrackedResourceOperations instead +::: + ```typespec interface Azure.ResourceManager.ResourceOperations ``` @@ -845,6 +853,10 @@ op Azure.ResourceManager.ArmResourceCreateOrUpdateAsync(provider: "Microsoft.Thi ### `ArmResourceCreateOrUpdateSync` {#Azure.ResourceManager.ArmResourceCreateOrUpdateSync} +:::warning +**Deprecated**: Please use ArmResourceCreateOrReplaceSync instead +::: + DEPRECATED: Please use ArmResourceCreateOrReplaceSync instead ```typespec @@ -863,6 +875,10 @@ op Azure.ResourceManager.ArmResourceCreateOrUpdateSync(provider: "Microsoft.This ### `ArmResourceDeleteAsync` {#Azure.ResourceManager.ArmResourceDeleteAsync} +:::warning +**Deprecated**: Use 'ArmResourceDeleteWithoutOkAsync' instead +::: + ```typespec op Azure.ResourceManager.ArmResourceDeleteAsync(provider: "Microsoft.ThisWillBeReplaced"): Response | Error ``` diff --git a/docs/libraries/typespec-client-generator-core/reference/decorators.md b/docs/libraries/typespec-client-generator-core/reference/decorators.md index 36bd651b37..d1cd81aaf6 100644 --- a/docs/libraries/typespec-client-generator-core/reference/decorators.md +++ b/docs/libraries/typespec-client-generator-core/reference/decorators.md @@ -201,6 +201,10 @@ interface MyInterface {} ### `@clientFormat` {#@Azure.ClientGenerator.Core.clientFormat} +:::warning +**Deprecated**: @clientFormat decorator is deprecated. Use `@encode` decorator in `@typespec/compiler` instead. +::: + DEPRECATED: Use `@encode` decorator in `@typespec/compiler` instead. Can be used to explain the client type that the current TYPESPEC @@ -291,6 +295,10 @@ op test: void; ### `@exclude` {#@Azure.ClientGenerator.Core.exclude} +:::warning +**Deprecated**: @exclude decorator is deprecated. Use `@usage` and `@access` decorator instead. +::: + DEPRECATED: Use `@usage` and `@access` decorator instead. Whether to exclude a model from generation for specific languages. By default we generate @@ -321,6 +329,10 @@ model ModelToExclude { ### `@flattenProperty` {#@Azure.ClientGenerator.Core.flattenProperty} +:::warning +**Deprecated**: @flattenProperty decorator is not recommended to use. +::: + Set whether a model property should be flattened or not. ```typespec @@ -349,6 +361,10 @@ model Bar {} ### `@include` {#@Azure.ClientGenerator.Core.include} +:::warning +**Deprecated**: @include decorator is deprecated. Use `@usage` and `@access` decorator instead. +::: + DEPRECATED: Use `@usage` and `@access` decorator instead. Whether to include a model in generation for specific languages. By default we generate @@ -379,6 +395,10 @@ model ModelToInclude { ### `@internal` {#@Azure.ClientGenerator.Core.internal} +:::warning +**Deprecated**: @internal decorator is deprecated. Use `@access` decorator instead. +::: + DEPRECATED: Use `@access` decorator instead. Whether to mark an operation as internal for specific languages, diff --git a/docs/release-notes/release-2024-07-16.md b/docs/release-notes/release-2024-07-16.md index f5c58ae968..afd82452a5 100644 --- a/docs/release-notes/release-2024-07-16.md +++ b/docs/release-notes/release-2024-07-16.md @@ -14,7 +14,15 @@ This release contains breaking changes and deprecation ### @azure-tools/typespec-autorest -- [#1105](https://github.com/Azure/typespec-azure/pull/1105) `x-ms-client-flatten` extension on some of resource properties property is now configurable to be emitted by autorest emitter. Default is false which will skip emission of that extension. +- [#1105](https://github.com/Azure/typespec-azure/pull/1105) `x-ms-client-flatten` extension on some of resource properties property is now configurable to be emitted by autorest emitter(`arm-resource-flattening` option). Default is false which will skip emission of that extension. + To revert to previous behavior update your `tspconfig.yaml` with the following + + ```diff + options: + "@azure-tools/typespec-autorest": + # ...other options + + arm-resource-flattening: true + ``` ### @azure-tools/typespec-azure-resource-manager diff --git a/packages/samples/common-types/src/types.tsp b/packages/samples/common-types/src/types.tsp index a5f7a68350..cd4544b40b 100644 --- a/packages/samples/common-types/src/types.tsp +++ b/packages/samples/common-types/src/types.tsp @@ -8,17 +8,19 @@ namespace Azure.ResourceManager.CommonTypes; @@extension(ApiVersionParameter.apiVersion, "x-ms-parameter-location", "client"); @@extension(SubscriptionIdParameter.subscriptionId, "x-ms-parameter-location", "client"); -op registerParams( - ...ApiVersionParameter, - ...LocationParameter, - ...ManagementGroupNameParameter, - ...OperationIdParameter, - ...ResourceGroupNameParameter, - ...ScopeParameter, - ...SubscriptionIdParameter, - ...TenantIdParameter, -): void; +interface RegisterParams { + v3( + ...ApiVersionParameter, + ...LocationParameter, + ...OperationIdParameter, + ...ResourceGroupNameParameter, + ...SubscriptionIdParameter, + ): void; + // Params added in v4 + @Versioning.added(Versions.v4) + v4(...ManagementGroupNameParameter, ...ScopeParameter, ...TenantIdParameter): void; +} @@extension(ErrorDetail.details, "x-ms-identifiers", ["message", "target"]); @@extension(Resource, "x-ms-azure-resource", true); diff --git a/packages/typespec-autorest-canonical/CHANGELOG.md b/packages/typespec-autorest-canonical/CHANGELOG.md index 267d5b65b1..5a72566a5b 100644 --- a/packages/typespec-autorest-canonical/CHANGELOG.md +++ b/packages/typespec-autorest-canonical/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog - @azure-tools/typespec-autorest-canonical +## 0.5.1 + +### Features + +- [#1237](https://github.com/Azure/typespec-azure/pull/1237) Use new `createTcgcContext` from tcgc lib, which is the minimal context object that handles scope + + ## 0.5.0 ### Bug Fixes diff --git a/packages/typespec-autorest-canonical/package.json b/packages/typespec-autorest-canonical/package.json index 23eeea5211..9822454506 100644 --- a/packages/typespec-autorest-canonical/package.json +++ b/packages/typespec-autorest-canonical/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/typespec-autorest-canonical", - "version": "0.5.0", + "version": "0.5.1", "author": "Microsoft Corporation", "description": "TypeSpec library for emitting canonical swagger", "homepage": "https://azure.github.io/typespec-azure", diff --git a/packages/typespec-autorest-canonical/test/primitive-types.test.ts b/packages/typespec-autorest-canonical/test/primitive-types.test.ts deleted file mode 100644 index 8eac9d95f4..0000000000 --- a/packages/typespec-autorest-canonical/test/primitive-types.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { OpenAPI2Parameter, OpenAPI2Schema } from "@azure-tools/typespec-autorest"; -import { expectDiagnostics } from "@typespec/compiler/testing"; -import { deepStrictEqual, ok } from "assert"; -import { describe, it } from "vitest"; -import { diagnoseOpenApiFor, oapiForModel, openApiFor } from "./test-host.js"; - -describe("handle typespec intrinsic types", () => { - const cases = [ - ["unknown", {}], - ["int8", { type: "integer", format: "int8" }], - ["int16", { type: "integer", format: "int16" }], - ["int32", { type: "integer", format: "int32" }], - ["int64", { type: "integer", format: "int64" }], - ["safeint", { type: "integer", format: "int64" }], - ["uint8", { type: "integer", format: "uint8" }], - ["uint16", { type: "integer", format: "uint16" }], - ["uint32", { type: "integer", format: "uint32" }], - ["uint64", { type: "integer", format: "uint64" }], - ["float32", { type: "number", format: "float" }], - ["float64", { type: "number", format: "double" }], - ["string", { type: "string" }], - ["boolean", { type: "boolean" }], - ["plainDate", { type: "string", format: "date" }], - ["utcDateTime", { type: "string", format: "date-time" }], - ["offsetDateTime", { type: "string", format: "date-time" }], - ["plainTime", { type: "string", format: "time" }], - ["duration", { type: "string", format: "duration" }], - ["bytes", { type: "string", format: "byte" }], - ["decimal", { type: "number", format: "decimal" }], - ["decimal128", { type: "number", format: "decimal" }], - ]; - - for (const test of cases) { - it("knows schema for " + test[0], async () => { - const res = await oapiForModel( - "Pet", - ` - model Pet { name: ${test[0]} }; - ` - ); - - const schema = res.defs.Pet.properties.name; - deepStrictEqual(schema, test[1]); - }); - } -}); - -describe("handle nonspecific intrinsic types", () => { - const cases = [ - [ - "numeric", - "Scalar type 'numeric' is not specific enough. The more specific type 'int64' has been chosen.", - ], - [ - "integer", - "Scalar type 'integer' is not specific enough. The more specific type 'int64' has been chosen.", - ], - [ - "float", - "Scalar type 'float' is not specific enough. The more specific type 'float64' has been chosen.", - ], - ]; - - for (const test of cases) { - it("reports nonspecific scalar for " + test[0], async () => { - const res = await diagnoseOpenApiFor( - ` - @service({title: "Testing model"}) - @route("/") - namespace root { - #suppress "@azure-tools/typespec-azure-core/use-standard-operations" "This is a test." - op read(): void; - - model Pet { name: ${test[0]} }; - } - ` - ); - - expectDiagnostics(res, { - code: "@azure-tools/typespec-autorest/nonspecific-scalar", - message: test[1], - }); - }); - } -}); - -it("defines models extended from primitives", async () => { - const res = await oapiForModel( - "Pet", - ` - scalar shortString extends string; - model Pet { name: shortString }; - ` - ); - - ok(res.isRef); - ok(res.defs.shortString, "expected definition named shortString"); - ok(res.defs.Pet, "expected definition named Pet"); - deepStrictEqual(res.defs.shortString, { - type: "string", - }); -}); - -it("apply description on extended primitive (string)", async () => { - const res = await oapiForModel( - "shortString", - ` - @doc("My custom description") - scalar shortString extends string; - ` - ); - - ok(res.isRef); - deepStrictEqual(res.defs.shortString, { - type: "string", - description: "My custom description", - }); -}); - -it("apply description on extended primitive (int32)", async () => { - const res = await oapiForModel( - "specialInt", - ` - @doc("My custom description") - scalar specialInt extends int32; - ` - ); - - ok(res.isRef); - deepStrictEqual(res.defs.specialInt, { - type: "integer", - format: "int32", - description: "My custom description", - }); -}); - -it("apply description on extended custom scalars", async () => { - const res = await oapiForModel( - "superSpecialint", - ` - @doc("My custom description") - scalar specialint extends int32; - @doc("Override specialint description") - scalar superSpecialint extends specialint; - ` - ); - - ok(res.isRef); - deepStrictEqual(res.defs.superSpecialint, { - type: "integer", - format: "int32", - description: "Override specialint description", - }); -}); - -it("defines scalar extended from primitives with attrs", async () => { - const res = await oapiForModel( - "Pet", - ` - @maxLength(10) @minLength(10) - scalar shortString extends string; - model Pet { name: shortString }; - ` - ); - - ok(res.isRef); - ok(res.defs.shortString, "expected definition named shortString"); - ok(res.defs.Pet, "expected definition named Pet"); - deepStrictEqual(res.defs.shortString, { - type: "string", - minLength: 10, - maxLength: 10, - }); -}); - -it("defines scalar extended from primitives with new attrs", async () => { - const res = await oapiForModel( - "Pet", - ` - @maxLength(10) - scalar shortString extends string; - @minLength(1) - scalar shortButNotEmptyString extends shortString; - model Pet { name: shortButNotEmptyString, breed: shortString }; - ` - ); - ok(res.isRef); - ok(res.defs.shortString, "expected definition named shortString"); - ok(res.defs.shortButNotEmptyString, "expected definition named shortButNotEmptyString"); - ok(res.defs.Pet, "expected definition named Pet"); - - deepStrictEqual(res.defs.shortString, { - type: "string", - maxLength: 10, - }); - deepStrictEqual(res.defs.shortButNotEmptyString, { - type: "string", - minLength: 1, - maxLength: 10, - }); -}); - -it("includes extensions passed on the scalar", async () => { - const res = await oapiForModel( - "Pet", - ` - @extension("x-custom", "my-value") - scalar Pet extends string; - ` - ); - - ok(res.defs.Pet, "expected definition named Pet"); - deepStrictEqual(res.defs.Pet, { - type: "string", - "x-custom": "my-value", - }); -}); - -describe("using @encode decorator", () => { - async function testEncode( - scalar: string, - expectedOpenApi: OpenAPI2Schema, - encoding?: string, - encodeAs?: string - ) { - const encodeAsParam = encodeAs ? `, ${encodeAs}` : ""; - const encodeDecorator = encoding ? `@encode("${encoding}"${encodeAsParam})` : ""; - const res1 = await oapiForModel("s", `${encodeDecorator} scalar s extends ${scalar};`); - deepStrictEqual(res1.defs.s, expectedOpenApi); - const res2 = await oapiForModel("Test", `model Test {${encodeDecorator} prop: ${scalar}};`); - deepStrictEqual(res2.defs.Test.properties.prop, expectedOpenApi); - } - - describe("utcDateTime", () => { - it("set format to 'date-time' by default", () => - testEncode("utcDateTime", { type: "string", format: "date-time" })); - it("set format to 'date-time-rfc7231' when encoding is rfc7231", () => - testEncode("utcDateTime", { type: "string", format: "date-time-rfc7231" }, "rfc7231")); - it("set format to 'date-time-rfc7231' for a header when encoding is rfc7231", async () => { - const oapi = await openApiFor(` - model Test {@header @encode("rfc7231") param: utcDateTime}; - - #suppress "@azure-tools/typespec-azure-core/use-standard-operations" "This is a test." - op read(...Test): void; - `); - const expected: OpenAPI2Parameter = { - name: "param", - in: "header", - required: true, - type: "string", - format: "date-time-rfc7231", - "x-ms-parameter-location": "method", - }; - deepStrictEqual(oapi.parameters.Test, expected); - }); - it("set format to 'http-date' when encoding is http-date", () => - testEncode("utcDateTime", { type: "string", format: "http-date" }, "http-date")); - it("set type to integer and format to 'unixtime' when encoding is unixTimestamp (unixTimestamp info is lost)", async () => { - const expected: OpenAPI2Schema = { type: "integer", format: "unixtime" }; - await testEncode("utcDateTime", expected, "unixTimestamp", "int32"); - await testEncode("utcDateTime", expected, "unixTimestamp", "int64"); - await testEncode("utcDateTime", expected, "unixTimestamp", "int8"); - await testEncode("utcDateTime", expected, "unixTimestamp", "uint8"); - }); - }); - - describe("offsetDateTime", () => { - it("set format to 'date-time' by default", () => - testEncode("offsetDateTime", { type: "string", format: "date-time" })); - it("set format to 'date-time-rfc7231' when encoding is rfc7231", () => - testEncode("offsetDateTime", { type: "string", format: "date-time-rfc7231" }, "rfc7231")); - it("set format to 'http-date' when encoding is http-date", () => - testEncode("offsetDateTime", { type: "string", format: "http-date" }, "http-date")); - }); - - describe("duration", () => { - it("set format to 'duration' by default", () => - testEncode("duration", { type: "string", format: "duration" })); - it("set integer with int32 format setting duration as seconds", () => - testEncode("duration", { type: "integer", format: "int32" }, "seconds", "int32")); - }); - - describe("bytes", () => { - it("set format to 'base64' by default", () => - testEncode("bytes", { type: "string", format: "byte" })); - it("set format to base64url when encoding bytes as base64url", () => - testEncode("bytes", { type: "string", format: "base64url" }, "base64url")); - }); -}); diff --git a/packages/typespec-autorest/CHANGELOG.md b/packages/typespec-autorest/CHANGELOG.md index 0c8ee57ea3..823cf32929 100644 --- a/packages/typespec-autorest/CHANGELOG.md +++ b/packages/typespec-autorest/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log - @azure-tools/typespec-autorest +## 0.44.1 + +### Features + +- [#1237](https://github.com/Azure/typespec-azure/pull/1237) Use new `createTcgcContext` from tcgc lib, which is the minimal context object that handles scope + + ## 0.44.0 ### Bug Fixes @@ -19,8 +26,15 @@ ### Breaking Changes -- [#1105](https://github.com/Azure/typespec-azure/pull/1105) `x-ms-client-flatten` extension on some of resource properties property is now configurable to be emitted by autorest emitter. Default is false which will skip emission of that extension. +- [#1105](https://github.com/Azure/typespec-azure/pull/1105) `x-ms-client-flatten` extension on some of resource properties property is now configurable to be emitted by autorest emitter(`arm-resource-flattening` option). Default is false which will skip emission of that extension. + To revert to previous behavior update your `tspconfig.yaml` with the following + ```diff + options: + "@azure-tools/typespec-autorest": + # ...other options + + arm-resource-flattening: true + ``` ## 0.43.0 diff --git a/packages/typespec-autorest/package.json b/packages/typespec-autorest/package.json index 127564ef2a..657958abbe 100644 --- a/packages/typespec-autorest/package.json +++ b/packages/typespec-autorest/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/typespec-autorest", - "version": "0.44.0", + "version": "0.44.1", "author": "Microsoft Corporation", "description": "TypeSpec library for emitting openapi from the TypeSpec REST protocol binding", "homepage": "https://azure.github.io/typespec-azure", diff --git a/packages/typespec-autorest/vitest.config.ts b/packages/typespec-autorest/vitest.config.ts index dd8d9aa35f..1f060db852 100644 --- a/packages/typespec-autorest/vitest.config.ts +++ b/packages/typespec-autorest/vitest.config.ts @@ -5,7 +5,7 @@ export default mergeConfig( defaultTypeSpecVitestConfig, defineConfig({ test: { - testTimeout: 10000, + testTimeout: 30000, }, }) ); diff --git a/packages/typespec-azure-core/lib/models.tsp b/packages/typespec-azure-core/lib/models.tsp index 7661e3dc07..f34f2adbc5 100644 --- a/packages/typespec-azure-core/lib/models.tsp +++ b/packages/typespec-azure-core/lib/models.tsp @@ -358,11 +358,18 @@ scalar azureLocation extends string; /** * A type definition that refers the id to an Azure Resource Manager resource. * - * Sample usage: - * otherArmId: ResourceIdentifier; - * networkId: ResourceIdentifier<[{type:"\\Microsoft.Network\\vnet"}]> - * vmIds: ResourceIdentifier<[{type:"\\Microsoft.Compute\\vm", scopes["*"]}]> * @template AllowedResourceTypes An array of allowed resource types for the resource reference + * + * @example + * + * ```tsp + * model MyModel { + * otherArmId: armResourceIdentifier; + * networkId: armResourceIdentifier<[{type:"Microsoft.Network/vnet"}]> + * vmIds: armResourceIdentifier<[{type:"Microsoft.Compute/vm", scopes: ["*"]}]> + * scoped: armResourceIdentifier<[{type:"Microsoft.Compute/vm", scopes: ["tenant", "resourceGroup"]}]> + * } + * ``` */ @doc("A type definition that refers the id to an Azure Resource Manager resource.") @format("arm-id") diff --git a/packages/typespec-client-generator-core/CHANGELOG.md b/packages/typespec-client-generator-core/CHANGELOG.md index 43e5449c9d..9e5e969c4a 100644 --- a/packages/typespec-client-generator-core/CHANGELOG.md +++ b/packages/typespec-client-generator-core/CHANGELOG.md @@ -1,5 +1,26 @@ # Change Log - @azure-tools/typespec-client-generator-core +## 0.44.2 + +### Bug Fixes + +- [#1231](https://github.com/Azure/typespec-azure/pull/1231) Fix the duplicate usageflags values for json and xml +- [#1203](https://github.com/Azure/typespec-azure/pull/1203) Have `@clientName` work for operation groups as well +- [#1222](https://github.com/Azure/typespec-azure/pull/1222) Validate `@clientName` conflict for operations inside interface + +### Features + +- [#1090](https://github.com/Azure/typespec-azure/pull/1090) Support model format of `@multipartBody` +- [#1237](https://github.com/Azure/typespec-azure/pull/1237) Expose createTcgcContext, which is the minimal context object that handles scope +- [#1223](https://github.com/Azure/typespec-azure/pull/1223) Report error diagnostic when trying to flattening a model with polymorphism +- [#1076](https://github.com/Azure/typespec-azure/pull/1076) Add example types support +- [#1204](https://github.com/Azure/typespec-azure/pull/1204) Add xml usage and change enumvalue arg representation in generic decorators + +### Breaking Changes + +- [#1015](https://github.com/Azure/typespec-azure/pull/1015) Refactor tcgc build-in types, please refer pr's description for details and migration guides + + ## 0.44.1 ### Bug Fixes diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index 13e4b1f39a..d29f159e2a 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -218,6 +218,8 @@ interface MyInterface {} #### `@clientFormat` +_Deprecated: @clientFormat decorator is deprecated. Use `@encode` decorator in `@typespec/compiler` instead._ + DEPRECATED: Use `@encode` decorator in `@typespec/compiler` instead. Can be used to explain the client type that the current TYPESPEC @@ -308,6 +310,8 @@ op test: void; #### `@exclude` +_Deprecated: @exclude decorator is deprecated. Use `@usage` and `@access` decorator instead._ + DEPRECATED: Use `@usage` and `@access` decorator instead. Whether to exclude a model from generation for specific languages. By default we generate @@ -338,6 +342,8 @@ model ModelToExclude { #### `@flattenProperty` +_Deprecated: @flattenProperty decorator is not recommended to use._ + Set whether a model property should be flattened or not. ```typespec @@ -366,6 +372,8 @@ model Bar {} #### `@include` +_Deprecated: @include decorator is deprecated. Use `@usage` and `@access` decorator instead._ + DEPRECATED: Use `@usage` and `@access` decorator instead. Whether to include a model in generation for specific languages. By default we generate @@ -396,6 +404,8 @@ model ModelToInclude { #### `@internal` +_Deprecated: @internal decorator is deprecated. Use `@access` decorator instead._ + DEPRECATED: Use `@access` decorator instead. Whether to mark an operation as internal for specific languages, diff --git a/packages/typespec-client-generator-core/doc/types.tsp b/packages/typespec-client-generator-core/doc/types.tsp deleted file mode 100644 index 9b8cb73a17..0000000000 --- a/packages/typespec-client-generator-core/doc/types.tsp +++ /dev/null @@ -1,794 +0,0 @@ -import "@typespec/http"; -using TypeSpec.Reflection; -using TypeSpec.Http; - -namespace TypeSpec.ClientGenerator.Core; - -/** - * Base SdkType that all types polymorphically extend from. - * - * @property __raw: the original TypeSpec type - * @property kind: the kind of type - * @property deprecation: deprecated message if the type is deprecated - */ -@discriminator("kind") -model SdkType { - __raw?: unknown; - deprecation?: string; -} - -/** - * Flags enum to keep track of usages for models and enums. - * - * @enum input: If the object is used as input - * @enum output: If the object is used as output - */ -enum UsageFlags { - input: 1, - output: 2, -} - -/** - * Access flags to keep track of access levels for operations and models - * - * @enum public: Whether the object is a publicly-accessible object - * @enum internal: Whether the object is an internal object - */ -enum AccessFlags { - public, - internal, -} - -/** - * Flags enum to keep track of visibility - * - * @enum read: Whether the object is read - */ -enum Visibility { - read: 1, -} - -/** - * These types are more primitive and roughly correspond to the TypeSpec built in types - * - * @property encode: How we represent the type to SDK users - */ -model SdkBuiltInType extends SdkType { - kind: - | "bytes" - | "boolean" - | "date" - | "time" - | "any" - | "int32" - | "int64" - | "float32" - | "float64" - | "decimal" - | "decimal128" - | "string" - | "guid" - | "url" - | "uuid" - | "password" - | "armId" - | "ipAddress" - | "azureLocation" - | "eTag"; - encode: string; -} - -/** - * Represents a datetime type. - * - * @property encode: How to encode the datetime and represent to users - * @property wireType: What type we end up sending over the wire for a datetime. Can be a string or an int type. - */ -model SdkDateTimeType extends SdkType { - kind: "datetime"; - encode: DateTimeKnownEncoding; - wireType: SdkType; -} - -/** - * Represents a duration type - * - * @property encode: How to encode the duration type and represent it to users. - * @property wireType: What type we end up sending over the wire for a duration. Can be a string, an int, or a float type. - */ -model SdkDurationType extends SdkType { - kind: "duration"; - encode: DurationKnownEncoding; - wireType: SdkType; -} - -/** - * An array type - * - * @property valueType: The type of each value in the array - */ -model SdkArrayType extends SdkType { - kind: "array"; - valueType: SdkType; -} - -/** - * An tuple type - * - * @property values: The values in the tuple - */ -model SdkTupleType extends SdkType { - kind: "tuple"; - values: SdkType[]; -} - -/** - * A dictionary type - * - * @property keyType: The type of the key. 99.9% of the time it's a string, but OpenAI does have it as an int. - * @property valueType: The type of the values - */ -model SdkDictionaryType extends SdkType { - kind: "dict"; - keyType: SdkType; - valueType: SdkType; -} - -/** - * Represents an enum type - * - * @property name: Name of the enum - * @property valueType: The type of the enum values - * @property values: List of enum values - * @property isFixed: Whether it's a fixed value enum - * @property description: Description of the enum - * @property details: Optional details of the enum object - * @property isFlags: Whether the enum represents a set of flags - * @property access: Access level of the enum - * @property usage: Usage cases for the enum. Used to filter when people only want input or output enums - * @property summary: Summary of an enum - * @property isUnionAsEnum: Whether the enum is converted from TypeSpec union type - */ -model SdkEnumType extends SdkType { - kind: "enum"; - name: string; - valueType: SdkBuiltInType; - values: SdkEnumValueType[]; - isFixed: boolean; - description?: string; - details?: string; - isFlags: boolean; - access: AccessFlags; - usage: UsageFlags; - summary: string; - nameSpace: string; - isUnionAsEnum: boolean; -} - -/** - * Represents an enum value type - * - * @property name: The name of the enum value - * @property value: The value of the enum - * @property enumType: The enum that this value belongs to - * @property valueType: Type of the value. Same as the type listed in SdkEnumType.valueType - * @property description: Description for the enum value - * @property details: Optional details on the enum value - */ -model SdkEnumValueType extends SdkType { - kind: "enumvalue"; - name: string; - value: string | numeric; - enumType: SdkEnumType; - valueType: SdkType; - description?: string; - details?: string; -} - -/** - * Represents a constant type - * - * @property value: The value of the constant - * @property valueType: The type of the constant value - */ -model SdkConstantType extends SdkType { - kind: "constant"; - value: string | numeric | boolean | null; - valueType: SdkBuiltInType; -} - -/** - * Represents a union type - * - * @property name: The name of the union type if it's a named union type, otherwise undefined - * @property values: The various values that are unioned - */ -@doc("Represents a union type") -model SdkUnionType extends SdkType { - kind: "union"; - name?: string; - values: SdkType[]; -} - -/** - * Represents a model type - * - * @property name: Name of the model - * @property description: Description of the model - * @property details: Optional details of the model - * @property properties: List of properties on the model - * @property access: Access level of the model - * @property usage: Usage cases for the model. Used to filter when people only want input or output models. - * @property additionalProperties: Model's additional properties type, if no additional properties, then undefined - * @property discriminatorValue: Value of the discriminator if this is a discriminated subtype. Will be undefined if not. - * @property discriminatedSubtypes: Mapping of discriminator value to this models discriminated subtypes if there are any. - * @property discriminatorProperty: The property that is the discriminator for this model if it's a discriminated subtype. Will be undefined if not. - * @property baseModel: The base model class of this model type if one exists. - */ -model SdkModelType extends SdkType { - kind: "model"; - name: string; - description?: string; - details?: string; - summary: string; - properties: SdkModelPropertyType[]; - access: AccessFlags; - usage: UsageFlags; - additionalProperties?: SdkType; - discriminatorValue?: string; - discriminatedSubtypes?: Record; - discriminatorProperty?: SdkModelPropertyType; - baseModel?: SdkModelType; - nameSpace: string; -} - -/** - * Base class for our property types - * - * @property kind: The kind of property - * @property __raw: The original TSP property - * @property type: The type of the property - * @property name: The name of the property in our client SDKs - * @property description: Description for the property - * @property details: Optional details of the property - * @property apiVersions: Api versions the property is available for - * @property onClient: Whether the property is on the client - * @property optional: Whether its an optional property - */ -@doc("Base class for our property types") -@discriminator("kind") -model SdkModelPropertyType { - __raw?: ModelProperty; - type: SdkType; - name: string; - description?: string; - details?: string; - apiVersions: string[]; - onClient: boolean; - optional: boolean; -} - -/** - * If one to one between method parameter and sdk service parameter: - * MethodParameter: { - * name: "pathParam", - * type: "string", - * kind: "method" - * } - * SdkServiceParameter: { - * name: "pathParam", - * type: "string", - * kind: "path" - * } - * mapping: { - * "": SdkServiceParameter - * } - * - * If one to many between method parameter and sdk service parameter - * MethodParameter: { - * name: "input", - * type: { - * kind: "model", - * properties: [ - * { - * name: "path_param", - * serializedName: "pathParam" - * type: "string", - * kind: "path" - * }, - * { - * name: "query_param", - * name: "queryParam", - * type: "string", - * kind: "query" - * } - * ] - * }, - * kind: "method" - * } - * PathServiceParameter: { - * name: "pathParam", - * serializedName: "pathParam", - * type: "string", - * kind: "path", - * mapping : { - * logicalPath: "path_param", - * type: "string", - * } - * } - * QueryServiceParameter: { - * name: "queryParam", - * serializedName: "queryParam", - * type: "string", - * kind: "query", - * mapping: { - * logicalPath: "query_param", - * type: "string" - * } - * } - */ -/** - * Represents a mapping from the method to the service or from the service to the method - */ -model SdkMethodServiceMapping { - logicalPath: string; - property: SdkModelPropertyType; -} - -/** - * Represents a method parameter - */ -model SdkMethodParameter extends SdkModelPropertyType { - kind: "method"; -} - -/** - * Represents a property for a body model type - * - * @property visibility: Visibility of the model - * @property discriminator: Whether the property is a discriminator - * @property serializedName: The name of the property we send over the wire to the services - */ -model SdkBodyModelPropertyType extends SdkModelPropertyType { - kind: "property"; - discriminator: boolean; - serializedName: string; - visibility?: Visibility; -} - -/** - * Represents a header parameter - * - * @property serializedName: The name of the property we send over the wire to the services - * @property collectionFormat: The format for a collection of headers - */ -model SdkHeaderServiceParameter extends SdkModelPropertyType { - kind: "header"; - serializedName: string; - collectionFormat?: "multi" | "csv" | "ssv" | "tsv" | "pipes"; - mapping: SdkMethodServiceMapping; -} - -/** - * Represents a query parameter. - * - * @property serializedName: The name of the property we send over the wire to the services - * @property collectionFormat: The format for a collection of queries - */ -model SdkQueryServiceParameter extends SdkModelPropertyType { - kind: "query"; - serializedName: string; - collectionFormat?: "multi" | "csv" | "ssv" | "tsv" | "pipes"; - mapping: SdkMethodServiceMapping; -} - -/** - * Represents a path parameter - * - * @property serializedName: The name of the property we send over the wire to the services - * @property urlEncode: Whether to url encode the path parameter - * @property validation: Any validation for the path parameter. Right now only including validation on path parameters because we only know about path params needing validation. - * @property location: Whether the parameter is a client-level or operation-level parameter - */ -model SdkPathServiceParameter extends SdkModelPropertyType { - kind: "path"; - serializedName: string; - urlEncode: boolean; - validation?: SdkValidation; - mapping: SdkMethodServiceMapping; - optional: false; -} - -/** - * Represents an Oauth2Auth flow. Copied from typespec/http because it doesn't export - * - * @property id: id of the authentication scheme - * @property description: Optional description - * @property flows: Flows for the authentication - */ -model Oauth2Auth { - type: "oauth2"; - id: string; - description?: string; - flows: TFlows; -} - -/** - * Credential type for a client - * - * @property scheme: Potential schemes for the credential - */ -model SdkCredentialType extends SdkType { - kind: "credential"; - scheme: BasicAuth | BearerAuth | ApiKeyAuth | Oauth2Auth; -} - -/** - * Represents a credential parameter - * - * @property type: The type of the credential. Can be either a single SdkCredentialType or a union of SdkCredentialTypes - */ -model SdkCredentialParameter extends SdkModelPropertyType { - kind: "credential"; - type: SdkCredentialType | SdkUnionType; - urlEncode: boolean; -} - -/** - * Represents the client endpoint parameter - */ -model SdkEndpointParameter extends SdkModelPropertyType { - kind: "endpoint"; - type: SdkType; // should be string - urlEncode: boolean; - serializedName?: string; -} - -/** - * Represents a body parameter to the service. - * - * @property contentTypes: The content types the body parameter is valid for - * @property defaultContentType: What content type to send by default if none are specified - * @property mapping: Mapping from the body parameter to the method parameter - */ -model SdkBodyServiceParameter extends SdkModelPropertyType { - kind: "body"; - contentTypes: string[]; - defaultContentType: string; - mapping: SdkMethodServiceMapping; -} - -/** - * Model representing all parameters on an operation - * - * @property __raw: The original TSP type - * @property parameters: List of query / header / path parameters - * @property body: The body parameter, if one exists - */ -model SdkOperationParameters { - __raw?: unknown; - parameters: (SdkQueryParameter | SdkHeaderParameter | SdkPathParameter)[]; - body?: SdkBodyParameter; -} - -/** - * Representing a response header - * - * @property __raw: The original TSP response header - * @property serializedName: The serialized name of the response header - * @property type: Type of the response header - */ -model SdkResponseHeader { - __raw?: ModelProperty; - serializedName: string; - type: SdkType; -} - -/** - * Representing a possible response of an operation - * - * @property __raw: The original TSP response - * @property type: The type of the response - */ -@discriminator("kind") -model SdkResponse { - __raw: unknown; - type: SdkType; -} - -/** - * Represents a response from a service operation - * - * @property headers: Headers returned as part of the response - */ -model SdkServiceOperationResponse extends SdkResponse { - kind: "operation"; - headers: SdkResponseHeader[]; -} - -/** - * Response from a method that calls a service operation - * - * @property mapping: Mapping from the responses of the service operation to the response the method returns - */ -model SdkServiceMethodResponse extends SdkResponse { - kind: "method"; - mapping: SdkMethodServiceMapping; -} - -/** - * Service parameter - */ -alias SdkServiceParameter = SdkQueryServiceParameter | SdkHeaderServiceParameter | SdkPathServiceParameter; - -/** - * Base class for representation of a client method. - * - * @property name: The name of the method - * @property access: The access level of the method - * @property parameters: Parameters object for the method - * @property description: Description of the operation - * @property details: Optional details of the operation - */ -@discriminator("kind") -model SdkMethod { - name: string; // return myFunc for Python and Java, but include projectedNames - access: AccessFlags; - parameters: SdkMethodParameter[]; - description?: string; - details?: string; -} - -/** - * Returns an sdk client from a top level client - * - * @property response: which client the operation returns - */ -model SdkClientAccessor extends SdkMethod { - kind: "client"; - response: SdkClientType; -} - -/** - * Represents a method that includes a service call - * - * @property operation: The operation that calls the service - * @property response: The model the method returns - * @property exceptions: Exceptions of the method - */ -model SdkServiceMethod extends SdkMethod { - kind: "methodforserviceoperation"; - operation: SdkServiceOperation; - response: SdkServiceMethodResponse; - exceptions: SdkServiceMethodResponse[]; -} - -/** - * Represents an operation to the service. Is HTTP specific. - * - * @property __raw: The original TSP HTTP operation - * @property path: Path of the operation - * @property verb: Http verb - * @property parameters: HTTP parameters for the service operation. These parameters will contain a mapping from the method to themselves - * @property responses: Non-error responses of the operation - * @property exceptions: Exceptions of the operation - */ -@discriminator("kind") -model SdkServiceOperation { - __raw: unknown; - path: string; - verb: string; - parameters: SdkServiceParameter[]; - bodyParameter?: SdkBodyServiceParameter; - responses: Record; // int status code -> SdkServiceOperationResponse - exception: SdkServiceOperationResponse; -} - -/** - * Representation of a basic operation - * - * @property kind: Discriminator for the type of operation - */ -model SdkBasicServiceOperation extends SdkServiceOperation { - kind: "basic"; -} - -/** - * Sdk paging operation options - * - * @property __raw_paged_metadata: The original TSP paging metadata - * @property itemsMapping: Mapping of the items to return in the iterable - * @property nextLinkPath: Where to get the next link. Off if we know there won't be a second page - * @property nextLinkOperation: The operation to call to get the next page, if there is one - */ -alias SdkPagingServiceOperationOptions = { - __raw_paged_metadata: unknown; - itemsMapping: SdkMethodServiceMapping; - nextLinkPath?: string; - nextLinkOperation?: SdkServiceOperation; -}; - -/** - * Representation of a paging operation - */ -model SdkPagingServiceOperation extends SdkServiceOperation { - kind: "paging"; - ...SdkPagingServiceOperationOptions; -} - -/** - * Sdk LRO operation options - * - * @property __raw_lro_metadata: The original TSP LRO metadata - */ -alias SdkLroServiceOperationOptions = { - __raw_lro_metadata: unknown; -}; - -/** - * Representation of a lro operation - */ -model SdkLroServiceOperation extends SdkServiceOperation { - kind: "lro"; - ...SdkLroServiceOperationOptions; -} - -/** - * Representation of a lro paging operation - */ -model SdkLroPagingServiceOperation extends SdkServiceOperation { - kind: "lropaging"; - ...SdkPagingServiceOperationOptions; - ...SdkLroServiceOperationOptions; -} - -/** - * Base class for validation on types - * - * @property kind: Kind for validation - */ -@discriminator("kind") -model SdkValidation {} - -/** - * Validations for a string type - * - * @property pattern: Any validation on the pattern of the string - * @property minLength: Validation on the min length of the string - * @property maxLength: Validation on the max length of the string - */ -model SdkStringValidation extends SdkValidation { - kind: "string"; - pattern?: string; - minLength?: int32; - maxLength?: int32; -} - -/** - * Validations for a numeric type - * - * @template Kind Kind of the numeric type - * @template Type SdkType of the min / max value - * @property minValue: Validation on the min value of the number - * @property maxValue: Validation on the max value of the number - */ -model SdkNumericValidationTemplate extends SdkValidation { - kind: Kind; - minValue?: Type; - maxValue?: Type; -} - -/** - * Validation for an int32 type - */ -model SdkInt32Validation is SdkNumericValidationTemplate<"int32", int32>; - -/** - * Validation for an int64 type - */ -model SdkInt64Validation is SdkNumericValidationTemplate<"int64", int64>; - -/** - * Validation for an float32 type - */ -model SdkFloat32Validation is SdkNumericValidationTemplate<"float32", float32>; - -/** - * Validation for an float64 type - */ -model SdkFloat64Validation is SdkNumericValidationTemplate<"float64", float64>; - -/** - * Representation of an SdkClient - * - * @property name: name of the client - * @property description: documentation of the client - * @property details: details of the client - * @property initialization: initialization options for the client. Will include endpoint and credential options. Name defaults to {SdkClient.name}Options - * @property methods: list of methods on the client. Can also be methods that return other clients. - * @property apiVersions: List of api versions the client is available for - * @property nameSpace: fully qualified namespace of the client - * @property arm: is the client an arm client - */ -model SdkClientType { - kind: "client"; - name: string; - description?: string; - details?: string; - initialization: SdkModelType; - methods: SdkMethod[]; - - // TODO: we have groups of clients (for example, network client is a grouping of resource clients). - // these do not have api versions. Should we do separate models for these? Or keep api versions as optional? - apiVersions: string[]; // look into the sorting of this, if versioning package already does order it - - nameSpace: string; - arm: boolean; -} - -/** - * Represents and entire SDK package - * - * @property name: Name of the package - * @property rootNamespace: Root namespace of the package - * @property clients: List of clients in the package - * @property models: List of models in the package - * @property enums: List of enums in the package - */ -model SdkPackage { - name: string; - rootNamespace: string; // TODO: how do we want to define this for languages in client.tsp. few samples for each language to fill in - clients: SdkClientType[]; - models: SdkModelType[]; // 2 options: Have it as a function that takes in filter, or add filter as a context parameter - enums: SdkEnumType[]; -} - -/** - * Context that is passed around for an emitter's instance in TCGC. - * - * @property program: The TypeSpec program, a Program type. - * @property emitContext: The emit context of the tsp emitter. Pass the full library name of your emitter - * @property generateProtocolMethods: Whether to generate protocol methods by default - * @property generateConvenienceMethods: Whether to generate convenience methods by default - * @property filterOutCoreModels: Whether to filter out core models in returned models. Defaults to true. - * @property packageName: The name of the package - * @property emitterName: The name of the emitter. If not passed, we will find the value ourselves. - * @property modelsMap: Type map we create to keep track of all models - * @property operationModelsMap: Type map we create to keep track of mapping between operations and the models they use. Used for transitive closure on access. - */ -model SdkContext { - program: unknown; - emitContext: unknown; - generateProtocolMethods: boolean; - generateConvenienceMethods: boolean; - filterOutCoreModels: boolean = true; - packageName?: string; - emitterName?: string; - modelsMap?: Record; - operationModelsMap?: Record; - returnEntireLroResult: boolean; -} - -/** - * getAllModelsRequestOptions - * - * @property input: boolean determining whether to return input models. Default is true - * @property output: boolean determining whether to return output models. Default is true - */ -alias GetAllModelsRequestOptions = { - input: boolean = true; - output: boolean = true; -}; - -/** - * Return all of the types we want to generate filtered by usage flags if filter is specified. - * - * @param context: the sdk context - * @returns List of models and enums to generate as types - */ -op getAllModels( - @doc("Sdk context") - context: SdkContext, - - ...GetAllModelsRequestOptions, -): (SdkModelType | SdkEnumType)[]; diff --git a/packages/typespec-client-generator-core/package.json b/packages/typespec-client-generator-core/package.json index 8ac20021d8..51678c95c0 100644 --- a/packages/typespec-client-generator-core/package.json +++ b/packages/typespec-client-generator-core/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/typespec-client-generator-core", - "version": "0.44.1", + "version": "0.44.2", "author": "Microsoft Corporation", "description": "TypeSpec Data Plane Generation library", "homepage": "https://azure.github.io/typespec-azure", @@ -62,6 +62,7 @@ "@azure-tools/typespec-azure-core": "workspace:~", "@typespec/compiler": "workspace:~", "@typespec/http": "workspace:~", + "@typespec/openapi": "workspace:~", "@typespec/rest": "workspace:~", "@typespec/versioning": "workspace:~" }, @@ -72,10 +73,10 @@ "@typespec/compiler": "workspace:~", "@typespec/http": "workspace:~", "@typespec/library-linter": "workspace:~", + "@typespec/openapi": "workspace:~", "@typespec/prettier-plugin-typespec": "workspace:~", "@typespec/rest": "workspace:~", "@typespec/tspd": "workspace:~", - "@typespec/versioning": "workspace:~", "@typespec/xml": "workspace:~", "@vitest/coverage-v8": "^2.0.4", "@vitest/ui": "^2.0.4", diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index bcfa75bf62..9b6b472567 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -17,6 +17,7 @@ import { Type, Union, createDiagnosticCollector, + getDiscriminator, getNamespaceFullName, getProjectedName, ignoreDiagnostics, @@ -45,6 +46,7 @@ import { UsageDecorator, } from "../generated-defs/Azure.ClientGenerator.Core.js"; import { defaultDecoratorsAllowList } from "./configs.js"; +import { handleClientExamples } from "./example.js"; import { AccessFlags, LanguageScopes, @@ -57,7 +59,12 @@ import { TCGCContext, UsageFlags, } from "./interfaces.js"; -import { AllScopes, clientNameKey, parseEmitterName } from "./internal-utils.js"; +import { + AllScopes, + clientNameKey, + getValidApiVersion, + parseEmitterName, +} from "./internal-utils.js"; import { createStateSymbol, reportDiagnostic } from "./lib.js"; import { getSdkPackage } from "./package.js"; import { getLibraryName } from "./public-utils.js"; @@ -246,14 +253,7 @@ function serviceVersioningProjection(context: TCGCContext, client: SdkClient) { ?.getVersions() .map((x) => x.value); if (!allApiVersions) return; - let apiVersion = context.apiVersion; - if ( - apiVersion === "latest" || - apiVersion === undefined || - !allApiVersions.includes(apiVersion) - ) { - apiVersion = allApiVersions[allApiVersions.length - 1]; - } + const apiVersion = getValidApiVersion(context, allApiVersions); if (apiVersion === undefined) return; const versionProjections = buildVersionProjections(context.program, client.service).filter( (v) => apiVersion === v.version @@ -629,14 +629,15 @@ export interface CreateSdkContextOptions { additionalDecorators?: string[]; } -export function createSdkContext< +export async function createSdkContext< TOptions extends Record = SdkEmitterOptions, TServiceOperation extends SdkServiceOperation = SdkHttpOperation, >( context: EmitContext, emitterName?: string, options?: CreateSdkContextOptions -): SdkContext { +): Promise> { + const diagnostics = createDiagnosticCollector(); const protocolOptions = true; // context.program.getLibraryOptions("generate-protocol-methods"); const convenienceOptions = true; // context.program.getLibraryOptions("generate-convenience-methods"); const generateProtocolMethods = context.options["generate-protocol-methods"] ?? protocolOptions; @@ -656,15 +657,15 @@ export function createSdkContext< packageName: context.options["package-name"], flattenUnionAsEnum: context.options["flatten-union-as-enum"] ?? true, apiVersion: options?.versioning?.strategy === "ignore" ? "all" : context.options["api-version"], + examplesDirectory: context.options["examples-directory"], decoratorsAllowList: [...defaultDecoratorsAllowList, ...(options?.additionalDecorators ?? [])], previewStringRegex: options?.versioning?.previewStringRegex || tcgcContext.previewStringRegex, }; - sdkContext.sdkPackage = getSdkPackage(sdkContext); - if (sdkContext.diagnostics) { - sdkContext.diagnostics = sdkContext.diagnostics.concat( - sdkContext.sdkPackage.diagnostics // eslint-disable-line deprecation/deprecation - ); + sdkContext.sdkPackage = diagnostics.pipe(getSdkPackage(sdkContext)); + for (const client of sdkContext.sdkPackage.clients) { + diagnostics.pipe(await handleClientExamples(sdkContext, client)); } + sdkContext.diagnostics = sdkContext.diagnostics.concat(diagnostics.diagnostics); return sdkContext; } @@ -992,6 +993,13 @@ export const $flattenProperty: FlattenPropertyDecorator = ( target: ModelProperty, scope?: LanguageScopes ) => { + if (getDiscriminator(context.program, target.type)) { + reportDiagnostic(context.program, { + code: "flatten-polymorphism", + format: {}, + target: target, + }); + } setScopedDecoratorData(context, $flattenProperty, flattenPropertyKey, target, true, scope); // eslint-disable-line deprecation/deprecation }; diff --git a/packages/typespec-client-generator-core/src/example.ts b/packages/typespec-client-generator-core/src/example.ts new file mode 100644 index 0000000000..6043c0011a --- /dev/null +++ b/packages/typespec-client-generator-core/src/example.ts @@ -0,0 +1,632 @@ +import { + Diagnostic, + DiagnosticCollector, + NoTarget, + SourceFile, + createDiagnosticCollector, + resolvePath, +} from "@typespec/compiler"; +import { HttpStatusCodeRange } from "@typespec/http"; +import { resolveOperationId } from "@typespec/openapi"; +import { + SdkAnyExample, + SdkArrayExample, + SdkArrayType, + SdkBodyModelPropertyType, + SdkClientType, + SdkDictionaryExample, + SdkDictionaryType, + SdkHttpOperation, + SdkHttpOperationExample, + SdkHttpParameter, + SdkHttpParameterExample, + SdkHttpResponse, + SdkHttpResponseExample, + SdkModelExample, + SdkModelPropertyType, + SdkModelType, + SdkNullExample, + SdkServiceMethod, + SdkServiceOperation, + SdkType, + SdkTypeExample, + SdkUnionExample, + TCGCContext, + isSdkFloatKind, + isSdkIntKind, +} from "./interfaces.js"; +import { getValidApiVersion } from "./internal-utils.js"; +import { createDiagnostic } from "./lib.js"; + +interface LoadedExample { + readonly relativePath: string; + readonly file: SourceFile; + readonly data: any; +} + +/** + * Load all examples for a client + * + * @param context + * @param apiVersion + * @returns a map of all operations' examples, key is operation's operation id, + * value is a map of examples, key is example's title, value is example's details + */ +async function loadExamples( + context: TCGCContext, + apiVersion: string | undefined +): Promise<[Map>, readonly Diagnostic[]]> { + const diagnostics = createDiagnosticCollector(); + if (!context.examplesDirectory) { + return diagnostics.wrap(new Map()); + } + + const exampleDir = apiVersion + ? resolvePath(context.examplesDirectory, apiVersion) + : resolvePath(context.examplesDirectory); + try { + if (!(await context.program.host.stat(exampleDir)).isDirectory()) + return diagnostics.wrap(new Map()); + } catch (err) { + diagnostics.add( + createDiagnostic({ + code: "example-loading", + messageId: "noDirectory", + format: { directory: exampleDir }, + target: NoTarget, + }) + ); + return diagnostics.wrap(new Map()); + } + + const map = new Map>(); + const exampleFiles = await context.program.host.readDir(exampleDir); + for (const fileName of exampleFiles) { + try { + const exampleFile = await context.program.host.readFile(resolvePath(exampleDir, fileName)); + const example = JSON.parse(exampleFile.text); + if (!example.operationId || !example.title) { + diagnostics.add( + createDiagnostic({ + code: "example-loading", + messageId: "noOperationId", + format: { filename: fileName }, + target: NoTarget, + }) + ); + continue; + } + + if (!map.has(example.operationId.toLowerCase())) { + map.set(example.operationId.toLowerCase(), {}); + } + const examples = map.get(example.operationId.toLowerCase())!; + + if (example.title in examples) { + diagnostics.add( + createDiagnostic({ + code: "duplicate-example-file", + target: NoTarget, + format: { + filename: fileName, + operationId: example.operationId, + title: example.title, + }, + }) + ); + } + + examples[example.title] = { + relativePath: fileName, + file: exampleFile, + data: example, + }; + } catch (err) { + diagnostics.add( + createDiagnostic({ + code: "example-loading", + messageId: "default", + format: { filename: fileName, error: err?.toString() ?? "" }, + target: NoTarget, + }) + ); + } + } + return diagnostics.wrap(map); +} + +export async function handleClientExamples( + context: TCGCContext, + client: SdkClientType +): Promise<[void, readonly Diagnostic[]]> { + const diagnostics = createDiagnosticCollector(); + + const examples = diagnostics.pipe( + await loadExamples(context, getValidApiVersion(context, client.apiVersions)) + ); + const methodQueue = [...client.methods]; + while (methodQueue.length > 0) { + const method = methodQueue.pop()!; + if (method.kind === "clientaccessor") { + methodQueue.push(...method.response.methods); + } else { + // since operation could have customization in client.tsp, we need to handle all the original operation (exclude the templated operation) + let operation = method.__raw; + while (operation && operation.templateMapper === undefined) { + const operationId = resolveOperationId(context.program, operation).toLowerCase(); + if (examples.has(operationId)) { + diagnostics.pipe(handleMethodExamples(context, method, examples.get(operationId)!)); + break; + } + operation = operation.sourceOperation; + } + } + } + return diagnostics.wrap(undefined); +} + +function handleMethodExamples( + context: TCGCContext, + method: SdkServiceMethod, + examples: Record +): [void, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + + if (method.operation.kind === "http") { + diagnostics.pipe(handleHttpOperationExamples(method.operation, examples)); + if (method.operation.examples) { + if (!context.__httpOperationExamples) { + context.__httpOperationExamples = new Map(); + } + context.__httpOperationExamples!.set(method.operation.__raw, method.operation.examples); + } + } + + return diagnostics.wrap(undefined); +} + +function handleHttpOperationExamples( + operation: SdkHttpOperation, + examples: Record +) { + const diagnostics = createDiagnosticCollector(); + operation.examples = []; + + for (const [title, example] of Object.entries(examples)) { + const operationExample = { + kind: "http", + name: title, + description: title, + filePath: example.file.path, + parameters: diagnostics.pipe( + handleHttpParameters( + operation.bodyParam + ? [...operation.parameters, operation.bodyParam] + : operation.parameters, + example.data, + example.relativePath + ) + ), + responses: diagnostics.pipe( + handleHttpResponses(operation.responses, example.data, example.relativePath) + ), + rawExample: example.data, + } as SdkHttpOperationExample; + + operation.examples.push(operationExample); + } + + return diagnostics.wrap(undefined); +} + +function handleHttpParameters( + parameters: SdkHttpParameter[], + example: any, + relativePath: string +): [SdkHttpParameterExample[], readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + const parameterExamples = [] as SdkHttpParameterExample[]; + if ("parameters" in example && typeof example.parameters === "object") { + for (const name of Object.keys(example.parameters)) { + let parameter = parameters.find( + (p) => (p.kind !== "body" && p.serializedName === name) || p.name === name + ); + // fallback to body in example for any body parameter + if (!parameter && name === "body") { + parameter = parameters.find((p) => p.kind === "body"); + } + if (parameter) { + const value = diagnostics.pipe( + getSdkTypeExample(parameter.type, example.parameters[parameter.name], relativePath) + ); + if (value) { + parameterExamples.push({ + parameter, + value, + }); + } + } else { + addExampleValueNoMappingDignostic( + diagnostics, + { [name]: example.parameters[name] }, + relativePath + ); + } + } + } + return diagnostics.wrap(parameterExamples); +} + +function handleHttpResponses( + responses: Map, + example: any, + relativePath: string +): [Map, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + const responseExamples = new Map(); + if ("responses" in example && typeof example.responses === "object") { + for (const code of Object.keys(example.responses)) { + const statusCode = parseInt(code, 10); + let found = false; + for (const [responseCode, response] of responses.entries()) { + if (responseCode === statusCode) { + responseExamples.set( + statusCode, + diagnostics.pipe(handleHttpResponse(response, example.responses[code], relativePath)) + ); + found = true; + break; + } else if ( + typeof responseCode === "object" && + responseCode.start <= statusCode && + responseCode.end >= statusCode + ) { + responseExamples.set( + statusCode, + diagnostics.pipe(handleHttpResponse(response, example.responses[code], relativePath)) + ); + found = true; + break; + } + } + if (!found) { + addExampleValueNoMappingDignostic( + diagnostics, + { [code]: example.responses[code] }, + relativePath + ); + } + } + } + return diagnostics.wrap(responseExamples); +} + +function handleHttpResponse( + response: SdkHttpResponse, + example: any, + relativePath: string +): [SdkHttpResponseExample, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + const responseExample = { + response, + headers: [], + } as SdkHttpResponseExample; + if (typeof example === "object") { + for (const name of Object.keys(example)) { + if (name === "description") { + continue; + } else if (name === "body") { + if (response.type) { + responseExample.bodyValue = diagnostics.pipe( + getSdkTypeExample(response.type, example.body, relativePath) + ); + } else { + addExampleValueNoMappingDignostic(diagnostics, { body: example.body }, relativePath); + } + } else if (name === "headers") { + for (const subName of Object.keys(example.headers)) { + const header = response.headers.find((p) => p.serializedName === subName); + if (header) { + const value = diagnostics.pipe( + getSdkTypeExample(header.type, example[name][subName], relativePath) + ); + if (value) { + responseExample.headers.push({ + header, + value, + }); + } + } else { + addExampleValueNoMappingDignostic( + diagnostics, + { [subName]: example[name][subName] }, + relativePath + ); + } + } + } else { + addExampleValueNoMappingDignostic(diagnostics, { [name]: example[name] }, relativePath); + } + } + } + return diagnostics.wrap(responseExample); +} + +function getSdkTypeExample( + type: SdkType | SdkModelPropertyType, + example: any, + relativePath: string +): [SdkTypeExample | undefined, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + + if (isSdkIntKind(type.kind) || isSdkFloatKind(type.kind)) { + return getSdkBaseTypeExample("number", type as SdkType, example, relativePath); + } else { + switch (type.kind) { + case "string": + case "bytes": + return getSdkBaseTypeExample("string", type as SdkType, example, relativePath); + case "boolean": + return getSdkBaseTypeExample("boolean", type as SdkType, example, relativePath); + case "url": + case "plainDate": + case "plainTime": + return getSdkBaseTypeExample("string", type as SdkType, example, relativePath); + case "nullable": + if (example === null) { + return diagnostics.wrap({ + kind: "null", + type, + value: null, + } as SdkNullExample); + } else { + return getSdkTypeExample(type.type, example, relativePath); + } + case "any": + return diagnostics.wrap({ + kind: "any", + type, + value: example, + } as SdkAnyExample); + case "constant": + if (example === type.value) { + return getSdkBaseTypeExample( + typeof type.value as "string" | "number" | "boolean", + type, + example, + relativePath + ); + } else { + addExampleValueNoMappingDignostic(diagnostics, example, relativePath); + return diagnostics.wrap(undefined); + } + case "enum": + if (type.values.some((v) => v.value === example)) { + return getSdkBaseTypeExample( + typeof example as "string" | "number", + type, + example, + relativePath + ); + } else { + addExampleValueNoMappingDignostic(diagnostics, example, relativePath); + return diagnostics.wrap(undefined); + } + case "enumvalue": + if (type.value === example) { + return getSdkBaseTypeExample( + typeof example as "string" | "number", + type, + example, + relativePath + ); + } else { + addExampleValueNoMappingDignostic(diagnostics, example, relativePath); + return diagnostics.wrap(undefined); + } + case "utcDateTime": + case "offsetDateTime": + case "duration": + const inner = diagnostics.pipe(getSdkTypeExample(type.wireType, example, relativePath)); + if (inner) { + inner.type = type; + } + return diagnostics.wrap(inner); + case "union": + return diagnostics.wrap({ + kind: "union", + type, + value: example, + } as SdkUnionExample); + case "array": + return getSdkArrayExample(type, example, relativePath); + case "dict": + return getSdkDictionaryExample(type, example, relativePath); + case "model": + return getSdkModelExample(type, example, relativePath); + } + } + return diagnostics.wrap(undefined); +} + +function getSdkBaseTypeExample( + kind: "string" | "number" | "boolean", + type: SdkType, + example: any, + relativePath: string +): [SdkTypeExample | undefined, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + if (typeof example === kind) { + return diagnostics.wrap({ + kind, + type, + value: example, + } as SdkTypeExample); + } else { + addExampleValueNoMappingDignostic(diagnostics, example, relativePath); + } + return diagnostics.wrap(undefined); +} + +function getSdkArrayExample( + type: SdkArrayType, + example: any, + relativePath: string +): [SdkArrayExample | undefined, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + if (Array.isArray(example)) { + const arrayExample = [] as SdkTypeExample[]; + for (const item of example) { + const result = diagnostics.pipe(getSdkTypeExample(type.valueType, item, relativePath)); + if (result) { + arrayExample.push(result); + } + } + return diagnostics.wrap({ + kind: "array", + type, + value: arrayExample, + } as SdkArrayExample); + } else { + addExampleValueNoMappingDignostic(diagnostics, example, relativePath); + return diagnostics.wrap(undefined); + } +} + +function getSdkDictionaryExample( + type: SdkDictionaryType, + example: any, + relativePath: string +): [SdkDictionaryExample | undefined, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + if (typeof example === "object") { + const dictionaryExample = {} as Record; + for (const key of Object.keys(example)) { + const result = diagnostics.pipe( + getSdkTypeExample(type.valueType, example[key], relativePath) + ); + if (result) { + dictionaryExample[key] = result; + } + } + return diagnostics.wrap({ + kind: "dict", + type, + value: dictionaryExample, + } as SdkDictionaryExample); + } else { + addExampleValueNoMappingDignostic(diagnostics, example, relativePath); + return diagnostics.wrap(undefined); + } +} + +function getSdkModelExample( + type: SdkModelType, + example: any, + relativePath: string +): [SdkModelExample | undefined, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + if (typeof example === "object") { + // handle discriminated model + if (type.discriminatorProperty) { + if ( + type.discriminatorProperty.name in example && + example[type.discriminatorProperty.name] in type.discriminatedSubtypes! + ) { + return getSdkModelExample( + type.discriminatedSubtypes![example[type.discriminatorProperty.name]], + example, + relativePath + ); + } else { + addExampleValueNoMappingDignostic(diagnostics, example, relativePath); + return diagnostics.wrap(undefined); + } + } + + let additionalPropertiesType: SdkType | undefined; + const additionalProperties: Record = new Map(); + const additionalPropertiesExample: Record = {}; + + const properties: Map = new Map(); + const propertiesExample: Record = {}; + + // get all properties type and additional properties type if exist + const modelQueue = [type]; + while (modelQueue.length > 0) { + const model = modelQueue.pop()!; + for (let property of model.properties) { + property = property as SdkBodyModelPropertyType; + if (!properties.has(property.serializedName)) { + properties.set(property.serializedName, property); + } + } + if (model.additionalProperties && additionalPropertiesType === undefined) { + additionalPropertiesType = model.additionalProperties; + } + if (model.baseModel) { + modelQueue.push(model.baseModel); + } + } + + for (const name of Object.keys(example)) { + const property = properties.get(name); + if (property) { + const result = diagnostics.pipe( + getSdkTypeExample(property.type, example[name], relativePath) + ); + if (result) { + propertiesExample[name] = result; + } + } else { + additionalProperties[name] = example[name]; + } + } + + // handle additional properties + if (Object.keys(additionalProperties).length > 0) { + if (additionalPropertiesType) { + for (const [name, value] of Object.entries(additionalProperties)) { + const result = diagnostics.pipe( + getSdkTypeExample(additionalPropertiesType, value, relativePath) + ); + if (result) { + additionalPropertiesExample[name] = result; + } + } + } else { + addExampleValueNoMappingDignostic(diagnostics, additionalProperties, relativePath); + } + } + + return diagnostics.wrap({ + kind: "model", + type, + value: propertiesExample, + additionalPropertiesValue: + Object.keys(additionalPropertiesExample).length > 0 + ? additionalPropertiesExample + : undefined, + } as SdkModelExample); + } else { + addExampleValueNoMappingDignostic(diagnostics, example, relativePath); + return diagnostics.wrap(undefined); + } +} + +function addExampleValueNoMappingDignostic( + diagnostics: DiagnosticCollector, + value: any, + relativePath: string +) { + diagnostics.add( + createDiagnostic({ + code: "example-value-no-mapping", + target: NoTarget, + format: { + value: JSON.stringify(value), + relativePath, + }, + }) + ); +} diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 01712f1f3a..07ea45cba5 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -56,6 +56,7 @@ import { addFormatInfo, getClientTypeWithDiagnostics, getSdkModelPropertyTypeBase, + getTypeSpecBuiltInType, } from "./types.js"; export function getSdkHttpOperation( @@ -233,11 +234,7 @@ function createContentTypeOrAcceptHeader( bodyObject: SdkBodyParameter | SdkHttpResponse ): Omit { const name = bodyObject.kind === "body" ? "contentType" : "accept"; - let type: SdkType = { - kind: "string", - encode: "string", - decorators: [], - }; + let type: SdkType = getTypeSpecBuiltInType(context, "string"); // for contentType, we treat it as a constant IFF there's one value and it's application/json. // this is to prevent a breaking change when a service adds more content types in the future. // e.g. the service accepting image/png then later image/jpeg should _not_ be a breaking change. diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 03953e8b6d..f4bf6cfd00 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -5,6 +5,7 @@ import { DurationKnownEncoding, EmitContext, Interface, + IntrinsicScalarName, Model, ModelProperty, Namespace, @@ -48,7 +49,9 @@ export interface TCGCContext { __rawClients?: SdkClient[]; apiVersion?: string; __service_projection?: Map; + __httpOperationExamples?: Map; originalProgram: Program; + examplesDirectory?: string; decoratorsAllowList?: string[]; previewStringRegex: RegExp; } @@ -68,6 +71,7 @@ export interface SdkEmitterOptions { "package-name"?: string; "flatten-union-as-enum"?: boolean; "api-version"?: string; + "examples-directory"?: string; } export interface SdkClient { @@ -151,8 +155,29 @@ export type SdkType = export interface SdkBuiltInType extends SdkTypeBase { kind: SdkBuiltInKinds; encode: string; + name: string; + baseType?: SdkBuiltInType; + crossLanguageDefinitionId: string; } +type TypeEquality = keyof T extends keyof U + ? keyof U extends keyof T + ? true + : false + : false; + +// these two vars are used to validate whether our SdkBuiltInKinds are exhaustive for all possible values from typespec +// if it is not, a typescript compilation error will be thrown here. +const _: TypeEquality, never> = true; +const __: TypeEquality, never> = true; + +type SupportedBuiltInKinds = + | keyof typeof SdkIntKindsEnum + | keyof typeof SdkFloatingPointKindsEnum + | keyof typeof SdkFixedPointKindsEnum + | keyof typeof SdkGenericBuiltInStringKindsEnum + | keyof typeof SdkBuiltInKindsMiscellaneousEnum; + enum SdkIntKindsEnum { numeric = "numeric", integer = "integer", @@ -167,30 +192,20 @@ enum SdkIntKindsEnum { uint64 = "uint64", } -enum SdkFloatKindsEnum { +enum SdkFloatingPointKindsEnum { float = "float", float32 = "float32", float64 = "float64", +} + +enum SdkFixedPointKindsEnum { decimal = "decimal", decimal128 = "decimal128", } -const SdkAzureBuiltInStringKindsMapping = { - uuid: "uuid", - ipV4Address: "ipV4Address", - ipV6Address: "ipV6Address", - eTag: "eTag", - armId: "armResourceIdentifier", - azureLocation: "azureLocation", -}; - enum SdkGenericBuiltInStringKindsEnum { string = "string", - password = "password", - guid = "guid", url = "url", - uri = "uri", - ipAddress = "ipAddress", } enum SdkBuiltInKindsMiscellaneousEnum { @@ -201,29 +216,21 @@ enum SdkBuiltInKindsMiscellaneousEnum { any = "any", } -export type SdkBuiltInKinds = - | keyof typeof SdkBuiltInKindsMiscellaneousEnum - | keyof typeof SdkIntKindsEnum - | keyof typeof SdkFloatKindsEnum - | keyof typeof SdkGenericBuiltInStringKindsEnum - | keyof typeof SdkAzureBuiltInStringKindsMapping; +export type SdkBuiltInKinds = Exclude | "any"; + +type SdkBuiltInKindsExcludes = "utcDateTime" | "offsetDateTime" | "duration"; export function getKnownScalars(): Record { const retval: Record = {}; const typespecNamespace = Object.keys(SdkBuiltInKindsMiscellaneousEnum) .concat(Object.keys(SdkIntKindsEnum)) - .concat(Object.keys(SdkFloatKindsEnum)) + .concat(Object.keys(SdkFloatingPointKindsEnum)) + .concat(Object.keys(SdkFixedPointKindsEnum)) .concat(Object.keys(SdkGenericBuiltInStringKindsEnum)); for (const kind of typespecNamespace) { if (!isSdkBuiltInKind(kind)) continue; // it will always be true retval[`TypeSpec.${kind}`] = kind; } - for (const kind in SdkAzureBuiltInStringKindsMapping) { - if (!isSdkBuiltInKind(kind)) continue; // it will always be true - const kindMappedName = - SdkAzureBuiltInStringKindsMapping[kind as keyof typeof SdkAzureBuiltInStringKindsMapping]; - retval[`Azure.Core.${kindMappedName}`] = kind; - } return retval; } @@ -232,8 +239,8 @@ export function isSdkBuiltInKind(kind: string): kind is SdkBuiltInKinds { kind in SdkBuiltInKindsMiscellaneousEnum || isSdkIntKind(kind) || isSdkFloatKind(kind) || - kind in SdkGenericBuiltInStringKindsEnum || - kind in SdkAzureBuiltInStringKindsMapping + isSdkFixedPointKind(kind) || + kind in SdkGenericBuiltInStringKindsEnum ); } @@ -241,8 +248,12 @@ export function isSdkIntKind(kind: string): kind is keyof typeof SdkIntKindsEnum return kind in SdkIntKindsEnum; } -export function isSdkFloatKind(kind: string): kind is keyof typeof SdkFloatKindsEnum { - return kind in SdkFloatKindsEnum; +export function isSdkFloatKind(kind: string): kind is keyof typeof SdkFloatingPointKindsEnum { + return kind in SdkFloatingPointKindsEnum; +} + +function isSdkFixedPointKind(kind: string): kind is keyof typeof SdkFixedPointKindsEnum { + return kind in SdkFixedPointKindsEnum; } const SdkDateTimeEncodingsConst = ["rfc3339", "rfc7231", "unixTimestamp"] as const; @@ -252,8 +263,11 @@ export function isSdkDateTimeEncodings(encoding: string): encoding is DateTimeKn } interface SdkDateTimeTypeBase extends SdkTypeBase { + name: string; + baseType?: SdkDateTimeType; encode: DateTimeKnownEncoding; wireType: SdkBuiltInType; + crossLanguageDefinitionId: string; } interface SdkUtcDateTimeType extends SdkDateTimeTypeBase { @@ -283,8 +297,11 @@ export type SdkOffsetDatetimeType = SdkOffsetDateTimeType; export interface SdkDurationType extends SdkTypeBase { kind: "duration"; + name: string; + baseType?: SdkDurationType; encode: DurationKnownEncoding; wireType: SdkBuiltInType; + crossLanguageDefinitionId: string; } export interface SdkArrayType extends SdkTypeBase { @@ -332,6 +349,7 @@ export interface SdkEnumValueType extends SdkTypeBase { enumType: SdkEnumType; valueType: SdkBuiltInType; } + export interface SdkConstantType extends SdkTypeBase { kind: "constant"; value: string | number | boolean | null; @@ -420,11 +438,28 @@ export type SdkModelPropertyType = | SdkBodyParameter | SdkHeaderParameter; +export interface MultipartOptions { + // whether this part is for file + isFilePart: boolean; + // whether this part is multi in request payload + isMulti: boolean; + // undefined if filename is not set explicitly in Typespec + filename?: SdkModelPropertyType; + // undefined if contentType is not set explicitly in Typespec + contentType?: SdkModelPropertyType; + // defined in Typespec or calculated by Typespec complier + defaultContentTypes: string[]; +} + export interface SdkBodyModelPropertyType extends SdkModelPropertyTypeBase { kind: "property"; discriminator: boolean; serializedName: string; + /* + @deprecated This property is deprecated. Use `.multipartOptions?.isFilePart` instead. + */ isMultipartFileInput: boolean; + multipartOptions?: MultipartOptions; visibility?: Visibility[]; flatten: boolean; } @@ -512,6 +547,7 @@ export interface SdkHttpOperation extends SdkServiceOperationBase { bodyParam?: SdkBodyParameter; responses: Map; exceptions: Map; + examples?: SdkHttpOperationExample[]; } /** @@ -607,10 +643,6 @@ export interface SdkPackage { clients: SdkClientType[]; models: SdkModelType[]; enums: SdkEnumType[]; - /** - * @deprecated This property is deprecated. Look at `.diagnostics` on SdkContext instead. - */ - diagnostics: readonly Diagnostic[]; crossLanguagePackageId: string; } @@ -636,4 +668,120 @@ export enum UsageFlags { Error = 1 << 7, // Set when model is used in conjunction with an application/json content type. Json = 1 << 8, + // Set when model is used in conjunction with an application/xml content type. + Xml = 1 << 9, +} + +interface SdkExampleBase { + kind: string; + name: string; + description: string; + filePath: string; + rawExample: any; +} + +export interface SdkHttpOperationExample extends SdkExampleBase { + kind: "http"; + parameters: SdkHttpParameterExample[]; + responses: Map; +} + +export interface SdkHttpParameterExample { + parameter: SdkHttpParameter; + value: SdkTypeExample; +} + +export interface SdkHttpResponseExample { + response: SdkHttpResponse; + headers: SdkHttpResponseHeaderExample[]; + bodyValue?: SdkTypeExample; +} + +export interface SdkHttpResponseHeaderExample { + header: SdkServiceResponseHeader; + value: SdkTypeExample; +} + +export type SdkTypeExample = + | SdkStringExample + | SdkNumberExample + | SdkBooleanExample + | SdkNullExample + | SdkAnyExample + | SdkArrayExample + | SdkDictionaryExample + | SdkUnionExample + | SdkModelExample; + +export interface SdkExampleTypeBase { + kind: string; + type: SdkType; + value: unknown; +} + +export interface SdkStringExample extends SdkExampleTypeBase { + kind: "string"; + type: + | SdkBuiltInType + | SdkDateTimeType + | SdkDurationType + | SdkEnumType + | SdkEnumValueType + | SdkConstantType; + value: string; +} + +export interface SdkNumberExample extends SdkExampleTypeBase { + kind: "number"; + type: + | SdkBuiltInType + | SdkDateTimeType + | SdkDurationType + | SdkEnumType + | SdkEnumValueType + | SdkConstantType; + value: number; +} + +export interface SdkBooleanExample extends SdkExampleTypeBase { + kind: "boolean"; + type: SdkBuiltInType | SdkConstantType; + value: boolean; +} + +export interface SdkNullExample extends SdkExampleTypeBase { + kind: "null"; + type: SdkNullableType; + value: null; +} + +export interface SdkAnyExample extends SdkExampleTypeBase { + kind: "any"; + type: SdkBuiltInType; + value: unknown; +} + +export interface SdkArrayExample extends SdkExampleTypeBase { + kind: "array"; + type: SdkArrayType; + value: SdkTypeExample[]; +} + +export interface SdkDictionaryExample extends SdkExampleTypeBase { + kind: "dict"; + type: SdkDictionaryType; + value: Record; +} + +export interface SdkUnionExample extends SdkExampleTypeBase { + kind: "union"; + type: SdkUnionType; + value: unknown; +} + +export interface SdkModelExample extends SdkExampleTypeBase { + kind: "model"; + type: SdkModelType; + value: Record; + additionalPropertiesValue?: Record; } diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 151930ee85..156c1fcc5a 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -41,6 +41,7 @@ import { getHttpOperationWithCache, isApiVersion, } from "./public-utils.js"; +import { getClientTypeWithDiagnostics } from "./types.js"; export const AllScopes = Symbol.for("@azure-core/typespec-client-generator-core/all-scopes"); @@ -296,7 +297,7 @@ export function getTypeDecorators( }; for (let i = 0; i < decorator.args.length; i++) { decoratorInfo.arguments[decorator.definition.parameters[i].name] = diagnostics.pipe( - getDecoratorArgValue(decorator.args[i].jsValue, type, decoratorName) + getDecoratorArgValue(context, decorator.args[i].jsValue, type, decoratorName) ); } retval.push(decoratorInfo); @@ -307,6 +308,7 @@ export function getTypeDecorators( } function getDecoratorArgValue( + context: TCGCContext, arg: | Type | Record @@ -323,7 +325,7 @@ function getDecoratorArgValue( const diagnostics = createDiagnosticCollector(); if (typeof arg === "object" && arg !== null && "kind" in arg) { if (arg.kind === "EnumMember") { - return diagnostics.wrap(arg.value ?? arg.name); + return diagnostics.wrap(diagnostics.pipe(getClientTypeWithDiagnostics(context, arg))); } if (arg.kind === "String" || arg.kind === "Number" || arg.kind === "Boolean") { return diagnostics.wrap(arg.value); @@ -475,11 +477,24 @@ export function getAnyType( const diagnostics = createDiagnosticCollector(); return diagnostics.wrap({ kind: "any", + name: "any", encode: "string", + crossLanguageDefinitionId: "", decorators: diagnostics.pipe(getTypeDecorators(context, type)), }); } +export function getValidApiVersion(context: TCGCContext, versions: string[]): string | undefined { + let apiVersion = context.apiVersion; + if (apiVersion === "all") { + return apiVersion; + } + if (apiVersion === "latest" || apiVersion === undefined || !versions.includes(apiVersion)) { + apiVersion = versions[versions.length - 1]; + } + return apiVersion; +} + export function getHttpOperationResponseHeaders( response: HttpOperationResponseContent ): ModelProperty[] { @@ -526,3 +541,8 @@ export function isJsonContentType(contentType: string): boolean { const regex = new RegExp(/^(application|text)\/(.+\+)?json$/); return regex.test(contentType); } + +export function isXmlContentType(contentType: string): boolean { + const regex = new RegExp(/^(application|text)\/(.+\+)?xml$/); + return regex.test(contentType); +} diff --git a/packages/typespec-client-generator-core/src/lib.ts b/packages/typespec-client-generator-core/src/lib.ts index 8d39e92393..a951ea8544 100644 --- a/packages/typespec-client-generator-core/src/lib.ts +++ b/packages/typespec-client-generator-core/src/lib.ts @@ -166,6 +166,32 @@ export const $lib = createTypeSpecLibrary({ nonDecorator: paramMessage`Client name: "${"name"}" is defined somewhere causing nameing conflicts in language scope: "${"scope"}"`, }, }, + "example-loading": { + severity: "warning", + messages: { + default: paramMessage`Skipped loading invalid example file: ${"filename"}. Error: ${"error"}`, + noDirectory: paramMessage`Skipping example loading from ${"directory"} because there was an error reading the directory.`, + noOperationId: paramMessage`Skipping example file ${"filename"} because it does not contain an operationId and/or title.`, + }, + }, + "duplicate-example-file": { + severity: "error", + messages: { + default: paramMessage`Example file ${"filename"} uses duplicate title '${"title"}' for operationId '${"operationId"}'`, + }, + }, + "example-value-no-mapping": { + severity: "warning", + messages: { + default: paramMessage`Value in example file '${"relativePath"}' does not follow its definition:\n${"value"}`, + }, + }, + "flatten-polymorphism": { + severity: "error", + messages: { + default: `Cannot flatten property of polymorphic type.`, + }, + }, }, }); diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 7f100a7bf2..9c27693d1a 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -13,6 +13,7 @@ import { resolveVersions } from "@typespec/versioning"; import { camelCase } from "change-case"; import { getAccess, + getClientNameOverride, listClients, listOperationGroups, listOperationsInOperationGroup, @@ -73,6 +74,7 @@ import { getClientTypeWithDiagnostics, getSdkCredentialParameter, getSdkModelPropertyType, + getTypeSpecBuiltInType, } from "./types.js"; function getSdkServiceOperation( @@ -437,11 +439,7 @@ function getSdkEndpointParameter( optional: false, serializedName: "endpoint", correspondingMethodParams: [], - type: { - kind: "string", - encode: "string", - decorators: [], - }, + type: getTypeSpecBuiltInType(context, "string"), isApiVersionParam: false, apiVersions: context.__tspTypeToApiVersions.get(client.type)!, crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, client.service)}.endpoint`, @@ -506,13 +504,18 @@ function createSdkClientType( ): [SdkClientType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); const isClient = client.kind === "SdkClient"; - const clientName = isClient ? client.name : client.type.name; + let name = ""; + if (isClient) { + name = client.name; + } else { + name = getClientNameOverride(context, client.type) ?? client.type.name; + } // NOTE: getSdkMethods recursively calls createSdkClientType const methods = diagnostics.pipe(getSdkMethods(context, client)); const docWrapper = getDocHelper(context, client.type); const sdkClientType: SdkClientType = { kind: "client", - name: clientName, + name, description: docWrapper.description, details: docWrapper.details, methods: methods, @@ -559,18 +562,17 @@ function populateApiVersionInformation(context: TCGCContext): void { export function getSdkPackage( context: TCGCContext -): SdkPackage { +): [SdkPackage, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); populateApiVersionInformation(context); const modelsAndEnums = diagnostics.pipe(getAllModelsWithDiagnostics(context)); const crossLanguagePackageId = diagnostics.pipe(getCrossLanguagePackageId(context)); - return { + return diagnostics.wrap({ name: getClientNamespaceString(context)!, rootNamespace: getClientNamespaceString(context)!, clients: listClients(context).map((c) => diagnostics.pipe(createSdkClientType(context, c))), models: modelsAndEnums.filter((x): x is SdkModelType => x.kind === "model"), enums: modelsAndEnums.filter((x): x is SdkEnumType => x.kind === "enum"), - diagnostics: diagnostics.diagnostics, crossLanguagePackageId, - }; + }); } diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index ac9a5d2a9a..88afbd6ff9 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -28,7 +28,7 @@ import { listOperationGroups, listOperationsInOperationGroup, } from "./decorators.js"; -import { TCGCContext } from "./interfaces.js"; +import { SdkHttpOperationExample, TCGCContext } from "./interfaces.js"; import { TspLiteralType, getClientNamespaceStringHelper, @@ -614,3 +614,13 @@ export function getHttpOperationWithCache( context.httpOperationCache.set(operation, httpOperation); return httpOperation; } + +/** + * Get the examples for a given http operation. + */ +export function getHttpOperationExamples( + context: TCGCContext, + operation: HttpOperation +): SdkHttpOperationExample[] { + return context.__httpOperationExamples?.get(operation) ?? []; +} diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index d967a0827a..ee2ae7ea04 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -5,8 +5,10 @@ import { DateTimeKnownEncoding, Diagnostic, DurationKnownEncoding, + EncodeData, Enum, EnumMember, + IntrinsicScalarName, IntrinsicType, Model, ModelProperty, @@ -17,13 +19,11 @@ import { Tuple, Type, Union, - UnionVariant, createDiagnosticCollector, getDiscriminator, getEncode, getFormat, getKnownValues, - getNamespaceFullName, getVisibility, ignoreDiagnostics, isErrorModel, @@ -31,10 +31,13 @@ import { } from "@typespec/compiler"; import { Authentication, + HttpOperationPart, Visibility, getAuthentication, + getHttpPart, getServers, isHeader, + isOrExtendsHttpFile, isStatusCode, } from "@typespec/http"; import { @@ -78,7 +81,6 @@ import { import { createGeneratedName, filterApiVersionsInEnum, - getAnyType, getAvailableApiVersions, getDocHelper, getHttpOperationResponseHeaders, @@ -93,6 +95,7 @@ import { isMultipartFormData, isMultipartOperation, isNeverOrVoidType, + isXmlContentType, updateWithApiVersionInformation, } from "./internal-utils.js"; import { createDiagnostic } from "./lib.js"; @@ -109,6 +112,28 @@ import { getVersions } from "@typespec/versioning"; import { UnionEnumVariant } from "../../typespec-azure-core/dist/src/helpers/union-enums.js"; import { getSdkHttpParameter, isSdkHttpParameter } from "./http.js"; +export function getTypeSpecBuiltInType( + context: TCGCContext, + kind: IntrinsicScalarName +): SdkBuiltInType { + const global = context.program.getGlobalNamespaceType(); + const typeSpecNamespace = global.namespaces!.get("TypeSpec"); + const type = typeSpecNamespace!.scalars.get(kind)!; + + return getSdkBuiltInType(context, type) as SdkBuiltInType; +} + +function getAnyType(context: TCGCContext, type: Type): [SdkBuiltInType, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + const anyType: SdkBuiltInType = { + ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, "any")), + name: getLibraryName(context, type), + encode: getEncodeHelper(context, type, "any"), + crossLanguageDefinitionId: "", + }; + return diagnostics.wrap(anyType); +} + function getEncodeHelper(context: TCGCContext, type: Type, kind: string): string { if (type.kind === "ModelProperty" || type.kind === "Scalar") { return getEncode(context.program, type)?.encoding || kind; @@ -130,7 +155,11 @@ export function addFormatInfo( propertyType: SdkType ): void { const innerType = propertyType.kind === "nullable" ? propertyType.type : propertyType; - const format = getFormat(context.program, type) ?? ""; + let format = getFormat(context.program, type) ?? ""; + + // special case: we treat format: uri the same as format: url + if (format === "uri") format = "url"; + if (isSdkBuiltInKind(format)) innerType.kind = format; } @@ -182,88 +211,211 @@ export function addEncodeInfo( /** * Mapping of typespec scalar kinds to the built in kinds exposed in the SDK + * @param context the TCGC context * @param scalar the original typespec scalar * @returns the corresponding sdk built in kind */ -function getScalarKind(scalar: Scalar): SdkBuiltInKinds { - if (isSdkBuiltInKind(scalar.name)) { +function getScalarKind(context: TCGCContext, scalar: Scalar): IntrinsicScalarName | "any" { + if (context.program.checker.isStdType(scalar)) { return scalar.name; } - throw Error(`Unknown scalar kind ${scalar.name}`); + + // for those scalar defined as `scalar newThing;`, + // the best we could do here is return as a `any` type with a name and namespace and let the generator figure what this is + if (scalar.baseScalar === undefined) { + return "any"; + } + + return getScalarKind(context, scalar.baseScalar); } /** - * Get the sdk built in type for a given typespec type - * @param context the sdk context - * @param type the typespec type - * @returns the corresponding sdk type + * This function converts a Scalar into SdkBuiltInType. + * @param context + * @param type + * @param kind + * @returns */ function getSdkBuiltInTypeWithDiagnostics( context: TCGCContext, - type: Scalar | IntrinsicType | NumericLiteral | StringLiteral | BooleanLiteral + type: Scalar, + kind: SdkBuiltInKinds ): [SdkBuiltInType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - if (context.program.checker.isStdType(type) || type.kind === "Intrinsic") { - let kind: SdkBuiltInKinds = "any"; - if (type.kind === "Scalar") { - if (isSdkBuiltInKind(type.name)) { - kind = getScalarKind(type); - } - } - const docWrapper = getDocHelper(context, type); - return diagnostics.wrap({ - ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, kind)), - encode: getEncodeHelper(context, type, kind), - description: docWrapper.description, - details: docWrapper.details, - }); - } else if (type.kind === "String" || type.kind === "Boolean" || type.kind === "Number") { - let kind: SdkBuiltInKinds; + const docWrapper = getDocHelper(context, type); + const stdType = { + ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, kind)), + name: getLibraryName(context, type), + encode: getEncodeHelper(context, type, kind), + description: docWrapper.description, + details: docWrapper.details, + baseType: type.baseScalar + ? diagnostics.pipe(getSdkBuiltInTypeWithDiagnostics(context, type.baseScalar, kind)) + : undefined, + crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, type), + }; + addEncodeInfo(context, type, stdType); + addFormatInfo(context, type, stdType); + return diagnostics.wrap(stdType); +} - if (type.kind === "String") { - kind = "string"; - } else if (type.kind === "Boolean") { - kind = "boolean"; - } else { - kind = intOrFloat(type.value); - } - return diagnostics.wrap({ - ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, kind)), - encode: getEncodeHelper(context, type, kind), - }); +/** + * This function calculates the encode and wireType for a datetime or duration type. + * We always first try to get the `@encode` decorator on this type and returns it if any. + * If we did not get anything from the encode, we try to get the baseType's encode and wireType. + * @param context + * @param encodeData + * @param baseType + * @returns + */ +function getEncodeInfoForDateTimeOrDuration( + context: TCGCContext, + encodeData: EncodeData | undefined, + baseType: SdkDateTimeType | SdkDurationType | undefined +): [string | undefined, SdkBuiltInType | undefined] { + const encode = encodeData?.encoding; + const wireType = encodeData?.type + ? (getClientType(context, encodeData.type) as SdkBuiltInType) + : undefined; + + // if we get something from the encode + if (encode || wireType) { + return [encode, wireType]; } - diagnostics.add( - createDiagnostic({ code: "unsupported-kind", target: type, format: { kind: type.kind } }) + + // if we did not get anything from the encode, try the baseType + return [baseType?.encode, baseType?.wireType]; +} + +/** + * This function converts a Scalar into SdkDateTimeType. + * @param context + * @param type + * @param kind + * @returns + */ +function getSdkDateTimeType( + context: TCGCContext, + type: Scalar, + kind: "utcDateTime" | "offsetDateTime" +): [SdkDateTimeType, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + const docWrapper = getDocHelper(context, type); + const baseType = type.baseScalar + ? diagnostics.pipe(getSdkDateTimeType(context, type.baseScalar, kind)) + : undefined; + const [encode, wireType] = getEncodeInfoForDateTimeOrDuration( + context, + getEncode(context.program, type), + baseType ); - return diagnostics.wrap(diagnostics.pipe(getAnyType(context, type))); + return diagnostics.wrap({ + ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, kind)), + name: getLibraryName(context, type), + encode: (encode ?? "rfc3339") as DateTimeKnownEncoding, + wireType: wireType ?? getTypeSpecBuiltInType(context, "string"), + baseType: baseType, + description: docWrapper.description, + details: docWrapper.details, + crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, type), + }); +} + +function getSdkDateTimeOrDurationOrBuiltInType( + context: TCGCContext, + type: Scalar +): [SdkDateTimeType | SdkDurationType | SdkBuiltInType, readonly Diagnostic[]] { + // follow the extends hierarchy to determine the final kind of this type + const kind = getScalarKind(context, type); + + if (kind === "utcDateTime" || kind === "offsetDateTime") { + return getSdkDateTimeType(context, type, kind); + } + if (kind === "duration") { + return getSdkDurationTypeWithDiagnostics(context, type, kind); + } + // handle the std types of typespec + return getSdkBuiltInTypeWithDiagnostics(context, type, kind); +} + +function getSdkTypeForLiteral( + context: TCGCContext, + type: NumericLiteral | StringLiteral | BooleanLiteral +): SdkBuiltInType { + let kind: SdkBuiltInKinds; + + if (type.kind === "String") { + kind = "string"; + } else if (type.kind === "Boolean") { + kind = "boolean"; + } else { + kind = intOrFloat(type.value); + } + return getTypeSpecBuiltInType(context, kind); +} + +function getSdkTypeForIntrinsic(context: TCGCContext, type: IntrinsicType): SdkBuiltInType { + const kind = "any"; + const diagnostics = createDiagnosticCollector(); + return { + ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, kind)), + name: getLibraryName(context, type), + crossLanguageDefinitionId: "", + encode: kind, + }; } export function getSdkBuiltInType( context: TCGCContext, type: Scalar | IntrinsicType | NumericLiteral | StringLiteral | BooleanLiteral -): SdkBuiltInType { - return ignoreDiagnostics(getSdkBuiltInTypeWithDiagnostics(context, type)); +): SdkDateTimeType | SdkDurationType | SdkBuiltInType { + switch (type.kind) { + case "Scalar": + return ignoreDiagnostics(getSdkDateTimeOrDurationOrBuiltInType(context, type)); + case "Intrinsic": + return getSdkTypeForIntrinsic(context, type); + case "String": + case "Number": + case "Boolean": + return getSdkTypeForLiteral(context, type); + } } export function getSdkDurationType(context: TCGCContext, type: Scalar): SdkDurationType { - return ignoreDiagnostics(getSdkDurationTypeWithDiagnostics(context, type)); + return ignoreDiagnostics(getSdkDurationTypeWithDiagnostics(context, type, "duration")); } +/** + * This function converts a Scalar into SdkDurationType. + * @param context + * @param type + * @param kind + * @returns + */ function getSdkDurationTypeWithDiagnostics( context: TCGCContext, - type: Scalar + type: Scalar, + kind: "duration" ): [SdkDurationType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - // we don't get encode info until we get to the property / parameter level - // so we insert the default. Later in properties, we will check - // for encoding info and override accordingly + const docWrapper = getDocHelper(context, type); + const baseType = type.baseScalar + ? diagnostics.pipe(getSdkDurationTypeWithDiagnostics(context, type.baseScalar, kind)) + : undefined; + const [encode, wireType] = getEncodeInfoForDateTimeOrDuration( + context, + getEncode(context.program, type), + baseType + ); return diagnostics.wrap({ - ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, "duration")), - encode: "ISO8601", - wireType: { - ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, "string")), - encode: "string", - }, + ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, kind)), + name: getLibraryName(context, type), + encode: (encode ?? "ISO8601") as DurationKnownEncoding, + wireType: wireType ?? getTypeSpecBuiltInType(context, "string"), + baseType: baseType, + description: docWrapper.description, + details: docWrapper.details, + crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, type), }); } @@ -407,7 +559,7 @@ function getSdkConstantWithDiagnostics( case "Number": case "String": case "Boolean": - const valueType = diagnostics.pipe(getSdkBuiltInTypeWithDiagnostics(context, type)); + const valueType = getSdkTypeForLiteral(context, type); return diagnostics.wrap({ ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, "constant")), value: type.value, @@ -504,11 +656,7 @@ function addDiscriminatorToModelType( discriminatorType = discriminatorProperty.type.enumType; } } else { - discriminatorType = { - kind: "string", - encode: "string", - decorators: [], - }; + discriminatorType = getTypeSpecBuiltInType(context, "string"); } const name = discriminatorProperty ? discriminatorProperty.name : discriminator.propertyName; model.properties.splice(0, 0, { @@ -619,14 +767,7 @@ function getSdkEnumValueType( ): [SdkBuiltInType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); let kind: "string" | "int32" | "float32" = "string"; - let type: EnumMember | UnionVariant; for (const value of values) { - if ((value as EnumMember).kind) { - type = value as EnumMember; - } else { - type = (value as UnionEnumVariant | UnionEnumVariant).type; - } - if (typeof value.value === "number") { kind = intOrFloat(value.value); if (kind === "float32") { @@ -638,10 +779,7 @@ function getSdkEnumValueType( } } - return diagnostics.wrap({ - ...diagnostics.pipe(getSdkTypeBaseHelper(context, type!, kind!)), - encode: kind!, - }); + return diagnostics.wrap(getTypeSpecBuiltInType(context, kind!)); } function getUnionAsEnumValueType( @@ -856,46 +994,25 @@ export function getClientTypeWithDiagnostics( case "Model": retval = diagnostics.pipe(getSdkArrayOrDictWithDiagnostics(context, type, operation)); if (retval === undefined) { - retval = diagnostics.pipe(getSdkModelWithDiagnostics(context, type, operation)); + const httpPart = getHttpPart(context.program, type); + if (httpPart === undefined) { + retval = diagnostics.pipe(getSdkModelWithDiagnostics(context, type, operation)); + } else { + retval = diagnostics.pipe( + getClientTypeWithDiagnostics(context, httpPart.type, operation) + ); + } } break; case "Intrinsic": - retval = diagnostics.pipe(getSdkBuiltInTypeWithDiagnostics(context, type)); + retval = getSdkTypeForIntrinsic(context, type); break; case "Scalar": - if (!context.program.checker.isStdType(type) && type.kind === "Scalar" && type.baseScalar) { - const baseType = diagnostics.pipe( - getClientTypeWithDiagnostics(context, type.baseScalar, operation) - ); - addEncodeInfo(context, type, baseType); - addFormatInfo(context, type, baseType); - retval = diagnostics.pipe(getKnownValuesEnum(context, type, operation)) ?? baseType; - const namespace = type.namespace ? getNamespaceFullName(type.namespace) : ""; - retval.kind = context.knownScalars[`${namespace}.${type.name}`] ?? retval.kind; - const docWrapper = getDocHelper(context, type); - retval.description = docWrapper.description; - retval.details = docWrapper.details; + retval = diagnostics.pipe(getKnownValuesEnum(context, type, operation)); + if (retval) { break; } - if (type.name === "utcDateTime" || type.name === "offsetDateTime") { - retval = { - ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, type.name)), - encode: "rfc3339", - wireType: { - ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, "string")), - encode: "string", - }, - } as SdkDateTimeType; - break; - } - if (type.name === "duration") { - retval = diagnostics.pipe(getSdkDurationTypeWithDiagnostics(context, type)); - break; - } - const scalarType = diagnostics.pipe(getSdkBuiltInTypeWithDiagnostics(context, type)); - // just add default encode, normally encode is on extended scalar and model property - addEncodeInfo(context, type, scalarType); - retval = scalarType; + retval = diagnostics.pipe(getSdkDateTimeOrDurationOrBuiltInType(context, type)); break; case "Enum": retval = diagnostics.pipe(getSdkEnumWithDiagnostics(context, type, operation)); @@ -1061,6 +1178,137 @@ export function getSdkModelPropertyTypeBase( }); } +function isFilePart(context: TCGCContext, type: SdkType): boolean { + if (type.kind === "array") { + // HttpFile[] + return isFilePart(context, type.valueType); + } else if (type.kind === "bytes") { + // Http + return true; + } else if (type.kind === "model") { + if (type.__raw && isOrExtendsHttpFile(context.program, type.__raw)) { + // Http + return true; + } + // HttpPart<{@body body: bytes}> or HttpPart<{@body body: File}> + const body = type.properties.find((x) => x.kind === "body"); + if (body) { + return isFilePart(context, body.type); + } + } + return false; +} + +function getHttpOperationParts(context: TCGCContext, operation: Operation): HttpOperationPart[] { + const body = getHttpOperationWithCache(context, operation).parameters.body; + if (body?.bodyKind === "multipart") { + return body.parts; + } + return []; +} + +function hasHttpPart(context: TCGCContext, type: Type): boolean { + if (type.kind === "Model") { + if (type.indexer) { + // HttpPart[] + return ( + type.indexer.key.name === "integer" && + getHttpPart(context.program, type.indexer.value) !== undefined + ); + } else { + // HttpPart + return getHttpPart(context.program, type) !== undefined; + } + } + return false; +} + +function getHttpOperationPart( + context: TCGCContext, + type: ModelProperty, + operation: Operation +): HttpOperationPart | undefined { + if (hasHttpPart(context, type.type)) { + const httpOperationParts = getHttpOperationParts(context, operation); + if ( + type.model && + httpOperationParts.length > 0 && + httpOperationParts.length === type.model.properties.size + ) { + const index = Array.from(type.model.properties.values()).findIndex((p) => p === type); + if (index !== -1) { + return httpOperationParts[index]; + } + } + } + return undefined; +} + +function updateMultiPartInfo( + context: TCGCContext, + type: ModelProperty, + base: SdkBodyModelPropertyType, + operation: Operation +): [void, readonly Diagnostic[]] { + const httpOperationPart = getHttpOperationPart(context, type, operation); + const diagnostics = createDiagnosticCollector(); + if (httpOperationPart) { + // body decorated with @multipartBody + base.multipartOptions = { + isFilePart: isFilePart(context, base.type), + isMulti: httpOperationPart.multi, + filename: httpOperationPart.filename + ? diagnostics.pipe(getSdkModelPropertyType(context, httpOperationPart.filename, operation)) + : undefined, + contentType: httpOperationPart.body.contentTypeProperty + ? diagnostics.pipe( + getSdkModelPropertyType(context, httpOperationPart.body.contentTypeProperty, operation) + ) + : undefined, + defaultContentTypes: httpOperationPart.body.contentTypes, + }; + // after https://github.com/microsoft/typespec/issues/3779 fixed, could use httpOperationPart.name directly + const httpPart = getHttpPart(context.program, type.type); + if (httpPart?.options?.name) { + base.serializedName = httpPart?.options?.name; + } + } else { + // common body + const httpOperation = getHttpOperationWithCache(context, operation); + const operationIsMultipart = Boolean( + httpOperation && httpOperation.parameters.body?.contentTypes.includes("multipart/form-data") + ); + if (operationIsMultipart) { + const isBytesInput = + base.type.kind === "bytes" || + (base.type.kind === "array" && base.type.valueType.kind === "bytes"); + // Currently we only recognize bytes and list of bytes as potential file inputs + if (isBytesInput && getEncode(context.program, type)) { + diagnostics.add( + createDiagnostic({ + code: "encoding-multipart-bytes", + target: type, + }) + ); + } + base.multipartOptions = { + isFilePart: isBytesInput, + isMulti: base.type.kind === "array", + defaultContentTypes: [], + }; + } + } + if (base.multipartOptions !== undefined) { + base.isMultipartFileInput = base.multipartOptions.isFilePart; + } + if (base.multipartOptions?.isMulti && base.type.kind === "array") { + // for "images: T[]" or "images: HttpPart[]", return type shall be "T" instead of "T[]" + base.type = base.type.valueType; + } + + return diagnostics.wrap(undefined); +} + export function getSdkModelPropertyType( context: TCGCContext, type: ModelProperty, @@ -1070,36 +1318,20 @@ export function getSdkModelPropertyType( const base = diagnostics.pipe(getSdkModelPropertyTypeBase(context, type, operation)); if (isSdkHttpParameter(context, type)) return getSdkHttpParameter(context, type, operation!); - // I'm a body model property - let operationIsMultipart = false; - if (operation) { - const httpOperation = getHttpOperationWithCache(context, operation); - operationIsMultipart = Boolean( - httpOperation && httpOperation.parameters.body?.contentTypes.includes("multipart/form-data") - ); - } - // Currently we only recognize bytes and list of bytes as potential file inputs - const isBytesInput = - base.type.kind === "bytes" || - (base.type.kind === "array" && base.type.valueType.kind === "bytes"); - if (isBytesInput && operationIsMultipart && getEncode(context.program, type)) { - diagnostics.add( - createDiagnostic({ - code: "encoding-multipart-bytes", - target: type, - }) - ); - } - return diagnostics.wrap({ + const result: SdkBodyModelPropertyType = { ...base, kind: "property", optional: type.optional, visibility: getSdkVisibility(context, type), discriminator: false, serializedName: getPropertyNames(context, type)[1], - isMultipartFileInput: isBytesInput && operationIsMultipart, + isMultipartFileInput: false, flatten: shouldFlattenProperty(context, type), - }); + }; + if (operation) { + diagnostics.pipe(updateMultiPartInfo(context, type, result, operation)); + } + return diagnostics.wrap(result); } function addPropertiesToModelType( @@ -1335,6 +1567,9 @@ function updateTypesFromOperation( if (httpBody.contentTypes.some((x) => isJsonContentType(x))) { updateUsageOfModel(context, UsageFlags.Json, sdkType); } + if (httpBody.contentTypes.some((x) => isXmlContentType(x))) { + updateUsageOfModel(context, UsageFlags.Xml, sdkType); + } if (httpBody.contentTypes.includes("application/merge-patch+json")) { // will also have Json type updateUsageOfModel(context, UsageFlags.JsonMergePatch, sdkType); diff --git a/packages/typespec-client-generator-core/src/validate.ts b/packages/typespec-client-generator-core/src/validate.ts index 365efc86bf..5af4dd87d9 100644 --- a/packages/typespec-client-generator-core/src/validate.ts +++ b/packages/typespec-client-generator-core/src/validate.ts @@ -57,6 +57,11 @@ function validateClientNamesPerNamespace( // Check for duplicate client names for operations validateClientNamesCore(tcgcContext, scope, namespace.operations.values()); + // check for duplicate client names for operations in interfaces + for (const item of namespace.interfaces.values()) { + validateClientNamesCore(tcgcContext, scope, item.operations.values()); + } + // Check for duplicate client names for interfaces validateClientNamesCore(tcgcContext, scope, namespace.interfaces.values()); diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 2ef945a06e..9e09587e18 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -435,6 +435,27 @@ describe("typespec-client-generator-core: decorators", () => { }, ]); }); + + it("with @clientName", async () => { + await runner.compileWithBuiltInService( + ` + @operationGroup + @clientName("ClientModel") + interface Model { + op foo(): void; + } + ` + ); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + const mainClient = sdkPackage.clients[0]; + strictEqual(mainClient.methods.length, 1); + + const clientAccessor = mainClient.methods[0]; + strictEqual(clientAccessor.kind, "clientaccessor"); + strictEqual(clientAccessor.response.kind, "client"); + strictEqual(clientAccessor.response.name, "ClientModel"); + }); }); describe("listOperationGroups without @client and @operationGroup", () => { @@ -1314,7 +1335,7 @@ describe("typespec-client-generator-core: decorators", () => { const { test } = await runner.compileWithBuiltInService(testCode); const actual = shouldGenerateProtocol( - createSdkContextTestHelper(runner.context.program, { + await createSdkContextTestHelper(runner.context.program, { generateProtocolMethods: globalValue, generateConvenienceMethods: false, }), @@ -1355,7 +1376,7 @@ describe("typespec-client-generator-core: decorators", () => { const { test } = await runner.compileWithBuiltInService(testCode); const actual = shouldGenerateConvenient( - createSdkContextTestHelper(runner.program, { + await createSdkContextTestHelper(runner.program, { generateProtocolMethods: false, generateConvenienceMethods: globalValue, }), @@ -1391,7 +1412,7 @@ describe("typespec-client-generator-core: decorators", () => { `); const actual = shouldGenerateConvenient( - createSdkContextTestHelper(runner.program, { + await createSdkContextTestHelper(runner.program, { generateProtocolMethods: false, generateConvenienceMethods: false, }), @@ -2545,6 +2566,30 @@ describe("typespec-client-generator-core: decorators", () => { code: "decorator-wrong-target", }); }); + + it("throws error when used on a polymorphism type", async () => { + const diagnostics = await runner.diagnose(` + @service + @test namespace MyService { + #suppress "deprecated" "@flattenProperty decorator is not recommended to use." + @test + model Model1{ + @flattenProperty + child: Model2; + } + + @test + @discriminator("kind") + model Model2{ + kind: string; + } + } + `); + + expectDiagnostics(diagnostics, { + code: "@azure-tools/typespec-client-generator-core/flatten-polymorphism", + }); + }); }); describe("@clientName", () => { @@ -2836,6 +2881,36 @@ describe("typespec-client-generator-core: decorators", () => { ]); }); + it("duplicate operation in interface with all language scopes", async () => { + const diagnostics = await runner.diagnose( + ` + @service + namespace Contoso.WidgetManager; + + interface C { + @clientName("b") + @route("/a") + op a(): void; + + @route("/b") + op b(): void; + } + ` + ); + + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: 'Client name: "b" is duplicated in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "b" is defined somewhere causing nameing conflicts in language scope: "AllScopes"', + }, + ]); + }); + it("duplicate scalar with all language scopes", async () => { const diagnostics = await runner.diagnose( ` @@ -4175,7 +4250,7 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(clients.length, 1); ok(clients[0].type); - const newSdkContext = createSdkContext(runnerWithVersion.context.emitContext); + const newSdkContext = await createSdkContext(runnerWithVersion.context.emitContext); clients = listClients(newSdkContext); strictEqual(clients.length, 1); ok(clients[0].type); diff --git a/packages/typespec-client-generator-core/test/examples/example-types.test.ts b/packages/typespec-client-generator-core/test/examples/example-types.test.ts new file mode 100644 index 0000000000..d9491ee692 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types.test.ts @@ -0,0 +1,892 @@ +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { deepStrictEqual, ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { + SdkDateTimeType, + SdkDurationType, + SdkHttpOperation, + SdkNullableType, + SdkServiceMethod, +} from "../../src/interfaces.js"; +import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; + +describe("typespec-client-generator-core: example types", () => { + let runner: SdkTestRunner; + + beforeEach(async () => { + runner = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-java", + "examples-directory": `./examples`, + }); + }); + + it("SdkStringExample", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getString.json", + `${__dirname}/example-types/getString.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getString(): string; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "string"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, "test"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "string"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkStringExample diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getStringDiagnostic.json", + `${__dirname}/example-types/getStringDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getStringDiagnostic(): string; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue, undefined); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'getStringDiagnostic.json' does not follow its definition:\n123`, + }); + }); + + it("SdkStringExample from constant", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getStringFromConstant.json", + `${__dirname}/example-types/getStringFromConstant.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getStringFromConstant(): "test"; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "string"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, "test"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "constant"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkStringExample from constant diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getStringFromConstantDiagnostic.json", + `${__dirname}/example-types/getStringFromConstantDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getStringFromConstantDiagnostic(): "test"; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue, undefined); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'getStringFromConstantDiagnostic.json' does not follow its definition:\n123`, + }); + }); + + it("SdkStringExample from enum", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getStringFromEnum.json", + `${__dirname}/example-types/getStringFromEnum.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + enum TestEnum { + one,two,three + } + op getStringFromEnum(): TestEnum; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "string"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, "one"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "enum"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkStringExample from enum diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getStringFromEnumDiagnostic.json", + `${__dirname}/example-types/getStringFromEnumDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + enum TestEnum { + one,two,three + } + op getStringFromEnumDiagnostic(): TestEnum; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue, undefined); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'getStringFromEnumDiagnostic.json' does not follow its definition:\n"four"`, + }); + }); + + it("SdkStringExample from enum value", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getStringFromEnumValue.json", + `${__dirname}/example-types/getStringFromEnumValue.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + enum TestEnum { + one,two,three + } + op getStringFromEnumValue(): TestEnum.one; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "string"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, "one"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "enumvalue"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkStringExample from enum value diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getStringFromEnumValueDiagnostic.json", + `${__dirname}/example-types/getStringFromEnumValueDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + enum TestEnum { + one,two,three + } + op getStringFromEnumValueDiagnostic(): TestEnum.one; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue, undefined); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'getStringFromEnumValueDiagnostic.json' does not follow its definition:\n"four"`, + }); + }); + + it("SdkStringExample from datetime", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getStringFromDataTime.json", + `${__dirname}/example-types/getStringFromDataTime.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getStringFromDataTime(): utcDateTime; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "string"); + strictEqual( + operation.examples[0].responses.get(200)?.bodyValue?.value, + "2022-08-26T18:38:00.000Z" + ); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "utcDateTime"); + strictEqual( + (operation.examples[0].responses.get(200)?.bodyValue?.type as SdkDateTimeType).wireType.kind, + "string" + ); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkStringExample from duration", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getStringFromDuration.json", + `${__dirname}/example-types/getStringFromDuration.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getStringFromDuration(): duration; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "string"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, "P40D"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "duration"); + strictEqual( + (operation.examples[0].responses.get(200)?.bodyValue?.type as SdkDurationType).wireType.kind, + "string" + ); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkNumberExample", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getNumber.json", + `${__dirname}/example-types/getNumber.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getNumber(): float32; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "number"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, 31.752); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "float32"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkNumberExample diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getNumberDiagnostic.json", + `${__dirname}/example-types/getNumberDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getNumberDiagnostic(): float32; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue, undefined); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'getNumberDiagnostic.json' does not follow its definition:\n"123"`, + }); + }); + + it("SdkNumberExample from datetime", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getNumberFromDateTime.json", + `${__dirname}/example-types/getNumberFromDateTime.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + @encode(DateTimeKnownEncoding.unixTimestamp, int64) + scalar timestamp extends utcDateTime; + + op getNumberFromDateTime(): timestamp; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "number"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, 1686566864); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "utcDateTime"); + strictEqual( + (operation.examples[0].responses.get(200)?.bodyValue?.type as SdkDateTimeType).wireType.kind, + "int64" + ); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkNumberExample from duration", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getNumberFromDuration.json", + `${__dirname}/example-types/getNumberFromDuration.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + @encode(DurationKnownEncoding.seconds, float) + scalar delta extends duration; + + op getNumberFromDuration(): delta; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "number"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, 62.525); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "duration"); + strictEqual( + (operation.examples[0].responses.get(200)?.bodyValue?.type as SdkDurationType).wireType.kind, + "float" + ); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkBooleanExample", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getBoolean.json", + `${__dirname}/example-types/getBoolean.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getBoolean(): boolean; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "boolean"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, true); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "boolean"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkBooleanExample diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getBooleanDiagnostic.json", + `${__dirname}/example-types/getBooleanDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getBooleanDiagnostic(): boolean; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue, undefined); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'getBooleanDiagnostic.json' does not follow its definition:\n123`, + }); + }); + + it("SdkNullExample", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getNull.json", + `${__dirname}/example-types/getNull.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getNull(): {@body body: string | null}; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "null"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, null); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "nullable"); + strictEqual( + (operation.examples[0].responses.get(200)?.bodyValue?.type as SdkNullableType).type.kind, + "string" + ); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkAnyExample", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getAny.json", + `${__dirname}/example-types/getAny.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getAny(): unknown; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "any"); + deepStrictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, { test: 123 }); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkUnionExample", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getUnion.json", + `${__dirname}/example-types/getUnion.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getUnion(): {@body body: string | int32}; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.kind, "union"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.value, "test"); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue?.type.kind, "union"); + }); + + it("SdkArrayExample", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getArray.json", + `${__dirname}/example-types/getArray.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getArray(): string[]; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + const example = operation.examples[0].responses.get(200)?.bodyValue; + ok(example); + strictEqual(example.kind, "array"); + strictEqual(example.value.length, 3); + strictEqual(example.type.kind, "array"); + strictEqual(example.type.valueType.kind, "string"); + strictEqual(example.value[0].value, "a"); + strictEqual(example.value[0].kind, "string"); + strictEqual(example.value[0].type.kind, "string"); + strictEqual(example.value[1].value, "b"); + strictEqual(example.value[1].kind, "string"); + strictEqual(example.value[1].type.kind, "string"); + strictEqual(example.value[2].value, "c"); + strictEqual(example.value[2].kind, "string"); + strictEqual(example.value[2].type.kind, "string"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkArrayExample diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getArrayDiagnostic.json", + `${__dirname}/example-types/getArrayDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getArrayDiagnostic(): string[]; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue, undefined); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'getArrayDiagnostic.json' does not follow its definition:\n"test"`, + }); + }); + + it("SdkDictionaryExample", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getDictionary.json", + `${__dirname}/example-types/getDictionary.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getDictionary(): Record; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + const example = operation.examples[0].responses.get(200)?.bodyValue; + ok(example); + strictEqual(example.kind, "dict"); + strictEqual(Object.keys(example.value).length, 3); + strictEqual(example.value["a"].value, "a"); + strictEqual(example.value["a"].kind, "string"); + strictEqual(example.value["a"].type.kind, "string"); + strictEqual(example.value["b"].value, "b"); + strictEqual(example.value["b"].kind, "string"); + strictEqual(example.value["b"].type.kind, "string"); + strictEqual(example.value["c"].value, "c"); + strictEqual(example.value["c"].kind, "string"); + strictEqual(example.value["c"].type.kind, "string"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkDictionaryExample diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getDictionaryDiagnostic.json", + `${__dirname}/example-types/getDictionaryDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op getDictionaryDiagnostic(): Record; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue, undefined); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'getDictionaryDiagnostic.json' does not follow its definition:\n"test"`, + }); + }); + + it("SdkModelExample", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getModel.json", + `${__dirname}/example-types/getModel.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + model Test { + a: string; + b: int32; + } + + op getModel(): Test; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + const example = operation.examples[0].responses.get(200)?.bodyValue; + ok(example); + strictEqual(example.kind, "model"); + strictEqual(example.type.kind, "model"); + strictEqual(example.type.name, "Test"); + strictEqual(Object.keys(example.value).length, 2); + strictEqual(example.value["a"].value, "a"); + strictEqual(example.value["a"].kind, "string"); + strictEqual(example.value["a"].type.kind, "string"); + strictEqual(example.value["b"].value, 2); + strictEqual(example.value["b"].kind, "number"); + strictEqual(example.value["b"].type.kind, "int32"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkModelExample diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getModelDiagnostic.json", + `${__dirname}/example-types/getModelDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + model Test { + a: string; + b: int32; + } + + op getModelDiagnostic(): Test; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'getModelDiagnostic.json' does not follow its definition:\n{"c":true}`, + }); + }); + + it("SdkModelExample from discriminated types", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getModelDiscriminator.json", + `${__dirname}/example-types/getModelDiscriminator.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + @discriminator("kind") + model Fish { + friends?: Fish[]; + hate?: Record; + partner?: Fish; + } + + @discriminator("sharktype") + model Shark extends Fish { + kind: "shark"; + age: int32; + } + + model Salmon extends Fish { + kind: "salmon"; + } + + model SawShark extends Shark { + sharktype: "saw"; + info: string[]; + prop: int32[]; + } + + model GoblinShark extends Shark { + sharktype: "goblin"; + } + + op getModelDiscriminator(): Shark; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + const example = operation.examples[0].responses.get(200)?.bodyValue; + ok(example); + strictEqual(example.kind, "model"); + strictEqual(example.type.kind, "model"); + strictEqual(example.type.name, "SawShark"); + strictEqual(Object.keys(example.value).length, 6); + strictEqual(example.value["kind"].value, "shark"); + strictEqual(example.value["kind"].kind, "string"); + strictEqual(example.value["kind"].type.kind, "constant"); + strictEqual(example.value["sharktype"].value, "saw"); + strictEqual(example.value["sharktype"].kind, "string"); + strictEqual(example.value["sharktype"].type.kind, "constant"); + + strictEqual(example.value["friends"].kind, "array"); + const friend = example.value["friends"].value[0]; + ok(friend); + strictEqual(friend.type.kind, "model"); + strictEqual(friend.type.name, "GoblinShark"); + strictEqual(friend.kind, "model"); + strictEqual(friend.value["kind"].value, "shark"); + strictEqual(friend.value["kind"].kind, "string"); + strictEqual(friend.value["kind"].type.kind, "constant"); + strictEqual(friend.value["sharktype"].value, "goblin"); + strictEqual(friend.value["sharktype"].kind, "string"); + strictEqual(friend.value["sharktype"].type.kind, "constant"); + + strictEqual(example.value["hate"].kind, "dict"); + const hate = example.value["hate"].value["most"]; + ok(hate); + strictEqual(hate.type.kind, "model"); + strictEqual(hate.type.name, "Salmon"); + strictEqual(hate.kind, "model"); + strictEqual(hate.value["kind"].value, "salmon"); + strictEqual(hate.value["kind"].kind, "string"); + strictEqual(hate.value["kind"].type.kind, "constant"); + + strictEqual(example.value["age"].value, 2); + strictEqual(example.value["age"].kind, "number"); + strictEqual(example.value["age"].type.kind, "int32"); + + strictEqual(example.value["prop"].kind, "array"); + strictEqual(example.value["prop"].value[0].value, 1); + strictEqual(example.value["prop"].value[0].kind, "number"); + strictEqual(example.value["prop"].value[0].type.kind, "int32"); + strictEqual(example.value["prop"].value[1].value, 2); + strictEqual(example.value["prop"].value[1].kind, "number"); + strictEqual(example.value["prop"].value[1].type.kind, "int32"); + strictEqual(example.value["prop"].value[2].value, 3); + strictEqual(example.value["prop"].value[2].kind, "number"); + strictEqual(example.value["prop"].value[2].type.kind, "int32"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("SdkModelExample from discriminated types diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getModelDiscriminatorDiagnostic.json", + `${__dirname}/example-types/getModelDiscriminatorDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + @discriminator("kind") + model Fish { + } + + @discriminator("sharktype") + model Shark extends Fish { + kind: "shark"; + } + + model Salmon extends Fish { + kind: "salmon"; + } + + model SawShark extends Shark { + sharktype: "saw"; + } + + model GoblinShark extends Shark { + sharktype: "goblin"; + } + + op getModelDiscriminatorDiagnostic(): Shark; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].responses.get(200)?.bodyValue, undefined); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'getModelDiscriminatorDiagnostic.json' does not follow its definition:\n{"kind":"shark","sharktype":"test","age":2}`, + }); + }); + 1; + it("SdkModelExample with additional properties", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getModelAdditionalProperties.json", + `${__dirname}/example-types/getModelAdditionalProperties.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + model Test { + a: string; + b: int32; + + ...Record; + } + + op getModelAdditionalProperties(): Test; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + const example = operation.examples[0].responses.get(200)?.bodyValue; + ok(example); + strictEqual(example.kind, "model"); + strictEqual(example.type.kind, "model"); + strictEqual(example.type.name, "Test"); + strictEqual(Object.keys(example.value).length, 2); + strictEqual(example.value["a"].value, "a"); + strictEqual(example.value["a"].kind, "string"); + strictEqual(example.value["a"].type.kind, "string"); + strictEqual(example.value["b"].value, 2); + strictEqual(example.value["b"].kind, "number"); + strictEqual(example.value["b"].type.kind, "int32"); + + ok(example.additionalPropertiesValue); + strictEqual(Object.keys(example.additionalPropertiesValue).length, 2); + strictEqual(example.additionalPropertiesValue["c"].value, true); + strictEqual(example.additionalPropertiesValue["c"].kind, "any"); + strictEqual(example.additionalPropertiesValue["c"].type.kind, "any"); + deepStrictEqual(example.additionalPropertiesValue["d"].value, [1, 2, 3]); + strictEqual(example.additionalPropertiesValue["d"].kind, "any"); + strictEqual(example.additionalPropertiesValue["d"].type.kind, "any"); + + expectDiagnostics(runner.context.diagnostics, []); + }); +}); diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getAny.json b/packages/typespec-client-generator-core/test/examples/example-types/getAny.json new file mode 100644 index 0000000000..f04bb12d0b --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getAny.json @@ -0,0 +1,12 @@ +{ + "operationId": "getAny", + "title": "getAny", + "parameters": {}, + "responses": { + "200": { + "body": { + "test": 123 + } + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getArray.json b/packages/typespec-client-generator-core/test/examples/example-types/getArray.json new file mode 100644 index 0000000000..7770605f29 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getArray.json @@ -0,0 +1,10 @@ +{ + "operationId": "getArray", + "title": "getArray", + "parameters": {}, + "responses": { + "200": { + "body": ["a", "b", "c"] + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getArrayDiagnostic.json b/packages/typespec-client-generator-core/test/examples/example-types/getArrayDiagnostic.json new file mode 100644 index 0000000000..a25fe6956b --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getArrayDiagnostic.json @@ -0,0 +1,10 @@ +{ + "operationId": "getArrayDiagnostic", + "title": "getArrayDiagnostic", + "parameters": {}, + "responses": { + "200": { + "body": "test" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getBoolean.json b/packages/typespec-client-generator-core/test/examples/example-types/getBoolean.json new file mode 100644 index 0000000000..e80d31001d --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getBoolean.json @@ -0,0 +1,10 @@ +{ + "operationId": "getBoolean", + "title": "getBoolean", + "parameters": {}, + "responses": { + "200": { + "body": true + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getBooleanDiagnostic.json b/packages/typespec-client-generator-core/test/examples/example-types/getBooleanDiagnostic.json new file mode 100644 index 0000000000..5f569a8019 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getBooleanDiagnostic.json @@ -0,0 +1,10 @@ +{ + "operationId": "getBooleanDiagnostic", + "title": "getBooleanDiagnostic", + "parameters": {}, + "responses": { + "200": { + "body": 123 + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getDictionary.json b/packages/typespec-client-generator-core/test/examples/example-types/getDictionary.json new file mode 100644 index 0000000000..c9506f6b4b --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getDictionary.json @@ -0,0 +1,14 @@ +{ + "operationId": "getDictionary", + "title": "getDictionary", + "parameters": {}, + "responses": { + "200": { + "body": { + "a": "a", + "b": "b", + "c": "c" + } + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getDictionaryDiagnostic.json b/packages/typespec-client-generator-core/test/examples/example-types/getDictionaryDiagnostic.json new file mode 100644 index 0000000000..d7680f8b31 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getDictionaryDiagnostic.json @@ -0,0 +1,10 @@ +{ + "operationId": "getDictionaryDiagnostic", + "title": "getDictionaryDiagnostic", + "parameters": {}, + "responses": { + "200": { + "body": "test" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getModel.json b/packages/typespec-client-generator-core/test/examples/example-types/getModel.json new file mode 100644 index 0000000000..de88bab8af --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getModel.json @@ -0,0 +1,13 @@ +{ + "operationId": "getModel", + "title": "getModel", + "parameters": {}, + "responses": { + "200": { + "body": { + "a": "a", + "b": 2 + } + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getModelAdditionalProperties.json b/packages/typespec-client-generator-core/test/examples/example-types/getModelAdditionalProperties.json new file mode 100644 index 0000000000..8fe79f1e79 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getModelAdditionalProperties.json @@ -0,0 +1,15 @@ +{ + "operationId": "getModelAdditionalProperties", + "title": "getModelAdditionalProperties", + "parameters": {}, + "responses": { + "200": { + "body": { + "a": "a", + "b": 2, + "c": true, + "d": [1, 2, 3] + } + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getModelDiagnostic.json b/packages/typespec-client-generator-core/test/examples/example-types/getModelDiagnostic.json new file mode 100644 index 0000000000..40a6d61e74 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getModelDiagnostic.json @@ -0,0 +1,14 @@ +{ + "operationId": "getModelDiagnostic", + "title": "getModelDiagnostic", + "parameters": {}, + "responses": { + "200": { + "body": { + "a": "a", + "b": 2, + "c": true + } + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getModelDiscriminator.json b/packages/typespec-client-generator-core/test/examples/example-types/getModelDiscriminator.json new file mode 100644 index 0000000000..e92102f011 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getModelDiscriminator.json @@ -0,0 +1,27 @@ +{ + "operationId": "getModelDiscriminator", + "title": "getModelDiscriminator", + "parameters": {}, + "responses": { + "200": { + "body": { + "kind": "shark", + "sharktype": "saw", + "friends": [ + { + "kind": "shark", + "sharktype": "goblin", + "age": 1 + } + ], + "hate": { + "most": { + "kind": "salmon" + } + }, + "age": 2, + "prop": [1, 2, 3] + } + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getModelDiscriminatorDiagnostic.json b/packages/typespec-client-generator-core/test/examples/example-types/getModelDiscriminatorDiagnostic.json new file mode 100644 index 0000000000..66a28f797e --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getModelDiscriminatorDiagnostic.json @@ -0,0 +1,14 @@ +{ + "operationId": "getModelDiscriminatorDiagnostic", + "title": "getModelDiscriminatorDiagnostic", + "parameters": {}, + "responses": { + "200": { + "body": { + "kind": "shark", + "sharktype": "test", + "age": 2 + } + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getNull.json b/packages/typespec-client-generator-core/test/examples/example-types/getNull.json new file mode 100644 index 0000000000..00585ff170 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getNull.json @@ -0,0 +1,10 @@ +{ + "operationId": "getNull", + "title": "getNull", + "parameters": {}, + "responses": { + "200": { + "body": null + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getNumber.json b/packages/typespec-client-generator-core/test/examples/example-types/getNumber.json new file mode 100644 index 0000000000..3ac703b54f --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getNumber.json @@ -0,0 +1,10 @@ +{ + "operationId": "getNumber", + "title": "getNumber", + "parameters": {}, + "responses": { + "200": { + "body": 31.752 + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getNumberDiagnostic.json b/packages/typespec-client-generator-core/test/examples/example-types/getNumberDiagnostic.json new file mode 100644 index 0000000000..0ae913bb57 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getNumberDiagnostic.json @@ -0,0 +1,10 @@ +{ + "operationId": "getNumberDiagnostic", + "title": "getNumberDiagnostic", + "parameters": {}, + "responses": { + "200": { + "body": "123" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getNumberFromDateTime.json b/packages/typespec-client-generator-core/test/examples/example-types/getNumberFromDateTime.json new file mode 100644 index 0000000000..5170a22a38 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getNumberFromDateTime.json @@ -0,0 +1,10 @@ +{ + "operationId": "getNumberFromDateTime", + "title": "getNumberFromDateTime", + "parameters": {}, + "responses": { + "200": { + "body": 1686566864 + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getNumberFromDuration.json b/packages/typespec-client-generator-core/test/examples/example-types/getNumberFromDuration.json new file mode 100644 index 0000000000..72d89f143c --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getNumberFromDuration.json @@ -0,0 +1,10 @@ +{ + "operationId": "getNumberFromDuration", + "title": "getNumberFromDuration", + "parameters": {}, + "responses": { + "200": { + "body": 62.525 + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getString.json b/packages/typespec-client-generator-core/test/examples/example-types/getString.json new file mode 100644 index 0000000000..f1cf65ecef --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getString.json @@ -0,0 +1,10 @@ +{ + "operationId": "getString", + "title": "getString", + "parameters": {}, + "responses": { + "200": { + "body": "test" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getStringDiagnostic.json b/packages/typespec-client-generator-core/test/examples/example-types/getStringDiagnostic.json new file mode 100644 index 0000000000..69f24eb949 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getStringDiagnostic.json @@ -0,0 +1,10 @@ +{ + "operationId": "getStringDiagnostic", + "title": "getStringDiagnostic", + "parameters": {}, + "responses": { + "200": { + "body": 123 + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getStringFromConstant.json b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromConstant.json new file mode 100644 index 0000000000..d19b457d3b --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromConstant.json @@ -0,0 +1,10 @@ +{ + "operationId": "getStringFromConstant", + "title": "getStringFromConstant", + "parameters": {}, + "responses": { + "200": { + "body": "test" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getStringFromConstantDiagnostic.json b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromConstantDiagnostic.json new file mode 100644 index 0000000000..7156e88840 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromConstantDiagnostic.json @@ -0,0 +1,10 @@ +{ + "operationId": "getStringFromConstantDiagnostic", + "title": "getStringFromConstantDiagnostic", + "parameters": {}, + "responses": { + "200": { + "body": 123 + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getStringFromDataTime.json b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromDataTime.json new file mode 100644 index 0000000000..946ae838c1 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromDataTime.json @@ -0,0 +1,10 @@ +{ + "operationId": "getStringFromDataTime", + "title": "getStringFromDataTime", + "parameters": {}, + "responses": { + "200": { + "body": "2022-08-26T18:38:00.000Z" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getStringFromDuration.json b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromDuration.json new file mode 100644 index 0000000000..8af9a53965 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromDuration.json @@ -0,0 +1,10 @@ +{ + "operationId": "getStringFromDuration", + "title": "getStringFromDuration", + "parameters": {}, + "responses": { + "200": { + "body": "P40D" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnum.json b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnum.json new file mode 100644 index 0000000000..44d1c3dac2 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnum.json @@ -0,0 +1,10 @@ +{ + "operationId": "getStringFromEnum", + "title": "getStringFromEnum", + "parameters": {}, + "responses": { + "200": { + "body": "one" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnumDiagnostic.json b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnumDiagnostic.json new file mode 100644 index 0000000000..8f53d9be10 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnumDiagnostic.json @@ -0,0 +1,10 @@ +{ + "operationId": "getStringFromEnumDiagnostic", + "title": "getStringFromEnumDiagnostic", + "parameters": {}, + "responses": { + "200": { + "body": "four" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnumValue.json b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnumValue.json new file mode 100644 index 0000000000..02a01069a0 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnumValue.json @@ -0,0 +1,10 @@ +{ + "operationId": "getStringFromEnumValue", + "title": "getStringFromEnumValue", + "parameters": {}, + "responses": { + "200": { + "body": "one" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnumValueDiagnostic.json b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnumValueDiagnostic.json new file mode 100644 index 0000000000..b11ac19ec9 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getStringFromEnumValueDiagnostic.json @@ -0,0 +1,10 @@ +{ + "operationId": "getStringFromEnumValueDiagnostic", + "title": "getStringFromEnumValueDiagnostic", + "parameters": {}, + "responses": { + "200": { + "body": "four" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/example-types/getUnion.json b/packages/typespec-client-generator-core/test/examples/example-types/getUnion.json new file mode 100644 index 0000000000..baf90b3860 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/example-types/getUnion.json @@ -0,0 +1,10 @@ +{ + "operationId": "getUnion", + "title": "getUnion", + "parameters": {}, + "responses": { + "200": { + "body": "test" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/helper.test.ts b/packages/typespec-client-generator-core/test/examples/helper.test.ts new file mode 100644 index 0000000000..f5974dbd4e --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/helper.test.ts @@ -0,0 +1,40 @@ +import { Operation } from "@typespec/compiler"; +import { strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { getHttpOperationExamples, getHttpOperationWithCache } from "../../src/public-utils.js"; +import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; + +describe("typespec-client-generator-core: helper", () => { + let runner: SdkTestRunner; + + beforeEach(async () => { + runner = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-java", + "examples-directory": `./examples`, + }); + }); + + it("getHttpOperationExamples", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/getOne.json", + `${__dirname}/helper/getOne.json` + ); + await runner.host.addRealTypeSpecFile( + "./examples/getTwo.json", + `${__dirname}/helper/getTwo.json` + ); + const { get } = await runner.compile(` + @service({}) + namespace TestClient { + @test + op get(): string; + } + `); + + const examples = getHttpOperationExamples( + runner.context, + getHttpOperationWithCache(runner.context, get as Operation) + ); + strictEqual(examples.length, 2); + }); +}); diff --git a/packages/typespec-client-generator-core/test/examples/helper/getOne.json b/packages/typespec-client-generator-core/test/examples/helper/getOne.json new file mode 100644 index 0000000000..29801a0b23 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/helper/getOne.json @@ -0,0 +1,10 @@ +{ + "operationId": "get", + "title": "get two", + "parameters": {}, + "responses": { + "200": { + "body": "two" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/helper/getTwo.json b/packages/typespec-client-generator-core/test/examples/helper/getTwo.json new file mode 100644 index 0000000000..4f667cc2a5 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/helper/getTwo.json @@ -0,0 +1,10 @@ +{ + "operationId": "get", + "title": "get one", + "parameters": {}, + "responses": { + "200": { + "body": "one" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/http-operation-examples.test.ts b/packages/typespec-client-generator-core/test/examples/http-operation-examples.test.ts new file mode 100644 index 0000000000..6ee6792fd1 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/http-operation-examples.test.ts @@ -0,0 +1,240 @@ +import { expectDiagnostics, resolveVirtualPath } from "@typespec/compiler/testing"; +import { deepStrictEqual, ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { SdkHttpOperation, SdkServiceMethod } from "../../src/interfaces.js"; +import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; + +describe("typespec-client-generator-core: http operation examples", () => { + let runner: SdkTestRunner; + + beforeEach(async () => { + runner = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-java", + "examples-directory": `./examples`, + }); + }); + + it("simple case", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/simple.json", + `${__dirname}/http-operation-examples/simple.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op simple(): void; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].kind, "http"); + strictEqual(operation.examples[0].name, "simple description"); + strictEqual(operation.examples[0].description, "simple description"); + strictEqual(operation.examples[0].filePath, resolveVirtualPath("./examples/simple.json")); + deepStrictEqual(operation.examples[0].rawExample, { + operationId: "simple", + title: "simple description", + parameters: {}, + responses: {}, + }); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("parameters", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/parameters.json", + `${__dirname}/http-operation-examples/parameters.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + @route("/{b}") + op parameters( + @header a: string, + @path b: string, + @query c: string, + @body d: string, + ): void; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].kind, "http"); + + const parameters = operation.examples[0].parameters; + ok(parameters); + strictEqual(parameters.length, 4); + + strictEqual(parameters[0].value.kind, "string"); + strictEqual(parameters[0].value.value, "header"); + strictEqual(parameters[0].value.type.kind, "string"); + + strictEqual(parameters[1].value.kind, "string"); + strictEqual(parameters[1].value.value, "path"); + strictEqual(parameters[1].value.type.kind, "string"); + + strictEqual(parameters[2].value.kind, "string"); + strictEqual(parameters[2].value.value, "query"); + strictEqual(parameters[2].value.type.kind, "string"); + + strictEqual(parameters[3].value.kind, "string"); + strictEqual(parameters[3].value.value, "body"); + strictEqual(parameters[3].value.type.kind, "string"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("parameters diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/parametersDiagnostic.json", + `${__dirname}/http-operation-examples/parametersDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + @route("/{b}") + op parametersDiagnostic( + @header a: string, + @path b: string, + @query c: string, + @body d: string, + ): void; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].kind, "http"); + + const parameters = operation.examples[0].parameters; + ok(parameters); + strictEqual(parameters.length, 0); + + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'parametersDiagnostic.json' does not follow its definition:\n{"test":"a"}`, + }); + }); + + it("responses", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/responses.json", + `${__dirname}/http-operation-examples/responses.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op responses(): { + @statusCode + code: 200, + @body + body: string + } | { + @statusCode + code: 201, + @header + test: string + }; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].kind, "http"); + + const okResponse = operation.examples[0].responses.get(200); + ok(okResponse); + deepStrictEqual(okResponse.response, operation.responses.get(200)); + ok(okResponse.bodyValue); + + strictEqual(okResponse.bodyValue.kind, "string"); + strictEqual(okResponse.bodyValue.value, "test"); + strictEqual(okResponse.bodyValue.type.kind, "string"); + + const createdResponse = operation.examples[0].responses.get(201); + ok(createdResponse); + deepStrictEqual(createdResponse.response, operation.responses.get(201)); + + strictEqual(createdResponse.bodyValue, undefined); + strictEqual(createdResponse.headers.length, 1); + + deepStrictEqual(createdResponse.headers[0].header, operation.responses.get(201)?.headers[0]); + strictEqual(createdResponse.headers[0].value.value, "test"); + strictEqual(createdResponse.headers[0].value.kind, "string"); + strictEqual(createdResponse.headers[0].value.type.kind, "string"); + + expectDiagnostics(runner.context.diagnostics, []); + }); + + it("responses diagnostic", async () => { + await runner.host.addRealTypeSpecFile( + "./examples/responsesDiagnostic.json", + `${__dirname}/http-operation-examples/responsesDiagnostic.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op responsesDiagnostic(): { + @statusCode + code: 200, + @body + body: string + } | { + @statusCode + code: 201, + @header + test: string + }; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + strictEqual(operation.examples[0].kind, "http"); + + strictEqual(operation.examples[0].responses.size, 1); + const createdResponse = operation.examples[0].responses.get(201); + ok(createdResponse); + deepStrictEqual(createdResponse.response, operation.responses.get(201)); + + strictEqual(createdResponse.bodyValue, undefined); + strictEqual(createdResponse.headers.length, 0); + + expectDiagnostics(runner.context.diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'responsesDiagnostic.json' does not follow its definition:\n{"a":"test"}`, + }, + { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'responsesDiagnostic.json' does not follow its definition:\n{"body":"test"}`, + }, + { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'responsesDiagnostic.json' does not follow its definition:\n{"test":1}`, + }, + { + code: "@azure-tools/typespec-client-generator-core/example-value-no-mapping", + message: `Value in example file 'responsesDiagnostic.json' does not follow its definition:\n{"203":{"headers":{},"body":"test"}}`, + }, + ]); + }); +}); diff --git a/packages/typespec-client-generator-core/test/examples/http-operation-examples/parameters.json b/packages/typespec-client-generator-core/test/examples/http-operation-examples/parameters.json new file mode 100644 index 0000000000..da862caee5 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/http-operation-examples/parameters.json @@ -0,0 +1,11 @@ +{ + "operationId": "parameters", + "title": "parameters", + "parameters": { + "a": "header", + "b": "path", + "c": "query", + "d": "body" + }, + "responses": {} +} diff --git a/packages/typespec-client-generator-core/test/examples/http-operation-examples/parametersDiagnostic.json b/packages/typespec-client-generator-core/test/examples/http-operation-examples/parametersDiagnostic.json new file mode 100644 index 0000000000..26846ae42d --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/http-operation-examples/parametersDiagnostic.json @@ -0,0 +1,8 @@ +{ + "operationId": "parametersDiagnostic", + "title": "parametersDiagnostic", + "parameters": { + "test": "a" + }, + "responses": {} +} diff --git a/packages/typespec-client-generator-core/test/examples/http-operation-examples/responses.json b/packages/typespec-client-generator-core/test/examples/http-operation-examples/responses.json new file mode 100644 index 0000000000..dbaded7c67 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/http-operation-examples/responses.json @@ -0,0 +1,16 @@ +{ + "operationId": "responses", + "title": "responses", + "parameters": {}, + "responses": { + "200": { + "headers": {}, + "body": "test" + }, + "201": { + "headers": { + "test": "test" + } + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/http-operation-examples/responsesDiagnostic.json b/packages/typespec-client-generator-core/test/examples/http-operation-examples/responsesDiagnostic.json new file mode 100644 index 0000000000..41e5e85967 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/http-operation-examples/responsesDiagnostic.json @@ -0,0 +1,18 @@ +{ + "operationId": "responsesDiagnostic", + "title": "responsesDiagnostic", + "parameters": {}, + "responses": { + "203": { + "headers": {}, + "body": "test" + }, + "201": { + "headers": { + "a": "test" + }, + "body": "test", + "test": 1 + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/http-operation-examples/simple.json b/packages/typespec-client-generator-core/test/examples/http-operation-examples/simple.json new file mode 100644 index 0000000000..d684e8ca9a --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/http-operation-examples/simple.json @@ -0,0 +1,6 @@ +{ + "operationId": "simple", + "title": "simple description", + "parameters": {}, + "responses": {} +} diff --git a/packages/typespec-client-generator-core/test/examples/load.test.ts b/packages/typespec-client-generator-core/test/examples/load.test.ts new file mode 100644 index 0000000000..e50873e426 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/load.test.ts @@ -0,0 +1,125 @@ +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { SdkHttpOperation, SdkServiceMethod } from "../../src/interfaces.js"; +import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; + +describe("typespec-client-generator-core: load examples", () => { + let runner: SdkTestRunner; + + beforeEach(async () => { + runner = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-java", + "examples-directory": `./examples`, + }); + }); + + it("no example folder found", async () => { + await runner.compile(` + @service({}) + namespace TestClient { + op get(): string; + } + `); + + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/example-loading", + }); + }); + + it("load example without version", async () => { + await runner.host.addRealTypeSpecFile("./examples/get.json", `${__dirname}/load/get.json`); + await runner.compile(` + @service({}) + namespace TestClient { + op get(): string; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + }); + + it("load example with version", async () => { + await runner.host.addRealTypeSpecFile("./examples/v3/get.json", `${__dirname}/load/get.json`); + await runner.compile(` + @service({}) + @versioned(Versions) + namespace TestClient { + op get(): string; + } + + enum Versions { + v1, + v2, + v3, + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + }); + + it("load multiple example for one operation", async () => { + await runner.host.addRealTypeSpecFile("./examples/get.json", `${__dirname}/load/get.json`); + await runner.host.addRealTypeSpecFile( + "./examples/getAnother.json", + `${__dirname}/load/getAnother.json` + ); + await runner.compile(` + @service({}) + namespace TestClient { + op get(): string; + } + `); + + const operation = ( + runner.context.sdkPackage.clients[0].methods[0] as SdkServiceMethod + ).operation; + ok(operation); + strictEqual(operation.examples?.length, 2); + }); + + it("load example with client customization", async () => { + await runner.host.addRealTypeSpecFile("./examples/get.json", `${__dirname}/load/get.json`); + await runner.compile(` + @service({}) + namespace TestClient { + op get(): string; + } + `); + + await runner.compileWithCustomization( + ` + @service({}) + namespace TestClient { + op get(): string; + } + `, + ` + @client({ + name: "FooClient", + service: TestClient + }) + namespace Customizations { + op test is TestClient.get; + } + ` + ); + + const client = runner.context.sdkPackage.clients[0]; + strictEqual(client.name, "FooClient"); + const method = client.methods[0] as SdkServiceMethod; + ok(method); + strictEqual(method.name, "test"); + const operation = method.operation; + ok(operation); + strictEqual(operation.examples?.length, 1); + }); +}); diff --git a/packages/typespec-client-generator-core/test/examples/load/get.json b/packages/typespec-client-generator-core/test/examples/load/get.json new file mode 100644 index 0000000000..5c3a0efc7f --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/load/get.json @@ -0,0 +1,11 @@ +{ + "operationId": "get", + "title": "get", + "parameters": {}, + "responses": { + "200": { + "description": "ARM operation completed successfully.", + "body": "test" + } + } +} diff --git a/packages/typespec-client-generator-core/test/examples/load/getAnother.json b/packages/typespec-client-generator-core/test/examples/load/getAnother.json new file mode 100644 index 0000000000..51c8823c72 --- /dev/null +++ b/packages/typespec-client-generator-core/test/examples/load/getAnother.json @@ -0,0 +1,11 @@ +{ + "operationId": "Get", + "title": "getAnother", + "parameters": {}, + "responses": { + "200": { + "description": "ARM operation completed successfully.", + "body": "test" + } + } +} diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index 72377d6cba..d9b8cea43c 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -2694,6 +2694,12 @@ describe("typespec-client-generator-core: package", () => { ok(nextLinkProperty); strictEqual(nextLinkProperty.kind, "property"); strictEqual(nextLinkProperty.type.kind, "url"); + strictEqual(nextLinkProperty.type.name, "ResourceLocation"); + strictEqual( + nextLinkProperty.type.crossLanguageDefinitionId, + "TypeSpec.Rest.ResourceLocation" + ); + strictEqual(nextLinkProperty.type.baseType?.kind, "url"); strictEqual(nextLinkProperty.serializedName, "nextLink"); strictEqual(nextLinkProperty.serializedName, listManufacturers.nextLinkPath); @@ -2704,6 +2710,7 @@ describe("typespec-client-generator-core: package", () => { strictEqual(clientRequestIdProperty.kind, "header"); }); }); + describe("spread", () => { it("plain model with no decorators", async () => { await runner.compile(`@server("http://localhost:3000", "endpoint") diff --git a/packages/typespec-client-generator-core/test/public-utils.test.ts b/packages/typespec-client-generator-core/test/public-utils.test.ts index 92af96a4fa..fab7935ead 100644 --- a/packages/typespec-client-generator-core/test/public-utils.test.ts +++ b/packages/typespec-client-generator-core/test/public-utils.test.ts @@ -293,7 +293,7 @@ describe("typespec-client-generator-core: public-utils", () => { `); strictEqual( getClientNamespaceString( - createSdkContextTestHelper(runner.context.program, { + await createSdkContextTestHelper(runner.context.program, { "generate-convenience-methods": true, "generate-protocol-methods": true, }) @@ -309,7 +309,7 @@ describe("typespec-client-generator-core: public-utils", () => { `); strictEqual( getClientNamespaceString( - createSdkContextTestHelper(runner.context.program, { + await createSdkContextTestHelper(runner.context.program, { "generate-convenience-methods": true, "generate-protocol-methods": true, }) @@ -323,7 +323,7 @@ describe("typespec-client-generator-core: public-utils", () => { `); strictEqual( getClientNamespaceString( - createSdkContextTestHelper(runner.context.program, { + await createSdkContextTestHelper(runner.context.program, { "generate-convenience-methods": true, "generate-protocol-methods": true, "package-name": "azure-pick-me", @@ -338,7 +338,7 @@ describe("typespec-client-generator-core: public-utils", () => { `); strictEqual( getClientNamespaceString( - createSdkContextTestHelper(runner.context.program, { + await createSdkContextTestHelper(runner.context.program, { "generate-convenience-methods": true, "generate-protocol-methods": true, "package-name": "Azure.Pick.Me", @@ -356,7 +356,7 @@ describe("typespec-client-generator-core: public-utils", () => { `); strictEqual( getClientNamespaceString( - createSdkContextTestHelper(runner.context.program, { + await createSdkContextTestHelper(runner.context.program, { "generate-convenience-methods": true, "generate-protocol-methods": true, "package-name": "azure.pick.me", @@ -371,7 +371,7 @@ describe("typespec-client-generator-core: public-utils", () => { `); strictEqual( getClientNamespaceString( - createSdkContextTestHelper(runner.context.program, { + await createSdkContextTestHelper(runner.context.program, { "generate-convenience-methods": true, "generate-protocol-methods": true, }) @@ -1430,7 +1430,6 @@ describe("typespec-client-generator-core: public-utils", () => { const models = runner.context.sdkPackage.models; const diagnostics = runner.context.diagnostics; ok(diagnostics); - deepStrictEqual(diagnostics, runner.context.sdkPackage.diagnostics); strictEqual(models.length, 4); const union = models[0].properties[0].type; strictEqual(union.kind, "union"); diff --git a/packages/typespec-client-generator-core/test/test-host.ts b/packages/typespec-client-generator-core/test/test-host.ts index 75379138ef..5f36842d17 100644 --- a/packages/typespec-client-generator-core/test/test-host.ts +++ b/packages/typespec-client-generator-core/test/test-host.ts @@ -2,6 +2,7 @@ import { Diagnostic, EmitContext, Program, Type } from "@typespec/compiler"; import { BasicTestRunner, StandardTestLibrary, + TestHost, TypeSpecTestLibrary, createTestHost, createTestWrapper, @@ -29,6 +30,7 @@ export async function createSdkTestHost(options: CreateSdkTestRunnerOptions = {} } export interface SdkTestRunner extends BasicTestRunner { + host: TestHost; context: SdkContext; compileWithBuiltInService(code: string): Promise>; compileWithBuiltInAzureCoreService(code: string): Promise>; @@ -40,21 +42,21 @@ export interface SdkTestRunner extends BasicTestRunner { ): Promise<[Record, readonly Diagnostic[]]>; } -export function createSdkContextTestHelper< +export async function createSdkContextTestHelper< TOptions extends Record = CreateSdkTestRunnerOptions, TServiceOperation extends SdkServiceOperation = SdkHttpOperation, >( program: Program, options: TOptions, sdkContextOption?: CreateSdkContextOptions -): SdkContext { +): Promise> { const emitContext: EmitContext = { program: program, emitterOutputDir: "dummy", options: options, getAssetEmitter: null as any, }; - return createSdkContext( + return await createSdkContext( emitContext, options.emitterName ?? "@azure-tools/typespec-csharp", sdkContextOption @@ -87,11 +89,13 @@ export async function createSdkTestRunner( autoUsings: autoUsings, }) as SdkTestRunner; + sdkTestRunner.host = host; + // compile const baseCompile = sdkTestRunner.compile; sdkTestRunner.compile = async function compile(code, compileOptions?) { const result = await baseCompile(code, compileOptions); - sdkTestRunner.context = createSdkContextTestHelper( + sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption @@ -103,7 +107,7 @@ export async function createSdkTestRunner( const baseDiagnose = sdkTestRunner.diagnose; sdkTestRunner.diagnose = async function diagnose(code, compileOptions?) { const result = await baseDiagnose(code, compileOptions); - sdkTestRunner.context = createSdkContextTestHelper( + sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption @@ -115,7 +119,7 @@ export async function createSdkTestRunner( const baseCompileAndDiagnose = sdkTestRunner.compileAndDiagnose; sdkTestRunner.compileAndDiagnose = async function compileAndDiagnose(code, compileOptions?) { const result = await baseCompileAndDiagnose(code, compileOptions); - sdkTestRunner.context = createSdkContextTestHelper( + sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption @@ -132,7 +136,7 @@ export async function createSdkTestRunner( noEmit: true, } ); - sdkTestRunner.context = createSdkContextTestHelper( + sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption @@ -154,7 +158,7 @@ export async function createSdkTestRunner( noEmit: true, } ); - sdkTestRunner.context = createSdkContextTestHelper( + sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption @@ -184,7 +188,7 @@ export async function createSdkTestRunner( host.addTypeSpecFile("./main.tsp", `${mainAutoCode}${mainCode}`); host.addTypeSpecFile("./client.tsp", `${clientAutoCode}${clientCode}`); const result = await host.compile("./client.tsp"); - sdkTestRunner.context = createSdkContextTestHelper( + sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption @@ -217,7 +221,7 @@ export async function createSdkTestRunner( noEmit: true, } ); - sdkTestRunner.context = createSdkContextTestHelper( + sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption @@ -230,7 +234,7 @@ export async function createSdkTestRunner( host.addTypeSpecFile("./main.tsp", `${mainAutoCode}${mainCode}`); host.addTypeSpecFile("./client.tsp", `${clientAutoCode}${clientCode}`); const result = await host.compileAndDiagnose("./client.tsp"); - sdkTestRunner.context = createSdkContextTestHelper( + sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption diff --git a/packages/typespec-client-generator-core/test/types/built-in-types.test.ts b/packages/typespec-client-generator-core/test/types/built-in-types.test.ts index 005928b854..7e4e205879 100644 --- a/packages/typespec-client-generator-core/test/types/built-in-types.test.ts +++ b/packages/typespec-client-generator-core/test/types/built-in-types.test.ts @@ -107,19 +107,23 @@ describe("typespec-client-generator-core: built-in types", () => { await runner.compileWithBuiltInService( ` @encode(BytesKnownEncoding.base64url) - scalar Base64rulBytes extends bytes; + scalar Base64UrlBytes extends bytes; @usage(Usage.input | Usage.output) @access(Access.public) model Test { - value: Base64rulBytes[]; + value: Base64UrlBytes[]; } ` ); const sdkType = getSdkTypeHelper(runner); strictEqual(sdkType.kind, "array"); strictEqual(sdkType.valueType.kind, "bytes"); + strictEqual(sdkType.valueType.name, "Base64UrlBytes"); strictEqual(sdkType.valueType.encode, "base64url"); + strictEqual(sdkType.valueType.crossLanguageDefinitionId, "TestService.Base64UrlBytes"); + strictEqual(sdkType.valueType.baseType?.kind, "bytes"); + strictEqual(sdkType.valueType.baseType.encode, "base64"); }); it("armId from Core", async function () { @@ -142,7 +146,10 @@ describe("typespec-client-generator-core: built-in types", () => { ` ); const models = runnerWithCore.context.sdkPackage.models; - strictEqual(models[0].properties[0].type.kind, "armId"); + const type = models[0].properties[0].type; + strictEqual(type.kind, "string"); + strictEqual(type.name, "armResourceIdentifier"); + strictEqual(type.crossLanguageDefinitionId, "Azure.Core.armResourceIdentifier"); }); it("format", async function () { @@ -157,15 +164,9 @@ describe("typespec-client-generator-core: built-in types", () => { @access(Access.public) model Test { urlScalar: url; - uuidScalar: uuid; - eTagScalar: eTag; @format("url") urlProperty: string; - @format("uuid") - uuidProperty: string; - @format("eTag") - eTagProperty: string; } ` ); @@ -208,7 +209,10 @@ describe("typespec-client-generator-core: built-in types", () => { strictEqual(userModel.properties.length, 2); const etagProperty = userModel.properties.find((x) => x.name === "etag"); ok(etagProperty); - strictEqual(etagProperty.type.kind, "eTag"); + strictEqual(etagProperty.type.kind, "string"); + strictEqual(etagProperty.type.name, "eTag"); + strictEqual(etagProperty.type.encode, "string"); + strictEqual(etagProperty.type.crossLanguageDefinitionId, "Azure.Core.eTag"); }); it("unknown format", async function () { @@ -252,7 +256,6 @@ describe("typespec-client-generator-core: built-in types", () => { ): void; ` ); - expectDiagnostics(runner.context.sdkPackage.diagnostics, []); expectDiagnostics(runner.context.diagnostics, []); const m = runner.context.sdkPackage.models.find((x) => x.name === "TestModel"); const e1 = runner.context.sdkPackage.enums.find((x) => x.name === "TestEnum"); @@ -288,7 +291,11 @@ describe("typespec-client-generator-core: built-in types", () => { ); const models = getAllModels(runner.context); strictEqual(models[0].kind, "model"); - strictEqual(models[0].properties[0].type.description, "title"); - strictEqual(models[0].properties[0].type.details, "doc"); + const type = models[0].properties[0].type; + strictEqual(type.kind, "string"); + strictEqual(type.name, "TestScalar"); + strictEqual(type.description, "title"); + strictEqual(type.details, "doc"); + strictEqual(type.crossLanguageDefinitionId, "TestService.TestScalar"); }); }); diff --git a/packages/typespec-client-generator-core/test/types/date-time-types.test.ts b/packages/typespec-client-generator-core/test/types/date-time-types.test.ts index 29e9f63ff9..72014a5da1 100644 --- a/packages/typespec-client-generator-core/test/types/date-time-types.test.ts +++ b/packages/typespec-client-generator-core/test/types/date-time-types.test.ts @@ -25,6 +25,7 @@ describe("typespec-client-generator-core: date-time types", () => { strictEqual(sdkType.wireType.kind, "string"); strictEqual(sdkType.encode, "rfc3339"); }); + it("rfc3339", async function () { await runner.compileWithBuiltInService( ` @@ -41,6 +42,7 @@ describe("typespec-client-generator-core: date-time types", () => { strictEqual(sdkType.wireType.kind, "string"); strictEqual(sdkType.encode, "rfc3339"); }); + it("rfc7231", async function () { await runner.compileWithBuiltInService( ` @@ -75,6 +77,41 @@ describe("typespec-client-generator-core: date-time types", () => { strictEqual(sdkType.encode, "unixTimestamp"); }); + it("encode propagation", async function () { + await runner.compileWithBuiltInService( + ` + @doc("doc") + @summary("title") + @encode(DateTimeKnownEncoding.unixTimestamp, int64) + scalar unixTimestampDatetime extends utcDateTime; + + scalar extraLayerDateTime extends unixTimestampDatetime; + + @usage(Usage.input | Usage.output) + @access(Access.public) + model Test { + value: extraLayerDateTime; + } + ` + ); + const sdkType = getSdkTypeHelper(runner); + strictEqual(sdkType.kind, "utcDateTime"); + strictEqual(sdkType.name, "extraLayerDateTime"); + strictEqual(sdkType.wireType.kind, "int64"); + strictEqual(sdkType.encode, "unixTimestamp"); + strictEqual(sdkType.crossLanguageDefinitionId, "TestService.extraLayerDateTime"); + strictEqual(sdkType.baseType?.kind, "utcDateTime"); + strictEqual(sdkType.baseType.name, "unixTimestampDatetime"); + strictEqual(sdkType.baseType.wireType.kind, "int64"); + strictEqual(sdkType.baseType.encode, "unixTimestamp"); + strictEqual(sdkType.baseType.crossLanguageDefinitionId, "TestService.unixTimestampDatetime"); + strictEqual(sdkType.baseType.baseType?.kind, "utcDateTime"); + strictEqual(sdkType.baseType.baseType.wireType.kind, "string"); + strictEqual(sdkType.baseType.baseType.encode, "rfc3339"); + strictEqual(sdkType.baseType.baseType.name, "utcDateTime"); + strictEqual(sdkType.baseType.baseType.crossLanguageDefinitionId, "TypeSpec.utcDateTime"); + }); + it("nullable unixTimestamp", async function () { await runner.compileWithBuiltInService( ` @@ -101,21 +138,25 @@ describe("typespec-client-generator-core: date-time types", () => { @doc("doc") @summary("title") @encode(DateTimeKnownEncoding.unixTimestamp, int64) - scalar unixTimestampDatetime extends utcDateTime; + scalar unixTimestampDateTime extends utcDateTime; @usage(Usage.input | Usage.output) @access(Access.public) model Test { - value: unixTimestampDatetime[]; + value: unixTimestampDateTime[]; } ` ); const sdkType = getSdkTypeHelper(runner); strictEqual(sdkType.kind, "array"); strictEqual(sdkType.valueType.kind, "utcDateTime"); - strictEqual(sdkType.valueType.wireType.kind, "int64"); + strictEqual(sdkType.valueType.name, "unixTimestampDateTime"); strictEqual(sdkType.valueType.encode, "unixTimestamp"); + strictEqual(sdkType.valueType.wireType?.kind, "int64"); strictEqual(sdkType.valueType.description, "title"); strictEqual(sdkType.valueType.details, "doc"); + strictEqual(sdkType.valueType.crossLanguageDefinitionId, "TestService.unixTimestampDateTime"); + strictEqual(sdkType.valueType.baseType?.kind, "utcDateTime"); + strictEqual(sdkType.valueType.baseType.wireType.kind, "string"); }); }); diff --git a/packages/typespec-client-generator-core/test/types/duration-type.test.ts b/packages/typespec-client-generator-core/test/types/duration-type.test.ts index aeb989affa..68fe2efedf 100644 --- a/packages/typespec-client-generator-core/test/types/duration-type.test.ts +++ b/packages/typespec-client-generator-core/test/types/duration-type.test.ts @@ -113,9 +113,17 @@ describe("typespec-client-generator-core: duration types", () => { const sdkType = getSdkTypeHelper(runner); strictEqual(sdkType.kind, "array"); strictEqual(sdkType.valueType.kind, "duration"); - strictEqual(sdkType.valueType.wireType.kind, "float32"); - strictEqual(sdkType.valueType.encode, "seconds"); + strictEqual(sdkType.valueType.name, "Float32Duration"); strictEqual(sdkType.valueType.description, "title"); strictEqual(sdkType.valueType.details, "doc"); + // the encode and wireType will only be added to the outer type + strictEqual(sdkType.valueType.encode, "seconds"); + strictEqual(sdkType.valueType.crossLanguageDefinitionId, "TestService.Float32Duration"); + strictEqual(sdkType.valueType.wireType?.kind, "float32"); + strictEqual(sdkType.valueType.baseType?.kind, "duration"); + // the encode and wireType on the baseType will have its default value + strictEqual(sdkType.valueType.baseType.wireType.kind, "string"); + strictEqual(sdkType.valueType.baseType.encode, "ISO8601"); + strictEqual(sdkType.valueType.baseType.crossLanguageDefinitionId, "TypeSpec.duration"); }); }); diff --git a/packages/typespec-client-generator-core/test/types/general-decorators-list.test.ts b/packages/typespec-client-generator-core/test/types/general-decorators-list.test.ts index 97eb1b860f..600470b198 100644 --- a/packages/typespec-client-generator-core/test/types/general-decorators-list.test.ts +++ b/packages/typespec-client-generator-core/test/types/general-decorators-list.test.ts @@ -3,6 +3,7 @@ import { expectDiagnostics } from "@typespec/compiler/testing"; import { XmlTestLibrary } from "@typespec/xml/testing"; import { deepStrictEqual, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; +import { SdkEnumValueType } from "../../src/interfaces.js"; import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; describe("typespec-client-generator-core: general decorators list", () => { @@ -72,14 +73,11 @@ describe("typespec-client-generator-core: general decorators list", () => { const models = runner.context.sdkPackage.models; strictEqual(models.length, 1); - deepStrictEqual(models[0].properties[0].decorators, [ - { - name: "TypeSpec.@encode", - arguments: { - encoding: "base64url", - }, - }, - ]); + strictEqual(models[0].properties[0].decorators[0].name, "TypeSpec.@encode"); + const encodeInfo = models[0].properties[0].decorators[0].arguments[ + "encoding" + ] as SdkEnumValueType; + strictEqual(encodeInfo.value, "base64url"); expectDiagnostics(runner.context.diagnostics, []); }); @@ -266,30 +264,17 @@ describe("typespec-client-generator-core: general decorators list", () => { const models = runner.context.sdkPackage.models; strictEqual(models.length, 1); - deepStrictEqual(models[0].decorators, [ - { - name: "TypeSpec.Xml.@ns", - arguments: { - ns: "https://example.com/ns1", - }, - }, - ]); - deepStrictEqual(models[0].properties[0].decorators, [ - { - name: "TypeSpec.Xml.@ns", - arguments: { - ns: "https://example.com/ns1", - }, - }, - ]); - deepStrictEqual(models[0].properties[1].decorators, [ - { - name: "TypeSpec.Xml.@ns", - arguments: { - ns: "https://example.com/ns2", - }, - }, - ]); + strictEqual(models[0].decorators[0].name, "TypeSpec.Xml.@ns"); + const modelArg = models[0].decorators[0].arguments["ns"] as SdkEnumValueType; + strictEqual(modelArg.value, "https://example.com/ns1"); + + strictEqual(models[0].properties[0].decorators[0].name, "TypeSpec.Xml.@ns"); + let propArg = models[0].properties[0].decorators[0].arguments["ns"] as SdkEnumValueType; + strictEqual(propArg.value, "https://example.com/ns1"); + + strictEqual(models[0].properties[1].decorators[0].name, "TypeSpec.Xml.@ns"); + propArg = models[0].properties[1].decorators[0].arguments["ns"] as SdkEnumValueType; + strictEqual(propArg.value, "https://example.com/ns2"); }); it("@unwrapped", async function () { diff --git a/packages/typespec-client-generator-core/test/types/model-types.test.ts b/packages/typespec-client-generator-core/test/types/model-types.test.ts index d0a139139a..7574650c58 100644 --- a/packages/typespec-client-generator-core/test/types/model-types.test.ts +++ b/packages/typespec-client-generator-core/test/types/model-types.test.ts @@ -268,7 +268,8 @@ describe("typespec-client-generator-core: model types", () => { strictEqual(kindProperty.discriminator, true); strictEqual(kindProperty.type.kind, "string"); strictEqual(kindProperty.__raw, undefined); - strictEqual(kindProperty.type.__raw, undefined); + strictEqual(kindProperty.type.__raw?.kind, "Scalar"); + strictEqual(kindProperty.type.__raw?.name, "string"); strictEqual(fish.discriminatorProperty, kindProperty); }); @@ -1467,4 +1468,38 @@ describe("typespec-client-generator-core: model types", () => { strictEqual(models[0].name, "Test"); strictEqual(models[0].properties.length, 0); }); + + it("xml usage", async () => { + await runner.compileAndDiagnose(` + @service({}) + namespace MyService { + model RoundTrip { + prop: string; + } + + model Input { + prop: string; + } + + @route("/test1") + op test1(@header("content-type") contentType: "application/xml", @body body: RoundTrip): RoundTrip; + + @route("/test2") + op test2(@header("content-type") contentType: "application/xml", @body body: Input): void; + } + `); + + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 2); + const roundTripModel = models.find((x) => x.name === "RoundTrip"); + const inputModel = models.find((x) => x.name === "Input"); + ok(roundTripModel); + strictEqual( + roundTripModel.usage, + UsageFlags.Input | UsageFlags.Output | UsageFlags.Json | UsageFlags.Xml + ); + + ok(inputModel); + strictEqual(inputModel.usage, UsageFlags.Input | UsageFlags.Xml); + }); }); diff --git a/packages/typespec-client-generator-core/test/types/multipart-types.test.ts b/packages/typespec-client-generator-core/test/types/multipart-types.test.ts index 3ee5488f9c..f1d8ebe1c8 100644 --- a/packages/typespec-client-generator-core/test/types/multipart-types.test.ts +++ b/packages/typespec-client-generator-core/test/types/multipart-types.test.ts @@ -1,8 +1,13 @@ /* eslint-disable deprecation/deprecation */ import { expectDiagnostics } from "@typespec/compiler/testing"; -import { ok, strictEqual } from "assert"; +import { deepEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { SdkClientType, SdkHttpOperation, UsageFlags } from "../../src/interfaces.js"; +import { + SdkBodyModelPropertyType, + SdkClientType, + SdkHttpOperation, + UsageFlags, +} from "../../src/interfaces.js"; import { getAllModelsWithDiagnostics } from "../../src/types.js"; import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; @@ -39,6 +44,8 @@ describe("typespec-client-generator-core: multipart types", () => { ok(profileImage); strictEqual(profileImage.kind, "property"); strictEqual(profileImage.isMultipartFileInput, true); + ok(profileImage.multipartOptions); + strictEqual(profileImage.multipartOptions.isFilePart, true); }); it("multipart conflicting model usage", async function () { await runner.compile( @@ -84,6 +91,8 @@ describe("typespec-client-generator-core: multipart types", () => { const modelAProp = modelA.properties[0]; strictEqual(modelAProp.kind, "property"); strictEqual(modelAProp.isMultipartFileInput, true); + ok(modelAProp.multipartOptions); + strictEqual(modelAProp.multipartOptions.isFilePart, true); const modelB = models.find((x) => x.name === "NormalOperationRequest"); ok(modelB); @@ -135,6 +144,9 @@ describe("typespec-client-generator-core: multipart types", () => { const pictures = model.properties[0]; strictEqual(pictures.kind, "property"); strictEqual(pictures.isMultipartFileInput, true); + ok(pictures.multipartOptions); + strictEqual(pictures.multipartOptions.isFilePart, true); + strictEqual(pictures.multipartOptions.isMulti, true); }); it("multipart with encoding bytes raises error", async function () { @@ -273,4 +285,335 @@ describe("typespec-client-generator-core: multipart types", () => { ok(address); strictEqual(address.usage & UsageFlags.MultipartFormData, 0); }); + + it("Json[] and bytes[] in multipart/form-data", async function () { + await runner.compileWithBuiltInService(` + model MultiPartRequest { + profileImages: bytes[]; + addresses: Address[]; + } + model Address { + city: string; + } + @post + op upload(@header contentType: "multipart/form-data", @body body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 2); + const multiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(multiPartRequest); + + for (const p of multiPartRequest.properties.values()) { + strictEqual(p.kind, "property"); + ok(p.multipartOptions); + ok(p.type.kind === "bytes" || p.type.kind === "model"); + strictEqual(p.multipartOptions.isMulti, true); + } + }); + + it("basic multipart with @multipartBody for model", async function () { + await runner.compileWithBuiltInService(` + model Address { + city: string; + } + model MultiPartRequest{ + id?: HttpPart; + profileImage: HttpPart; + address: HttpPart
; + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 2); + const MultiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(MultiPartRequest); + ok(MultiPartRequest.usage & UsageFlags.MultipartFormData); + const id = MultiPartRequest.properties.find((x) => x.name === "id") as SdkBodyModelPropertyType; + strictEqual(id.optional, true); + ok(id.multipartOptions); + strictEqual(id.multipartOptions.isFilePart, false); + deepEqual(id.multipartOptions.defaultContentTypes, ["text/plain"]); + const profileImage = MultiPartRequest.properties.find( + (x) => x.name === "profileImage" + ) as SdkBodyModelPropertyType; + strictEqual(profileImage.optional, false); + ok(profileImage.multipartOptions); + strictEqual(profileImage.multipartOptions.isFilePart, true); + strictEqual(profileImage.multipartOptions.filename, undefined); + strictEqual(profileImage.multipartOptions.contentType, undefined); + deepEqual(profileImage.multipartOptions.defaultContentTypes, ["application/octet-stream"]); + const address = MultiPartRequest.properties.find( + (x) => x.name === "address" + ) as SdkBodyModelPropertyType; + strictEqual(address.optional, false); + ok(address.multipartOptions); + strictEqual(address.multipartOptions.isFilePart, false); + deepEqual(address.multipartOptions.defaultContentTypes, ["application/json"]); + strictEqual(address.type.kind, "model"); + }); + + it("File[] of multipart with @multipartBody for model", async function () { + await runner.compileWithBuiltInService(` + model MultiPartRequest{ + fileArrayOnePart: HttpPart; + fileArrayMultiParts: HttpPart[]; + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 2); + const MultiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(MultiPartRequest); + const fileArrayOnePart = MultiPartRequest.properties.find( + (x) => x.name === "fileArrayOnePart" + ) as SdkBodyModelPropertyType; + ok(fileArrayOnePart); + ok(fileArrayOnePart.multipartOptions); + strictEqual(fileArrayOnePart.type.kind, "array"); + strictEqual(fileArrayOnePart.multipartOptions.isMulti, false); + strictEqual(fileArrayOnePart.multipartOptions.filename, undefined); + strictEqual(fileArrayOnePart.multipartOptions.contentType, undefined); + // Maybe we won't meet this case in real world, but we still need to test it. + deepEqual(fileArrayOnePart.multipartOptions.defaultContentTypes, ["application/json"]); + + const fileArrayMultiParts = MultiPartRequest.properties.find( + (x) => x.name === "fileArrayMultiParts" + ) as SdkBodyModelPropertyType; + ok(fileArrayMultiParts); + ok(fileArrayMultiParts.multipartOptions); + strictEqual(fileArrayMultiParts.type.kind, "model"); + strictEqual(fileArrayMultiParts.multipartOptions.isMulti, true); + ok(fileArrayMultiParts.multipartOptions.filename); + strictEqual(fileArrayMultiParts.multipartOptions.filename.optional, true); + ok(fileArrayMultiParts.multipartOptions.contentType); + strictEqual(fileArrayMultiParts.multipartOptions.contentType.optional, true); + // Typespec compiler will set default content type to ["*/*"] for "HttpPart[]" + deepEqual(fileArrayMultiParts.multipartOptions.defaultContentTypes, ["*/*"]); + }); + + it("File with specific content-type", async function () { + await runner.compileWithBuiltInService(` + model RequiredMetaData extends File { + filename: string; + contentType: "image/png"; + } + model MultiPartRequest{ + file: HttpPart; + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + const MultiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(MultiPartRequest); + const fileOptionalFileName = MultiPartRequest.properties.find( + (x) => x.name === "file" + ) as SdkBodyModelPropertyType; + ok(fileOptionalFileName); + ok(fileOptionalFileName.multipartOptions); + deepEqual(fileOptionalFileName.multipartOptions.defaultContentTypes, ["image/png"]); + }); + + it("File of multipart with @multipartBody for model", async function () { + await runner.compileWithBuiltInService(` + model RequiredMetaData extends File { + filename: string; + contentType: string; + } + model MultiPartRequest{ + fileOptionalFileName: HttpPart; + fileRequiredFileName: HttpPart; + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 3); + const MultiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(MultiPartRequest); + ok(MultiPartRequest.usage & UsageFlags.MultipartFormData); + const fileOptionalFileName = MultiPartRequest.properties.find( + (x) => x.name === "fileOptionalFileName" + ) as SdkBodyModelPropertyType; + ok(fileOptionalFileName); + strictEqual(fileOptionalFileName.optional, false); + ok(fileOptionalFileName.multipartOptions); + strictEqual(fileOptionalFileName.name, "fileOptionalFileName"); + strictEqual(fileOptionalFileName.multipartOptions.isFilePart, true); + ok(fileOptionalFileName.multipartOptions.filename); + strictEqual(fileOptionalFileName.multipartOptions.filename.optional, true); + ok(fileOptionalFileName.multipartOptions.contentType); + strictEqual(fileOptionalFileName.multipartOptions.contentType.optional, true); + + const fileRequiredFileName = MultiPartRequest.properties.find( + (x) => x.name === "fileRequiredFileName" + ) as SdkBodyModelPropertyType; + ok(fileRequiredFileName); + strictEqual(fileRequiredFileName.optional, false); + ok(fileRequiredFileName.multipartOptions); + strictEqual(fileRequiredFileName.name, "fileRequiredFileName"); + strictEqual(fileRequiredFileName.multipartOptions.isFilePart, true); + ok(fileRequiredFileName.multipartOptions.filename); + strictEqual(fileRequiredFileName.multipartOptions.filename.optional, false); + ok(fileRequiredFileName.multipartOptions.contentType); + strictEqual(fileRequiredFileName.multipartOptions.contentType.optional, false); + }); + + it("check 'multi' of multipart with @multipartBody for model", async function () { + await runner.compileWithBuiltInService(` + model Address { + city: string; + } + model MultiPartRequest { + stringsOnePart: HttpPart; + stringsMultiParts: HttpPart[]; + bytesOnePart: HttpPart; + bytesMultiParts: HttpPart[]; + addressesOnePart: HttpPart; + addressesMultiParts: HttpPart
[]; + filesOnePart: HttpPart; + filesMultiParts: HttpPart[]; + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 3); + const MultiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(MultiPartRequest); + for (const p of MultiPartRequest.properties.values()) { + strictEqual(p.kind, "property"); + ok(p.multipartOptions); + strictEqual(p.multipartOptions.isMulti, p.name.toLowerCase().includes("multi")); + } + }); + + it("check returned sdkType of multipart with @multipartBody for model", async function () { + await runner.compileWithBuiltInService(` + model MultiPartRequest { + stringsOnePart: HttpPart; + stringsMultiParts: HttpPart[]; + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 1); + const MultiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(MultiPartRequest); + const stringsOnePart = MultiPartRequest.properties.find( + (x) => x.name === "stringsOnePart" + ) as SdkBodyModelPropertyType; + ok(stringsOnePart); + strictEqual(stringsOnePart.type.kind, "array"); + ok(stringsOnePart.multipartOptions); + strictEqual(stringsOnePart.multipartOptions.isMulti, false); + const stringsMultiParts = MultiPartRequest.properties.find( + (x) => x.name === "stringsMultiParts" + ) as SdkBodyModelPropertyType; + ok(stringsMultiParts); + strictEqual(stringsMultiParts.type.kind, "string"); + ok(stringsMultiParts.multipartOptions); + strictEqual(stringsMultiParts.multipartOptions.isMulti, true); + }); + + it("check content-type in multipart with @multipartBody for model", async function () { + await runner.compileWithBuiltInService(` + model MultiPartRequest { + stringWithoutContentType: HttpPart, + stringWithContentType: HttpPart<{@body body: string, @header contentType: "text/html"}>, + bytesWithoutContentType: HttpPart, + bytesWithContentType: HttpPart<{@body body: string, @header contentType: "image/png"}> + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 3); + const MultiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(MultiPartRequest); + const stringWithoutContentType = MultiPartRequest.properties.find( + (x) => x.name === "stringWithoutContentType" + ) as SdkBodyModelPropertyType; + ok(stringWithoutContentType); + strictEqual(stringWithoutContentType.type.kind, "string"); + ok(stringWithoutContentType.multipartOptions); + strictEqual(stringWithoutContentType.multipartOptions.contentType, undefined); + deepEqual(stringWithoutContentType.multipartOptions.defaultContentTypes, ["text/plain"]); + + const stringWithContentType = MultiPartRequest.properties.find( + (x) => x.name === "stringWithContentType" + ) as SdkBodyModelPropertyType; + ok(stringWithContentType); + strictEqual(stringWithContentType.type.kind, "model"); + ok(stringWithContentType.multipartOptions); + ok(stringWithContentType.multipartOptions.contentType); + deepEqual(stringWithContentType.multipartOptions.defaultContentTypes, ["text/html"]); + + const bytesWithoutContentType = MultiPartRequest.properties.find( + (x) => x.name === "bytesWithoutContentType" + ) as SdkBodyModelPropertyType; + ok(bytesWithoutContentType); + strictEqual(bytesWithoutContentType.type.kind, "bytes"); + ok(bytesWithoutContentType.multipartOptions); + strictEqual(bytesWithoutContentType.multipartOptions.contentType, undefined); + deepEqual(bytesWithoutContentType.multipartOptions.defaultContentTypes, [ + "application/octet-stream", + ]); + + const bytesWithContentType = MultiPartRequest.properties.find( + (x) => x.name === "bytesWithContentType" + ) as SdkBodyModelPropertyType; + ok(bytesWithContentType); + strictEqual(bytesWithContentType.type.kind, "model"); + ok(bytesWithContentType.multipartOptions); + ok(bytesWithContentType.multipartOptions.contentType); + deepEqual(bytesWithContentType.multipartOptions.defaultContentTypes, ["image/png"]); + }); + + it("check isFilePart in multipart with @multipartBody for model", async function () { + await runner.compileWithBuiltInService(` + model MultiPartRequest { + bytesRaw: HttpPart, + bytesArrayRaw: HttpPart[], + fileRaw: HttpPart, + fileArrayRaw: HttpPart[], + bytesWithBody: HttpPart<{@body body: bytes}>, + bytesArrayWithBody: HttpPart<{@body body: bytes}>[], + fileWithBody: HttpPart<{@body body: File}>, + fileArrayWithBody: HttpPart<{@body body: File}>[], + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + const MultiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(MultiPartRequest); + + for (const p of MultiPartRequest.properties.values()) { + strictEqual(p.kind, "property"); + ok(p.multipartOptions); + strictEqual(p.multipartOptions.isFilePart, true); + strictEqual(p.multipartOptions.isMulti, p.name.toLowerCase().includes("array")); + } + }); + + it("check serialized name with @multipartBody for model", async function () { + await runner.compileWithBuiltInService(` + model MultiPartRequest { + name: HttpPart, + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + const MultiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(MultiPartRequest); + const nameProperty = MultiPartRequest.properties.find((x) => x.name === "name"); + ok(nameProperty); + strictEqual(nameProperty.name, "name"); + strictEqual((nameProperty as SdkBodyModelPropertyType).serializedName, "serializedName"); + }); }); diff --git a/packages/typespec-client-generator-core/test/types/usage-flags.test.ts b/packages/typespec-client-generator-core/test/types/usage-flags.test.ts new file mode 100644 index 0000000000..443cf5a85f --- /dev/null +++ b/packages/typespec-client-generator-core/test/types/usage-flags.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { UsageFlags } from "../../src/interfaces.js"; + +describe("typespec-client-generator-core: usage flags", () => { + it("all possible values in UsageFlags should be orthogonal", async () => { + const values = Object.values(UsageFlags).filter( + (value) => typeof value === "number" + ) as number[]; + + for (let i = 0; i < values.length; i++) { + for (let j = i + 1; j < values.length; j++) { + expect(values[i] & values[j]).toBe(0); + } + } + }); +}); diff --git a/packages/website/versioned_docs/version-latest/release-notes/release-2024-07-16.md b/packages/website/versioned_docs/version-latest/release-notes/release-2024-07-16.md index f5c58ae968..afd82452a5 100644 --- a/packages/website/versioned_docs/version-latest/release-notes/release-2024-07-16.md +++ b/packages/website/versioned_docs/version-latest/release-notes/release-2024-07-16.md @@ -14,7 +14,15 @@ This release contains breaking changes and deprecation ### @azure-tools/typespec-autorest -- [#1105](https://github.com/Azure/typespec-azure/pull/1105) `x-ms-client-flatten` extension on some of resource properties property is now configurable to be emitted by autorest emitter. Default is false which will skip emission of that extension. +- [#1105](https://github.com/Azure/typespec-azure/pull/1105) `x-ms-client-flatten` extension on some of resource properties property is now configurable to be emitted by autorest emitter(`arm-resource-flattening` option). Default is false which will skip emission of that extension. + To revert to previous behavior update your `tspconfig.yaml` with the following + + ```diff + options: + "@azure-tools/typespec-autorest": + # ...other options + + arm-resource-flattening: true + ``` ### @azure-tools/typespec-azure-resource-manager diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc908f43ff..d69297e51f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,8 +205,8 @@ importers: specifier: ~4.4.0 version: 4.4.0 '@azure/storage-blob': - specifier: ~12.23.0 - version: 12.23.0 + specifier: ~12.24.0 + version: 12.24.0 '@pnpm/find-workspace-packages': specifier: ^6.0.9 version: 6.0.9(@pnpm/logger@5.0.0) @@ -2151,6 +2151,9 @@ importers: packages/typespec-client-generator-core: dependencies: + '@typespec/versioning': + specifier: workspace:~ + version: link:../../core/packages/versioning change-case: specifier: ~5.4.4 version: 5.4.4 @@ -2176,6 +2179,9 @@ importers: '@typespec/library-linter': specifier: workspace:~ version: link:../../core/packages/library-linter + '@typespec/openapi': + specifier: workspace:~ + version: link:../../core/packages/openapi '@typespec/prettier-plugin-typespec': specifier: workspace:~ version: link:../../core/packages/prettier-plugin-typespec @@ -2185,9 +2191,6 @@ importers: '@typespec/tspd': specifier: workspace:~ version: link:../../core/packages/tspd - '@typespec/versioning': - specifier: workspace:~ - version: link:../../core/packages/versioning '@typespec/xml': specifier: workspace:~ version: link:../../core/packages/xml @@ -2512,8 +2515,8 @@ packages: resolution: {integrity: sha512-8ECtug4RL+zsgh20VL8KYHjrRO3MJOeAKEPRXT2lwtiu5U3BdyIdBb50+QZthEkIi60K6pc/pdOx/k5Jp4sLng==} engines: {node: '>=16'} - '@azure/storage-blob@12.23.0': - resolution: {integrity: sha512-c1KJ5R5hqR/HtvmFtTn/Y1BNMq45NUBp0LZH7yF8WFMET+wmESgEr0FVTu/Z5NonmfUjbgJZG5Nh8xHc5RdWGQ==} + '@azure/storage-blob@12.24.0': + resolution: {integrity: sha512-l8cmWM4C7RoNCBOImoFMxhTXe1Lr+8uQ/IgnhRNMpfoA9bAFWoLG4XrWm6O5rKXortreVQuD+fc1hbzWklOZbw==} engines: {node: '>=18.0.0'} '@babel/code-frame@7.12.11': @@ -12631,7 +12634,7 @@ snapshots: jsonwebtoken: 9.0.2 uuid: 8.3.2 - '@azure/storage-blob@12.23.0': + '@azure/storage-blob@12.24.0': dependencies: '@azure/abort-controller': 1.1.0 '@azure/core-auth': 1.7.2