diff --git a/packages/typespec-rust/.scripts/tspcompile.js b/packages/typespec-rust/.scripts/tspcompile.js index fe6de142e..b31ed16cf 100644 --- a/packages/typespec-rust/.scripts/tspcompile.js +++ b/packages/typespec-rust/.scripts/tspcompile.js @@ -234,6 +234,12 @@ function generate(crate, input, outputDir, additionalArgs) { additionalArgs[i] = `--option="@azure-tools/typespec-rust.${additionalArgs[i]}"`; } } + + const tspConfigPath = `${outputDir}/tspconfig.yaml`; + if (fs.existsSync(tspConfigPath)) { + additionalArgs.push(`--config=${tspConfigPath}`); + } + sem.take(async function() { // if a tsp file isn't specified, first check // for a client.tsp file. if that doesn't exist diff --git a/packages/typespec-rust/src/codegen/context.ts b/packages/typespec-rust/src/codegen/context.ts index d10fc3b73..0f49b2219 100644 --- a/packages/typespec-rust/src/codegen/context.ts +++ b/packages/typespec-rust/src/codegen/context.ts @@ -131,6 +131,33 @@ export class Context { return content; } + /** + * returns the impl azure_core::Error for the error type. + * if no impl is required, the empty string is returned. + * + * @param model the model for which to implement TryFrom + * @param use the use statement builder currently in scope + * @returns the impl TryFrom block for type or the empty string + */ + getTryFromForError(model: rust.Model, use: Use): string { + if ((model.flags & rust.ModelFlags.Error) === 0) { + return ''; + } + + use.add('azure_core::error', 'ErrorKind'); + const indent = new helpers.indentation(); + let content = `impl TryFrom for ${helpers.getTypeDeclaration(model)} {\n`; + content += `${indent.get()}type Error = azure_core::Error;\n`; + content += `${indent.get()}fn try_from(error: azure_core::Error) -> std::result::Result {\n`; + content += `${indent.push().get()}match error.kind() {` + content += `${indent.push().get()}ErrorKind::HttpResponse { raw_response: Some(raw_response), .. } => Ok(serde_json::from_str(raw_response.body().clone().into_string()?.as_ref())?),`; + content += `${indent.get()}_ => Err(azure_core::Error::with_message(azure_core::error::ErrorKind::DataConversion, "ErrorKind was not HttpResponse and could not be parsed."))`; + content += `${indent.pop().get()}}\n`; + content += `${indent.pop().get()}}\n`; + content += '}\n\n'; + return content; + } + /** * returns the body format for the provided model * diff --git a/packages/typespec-rust/src/codegen/models.ts b/packages/typespec-rust/src/codegen/models.ts index 523dd2361..60c6125c9 100644 --- a/packages/typespec-rust/src/codegen/models.ts +++ b/packages/typespec-rust/src/codegen/models.ts @@ -294,6 +294,13 @@ function emitModelImpls(crate: rust.Crate, context: Context): helpers.Module | u entries.push(forReq); } + const forErr = context.getTryFromForError(model, use); + if (forErr) { + use.addForType(model); + entries.push(forErr); + } + + const pageImpl = context.getPageImplForType(model, use); if (pageImpl) { use.addForType(model); diff --git a/packages/typespec-rust/src/codemodel/types.ts b/packages/typespec-rust/src/codemodel/types.ts index 272f6a642..63137de55 100644 --- a/packages/typespec-rust/src/codemodel/types.ts +++ b/packages/typespec-rust/src/codemodel/types.ts @@ -340,6 +340,9 @@ export enum ModelFlags { /** model is a sub-type in a polymorphic discriminated union */ PolymorphicSubtype = 4, + + /** model is an error */ + Error = 8, } /** DateTimeEncoding is the wire format of the date/time */ diff --git a/packages/typespec-rust/src/lib.ts b/packages/typespec-rust/src/lib.ts index 097c57a18..969e157b8 100644 --- a/packages/typespec-rust/src/lib.ts +++ b/packages/typespec-rust/src/lib.ts @@ -19,6 +19,8 @@ export interface RustEmitterOptions { 'overwrite-lib-rs': boolean; /** Whether to omit documentation links in generated code. Defaults to false */ 'temp-omit-doc-links': boolean; + /** Whether to emit error types */ + 'emit-error-types': boolean; } const EmitterOptionsSchema: JSONSchemaType = { @@ -59,6 +61,12 @@ const EmitterOptionsSchema: JSONSchemaType = { default: false, description: 'Whether to omit documentation links in generated code. Defaults to false' }, + 'emit-error-types': { + type: 'boolean', + nullable: false, + default: false, + description: 'Whether to emit error types. Defaults to false' + }, }, required: [ 'crate-name', diff --git a/packages/typespec-rust/src/tcgcadapter/adapter.ts b/packages/typespec-rust/src/tcgcadapter/adapter.ts index 9b3c303a1..2352b8e13 100644 --- a/packages/typespec-rust/src/tcgcadapter/adapter.ts +++ b/packages/typespec-rust/src/tcgcadapter/adapter.ts @@ -180,8 +180,36 @@ export class Adapter { const processedTypes = new Set(); + const getErrorModelNames = function(clients: tcgc.SdkClientType[], visitedClientNames = new Set()) : Set { + const errorModelNames = new Set(); + for (const client of clients) { + if (visitedClientNames.has(client.name)) { + continue; + } + visitedClientNames.add(client.name); + for (const errorModelName + of client.methods.flatMap(mt => mt.operation.exceptions).filter( + e => e.type?.kind === 'model').map(md => (md.type as tcgc.SdkModelType).name) + ) { + errorModelNames.add(errorModelName); + } + + for (const errorModelName of getErrorModelNames(client.children ?? [], visitedClientNames).values()) { + errorModelNames.add(errorModelName); + } + } + + return errorModelNames; + } + + const errorModelNames = getErrorModelNames(this.ctx.sdkPackage.clients); + for (const model of this.ctx.sdkPackage.models) { - if ((model.usage & tcgc.UsageFlags.Input) === 0 && (model.usage & tcgc.UsageFlags.Output) === 0 && (model.usage & tcgc.UsageFlags.Spread) === 0) { + let usageFlags = tcgc.UsageFlags.Input | tcgc.UsageFlags.Output | tcgc.UsageFlags.Spread; + if (this.options['emit-error-types']) { + usageFlags |= tcgc.UsageFlags.Exception; + } + if ((model.usage & usageFlags) === 0) { // skip types without input and output usage. this will include core // types unless they're explicitly referenced (e.g. a model property). // we keep the models for spread params as we internally use them. @@ -210,6 +238,9 @@ export class Adapter { this.getExternalType(model.external); } else { const rustModel = this.getModel(model); + if ((model.usage & tcgc.UsageFlags.Exception) !== 0 && errorModelNames.has(model.name)) { + rustModel.flags |= rust.ModelFlags.Error; + } this.crate.models.push(rustModel); } } @@ -372,7 +403,6 @@ export class Adapter { // include error and LRO polling types as output types if ( (model.usage & tcgc.UsageFlags.Output) === tcgc.UsageFlags.Output || - (model.usage & tcgc.UsageFlags.Exception) === tcgc.UsageFlags.Exception || (model.usage & tcgc.UsageFlags.LroPolling) === tcgc.UsageFlags.LroPolling ) { modelFlags |= rust.ModelFlags.Output; diff --git a/packages/typespec-rust/test/Cargo.lock b/packages/typespec-rust/test/Cargo.lock index c4abd2666..11cda5934 100644 --- a/packages/typespec-rust/test/Cargo.lock +++ b/packages/typespec-rust/test/Cargo.lock @@ -1795,6 +1795,7 @@ dependencies = [ "async-trait", "azure_core", "serde", + "serde_json", "tokio", ] diff --git a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/Cargo.toml b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/Cargo.toml index 32d3ba2c2..9967a1d72 100644 --- a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/Cargo.toml +++ b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/Cargo.toml @@ -13,6 +13,7 @@ default = ["azure_core/default"] [dependencies] azure_core = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } [dev-dependencies] async-trait = { workspace = true } diff --git a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/src/generated/models/models.rs b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/src/generated/models/models.rs index e200f682e..69b76a73b 100644 --- a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/src/generated/models/models.rs +++ b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/src/generated/models/models.rs @@ -4,10 +4,58 @@ // Code generated by Microsoft (R) Rust Code Generator. DO NOT EDIT. use super::{CreatedByType, ManagedServiceIdentityType}; -use azure_core::{fmt::SafeDebug, time::OffsetDateTime}; +use azure_core::{fmt::SafeDebug, time::OffsetDateTime, Value}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +/// Api error. +#[derive(Clone, Deserialize, SafeDebug, Serialize)] +pub struct ApiError { + /// The error code. + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + + /// The Api error details + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option>, + + /// The Api inner error + #[serde(skip_serializing_if = "Option::is_none")] + pub innererror: Option, + + /// The error message. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// The target of the particular error. + #[serde(skip_serializing_if = "Option::is_none")] + pub target: Option, +} + +/// Api error base. +#[derive(Clone, Deserialize, SafeDebug, Serialize)] +pub struct ApiErrorBase { + /// The error code. + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + + /// The error message. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// The target of the particular error. + #[serde(skip_serializing_if = "Option::is_none")] + pub target: Option, +} + +/// An error response. +#[derive(Clone, Default, Deserialize, SafeDebug, Serialize)] +pub struct CloudError { + /// Api error. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + /// Concrete tracked resource types can be created by aliasing this type using a specific property type. #[derive(Clone, Default, Deserialize, SafeDebug, Serialize)] pub struct ConfidentialResource { @@ -51,6 +99,64 @@ pub struct ConfidentialResourceProperties { pub username: Option, } +/// The resource management error additional info. +#[derive(Clone, Deserialize, SafeDebug, Serialize)] +pub struct ErrorAdditionalInfo { + /// The additional info. + #[serde(skip_serializing_if = "Option::is_none")] + pub info: Option, + + /// The additional info type. + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_prop: Option, +} + +/// The error detail. +#[derive(Clone, Deserialize, SafeDebug, Serialize)] +pub struct ErrorDetail { + /// The error additional info. + #[serde(rename = "additionalInfo", skip_serializing_if = "Option::is_none")] + pub additional_info: Option>, + + /// The error code. + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + + /// The error details. + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option>, + + /// The error message. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// The error target. + #[serde(skip_serializing_if = "Option::is_none")] + pub target: Option, +} + +/// Error response +/// +/// Common error response for all Azure Resource Manager APIs to return error details for failed operations. +#[derive(Clone, Default, Deserialize, SafeDebug, Serialize)] +pub struct ErrorResponse { + /// The error object. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// Inner error details. +#[derive(Clone, Deserialize, SafeDebug, Serialize)] +pub struct InnerError { + /// The internal error message or exception dump. + #[serde(skip_serializing_if = "Option::is_none")] + pub errordetail: Option, + + /// The exception type. + #[serde(skip_serializing_if = "Option::is_none")] + pub exceptiontype: Option, +} + /// Concrete tracked resource types can be created by aliasing this type using a specific property type. #[derive(Clone, Default, Deserialize, SafeDebug, Serialize)] pub struct ManagedIdentityTrackedResource { diff --git a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/src/generated/models/models_impl.rs b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/src/generated/models/models_impl.rs index 36a5f9744..920ca03f1 100644 --- a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/src/generated/models/models_impl.rs +++ b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/src/generated/models/models_impl.rs @@ -3,8 +3,8 @@ // // Code generated by Microsoft (R) Rust Code Generator. DO NOT EDIT. -use super::{ConfidentialResource, ManagedIdentityTrackedResource}; -use azure_core::{http::RequestContent, json::to_json, Result}; +use super::{CloudError, ConfidentialResource, ErrorResponse, ManagedIdentityTrackedResource}; +use azure_core::{error::ErrorKind, http::RequestContent, json::to_json, Result}; impl TryFrom for RequestContent { type Error = azure_core::Error; @@ -19,3 +19,39 @@ impl TryFrom for RequestContent for CloudError { + type Error = azure_core::Error; + fn try_from(error: azure_core::Error) -> std::result::Result { + match error.kind() { + ErrorKind::HttpResponse { + raw_response: Some(raw_response), + .. + } => Ok(serde_json::from_str( + raw_response.body().clone().into_string()?.as_ref(), + )?), + _ => Err(azure_core::Error::with_message( + azure_core::error::ErrorKind::DataConversion, + "ErrorKind was not HttpResponse and could not be parsed.", + )), + } + } +} + +impl TryFrom for ErrorResponse { + type Error = azure_core::Error; + fn try_from(error: azure_core::Error) -> std::result::Result { + match error.kind() { + ErrorKind::HttpResponse { + raw_response: Some(raw_response), + .. + } => Ok(serde_json::from_str( + raw_response.body().clone().into_string()?.as_ref(), + )?), + _ => Err(azure_core::Error::with_message( + azure_core::error::ErrorKind::DataConversion, + "ErrorKind was not HttpResponse and could not be parsed.", + )), + } + } +} diff --git a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tests/common/mod.rs b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tests/common/mod.rs new file mode 100644 index 000000000..019b29181 --- /dev/null +++ b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tests/common/mod.rs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Licensed under the MIT License. See License.txt in the project root for license information. + +use azure_core::credentials::{AccessToken, TokenCredential, TokenRequestOptions}; +use azure_core::time::OffsetDateTime; +use azure_core::Result; +use spector_armcommon::CommonPropertiesClient; +use std::sync::Arc; + +#[derive(Debug)] +struct FakeTokenCredential { + pub token: String, +} + +impl FakeTokenCredential { + pub fn new(token: String) -> Self { + FakeTokenCredential { token } + } +} + +#[async_trait::async_trait] +impl TokenCredential for FakeTokenCredential { + async fn get_token( + &self, + _scopes: &[&str], + _options: Option>, + ) -> Result { + Ok(AccessToken::new( + self.token.clone(), + OffsetDateTime::now_utc(), + )) + } +} + +pub fn create_client() -> CommonPropertiesClient { + CommonPropertiesClient::new( + "http://localhost:3000", + Arc::new(FakeTokenCredential::new("fake_token".to_string())), + "00000000-0000-0000-0000-000000000000".to_string(), + None, + ) + .unwrap() +} diff --git a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tests/common_properties_error_client_test.rs b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tests/common_properties_error_client_test.rs new file mode 100644 index 000000000..f8d0c8ef8 --- /dev/null +++ b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tests/common_properties_error_client_test.rs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Licensed under the MIT License. See License.txt in the project root for license information. + +mod common; + +use azure_core::http::StatusCode; +use spector_armcommon::models::{CloudError, ConfidentialResource, ConfidentialResourceProperties}; + +#[tokio::test] +async fn get_for_predefined_error() { + let client = common::create_client(); + let rsp = client + .get_common_properties_error_client() + .get_for_predefined_error("test-rg", "resource", None) + .await; + + let err = rsp.unwrap_err(); + assert_eq!(err.http_status(), Some(StatusCode::NotFound)); + assert_eq!(err.to_string(), "The Resource 'Azure.ResourceManager.CommonProperties/confidentialResources/confidential' under resource group 'test-rg' was not found."); +} + +#[tokio::test] +async fn create_for_user_defined_error() { + let resource = ConfidentialResource { + location: Some("eastus".to_string()), + properties: Some(ConfidentialResourceProperties { + username: Some("00".to_string()), + ..Default::default() + }), + ..Default::default() + }; + + let client = common::create_client(); + let rsp = client + .get_common_properties_error_client() + .create_for_user_defined_error("test-rg", "resource", resource.try_into().unwrap(), None) + .await; + + let err = rsp.unwrap_err(); + assert_eq!(err.http_status(), Some(StatusCode::BadRequest)); + assert_eq!(err.to_string(), "Username should not contain only numbers."); + + let cloud_err: CloudError = err.try_into().unwrap(); + + let cloud_error = cloud_err.error.as_ref(); + assert!(cloud_error.is_some()); + assert_eq!(cloud_error.unwrap().code, Some("BadRequest".to_string())); + assert_eq!( + cloud_error.unwrap().message, + Some("Username should not contain only numbers.".to_string()) + ); + + let cloud_inner = cloud_error.unwrap().innererror.as_ref(); + assert!(cloud_inner.is_some()); + assert_eq!( + cloud_inner.unwrap().exceptiontype, + Some("general".to_string()) + ); +} diff --git a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tests/common_properties_managed_identity_client_test.rs b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tests/common_properties_managed_identity_client_test.rs index 53a8c07fb..267a09afd 100644 --- a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tests/common_properties_managed_identity_client_test.rs +++ b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tests/common_properties_managed_identity_client_test.rs @@ -2,51 +2,13 @@ // // Licensed under the MIT License. See License.txt in the project root for license information. -use azure_core::credentials::{AccessToken, TokenCredential, TokenRequestOptions}; -use azure_core::time::OffsetDateTime; -use azure_core::Result; +mod common; + use spector_armcommon::models::{ ManagedIdentityTrackedResource, ManagedIdentityTrackedResourceProperties, ManagedServiceIdentity, ManagedServiceIdentityType, UserAssignedIdentity, }; -use spector_armcommon::CommonPropertiesClient; use std::collections::HashMap; -use std::sync::Arc; - -#[derive(Debug)] -struct FakeTokenCredential { - pub token: String, -} - -impl FakeTokenCredential { - pub fn new(token: String) -> Self { - FakeTokenCredential { token } - } -} - -#[async_trait::async_trait] -impl TokenCredential for FakeTokenCredential { - async fn get_token( - &self, - _scopes: &[&str], - _options: Option>, - ) -> Result { - Ok(AccessToken::new( - self.token.clone(), - OffsetDateTime::now_utc(), - )) - } -} - -fn create_client() -> CommonPropertiesClient { - CommonPropertiesClient::new( - "http://localhost:3000", - Arc::new(FakeTokenCredential::new("fake_token".to_string())), - "00000000-0000-0000-0000-000000000000".to_string(), - None, - ) - .unwrap() -} fn get_valid_mi_resource() -> ManagedIdentityTrackedResource { ManagedIdentityTrackedResource { @@ -55,7 +17,7 @@ fn get_valid_mi_resource() -> ManagedIdentityTrackedResource { principal_id: Some("00000000-0000-0000-0000-000000000000".to_string()), tenant_id: Some("00000000-0000-0000-0000-000000000000".to_string()), type_prop: - Some(spector_armcommon::models::ManagedServiceIdentityType::SystemAssigned), + Some(ManagedServiceIdentityType::SystemAssigned), ..Default::default() }), location: Some("eastus".to_string()), @@ -74,14 +36,14 @@ fn get_valid_mi_resource() -> ManagedIdentityTrackedResource { async fn create_with_system_assigned() { let resource = ManagedIdentityTrackedResource { identity: Some(ManagedServiceIdentity { - type_prop: Some(spector_armcommon::models::ManagedServiceIdentityType::SystemAssigned), + type_prop: Some(ManagedServiceIdentityType::SystemAssigned), ..Default::default() }), location: Some("eastus".to_string()), ..Default::default() }; - let client = create_client(); + let client = common::create_client(); let resp = client .get_common_properties_managed_identity_client() .create_with_system_assigned("test-rg", "identity", resource.try_into().unwrap(), None) @@ -111,7 +73,7 @@ async fn create_with_system_assigned() { #[tokio::test] async fn get() { - let client = create_client(); + let client = common::create_client(); let resp = client .get_common_properties_managed_identity_client() .get("test-rg", "identity", None) @@ -153,7 +115,7 @@ async fn update_with_user_assigned_and_system_assigned() { ..Default::default() }; - let client = create_client(); + let client = common::create_client(); let resp = client .get_common_properties_managed_identity_client() .update_with_user_assigned_and_system_assigned( diff --git a/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tspconfig.yaml b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tspconfig.yaml new file mode 100644 index 000000000..c6df08f53 --- /dev/null +++ b/packages/typespec-rust/test/spector/azure/resource-manager/common-properties/tspconfig.yaml @@ -0,0 +1,3 @@ +options: + "@azure-tools/typespec-rust": + emit-error-types: true