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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/typespec-rust/.scripts/tspcompile.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ function generate(crate, input, outputDir, additionalArgs) {
additionalArgs[i] = `--option="@azure-tools/typespec-rust.${additionalArgs[i]}"`;
}
}

const tspConfigPath = `${outputDir}/tspconfig.yaml`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also set per-project additional args inline.

'spector_armcommon': {input: 'azure/resource-manager/common-properties', args: ['emit-error-types=true']},

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
Expand Down
27 changes: 27 additions & 0 deletions packages/typespec-rust/src/codegen/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> 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<azure_core::Error> 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<Self, Self::Error> {\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())?),`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Don't call serde_json directly. You can use raw_response.body().json()? even, or call azure_core::json directly, but...
  2. Why are you assuming JSON? Shouldn't this take into account the format the client actually uses, same as how the F: Format is determined for Response<T, F>?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're also needless cloning the body. Deserializing just needs a reference. See https://azure.github.io/azure-sdk/rust_implementation.html for an example of a TryFrom for an error.

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
*
Expand Down
7 changes: 7 additions & 0 deletions packages/typespec-rust/src/codegen/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions packages/typespec-rust/src/codemodel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
8 changes: 8 additions & 0 deletions packages/typespec-rust/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RustEmitterOptions> = {
Expand Down Expand Up @@ -59,6 +61,12 @@ const EmitterOptionsSchema: JSONSchemaType<RustEmitterOptions> = {
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'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should always emit error models. Why not? TryFrom should be optional. In fact, we need it to be because what if they also want to take headers into account like Storage does? They'd need to implement TryFrom themselves.

},
},
required: [
'crate-name',
Expand Down
34 changes: 32 additions & 2 deletions packages/typespec-rust/src/tcgcadapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,36 @@ export class Adapter {

const processedTypes = new Set<string>();

const getErrorModelNames = function(clients: tcgc.SdkClientType<tcgc.SdkHttpOperation>[], visitedClientNames = new Set<string>()) : Set<string> {
const errorModelNames = new Set<string>();
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.
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -372,7 +403,6 @@ export class Adapter {
// include error and LRO polling types as output types
if (
<tcgc.UsageFlags>(model.usage & tcgc.UsageFlags.Output) === tcgc.UsageFlags.Output ||
<tcgc.UsageFlags>(model.usage & tcgc.UsageFlags.Exception) === tcgc.UsageFlags.Exception ||
<tcgc.UsageFlags>(model.usage & tcgc.UsageFlags.LroPolling) === tcgc.UsageFlags.LroPolling
) {
modelFlags |= rust.ModelFlags.Output;
Expand Down
1 change: 1 addition & 0 deletions packages/typespec-rust/test/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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<TokenRequestOptions<'_>>,
) -> Result<AccessToken> {
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()
}
Loading