diff --git a/eng/dict/rust-custom.txt b/eng/dict/rust-custom.txt index 05725577b2..6598b39835 100644 --- a/eng/dict/rust-custom.txt +++ b/eng/dict/rust-custom.txt @@ -1,4 +1,6 @@ bindgen +impl +impls newtype repr rustc diff --git a/eng/test/mock_transport/src/mock_response.rs b/eng/test/mock_transport/src/mock_response.rs index d6ff83e8d2..142664ef29 100644 --- a/eng/test/mock_transport/src/mock_response.rs +++ b/eng/test/mock_transport/src/mock_response.rs @@ -21,7 +21,7 @@ impl From for Response { fn from(mock_response: MockResponse) -> Self { let bytes_stream: azure_core::BytesStream = mock_response.body.into(); - Self::new( + Self::from_stream( mock_response.status, mock_response.headers, Box::pin(bytes_stream), @@ -46,7 +46,7 @@ impl MockResponse { "an error occurred fetching the next part of the byte stream", )?; - let response = Response::new( + let response = Response::from_stream( status_code, header_map.clone(), Box::pin(BytesStream::new(response_bytes.clone())), diff --git a/sdk/core/azure_core/Cargo.toml b/sdk/core/azure_core/Cargo.toml index 80848713b5..2ca9328f56 100644 --- a/sdk/core/azure_core/Cargo.toml +++ b/sdk/core/azure_core/Cargo.toml @@ -62,5 +62,4 @@ features = [ "reqwest_rustls", "hmac_rust", "hmac_openssl", - "xml", ] diff --git a/sdk/core/azure_core/src/lib.rs b/sdk/core/azure_core/src/lib.rs index b0dd236578..081bf2a763 100644 --- a/sdk/core/azure_core/src/lib.rs +++ b/sdk/core/azure_core/src/lib.rs @@ -15,6 +15,14 @@ #![deny(missing_debug_implementations, nonstandard_style)] // #![warn(missing_docs, future_incompatible, unreachable_pub)] +// Docs.rs build is done with the nightly compiler, so we can enable nightly features in that build. +// In this case we enable two features: +// - `doc_auto_cfg`: Automatically scans `cfg` attributes and uses them to show those required configurations in the generated documentation. +// - `doc_cfg_hide`: Ignore the `doc` configuration for `doc_auto_cfg`. +// See https://doc.rust-lang.org/rustdoc/unstable-features.html#doc_auto_cfg-automatically-generate-doccfg for more details. +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg_hide))] + #[macro_use] mod macros; @@ -38,7 +46,9 @@ pub use models::*; pub use options::*; pub use pipeline::*; pub use policies::*; -pub use typespec_client_core::http::response::{Model, PinnedStream, Response, ResponseBody}; +pub use typespec_client_core::http::{ + LazyResponse, PinnedStream, Response, ResponseBody, ResponseFuture, +}; // Re-export typespec types that are not specific to Azure. pub use typespec::{Error, Result}; diff --git a/sdk/cosmos/azure_data_cosmos/examples/cosmos_metadata.rs b/sdk/cosmos/azure_data_cosmos/examples/cosmos_metadata.rs index 9e6034127f..9ef8a0672c 100644 --- a/sdk/cosmos/azure_data_cosmos/examples/cosmos_metadata.rs +++ b/sdk/cosmos/azure_data_cosmos/examples/cosmos_metadata.rs @@ -39,16 +39,12 @@ pub async fn main() -> Result<(), Box> { let db_client = client.database_client(&args.database); if let Some(container_name) = args.container { let container_client = db_client.container_client(container_name); - let response = container_client - .read(None) - .await? - .deserialize_body() - .await?; - println!("{:?}", response); + let response = container_client.read(None).await?; + println!("{:?}", response.into_body()); return Ok(()); } else { - let response = db_client.read(None).await?.deserialize_body().await?; - println!("{:?}", response); + let response = db_client.read(None).await?; + println!("{:?}", response.into_body()); } Ok(()) } diff --git a/sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs b/sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs index a2820b81d7..847c719813 100644 --- a/sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs +++ b/sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs @@ -34,17 +34,14 @@ pub trait ContainerClientMethods { /// # async fn doc() { /// # use azure_data_cosmos::clients::{ContainerClient, ContainerClientMethods}; /// # let container_client: ContainerClient = panic!("this is a non-running example"); - /// let response = container_client.read(None) - /// .await.unwrap() - /// .deserialize_body() - /// .await.unwrap(); + /// let response = container_client.read(None).await.unwrap(); /// # } /// ``` #[allow(async_fn_in_trait)] // REASON: See https://github.com/Azure/azure-sdk-for-rust/issues/1796 for detailed justification - async fn read( + fn read( &self, options: Option, - ) -> azure_core::Result>; + ) -> azure_core::ResponseFuture; /// Executes a single-partition query against items in the container. /// @@ -129,17 +126,16 @@ impl ContainerClient { } impl ContainerClientMethods for ContainerClient { - async fn read( + fn read( &self, #[allow(unused_variables)] // This is a documented public API so prefixing with '_' is undesirable. options: Option, - ) -> azure_core::Result> { - let mut req = Request::new(self.container_url.clone(), azure_core::Method::Get); + ) -> azure_core::ResponseFuture { + let req = Request::new(self.container_url.clone(), azure_core::Method::Get); self.pipeline - .send(Context::new(), &mut req, ResourceType::Containers) - .await + .send(Context::new(), req, ResourceType::Containers) } fn query_items( @@ -159,16 +155,6 @@ impl ContainerClientMethods for ContainerClient { documents: Vec, } - // We have to manually implement Model, because the derive macro doesn't support auto-inferring type and lifetime bounds. - // See https://github.com/Azure/azure-sdk-for-rust/issues/1803 - impl azure_core::Model for QueryResponseModel { - async fn from_response_body( - body: azure_core::ResponseBody, - ) -> typespec_client_core::Result { - body.json().await - } - } - let mut url = self.container_url.clone(); url.append_path_segments(["docs"]); let mut base_req = Request::new(url, azure_core::Method::Post); @@ -196,7 +182,7 @@ impl ContainerClientMethods for ContainerClient { } let resp = pipeline - .send(Context::new(), &mut req, ResourceType::Items) + .send(Context::new(), req, ResourceType::Items) .await?; let query_metrics = resp @@ -208,7 +194,7 @@ impl ContainerClientMethods for ContainerClient { let continuation_token = resp.headers().get_optional_string(&constants::CONTINUATION); - let query_response: QueryResponseModel = resp.deserialize_body().await?; + let query_response: QueryResponseModel = resp.into_body(); let query_results = QueryResults { items: query_response.documents, diff --git a/sdk/cosmos/azure_data_cosmos/src/clients/database_client.rs b/sdk/cosmos/azure_data_cosmos/src/clients/database_client.rs index 57821c1d3b..4293a42463 100644 --- a/sdk/cosmos/azure_data_cosmos/src/clients/database_client.rs +++ b/sdk/cosmos/azure_data_cosmos/src/clients/database_client.rs @@ -30,17 +30,14 @@ pub trait DatabaseClientMethods { /// # async fn doc() { /// # use azure_data_cosmos::clients::{DatabaseClient, DatabaseClientMethods}; /// # let database_client: DatabaseClient = panic!("this is a non-running example"); - /// let response = database_client.read(None) - /// .await.unwrap() - /// .deserialize_body() - /// .await.unwrap(); + /// let response = database_client.read(None).await.unwrap(); /// # } /// ``` #[allow(async_fn_in_trait)] // REASON: See https://github.com/Azure/azure-sdk-for-rust/issues/1796 for detailed justification - async fn read( + fn read( &self, options: Option, - ) -> azure_core::Result>; + ) -> azure_core::ResponseFuture; /// Gets a [`ContainerClient`] that can be used to access the collection with the specified name. /// @@ -70,17 +67,16 @@ impl DatabaseClient { } impl DatabaseClientMethods for DatabaseClient { - async fn read( + fn read( &self, #[allow(unused_variables)] // This is a documented public API so prefixing with '_' is undesirable. options: Option, - ) -> azure_core::Result> { - let mut req = Request::new(self.database_url.clone(), azure_core::Method::Get); + ) -> azure_core::ResponseFuture { + let req = Request::new(self.database_url.clone(), azure_core::Method::Get); self.pipeline - .send(Context::new(), &mut req, ResourceType::Databases) - .await + .send(Context::new(), req, ResourceType::Databases) } fn container_client(&self, name: impl AsRef) -> ContainerClient { diff --git a/sdk/cosmos/azure_data_cosmos/src/models/mod.rs b/sdk/cosmos/azure_data_cosmos/src/models/mod.rs index 5cb7055d2b..b2b37580ee 100644 --- a/sdk/cosmos/azure_data_cosmos/src/models/mod.rs +++ b/sdk/cosmos/azure_data_cosmos/src/models/mod.rs @@ -5,7 +5,7 @@ use azure_core::{ date::{ComponentRange, OffsetDateTime}, - Continuable, Model, + Continuable, }; use serde::{Deserialize, Serialize}; @@ -72,7 +72,7 @@ pub struct SystemProperties { /// Properties of a Cosmos DB database. /// /// Returned by [`DatabaseClient::read()`](crate::clients::DatabaseClient::read()). -#[derive(Model, Debug, Deserialize)] +#[derive(Debug, Deserialize)] pub struct DatabaseProperties { /// The ID of the database. pub id: String, @@ -85,7 +85,7 @@ pub struct DatabaseProperties { /// Properties of a Cosmos DB container. /// /// Returned by [`ContainerClient::read()`](crate::clients::ContainerClient::read()). -#[derive(Model, Debug, Deserialize)] +#[derive(Debug, Deserialize)] pub struct ContainerProperties { /// The ID of the container. pub id: String, diff --git a/sdk/cosmos/azure_data_cosmos/src/pipeline/mod.rs b/sdk/cosmos/azure_data_cosmos/src/pipeline/mod.rs index e1d6bfbc84..8a00138d0f 100644 --- a/sdk/cosmos/azure_data_cosmos/src/pipeline/mod.rs +++ b/sdk/cosmos/azure_data_cosmos/src/pipeline/mod.rs @@ -6,6 +6,7 @@ mod authorization_policy; use std::sync::Arc; pub(crate) use authorization_policy::{AuthorizationPolicy, ResourceType}; +use serde::de::DeserializeOwned; /// Newtype that wraps an Azure Core pipeline to provide a Cosmos-specific pipeline which configures our authorization policy and enforces that a [`ResourceType`] is set on the context. #[derive(Debug, Clone)] @@ -25,13 +26,14 @@ impl CosmosPipeline { )) } - pub async fn send( - &self, - ctx: azure_core::Context<'_>, - request: &mut azure_core::Request, + pub fn send<'a, T: DeserializeOwned>( + &'a self, + ctx: azure_core::Context<'a>, + request: azure_core::Request, resource_type: ResourceType, - ) -> azure_core::Result> { + ) -> azure_core::ResponseFuture<'a, T> { + // We know all our APIs use JSON, so we can just create a wrapper that calls '.json' for us. let ctx = ctx.with_value(resource_type); - self.0.send(&ctx, request).await + self.0.send(ctx, request).json() } } diff --git a/sdk/identity/azure_identity/src/credentials/client_certificate_credentials.rs b/sdk/identity/azure_identity/src/credentials/client_certificate_credentials.rs index e268ea0f2b..edce5a9dfe 100644 --- a/sdk/identity/azure_identity/src/credentials/client_certificate_credentials.rs +++ b/sdk/identity/azure_identity/src/credentials/client_certificate_credentials.rs @@ -21,7 +21,6 @@ use openssl::{ use serde::Deserialize; use std::{str, sync::Arc, time::Duration}; use time::OffsetDateTime; -use typespec_client_core::http::Model; use url::form_urlencoded; /// Refresh time to use in seconds. @@ -255,7 +254,7 @@ impl ClientCertificateCredential { return Err(http_response_from_body(rsp_status, &rsp_body).into_error()); } - let response: AadTokenResponse = rsp.deserialize_body_into().await?; + let response: AadTokenResponse = rsp.into_body().json().await?; Ok(AccessToken::new( response.access_token, OffsetDateTime::now_utc() + Duration::from_secs(response.expires_in), @@ -326,7 +325,7 @@ impl ClientCertificateCredential { } } -#[derive(Model, Deserialize, Debug, Default)] +#[derive(Deserialize, Debug, Default)] #[serde(default)] struct AadTokenResponse { token_type: String, diff --git a/sdk/identity/azure_identity/src/federated_credentials_flow/mod.rs b/sdk/identity/azure_identity/src/federated_credentials_flow/mod.rs index c9d1894584..204410014c 100644 --- a/sdk/identity/azure_identity/src/federated_credentials_flow/mod.rs +++ b/sdk/identity/azure_identity/src/federated_credentials_flow/mod.rs @@ -49,7 +49,7 @@ pub async fn authorize( let rsp_status = rsp.status(); debug!("rsp_status == {:?}", rsp_status); if rsp_status.is_success() { - rsp.deserialize_body_into().await + rsp.into_body().json().await } else { let rsp_body = rsp.into_body().collect().await?; let text = std::str::from_utf8(&rsp_body)?; diff --git a/sdk/identity/azure_identity/src/federated_credentials_flow/response.rs b/sdk/identity/azure_identity/src/federated_credentials_flow/response.rs index e0657d0d37..fd2202658f 100644 --- a/sdk/identity/azure_identity/src/federated_credentials_flow/response.rs +++ b/sdk/identity/azure_identity/src/federated_credentials_flow/response.rs @@ -6,7 +6,6 @@ use azure_core::credentials::Secret; use serde::{Deserialize, Deserializer}; use time::OffsetDateTime; -use typespec_client_core::Model; #[derive(Debug, Clone, Deserialize)] struct RawLoginResponse { @@ -19,7 +18,7 @@ struct RawLoginResponse { access_token: String, } -#[derive(Model, Debug, Clone)] +#[derive(Debug, Clone)] pub struct LoginResponse { pub token_type: String, pub expires_in: u64, diff --git a/sdk/identity/azure_identity/src/refresh_token.rs b/sdk/identity/azure_identity/src/refresh_token.rs index 7cbb8ea68f..35369c3e95 100644 --- a/sdk/identity/azure_identity/src/refresh_token.rs +++ b/sdk/identity/azure_identity/src/refresh_token.rs @@ -14,7 +14,6 @@ use azure_core::{ use serde::Deserialize; use std::fmt; use std::sync::Arc; -use typespec_client_core::Model; use url::form_urlencoded; /// Exchange a refresh token for a new access token and refresh token. @@ -54,9 +53,7 @@ pub async fn exchange( let rsp_status = rsp.status(); if rsp_status.is_success() { - rsp.deserialize_body_into() - .await - .map_kind(ErrorKind::Credential) + rsp.into_body().json().await.map_kind(ErrorKind::Credential) } else { let rsp_body = rsp.into_body().collect().await?; let token_error: RefreshTokenError = @@ -67,7 +64,7 @@ pub async fn exchange( /// A refresh token #[allow(dead_code)] -#[derive(Model, Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct RefreshTokenResponse { token_type: String, #[serde(rename = "scope", deserialize_with = "deserialize::split")] diff --git a/sdk/typespec/typespec_client_core/Cargo.toml b/sdk/typespec/typespec_client_core/Cargo.toml index 50b0749326..2ca2318781 100644 --- a/sdk/typespec/typespec_client_core/Cargo.toml +++ b/sdk/typespec/typespec_client_core/Cargo.toml @@ -41,7 +41,6 @@ tokio = { workspace = true, features = ["macros", "rt", "time"] } [dev-dependencies] once_cell.workspace = true tokio.workspace = true -typespec_derive.workspace = true [features] default = [ diff --git a/sdk/typespec/typespec_client_core/src/error/http_error.rs b/sdk/typespec/typespec_client_core/src/error/http_error.rs index 67d6a87fd0..41589ef88a 100644 --- a/sdk/typespec/typespec_client_core/src/error/http_error.rs +++ b/sdk/typespec/typespec_client_core/src/error/http_error.rs @@ -25,7 +25,7 @@ impl HttpError { /// Create an error from an HTTP response. /// /// This does not check whether the response was successful and should only be used with unsuccessful responses. - pub async fn new(response: Response<()>) -> Self { + pub async fn new(response: Response) -> Self { let status = response.status(); let headers: HashMap = response .headers() diff --git a/sdk/typespec/typespec_client_core/src/http/clients/reqwest.rs b/sdk/typespec/typespec_client_core/src/http/clients/reqwest.rs index 82add9d58e..0f6ecbd105 100644 --- a/sdk/typespec/typespec_client_core/src/http/clients/reqwest.rs +++ b/sdk/typespec/typespec_client_core/src/http/clients/reqwest.rs @@ -4,8 +4,7 @@ use crate::http::{ headers::{HeaderName, HeaderValue, Headers}, request::{Body, Request}, - response::PinnedStream, - HttpClient, Method, Response, StatusCode, + HttpClient, Method, PinnedStream, Response, StatusCode, }; use async_trait::async_trait; use futures::TryStreamExt; @@ -77,7 +76,11 @@ impl HttpClient for ::reqwest::Client { ) })); - Ok(Response::new(try_from_status(status)?, headers, body)) + Ok(Response::from_stream( + try_from_status(status)?, + headers, + body, + )) } } diff --git a/sdk/typespec/typespec_client_core/src/http/mod.rs b/sdk/typespec/typespec_client_core/src/http/mod.rs index 2e906b06e4..e30f54486d 100644 --- a/sdk/typespec/typespec_client_core/src/http/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/mod.rs @@ -12,7 +12,9 @@ mod pageable; mod pipeline; pub mod policies; pub mod request; -pub mod response; +mod response; +mod response_body; +mod response_future; pub use clients::*; pub use context::*; @@ -22,12 +24,23 @@ pub use options::*; pub use pageable::*; pub use pipeline::*; pub use request::{Body, Request, RequestContent}; -pub use response::{Model, Response}; +pub use response::Response; +pub use response_body::ResponseBody; +pub use response_future::{LazyResponse, ResponseFuture}; // Re-export important types. pub use http_types::{Method, StatusCode}; pub use url::Url; +use bytes::Bytes; +use futures::Stream; +use std::pin::Pin; + +#[cfg(not(target_arch = "wasm32"))] +pub type PinnedStream = Pin> + Send + Sync>>; +#[cfg(target_arch = "wasm32")] +pub type PinnedStream = Pin>>>; + /// Add a new query pair into the target [`Url`]'s query string. pub trait AppendToUrlQuery { fn append_to_url_query(&self, url: &mut Url); diff --git a/sdk/typespec/typespec_client_core/src/http/options/transport.rs b/sdk/typespec/typespec_client_core/src/http/options/transport.rs index e071733941..931d32673b 100644 --- a/sdk/typespec/typespec_client_core/src/http/options/transport.rs +++ b/sdk/typespec/typespec_client_core/src/http/options/transport.rs @@ -36,14 +36,12 @@ impl TransportOptions { } /// Use these options to send a request. - pub async fn send(&self, ctx: &Context<'_>, request: &mut Request) -> Result> { + pub async fn send(&self, ctx: &Context<'_>, request: &mut Request) -> Result { use TransportOptionsImpl as I; - let raw_response = match &self.inner { + match &self.inner { I::Http { http_client } => http_client.execute_request(request).await, I::Custom(s) => s.send(ctx, request, &[]).await, - }; - - raw_response.map(|r| r.with_default_deserialize_type()) + } } } diff --git a/sdk/typespec/typespec_client_core/src/http/pipeline.rs b/sdk/typespec/typespec_client_core/src/http/pipeline.rs index 9b26bca570..803cc77751 100644 --- a/sdk/typespec/typespec_client_core/src/http/pipeline.rs +++ b/sdk/typespec/typespec_client_core/src/http/pipeline.rs @@ -3,7 +3,7 @@ use crate::http::{ policies::{CustomHeadersPolicy, Policy, TransportPolicy}, - ClientOptions, Context, Request, Response, RetryOptions, + ClientOptions, Context, Request, ResponseBody, RetryOptions, }; use std::sync::Arc; @@ -78,15 +78,18 @@ impl Pipeline { &self.pipeline } - pub async fn send( - &self, - ctx: &Context<'_>, - request: &mut Request, - ) -> crate::Result> { - self.pipeline[0] - .send(ctx, request, &self.pipeline[1..]) - .await - .map(|resp| resp.with_default_deserialize_type()) + // The outer Pipeline::send method takes ownership of the Context and Request, to allow them to be captured in the ResponseFuture + // Pipeline _policies_ must not take these by value though, as policies may be executed with the same request multiple times (such as in retry scenarios) + pub fn send<'a>( + &'a self, + ctx: Context<'a>, + mut request: Request, + ) -> crate::http::ResponseFuture<'a, ResponseBody> { + crate::http::ResponseFuture::new(async move { + self.pipeline[0] + .send(&ctx, &mut request, &self.pipeline[1..]) + .await + }) } } @@ -94,12 +97,14 @@ impl Pipeline { mod tests { use super::*; use crate::{ - http::{headers::Headers, policies::PolicyResult, Method, StatusCode, TransportOptions}, + http::{ + headers::Headers, policies::PolicyResult, Method, Response, StatusCode, + TransportOptions, + }, stream::BytesStream, }; use bytes::Bytes; use serde::Deserialize; - use typespec_derive::Model; #[tokio::test] async fn deserializes_response() { @@ -117,13 +122,15 @@ mod tests { ) -> PolicyResult { let buffer = Bytes::from_static(br#"{"foo":1,"bar":"baz"}"#); let stream: BytesStream = buffer.into(); - let response = Response::new(StatusCode::Ok, Headers::new(), Box::pin(stream)); - Ok(std::future::ready(response).await) + Ok(Response::from_stream( + StatusCode::Ok, + Headers::new(), + Box::pin(stream), + )) } } - #[derive(Model, Debug, Deserialize)] - #[typespec(crate = "crate")] + #[derive(Debug, Deserialize)] struct Model { foo: i32, bar: String, @@ -133,14 +140,13 @@ mod tests { ClientOptions::new(TransportOptions::new_custom_policy(Arc::new(Responder {}))); let pipeline = Pipeline::new(options, Vec::new(), Vec::new()); - let mut request = Request::new("http://localhost".parse().unwrap(), Method::Get); + let request = Request::new("http://localhost".parse().unwrap(), Method::Get); let model: Model = pipeline - .send(&Context::default(), &mut request) + .send(Context::default(), request) + .json() .await .unwrap() - .deserialize_body() - .await - .unwrap(); + .into_body(); assert_eq!(1, model.foo); assert_eq!("baz", &model.bar); diff --git a/sdk/typespec/typespec_client_core/src/http/response.rs b/sdk/typespec/typespec_client_core/src/http/response.rs index e715a51681..b3a3aba06c 100644 --- a/sdk/typespec/typespec_client_core/src/http/response.rs +++ b/sdk/typespec/typespec_client_core/src/http/response.rs @@ -1,75 +1,45 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::http::{headers::Headers, StatusCode}; +use crate::http::{headers::Headers, PinnedStream, ResponseBody, StatusCode}; use bytes::Bytes; -use futures::{Stream, StreamExt}; -use serde::de::DeserializeOwned; -use std::future::Future; -use std::{fmt, marker::PhantomData, pin::Pin}; -use typespec::error::{ErrorKind, ResultExt}; - -#[cfg(feature = "derive")] -pub use typespec_derive::Model; - -#[cfg(not(target_arch = "wasm32"))] -pub type PinnedStream = Pin> + Send + Sync>>; -#[cfg(target_arch = "wasm32")] -pub type PinnedStream = Pin>>>; - -/// Trait that represents types that can be deserialized from an HTTP response body. -pub trait Model: Sized { - /// Deserialize the response body into type `Self`. - /// - /// A [`ResponseBody`] represents a stream of bytes coming from the server. - /// The server may still be sending data, so it's up to implementors whether they want to wait for the entire body to be received or not. - /// For example, a type representing a simple REST API response will want to wait for the entire body to be received and then parse the body. - /// However, a type representing the download of a large file, may not want to do that and instead prepare to stream the body to a file or other destination. - #[cfg(not(target_arch = "wasm32"))] - fn from_response_body( - body: ResponseBody, - ) -> impl Future> + Send + Sync; - - #[cfg(target_arch = "wasm32")] - fn from_response_body(body: ResponseBody) -> impl Future>; -} +use std::fmt; /// An HTTP response. /// -/// The type parameter `T` is a marker type that indicates what the caller should expect to be able to deserialize the body into. -/// Service client methods should return a `Response` where `SomeModel` is the service-specific response type. -/// For example, a service client method that returns a list of secrets should return `Response`. -/// -/// Given a `Response`, a user can deserialize the body into the intended body type `T` by calling [`Response::deserialize_body`]. -/// However, because the type `T` is just a marker type, the user can also deserialize the body into a different type by calling [`Response::deserialize_body_into`]. -pub struct Response { +/// The type parameter `T` represents the type of the body. +/// Usually, this will be a concrete model type, specific to the API called. +/// In some cases, the body may be of type [`ResponseBody`], which represents a raw, unparsed, HTTP body. +pub struct Response { status: StatusCode, headers: Headers, - body: ResponseBody, - phantom: PhantomData, + body: T, } impl Response { - /// Create an HTTP response from an asynchronous stream of bytes. - pub fn new(status: StatusCode, headers: Headers, stream: PinnedStream) -> Self { + /// Create an HTTP response wrapping the provided body. + pub fn new(status: StatusCode, headers: Headers, body: T) -> Self { Self { status, headers, - body: ResponseBody::new(stream), - phantom: PhantomData, + body, } } +} + +impl Response { + /// Create an HTTP response from an asynchronous stream of bytes. + pub fn from_stream(status: StatusCode, headers: Headers, stream: PinnedStream) -> Self { + Self::new(status, headers, ResponseBody::new(stream)) + } /// Create an HTTP response from raw bytes. pub fn from_bytes(status: StatusCode, headers: Headers, bytes: impl Into) -> Self { - Self { - status, - headers, - body: ResponseBody::from_bytes(bytes), - phantom: PhantomData, - } + Self::new(status, headers, ResponseBody::from_bytes(bytes)) } +} +impl Response { /// Get the status code from the response. pub fn status(&self) -> StatusCode { self.status @@ -80,114 +50,14 @@ impl Response { &self.headers } - /// Deconstruct the HTTP response into its components. - pub fn deconstruct(self) -> (StatusCode, Headers, ResponseBody) { - (self.status, self.headers, self.body) - } - - /// Fetches the entire body and returns it as raw bytes. - /// - /// This method will force the entire body to be downloaded from the server and consume the response. - /// If you want to parse the body into a type, use [`read_body`](Response::deserialize_body) instead. - pub fn into_body(self) -> ResponseBody { + /// Consumes the response and returns the body. + pub fn into_body(self) -> T { self.body } - /// Fetches the entire body and tries to convert it into type `U`. - /// - /// This method is intended for use in rare cases where the body of a service response should be parsed into a user-provided type. - /// - /// # Example - /// ```rust - /// # pub struct GetSecretResponse { } - /// use typespec_client_core::http::{Model, Response}; - /// # #[cfg(not(feature = "derive"))] - /// # use typespec_derive::Model; - /// use serde::Deserialize; - /// use bytes::Bytes; - /// - /// #[derive(Model, Deserialize)] - /// struct MySecretResponse { - /// value: String, - /// } - /// - /// async fn parse_response(response: Response) { - /// // Calling `deserialize_body_into` will parse the body into `MySecretResponse` instead of `GetSecretResponse`. - /// let my_struct: MySecretResponse = response.deserialize_body_into().await.unwrap(); - /// assert_eq!("hunter2", my_struct.value); - /// } - /// - /// # #[tokio::main] - /// # async fn main() { - /// # let r: Response = typespec_client_core::http::Response::from_bytes( - /// # http_types::StatusCode::Ok, - /// # typespec_client_core::http::headers::Headers::new(), - /// # "{\"name\":\"database_password\",\"value\":\"hunter2\"}", - /// # ); - /// # parse_response(r).await; - /// # } - /// ``` - pub async fn deserialize_body_into(self) -> crate::Result { - U::from_response_body(self.body).await - } -} - -impl Response<()> { - /// Changes the type of the response body. - /// - /// Used to set the "type" of an untyped `Response<()>`, transforming it into a `Response`. - pub(crate) fn with_default_deserialize_type(self) -> Response { - Response { - status: self.status, - headers: self.headers, - body: self.body, - phantom: PhantomData, - } - } -} - -impl Response { - /// Fetches the entire body and tries to convert it into type `T`. - /// - /// This is the preferred method for parsing the body of a service response into it's default model type. - /// - /// # Example - /// ```rust - /// # use serde::Deserialize; - /// # use typespec_client_core::http::Model; - /// # #[cfg(not(feature = "derive"))] - /// # use typespec_derive::Model; - /// # #[derive(Model, Deserialize)] - /// # pub struct GetSecretResponse { - /// # name: String, - /// # value: String, - /// # } - /// # pub struct SecretClient { } - /// # impl SecretClient { - /// # pub async fn get_secret(&self) -> typespec_client_core::http::Response { - /// # typespec_client_core::http::Response::from_bytes( - /// # http_types::StatusCode::Ok, - /// # typespec_client_core::http::headers::Headers::new(), - /// # "{\"name\":\"database_password\",\"value\":\"hunter2\"}", - /// # ) - /// # } - /// # } - /// # pub fn create_secret_client() -> SecretClient { - /// # SecretClient { } - /// # } - /// - /// # #[tokio::main] - /// # async fn main() { - /// let secret_client = create_secret_client(); - /// let response = secret_client.get_secret().await; - /// assert_eq!(response.status(), http_types::StatusCode::Ok); - /// let model = response.deserialize_body().await.unwrap(); - /// assert_eq!(model.name, "database_password"); - /// assert_eq!(model.value, "hunter2"); - /// # } - /// ``` - pub async fn deserialize_body(self) -> crate::Result { - T::from_response_body(self.body).await + /// Deconstruct the HTTP response into its components. + pub fn deconstruct(self) -> (StatusCode, Headers, T) { + (self.status, self.headers, self.body) } } @@ -200,214 +70,3 @@ impl fmt::Debug for Response { .finish() } } - -/// A response body stream. -/// -/// This body can either be streamed or collected into [`Bytes`]. -#[pin_project::pin_project] -pub struct ResponseBody(#[pin] PinnedStream); - -impl ResponseBody { - /// Create a new [`ResponseBody`] from an async stream of bytes. - fn new(stream: PinnedStream) -> Self { - Self(stream) - } - - /// Create a new [`ResponseBody`] from a byte slice. - fn from_bytes(bytes: impl Into) -> Self { - let bytes = bytes.into(); - Self::new(Box::pin(futures::stream::once(async move { Ok(bytes) }))) - } - - /// Collect the stream into a [`Bytes`] collection. - pub async fn collect(mut self) -> crate::Result { - let mut final_result = Vec::new(); - - while let Some(res) = self.0.next().await { - final_result.extend(&res?); - } - - Ok(final_result.into()) - } - - /// Collect the stream into a [`String`]. - pub async fn collect_string(self) -> crate::Result { - std::str::from_utf8(&self.collect().await?) - .context( - ErrorKind::DataConversion, - "response body was not utf-8 like expected", - ) - .map(ToOwned::to_owned) - } - - /// Deserialize the JSON stream into type `T`. - #[cfg(feature = "json")] - pub async fn json(self) -> crate::Result - where - T: DeserializeOwned, - { - let body = self.collect().await?; - crate::json::from_json(body) - } - - /// Deserialize the XML stream into type `T`. - #[cfg(feature = "xml")] - pub async fn xml(self) -> crate::Result - where - T: DeserializeOwned, - { - let body = self.collect().await?; - crate::xml::read_xml(&body) - } -} - -impl Stream for ResponseBody { - type Item = crate::Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - let this = self.project(); - this.0.poll_next(cx) - } -} - -impl fmt::Debug for ResponseBody { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("ResponseBody") - } -} - -#[cfg(test)] -mod tests { - use crate::http::headers::Headers; - use crate::http::{response::ResponseBody, Model, Response}; - use typespec::error::ErrorKind; - - #[tokio::test] - pub async fn body_type_controls_consumption_of_response_body() { - pub struct LazyBody; - impl Model for LazyBody { - async fn from_response_body(_body: ResponseBody) -> crate::Result { - // Don't actually consume the body - Ok(LazyBody) - } - } - - // Create a response that fails as you read the body. - let response = Response::<()>::new( - http_types::StatusCode::Ok, - Headers::new(), - Box::pin(futures::stream::once(async { - Err(ErrorKind::Other.into_error()) - })), - ); - - // Because LazyBody chose not to consume the body, this should succeed. - let res: crate::Result = response.deserialize_body_into().await; - assert!(res.is_ok()); - } - - mod json { - use crate::http::headers::Headers; - use crate::http::Response; - use http_types::StatusCode; - use serde::Deserialize; - use typespec_derive::Model; - - /// An example JSON-serialized response type. - #[derive(Model, Deserialize)] - #[typespec(crate = "crate")] - struct GetSecretResponse { - name: String, - value: String, - } - - /// A sample service client function. - fn get_secret() -> Response { - Response::from_bytes( - StatusCode::Ok, - Headers::new(), - "{\"name\":\"my_secret\",\"value\":\"my_value\"}", - ) - } - - #[tokio::test] - pub async fn deserialize_default_type() { - let response = get_secret(); - let secret = response.deserialize_body().await.unwrap(); - assert_eq!(secret.name, "my_secret"); - assert_eq!(secret.value, "my_value"); - } - - #[tokio::test] - pub async fn deserialize_alternate_type() { - #[derive(Model, Deserialize)] - #[typespec(crate = "crate")] - struct MySecretResponse { - #[serde(rename = "name")] - yon_name: String, - #[serde(rename = "value")] - yon_value: String, - } - - let response = get_secret(); - let secret: MySecretResponse = response.deserialize_body_into().await.unwrap(); - assert_eq!(secret.yon_name, "my_secret"); - assert_eq!(secret.yon_value, "my_value"); - } - } - - #[cfg(feature = "xml")] - mod xml { - use crate::http::headers::Headers; - use crate::http::Response; - use http_types::StatusCode; - use serde::Deserialize; - use typespec_derive::Model; - - /// An example XML-serialized response type. - #[derive(Model, Deserialize)] - #[typespec(crate = "crate")] - #[typespec(format = "xml")] - struct GetSecretResponse { - name: String, - value: String, - } - - /// A sample service client function. - fn get_secret() -> Response { - Response::from_bytes( - StatusCode::Ok, - Headers::new(), - "my_secretmy_value", - ) - } - - #[tokio::test] - pub async fn deserialize_default_type() { - let response = get_secret(); - let secret = response.deserialize_body().await.unwrap(); - assert_eq!(secret.name, "my_secret"); - assert_eq!(secret.value, "my_value"); - } - - #[tokio::test] - pub async fn deserialize_alternate_type() { - #[derive(Model, Deserialize)] - #[typespec(crate = "crate")] - #[typespec(format = "xml")] - struct MySecretResponse { - #[serde(rename = "name")] - yon_name: String, - #[serde(rename = "value")] - yon_value: String, - } - - let response = get_secret(); - let secret: MySecretResponse = response.deserialize_body_into().await.unwrap(); - assert_eq!(secret.yon_name, "my_secret"); - assert_eq!(secret.yon_value, "my_value"); - } - } -} diff --git a/sdk/typespec/typespec_client_core/src/http/response_body.rs b/sdk/typespec/typespec_client_core/src/http/response_body.rs new file mode 100644 index 0000000000..ac5bab9e21 --- /dev/null +++ b/sdk/typespec/typespec_client_core/src/http/response_body.rs @@ -0,0 +1,85 @@ +use std::{fmt, pin::Pin}; + +use bytes::Bytes; +use futures::{Stream, StreamExt}; +use serde::de::DeserializeOwned; +use typespec::error::{ErrorKind, ResultExt}; + +use crate::http::PinnedStream; + +/// A response body stream. +/// +/// This body can either be streamed or collected into [`Bytes`]. +#[pin_project::pin_project] +pub struct ResponseBody(#[pin] PinnedStream); + +impl ResponseBody { + /// Create a new [`ResponseBody`] from an async stream of bytes. + pub(crate) fn new(stream: PinnedStream) -> Self { + Self(stream) + } + + /// Create a new [`ResponseBody`] from a byte slice. + pub(crate) fn from_bytes(bytes: impl Into) -> Self { + let bytes = bytes.into(); + Self::new(Box::pin(futures::stream::once(async move { Ok(bytes) }))) + } + + /// Collect the stream into a [`Bytes`] collection. + pub async fn collect(mut self) -> crate::Result { + let mut final_result = Vec::new(); + + while let Some(res) = self.0.next().await { + final_result.extend(&res?); + } + + Ok(final_result.into()) + } + + /// Collect the stream into a [`String`]. + pub async fn collect_string(self) -> crate::Result { + std::str::from_utf8(&self.collect().await?) + .context( + ErrorKind::DataConversion, + "response body was not utf-8 like expected", + ) + .map(ToOwned::to_owned) + } + + /// Deserialize the JSON stream into type `T`. + #[cfg(feature = "json")] + pub async fn json(self) -> crate::Result + where + T: DeserializeOwned, + { + let body = self.collect().await?; + crate::json::from_json(body) + } + + /// Deserialize the XML stream into type `T`. + #[cfg(feature = "xml")] + pub async fn xml(self) -> crate::Result + where + T: DeserializeOwned, + { + let body = self.collect().await?; + crate::xml::read_xml(&body) + } +} + +impl Stream for ResponseBody { + type Item = crate::Result; + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.project(); + this.0.poll_next(cx) + } +} + +impl fmt::Debug for ResponseBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ResponseBody") + } +} diff --git a/sdk/typespec/typespec_client_core/src/http/response_future.rs b/sdk/typespec/typespec_client_core/src/http/response_future.rs new file mode 100644 index 0000000000..4e36bc1a3f --- /dev/null +++ b/sdk/typespec/typespec_client_core/src/http/response_future.rs @@ -0,0 +1,520 @@ +use std::{ + future::{Future, IntoFuture}, + pin::Pin, +}; + +use serde::de::DeserializeOwned; +use typespec::Error; + +use crate::http::{headers::Headers, Response, ResponseBody, StatusCode}; + +// Below are a series of types that serve to just keep the amount of repetition down. +// Wasm32 targets don't have Send or Sync, because they are single-threaded, so we need to exclude those from our trait objects on those platforms. + +/// Utility shorthand for a Boxed future that outputs T and can capture values with lifetime 'a. +#[cfg(not(target_arch = "wasm32"))] +type BoxedFuture<'a, T> = Pin + Send + 'a>>; + +/// Utility shorthand for a Boxed future that outputs T and can capture values with lifetime 'a. +#[cfg(target_arch = "wasm32")] +type BoxedFuture<'a, T> = Pin + 'a>>; + +/// Utility shorthand for a Boxed Future that takes a ResponseBody and "deserializes" the body, returning a BoxedFuture with the result. +#[cfg(not(target_arch = "wasm32"))] +pub type Deserializer<'a, T> = + Box BoxedFuture<'a, Result> + Send + 'a>; + +/// Utility shorthand for a Boxed Future that takes a ResponseBody and "deserializes" the body, returning a BoxedFuture with the result. +#[cfg(target_arch = "wasm32")] +pub type Deserializer<'a, T> = + Box BoxedFuture<'a, Result> + 'a>; + +/// Represents a response where the body has not been downloaded yet, but can be downloaded and deserialized into a value of type `T`. +/// +/// You get a [`LazyResponse`] by calling [`ResponseFuture::lazy()`] on a [`ResponseFuture`] you received from a service API. +/// To read the response and deserialize it, call [`LazyResponse::into_body()`]. +/// +/// See [`ResponseFuture::lazy()`] for more information. +pub struct LazyResponse<'a, T> { + response: Response, + deserializer: Deserializer<'a, T>, +} + +impl<'a, T> LazyResponse<'a, T> { + /// Get the status code from the response. + pub fn status(&self) -> StatusCode { + self.response.status() + } + + /// Get the headers from the response. + pub fn headers(&self) -> &Headers { + self.response.headers() + } + + /// Consumes the response and returns the body. + /// + /// This method is asynchronous because the body of a [`LazyResponse`] has not been yet been read from the transport. + /// Calling this method will force the body to be read, wait for it to complete, and deserialize it into the target type `T`. + pub async fn into_body(self) -> Result { + let body = self.response.into_body(); + (self.deserializer)(body).await + } + + /// Deconstruct the HTTP response into its components. + /// + /// This method is asynchronous because the body of a [`LazyResponse`] has not been yet been read from the transport. + /// Calling this method will force the body to be read, wait for it to complete, and deserialize it into the target type `T`. + pub async fn deconstruct(self) -> Result<(StatusCode, Headers, T), Error> { + let (status, headers, body) = self.response.deconstruct(); + let body = (self.deserializer)(body).await?; + Ok((status, headers, body)) + } +} + +/// Represents a future that, when awaited, will produce a [`Response`]. +/// +/// In most scenarios, a [`ResponseFuture`] that you get from a client API should just be awaited. +/// Awaiting that future will execute the request, download the entire HTTP body, +/// deserialize it into the target type `T`, and return a [`Response`] with that value, along with the status code and response headers. +/// +/// However, you can also use [`ResponseFuture::lazy`] and [`ResponseFuture::raw`] to change how the response is processed. +/// These methods allow you to defer downloading the entire body until later, or forgo deserializing the body entirely. +/// +/// # Examples +/// +/// ```rust +/// # use serde::Deserialize; +/// # use typespec_client_core::http::{Response, ResponseFuture, StatusCode, headers::Headers}; +/// # +/// # struct SecretClient; +/// # +/// # #[derive(Debug, Deserialize)] +/// # struct Secret { +/// # pub name: String, +/// # pub value: String, +/// # } +/// # +/// # impl SecretClient { +/// # pub fn new() -> Self { Self } +/// # pub fn get_secret(&self, name: &str) -> ResponseFuture { +/// # ResponseFuture::new(async { +/// # Ok(Response::from_bytes(StatusCode::Ok, Headers::new(), r#"{"name":"secret_password", "value": "hunter2"}"#)) +/// # }).json() +/// # } +/// # } +/// # #[tokio::main] +/// # async fn main() { +/// let secret_client = SecretClient::new(); +/// let response = secret_client.get_secret("secret_password").await.unwrap(); +/// let secret = response.into_body(); +/// assert_eq!("secret_password", secret.name); +/// assert_eq!("hunter2", secret.value); +/// # } +/// ``` +pub struct ResponseFuture<'a, T = ResponseBody> { + future: BoxedFuture<'a, Result>, + deserializer: Deserializer<'a, T>, +} + +// This impl is constrained on `T: DeserializeOwned` but that's not technically necessary. +// We do that to prevent these methods from appearing on `ResponseFuture`, because they are not relevant to that scenario. +// Having said that, these methods would be **safe to call** on `ResponseFuture`, since all they do is strip the deserializer, or convert the future to a [`LazyResponse`]. +// Neither of those operations are invalid on a `ResponseFuture`, they're just meaningless no-ops. +// Rust doesn't have a way to provide negative type bounds, so this is the best we can do. +impl<'a, T: DeserializeOwned + 'a> ResponseFuture<'a, T> { + /// Executes the request associated with this future, but skips any deserialization and returns a [`Response`] + /// which can be used to read the raw body of the HTTP response. + /// + /// This is intended for scenarios where you want to perform some custom deserialization on the response body. + /// For example, if you want to deserialize the response in to your own custom type. + /// Once the request has completed, you can use [`Response::into_body`] to extract the [`ResponseBody`] and then call + /// a deserialization method like [`ResponseBody::json`] to download the body and deserialize it. + /// + /// NOTE: Calling [`ResponseFuture::raw`] implies [`ResponseFuture::lazy`] as well, the body will not be read until you choose to do so by calling a method on [`ResponseBody`]. + /// + /// # Examples + /// + /// ```rust + /// # use serde::Deserialize; + /// # use typespec_client_core::http::{Response, ResponseFuture, StatusCode, headers::Headers}; + /// # + /// # struct SecretClient; + /// # + /// # #[derive(Debug, Deserialize)] + /// # struct Secret { + /// # name: String, + /// # value: String, + /// # } + /// #[derive(Debug, Deserialize)] + /// struct MyCustomSecret { + /// #[serde(rename = "name")] + /// pub my_custom_name: String, + /// #[serde(rename = "value")] + /// pub my_custom_value: String, + /// } + /// # + /// # impl SecretClient { + /// # pub fn new() -> Self { Self } + /// # pub fn get_secret(&self, name: &str) -> ResponseFuture { + /// # ResponseFuture::new(async { + /// # Ok(Response::from_bytes(StatusCode::Ok, Headers::new(), r#"{"name":"secret_password", "value": "hunter2"}"#)) + /// # }).json() + /// # } + /// # } + /// # #[tokio::main] + /// # async fn main() { + /// let secret_client = SecretClient::new(); + /// let response = secret_client.get_secret("secret_password").raw().await.unwrap(); + /// let raw_body = response.into_body(); + /// let secret: MyCustomSecret = raw_body.json().await.unwrap(); + /// assert_eq!("secret_password", secret.my_custom_name); + /// assert_eq!("hunter2", secret.my_custom_value); + /// # } + /// ``` + pub async fn raw(self) -> Result { + self.future.await + } + + /// Executes the request associated with this future, but does not read the full response body from the transport, + /// returns a [`LazyResponse`] that you can use to read the body at a later point, if at all. + /// + /// This is intended for scenarios where either you don't care about the response body (only the status code and headers) + /// **or** you want to defer reading the body and make a decision later based on the status code and headers. + /// Once the request has completed and the status code and headers are available, this returns and you can use [`LazyResponse::into_body`] to deserialize the value. + /// + /// # Examples + /// + /// ```rust + /// # use serde::Deserialize; + /// # use typespec_client_core::http::{Response, ResponseFuture, StatusCode, headers::Headers}; + /// # + /// # struct BlobClient; + /// # + /// # #[derive(Debug, Deserialize)] + /// # struct Blob { + /// # pub content: String, + /// # } + /// # + /// # impl BlobClient { + /// # pub fn new() -> Self { Self } + /// # pub fn get_blob(&self, name: &str) -> ResponseFuture { + /// # ResponseFuture::new(async { + /// # Ok(Response::from_bytes(StatusCode::Ok, Headers::new(), r#"{"content": "the large blob content"}"#)) + /// # }).json() + /// # } + /// # } + /// # #[tokio::main] + /// # async fn main() { + /// let blob_client = BlobClient::new(); + /// let response = blob_client.get_blob("really_big_file.zip").lazy().await.unwrap(); + /// + /// // You now have the option to read the body IF you want by calling 'into_body'. + /// // This is an async call because it needs to wait for the body to be downloaded and deserialized. + /// let blob = response.into_body().await.unwrap(); + /// assert_eq!("the large blob content", blob.content); + /// # } + /// ``` + pub async fn lazy(self) -> Result, Error> { + let response = self.future.await?; + Ok(LazyResponse { + response, + deserializer: self.deserializer, + }) + } +} + +impl<'a, T: 'a> IntoFuture for ResponseFuture<'a, T> { + type Output = Result, Error>; + type IntoFuture = BoxedFuture<'a, Self::Output>; + + /// Executes the request and deserializes the response into the target type `T`. + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + let resp = self.future.await?; + let (status, headers, body) = resp.deconstruct(); + let body = (self.deserializer)(body).await?; + Ok(Response::new(status, headers, body)) + }) + } +} + +// The order of impls actually matters to the docs. +// Putting this one at the bottom keeps it below the other impl(s) which provide methods that the user is more likely to want to use. +impl<'a> ResponseFuture<'a, ResponseBody> { + /// Create a new [`ResponseFuture`] by wrapping the provided future, which returns the raw HTTP body. + /// + /// This function returns a [`ResponseFuture`] that returns a [`Response`] when awaited. + /// [`ResponseBody`] represents a raw, unparsed, HTTP body. + /// If you want to transform the future into one that parses the HTTP body, use a function like [`ResponseFuture::json`] to attach a deserializer to the future. + #[cfg(not(target_arch = "wasm32"))] + pub fn new(future: impl Future> + Send + 'a) -> Self { + ResponseFuture { + future: Box::pin(future), + deserializer: Box::new(|body| Box::pin(std::future::ready(Ok(body)))), + } + } + + /// Create a new [`ResponseFuture`] by wrapping the provided future, which returns the raw HTTP body. + /// + /// This function returns a [`ResponseFuture`] that returns a [`Response`] when awaited. + /// [`ResponseBody`] represents a raw, unparsed, HTTP body. + /// If you want to transform the future into one that parses the HTTP body, use a function like [`ResponseFuture::json`] to attach a deserializer to the future. + #[cfg(target_arch = "wasm32")] + pub fn new(future: impl Future> + 'a) -> Self { + ResponseFuture { + future: Box::pin(future), + deserializer: Box::new(|body| Box::pin(std::future::ready(Ok(body)))), + } + } + + /// Converts a [`ResponseFuture`] into a [`ResponseFuture`], by deserializing the body as JSON into a `T`. + #[cfg(feature = "json")] + pub fn json(self) -> ResponseFuture<'a, T> + where + T: DeserializeOwned, + { + ResponseFuture { + future: self.future, + deserializer: Box::new(|body| Box::pin(async { body.json().await })), + } + } + + /// Converts a [`ResponseFuture`] into a [`ResponseFuture`], by deserializing the body as XML into a `T`. + #[cfg(feature = "xml")] + pub fn xml(self) -> ResponseFuture<'a, T> + where + T: DeserializeOwned, + { + ResponseFuture { + future: self.future, + deserializer: Box::new(|body| Box::pin(async { body.xml().await })), + } + } +} + +#[cfg(test)] +mod tests { + use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + task::Poll, + }; + + use bytes::Bytes; + use futures::Stream; + use serde::Deserialize; + use typespec::Error; + + use crate::http::{headers::Headers, Response, StatusCode}; + + use super::ResponseFuture; + + #[derive(Deserialize)] + struct Secret { + pub name: String, + pub value: String, + } + + #[derive(Clone)] + struct BodyReadTracker(Arc); + + impl BodyReadTracker { + pub fn new() -> Self { + Self(Arc::new(AtomicBool::new(false))) + } + + pub fn was_body_read(&self) -> bool { + self.0.load(Ordering::SeqCst) + } + + pub fn mark_body_read(&self) { + self.0.store(true, Ordering::SeqCst) + } + } + + #[derive(Clone)] + struct FakeBodyStream { + tracker: BodyReadTracker, + bytes: Vec, + complete: bool, + } + + impl FakeBodyStream { + pub fn new(body: impl Into>, tracker: BodyReadTracker) -> Self { + Self { + tracker, + bytes: body.into(), + complete: false, + } + } + } + + impl Stream for FakeBodyStream { + type Item = Result; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + if self.complete { + Poll::Ready(None) + } else { + self.tracker.mark_body_read(); + self.complete = true; + Poll::Ready(Some(Ok(self.bytes.clone().into()))) + } + } + } + + // A fake pipeline that just returns the provided value + struct FakePipeline { + pub body: FakeBodyStream, + pub tracker: BodyReadTracker, + } + + impl FakePipeline { + pub fn new(body: impl Into>) -> Self { + let tracker = BodyReadTracker::new(); + Self { + body: FakeBodyStream::new(body, tracker.clone()), + tracker, + } + } + + pub fn send(&self) -> ResponseFuture { + ResponseFuture::new(async { + Ok(Response::from_stream( + StatusCode::Ok, + Headers::new(), + Box::pin(self.body.clone()), + )) + }) + } + } + + struct FakeSecretClient(pub FakePipeline); + + impl FakeSecretClient { + pub fn new() -> Self { + FakeSecretClient(FakePipeline::new( + r#"{"name":"secret_password","value":"hunter2"}"#, + )) + } + + pub fn get_secret(&self) -> ResponseFuture { + self.0.send().json() + } + } + + #[cfg(feature = "xml")] + struct FakeSecretClientXml(pub FakePipeline); + + #[cfg(feature = "xml")] + impl FakeSecretClientXml { + pub fn new() -> Self { + FakeSecretClientXml(FakePipeline::new( + r" + + secret_password + hunter2 + ", + )) + } + + pub fn get_secret(&self) -> ResponseFuture { + self.0.send().xml() + } + } + + #[tokio::test] + pub async fn response_future_returns_json_model_when_awaited() -> Result<(), Error> { + let client = FakeSecretClient::new(); + assert!(!client.0.tracker.was_body_read()); + let response = client.get_secret().await?; + assert!(client.0.tracker.was_body_read()); + let secret = response.into_body(); + assert_eq!("secret_password", secret.name); + assert_eq!("hunter2", secret.value); + Ok(()) + } + + #[cfg(feature = "xml")] + #[tokio::test] + pub async fn response_future_returns_xml_model_when_awaited() -> Result<(), Error> { + let client = FakeSecretClientXml::new(); + assert!(!client.0.tracker.was_body_read()); + let response = client.get_secret().await?; + assert!(client.0.tracker.was_body_read()); + let secret = response.into_body(); + assert_eq!("secret_password", secret.name); + assert_eq!("hunter2", secret.value); + Ok(()) + } + + #[tokio::test] + pub async fn response_future_returns_json_model_lazily_when_lazy_called() -> Result<(), Error> { + let client = FakeSecretClient::new(); + assert!(!client.0.tracker.was_body_read()); + let response = client.get_secret().lazy().await?; + assert!(!client.0.tracker.was_body_read()); + let secret = response.into_body().await?; + assert!(client.0.tracker.was_body_read()); + assert_eq!("secret_password", secret.name); + assert_eq!("hunter2", secret.value); + Ok(()) + } + + #[cfg(feature = "xml")] + #[tokio::test] + pub async fn response_future_returns_xml_model_lazily_when_lazy_called() -> Result<(), Error> { + let client = FakeSecretClientXml::new(); + assert!(!client.0.tracker.was_body_read()); + let response = client.get_secret().lazy().await?; + assert!(!client.0.tracker.was_body_read()); + let secret = response.into_body().await?; + assert!(client.0.tracker.was_body_read()); + assert_eq!("secret_password", secret.name); + assert_eq!("hunter2", secret.value); + Ok(()) + } + + #[tokio::test] + pub async fn response_future_returns_raw_bytes_when_raw_called() -> Result<(), Error> { + let client = FakeSecretClient::new(); + assert!(!client.0.tracker.was_body_read()); + let response = client.get_secret().raw().await?; + assert!(!client.0.tracker.was_body_read()); + let body = response.into_body(); + let bytes = body.collect().await?; + assert!(client.0.tracker.was_body_read()); + assert_eq!( + br#"{"name":"secret_password","value":"hunter2"}"#, + bytes.as_ref() + ); + Ok(()) + } + + #[tokio::test] + pub async fn can_parse_custom_model_from_raw_bytes() -> Result<(), Error> { + #[derive(Deserialize)] + struct MySecret { + #[serde(rename = "name")] + my_name: String, + #[serde(rename = "value")] + my_value: String, + } + + let client = FakeSecretClient::new(); + assert!(!client.0.tracker.was_body_read()); + let response = client.get_secret().raw().await?; + assert!(!client.0.tracker.was_body_read()); + let body = response.into_body(); + let secret: MySecret = body.json().await?; + assert!(client.0.tracker.was_body_read()); + assert_eq!("secret_password", secret.my_name); + assert_eq!("hunter2", secret.my_value); + Ok(()) + } +} diff --git a/sdk/typespec/typespec_client_core/src/lib.rs b/sdk/typespec/typespec_client_core/src/lib.rs index 2bcefb3ceb..60ef6dc1c5 100644 --- a/sdk/typespec/typespec_client_core/src/lib.rs +++ b/sdk/typespec/typespec_client_core/src/lib.rs @@ -2,6 +2,13 @@ // Licensed under the MIT License. #![doc = include_str!("../README.md")] +// Docs.rs build is done with the nightly compiler, so we can enable nightly features in that build. +// In this case we enable two features: +// - `doc_auto_cfg`: Automatically scans `cfg` attributes and uses them to show those required configurations in the generated documentation. +// - `doc_cfg_hide`: Ignore the `doc` configuration for `doc_auto_cfg`. +// See https://doc.rust-lang.org/rustdoc/unstable-features.html#doc_auto_cfg-automatically-generate-doccfg for more details. +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg_hide))] #[macro_use] mod macros; @@ -20,6 +27,3 @@ pub mod xml; pub use crate::error::{Error, Result}; pub use uuid::Uuid; - -#[cfg(feature = "derive")] -pub use typespec_derive::Model; diff --git a/sdk/typespec/typespec_derive/src/lib.rs b/sdk/typespec/typespec_derive/src/lib.rs index 02112f0b2a..d194bed477 100644 --- a/sdk/typespec/typespec_derive/src/lib.rs +++ b/sdk/typespec/typespec_derive/src/lib.rs @@ -1,85 +1,2 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -use syn::{parse::ParseStream, parse_macro_input, spanned::Spanned, DeriveInput, Error, LitStr}; - -extern crate proc_macro; - -mod model; - -type Result = ::std::result::Result; - -// NOTE: Proc macros must appear in the root of the crate. Just re-exporting them with `pub use` is **not sufficient**. -// So, all the top-level entry functions for the proc macros will appear here, but they just call inner "impl" functions in the modules. - -/// Defines the function signature expected by run_derive_macro -type DeriveImpl = fn(DeriveInput) -> Result; - -/// Runs the provided derive macro implementation, automatically generating errors if it returns errors. -fn run_derive_macro(input: proc_macro::TokenStream, imp: DeriveImpl) -> proc_macro::TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - match imp(ast) { - Ok(tokens) => tokens.into(), - Err(e) => e.to_compile_error().into(), - } -} - -/// Parses a `syn::parse::ParseStream` that is expected to contain a string literal and extracts the `syn::LitStr`. -fn parse_literal_string(value: ParseStream) -> Result { - let expr: syn::Expr = value - .parse() - .map_err(|_| Error::new(value.span(), "expected string literal"))?; - match expr { - syn::Expr::Lit(lit) => match lit.lit { - syn::Lit::Str(s) => Ok(s), - _ => Err(Error::new(lit.span(), "expected string literal")), - }, - _ => Err(Error::new(expr.span(), "expected string literal")), - } -} - -/// Derive macro for implementing `Model` trait. -/// -/// Deriving this trait allows a type to be deserialized from an HTTP response body. -/// By default, the type must also implement `serde::Deserialize`, or the generated code will not compile. -/// -/// ## Attributes -/// -/// The following attributes are supported on the struct itself: -/// -/// ### `#[typespec(format)]` -/// -/// The format attribute specifies the format of the response body. The default is `json`. -/// If compiling with the `xml` feature, the value `xml` is also supported. -/// -/// ```rust -/// # use typespec_derive::Model; -/// # use serde::Deserialize; -/// #[derive(Model, Deserialize)] -/// #[typespec(format = "xml")] -/// struct MyModel { -/// value: String -/// } -/// ``` -/// -/// **NOTE:** Using formats other than JSON may require enabling additional features in `typespec_client_core`. -/// -/// ### `#[typespec(crate)]` -/// -/// The 'crate' attribute specifies an alternate module path, other than the default of `typespec_client_core`, to reference the typespec client crate. -/// -/// ```rust -/// # use typespec_derive::Model; -/// # use serde::Deserialize; -/// extern crate typespec_client_core as my_typespec; -/// -/// #[derive(Model, Deserialize)] -/// #[typespec(crate = "my_typespec")] -/// struct MyModel { -/// value: String -/// } -/// ``` -#[proc_macro_derive(Model, attributes(typespec))] -pub fn derive_model(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - run_derive_macro(input, model::derive_model_impl) -} diff --git a/sdk/typespec/typespec_derive/src/model.rs b/sdk/typespec/typespec_derive/src/model.rs deleted file mode 100644 index 81362f892e..0000000000 --- a/sdk/typespec/typespec_derive/src/model.rs +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -use proc_macro2::TokenStream; -use syn::spanned::Spanned; -use syn::{Attribute, DeriveInput, Error, Meta, Path}; - -use crate::{parse_literal_string, Result}; - -pub fn derive_model_impl(ast: DeriveInput) -> Result { - let body = generate_body(ast)?; - - // We wrap the generated code in a const block to give it a unique scope. - let gen = quote::quote! { - #[doc(hidden)] - const _: () = { - #body - }; - }; - Ok(gen) -} - -fn generate_body(ast: DeriveInput) -> Result { - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); - let name = &ast.ident; - - // Parse attributes - let attrs = Attrs::from_attrs(&ast.attrs)?; - - let format = attrs.format.unwrap_or(Format::Json); - let deserialize_body = match format { - Format::Json => quote::quote! { - body.json().await - }, - Format::Xml => quote::quote! { - body.xml().await - }, - }; - - // If the standard path is used, we need to add 'extern crate', because it's possible the calling code - // depends on typespec_client_core transitively, which means it's not in scope by default. - // That's not necessary when using a custom path because we assume the user has done that work. - let typespec_import = match attrs.typespec_path { - Some(path) => quote::quote! { - use #path as _typespec_client_core; - }, - None => quote::quote! { - #[allow(unused_extern_crates, clippy::useless_attribute)] - extern crate typespec_client_core as _typespec_client_core; - }, - }; - - Ok(quote::quote! { - #typespec_import - - #[automatically_derived] - impl #impl_generics _typespec_client_core::http::Model for #name #ty_generics #where_clause { - async fn from_response_body(body: _typespec_client_core::http::response::ResponseBody) -> _typespec_client_core::Result { - #deserialize_body - } - } - }) -} - -enum Format { - Json, - Xml, -} - -struct Attrs { - pub typespec_path: Option, - pub format: Option, -} - -impl Attrs { - pub fn from_attrs(attributes: &[Attribute]) -> Result { - let mut attrs = Attrs { - typespec_path: None, - format: None, - }; - - let mut result = Ok(()); - for attribute in attributes.iter().filter(|a| a.path().is_ident("typespec")) { - result = match (result, parse_attr(attribute, &mut attrs)) { - (Ok(()), Err(e)) => Err(e), - (Err(mut e1), Err(e2)) => { - e1.combine(e2); - Err(e1) - } - (e, Ok(())) => e, - }; - } - - result.map(|_| attrs) - } -} - -const INVALID_TYPESPEC_ATTRIBUTE_MESSAGE: &str = - "invalid typespec attribute, expected attribute in form #[typespec(key = value)]"; - -fn parse_attr(attribute: &Attribute, attrs: &mut Attrs) -> Result<()> { - let Meta::List(meta_list) = &attribute.meta else { - return Err(Error::new( - attribute.span(), - INVALID_TYPESPEC_ATTRIBUTE_MESSAGE, - )); - }; - - meta_list.parse_nested_meta(|meta| { - let ident = meta - .path - .get_ident() - .ok_or_else(|| Error::new(attribute.span(), INVALID_TYPESPEC_ATTRIBUTE_MESSAGE))?; - let value = meta - .value() - .map_err(|_| Error::new(attribute.span(), INVALID_TYPESPEC_ATTRIBUTE_MESSAGE))?; - - match ident.to_string().as_str() { - "crate" => { - let lit = parse_literal_string(value)?; - let path = lit.parse().map_err(|_| { - Error::new(lit.span(), format!("invalid module path: {}", lit.value())) - })?; - attrs.typespec_path = Some(path); - Ok(()) - } - "format" => { - let lit = parse_literal_string(value)?; - attrs.format = Some(match lit.value().as_str() { - "json" => Format::Json, - "xml" => Format::Xml, - x => { - return Err(Error::new(lit.span(), format!("Unknown format '{}'", x))); - } - }); - Ok(()) - } - x => Err(Error::new( - meta.path.span(), - format!("unknown typespec attribute '#[typespec({})'", x), - )), - } - }) -} diff --git a/sdk/typespec/typespec_derive/tests/compilation-tests.rs b/sdk/typespec/typespec_derive/tests/compilation-tests.rs deleted file mode 100644 index b5ff7af5f7..0000000000 --- a/sdk/typespec/typespec_derive/tests/compilation-tests.rs +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -use std::{collections::HashMap, io::BufReader, path::PathBuf, process::Stdio}; - -use cargo_metadata::{diagnostic::DiagnosticLevel, Message}; -use serde::Serialize; - -#[derive(Serialize)] -struct MessageExpectation { - pub level: DiagnosticLevel, - pub code: Option, - pub message: Option, - pub spans: Vec, -} - -#[derive(Serialize)] -struct MessageSpan { - pub file_name: String, - pub line: usize, // We only check the line number of the span because other properties (like highlight span) can vary by compiler version. -} - -#[derive(Serialize)] -#[serde(transparent)] -struct FileResult { - pub messages: Vec, -} - -#[test] -pub fn compilation_tests() { - let test_root = { - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.push("tests"); - p.push("data"); - p.push("compilation-tests"); - p - }; - let repo_root = { - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); // [root]/sdk/typespec/typespec_derive - p.pop(); // [root]/sdk/typespec - p.pop(); // [root]/sdk - p.pop(); // [root] - p - }; - - // Probably save to assume cargo is on the path, but if that's not the case, tests will start failing and we'll figure it out. - let mut compilation = std::process::Command::new("cargo") - .arg("build") - .arg("--message-format") - .arg("json") - .current_dir(test_root.clone()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - // An error here does not mean a non-zero exit code, it means a failure to run the process. - .expect("failed to execute compilation"); - let reader = BufReader::new(compilation.stdout.take().unwrap()); - let messages = Message::parse_stream(reader); - let file_messages = messages - .filter_map(|m| match m.expect("failed to parse message") { - Message::CompilerMessage(m) => Some(m), - _ => None, - }) - // Group by primary_span's src path - .fold(HashMap::new(), |mut map, msg| { - let Some(primary_span) = msg.message.spans.iter().find(|s| s.is_primary) else { - // No primary span, don't add this to the map. - return map; - }; - - // Convert the spans - let spans: Vec = msg - .message - .spans - .iter() - .map(|span| { - let mut span_file_name = PathBuf::from(&span.file_name); - if span_file_name.is_relative() { - span_file_name = test_root.join(span_file_name) - } - assert!(span_file_name.is_absolute()); - - #[allow(clippy::expect_fun_call)] - let relative_span_path = span_file_name - .strip_prefix(&repo_root) - .expect(&format!( - "span path {} is not relative to test_root {:?}", - &span.file_name, &repo_root - )) - .to_path_buf(); - - MessageSpan { - file_name: relative_span_path - .to_str() - .expect("failed to convert span path to string") - .into(), - line: span.line_start, - } - }) - .collect(); - - let expectation = MessageExpectation { - code: msg.message.code.clone().map(|c| c.code), - level: msg.message.level, - message: match msg.message.code { - // If there's a 'code', the message comes from rustc (not our macro). - // In that case, clear the 'rendered' and 'message' properties, they can be volatile from compiler version to compiler version - Some(_) => None, - None => Some(msg.message.message), - }, - spans, - }; - - map.entry(primary_span.file_name.clone()) - .or_insert_with(|| FileResult { - messages: Vec::new(), - }) - .messages - .push(expectation); - map - }); - - // Now, for each group, generate/validate baselines depending on the env var AZSDK_GENERATE_BASELINES - let generate_baselines = std::env::var("AZSDK_GENERATE_BASELINES") - .map(|b| b.as_str() == "1") - .unwrap_or(false); - - let mut errors = String::new(); - for (src_path, messages) in file_messages.iter() { - let baseline_path = { - let mut p = test_root.clone(); - p.push(src_path); - p.set_extension("expected.json"); - p - }; - - let actual_path = { - let mut p = test_root.clone(); - p.push(src_path); - p.set_extension("actual.json"); - p - }; - - let serialized = serde_json::to_string_pretty(&messages) - .expect("failed to serialize message") - .trim() - .to_string(); - - if generate_baselines { - std::fs::write(&baseline_path, serialized).expect("failed to write baseline"); - } else { - assert!(baseline_path.exists()); - - // Write the actual file - std::fs::write(&actual_path, serialized.clone()).expect("failed to write actual"); - - // Read the baseline file - let baseline = - String::from_utf8(std::fs::read(&baseline_path).expect("failed to read baseline")) - .expect("invalid baseline file") - .trim() - .to_string(); - if baseline != serialized { - let diff_command = format!("diff {:?} {:?}", baseline_path, actual_path); - errors.push_str(&format!( - "=== {} does not match baseline ===\nRun `{}` to compare.\n\nActual Payload:\n{}\n===\n", - src_path, diff_command, serialized - )); - } - } - } - - if !errors.is_empty() { - panic!("{}", errors); - } -} diff --git a/sdk/typespec/typespec_derive/tests/data/compilation-tests/Cargo.toml b/sdk/typespec/typespec_derive/tests/data/compilation-tests/Cargo.toml deleted file mode 100644 index 61b6ec47ae..0000000000 --- a/sdk/typespec/typespec_derive/tests/data/compilation-tests/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[workspace] -# This package is INTENTIONALLY omitted from the workspace because it contains files that do not compile (as part of testing compilation error reporting) -# So we create a workspace node to tell cargo this is the root of a workspace - -[package] -name = "compilation_tests" -version = "0.1.0" -edition = "2021" -publish = false - -[dependencies] -typespec_client_core = { path = "../../typespec_client_core", features = ["derive"] } -serde = { version = "1.0", features = ["derive"] } diff --git a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/.gitignore b/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/.gitignore deleted file mode 100644 index c70d9603b7..0000000000 --- a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Compilation test actual output, which should not be checked in. -*.actual.json diff --git a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/lib.rs b/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/lib.rs deleted file mode 100644 index ee2d47aec7..0000000000 --- a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -mod model; diff --git a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/bad_attributes.expected.json b/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/bad_attributes.expected.json deleted file mode 100644 index 3883d41756..0000000000 --- a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/bad_attributes.expected.json +++ /dev/null @@ -1,90 +0,0 @@ -[ - { - "level": "error", - "code": null, - "message": "invalid typespec attribute, expected attribute in form #[typespec(key = value)]", - "spans": [ - { - "file_name": "sdk/typespec/typespec_derive/compilation-tests/src/model/bad_attributes.rs", - "line": 8 - } - ] - }, - { - "level": "error", - "code": null, - "message": "expected string literal", - "spans": [ - { - "file_name": "sdk/typespec/typespec_derive/compilation-tests/src/model/bad_attributes.rs", - "line": 12 - } - ] - }, - { - "level": "error", - "code": null, - "message": "expected string literal", - "spans": [ - { - "file_name": "sdk/typespec/typespec_derive/compilation-tests/src/model/bad_attributes.rs", - "line": 16 - } - ] - }, - { - "level": "error", - "code": null, - "message": "expected string literal", - "spans": [ - { - "file_name": "sdk/typespec/typespec_derive/compilation-tests/src/model/bad_attributes.rs", - "line": 20 - } - ] - }, - { - "level": "error", - "code": null, - "message": "invalid module path: a b c", - "spans": [ - { - "file_name": "sdk/typespec/typespec_derive/compilation-tests/src/model/bad_attributes.rs", - "line": 24 - } - ] - }, - { - "level": "error", - "code": null, - "message": "expected string literal", - "spans": [ - { - "file_name": "sdk/typespec/typespec_derive/compilation-tests/src/model/bad_attributes.rs", - "line": 28 - } - ] - }, - { - "level": "error", - "code": null, - "message": "invalid typespec attribute, expected attribute in form #[typespec(key = value)]", - "spans": [ - { - "file_name": "sdk/typespec/typespec_derive/compilation-tests/src/model/bad_attributes.rs", - "line": 32 - } - ] - }, - { - "level": "error", - "code": null, - "message": "invalid typespec attribute, expected attribute in form #[typespec(key = value)]", - "spans": [ - { - "file_name": "sdk/typespec/typespec_derive/compilation-tests/src/model/bad_attributes.rs", - "line": 36 - } - ] - } -] diff --git a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/bad_attributes.rs b/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/bad_attributes.rs deleted file mode 100644 index fd8c6ba4d6..0000000000 --- a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/bad_attributes.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -use serde::Deserialize; -use typespec_client_core::http::Model; - -#[derive(Model, Deserialize)] -#[typespec(foobar)] -pub struct NotAValidAttribute {} - -#[derive(Model, Deserialize)] -#[typespec(crate = 42)] -pub struct NumericLiteralCrate {} - -#[derive(Model, Deserialize)] -#[typespec(crate = "a" + "b")] -pub struct BinExprCrate {} - -#[derive(Model, Deserialize)] -#[typespec(crate = @)] -pub struct UnexpectedTokenCrate {} - -#[derive(Model, Deserialize)] -#[typespec(crate = "a b c")] -pub struct InvalidPathOnCrate {} - -#[derive(Model, Deserialize)] -#[typespec(format = 42)] -pub struct NotAStringLiteralOnFormat {} - -#[derive(Model, Deserialize)] -#[typespec(format("json"))] -pub struct IncorrectAttributeFormat {} - -#[derive(Model, Deserialize)] -#[typespec = "whoop"] -pub struct NotAMetaListAttribute {} diff --git a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/mod.rs b/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/mod.rs deleted file mode 100644 index 33cc9ece30..0000000000 --- a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod bad_attributes; -mod not_derive_deserialize; -mod unknown_format; diff --git a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/not_derive_deserialize.expected.json b/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/not_derive_deserialize.expected.json deleted file mode 100644 index 2aead6384e..0000000000 --- a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/not_derive_deserialize.expected.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "level": "error", - "code": "E0277", - "message": null, - "spans": [ - { - "file_name": "sdk/typespec/typespec_derive/compilation-tests/src/model/not_derive_deserialize.rs", - "line": 6 - } - ] - } -] \ No newline at end of file diff --git a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/not_derive_deserialize.rs b/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/not_derive_deserialize.rs deleted file mode 100644 index a9778a90ab..0000000000 --- a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/not_derive_deserialize.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -use typespec_client_core::http::Model; - -#[derive(Model)] -pub struct MyModel {} diff --git a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/unknown_format.expected.json b/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/unknown_format.expected.json deleted file mode 100644 index 63e465fd4d..0000000000 --- a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/unknown_format.expected.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "level": "error", - "code": null, - "message": "Unknown format 'foobar'", - "spans": [ - { - "file_name": "sdk/typespec/typespec_derive/compilation-tests/src/model/unknown_format.rs", - "line": 8 - } - ] - } -] diff --git a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/unknown_format.rs b/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/unknown_format.rs deleted file mode 100644 index 554077fd26..0000000000 --- a/sdk/typespec/typespec_derive/tests/data/compilation-tests/src/model/unknown_format.rs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -use serde::Deserialize; -use typespec_client_core::http::Model; - -#[derive(Model, Deserialize)] -#[typespec(format = "foobar")] -pub struct MyModel {}