diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 422a604..bf6d296 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -116,24 +116,36 @@ jobs: matrix: include: - triple: x86_64-unknown-linux-gnu + features: --all-features os: ubuntu-latest - triple: aarch64-unknown-linux-gnu + features: --all-features os: ubuntu-latest - triple: armv7-unknown-linux-gnueabihf + features: --all-features os: ubuntu-latest - triple: arm-unknown-linux-gnueabihf + features: --all-features os: ubuntu-latest - triple: riscv64gc-unknown-linux-gnu + features: --all-features + os: ubuntu-latest + - triple: wasm32-unknown-unknown + features: --no-default-features --features=async-http-client os: ubuntu-latest - triple: x86_64-apple-darwin + features: --all-features os: macos-latest - triple: aarch64-apple-darwin + features: --all-features os: macos-latest - triple: x86_64-pc-windows-msvc + features: --all-features os: windows-latest - triple: aarch64-pc-windows-msvc + features: --all-features os: windows-latest env: RUSTFLAGS: --deny warnings @@ -156,4 +168,4 @@ jobs: key: release-${{ matrix.triple }}-${{ steps.rust.outputs.cachekey }}-${{ hashFiles('**/Cargo.*') }} path: target/ - - run: ${{ runner.os == 'Linux' && 'cross' || 'cargo' }} build --release --offline --all-features --target ${{ matrix.triple }} + - run: ${{ runner.os == 'Linux' && 'cross' || 'cargo' }} build --release --offline ${{ matrix.features }} --target ${{ matrix.triple }} diff --git a/Cargo.toml b/Cargo.toml index 8de5ab2..93a3242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,13 +96,19 @@ default-features = false features = ["client"] optional = true -[dependencies.reqwest] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.reqwest] version = "0.12" default-features = false features = ["multipart", "stream", "rustls-tls"] optional = true -[dependencies.tokio] +[target.'cfg(target_arch = "wasm32")'.dependencies.reqwest] +version = "0.12" +default-features = false +features = ["multipart", "stream"] +optional = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "1" features = ["fs"] optional = true diff --git a/src/client_reqwest.rs b/src/client_reqwest.rs index 375359f..59fa97e 100644 --- a/src/client_reqwest.rs +++ b/src/client_reqwest.rs @@ -1,11 +1,7 @@ use std::path::PathBuf; -use std::time::Duration; use async_trait::async_trait; use bon::Builder; -use reqwest::multipart; -use serde_json::Value; -use tokio::fs::File; use crate::trait_async::AsyncTelegramApi; use crate::Error; @@ -17,16 +13,21 @@ pub struct AsyncApi { #[builder(into)] pub api_url: String, - #[builder( - default = reqwest::ClientBuilder::new() - .connect_timeout(Duration::from_secs(10)) - .timeout(Duration::from_secs(500)) - .build() - .unwrap() - )] + #[builder(default = default_client())] pub client: reqwest::Client, } +fn default_client() -> reqwest::Client { + let client_builder = reqwest::ClientBuilder::new(); + + #[cfg(not(target_arch = "wasm32"))] + let client_builder = client_builder + .connect_timeout(std::time::Duration::from_secs(10)) + .timeout(std::time::Duration::from_secs(500)); + + client_builder.build().unwrap() +} + impl AsyncApi { /// Create a new `AsyncApi`. You can use [`AsyncApi::new_url`] or [`AsyncApi::builder`] for more options. pub fn new(api_key: &str) -> Self { @@ -66,7 +67,9 @@ impl From for Error { } } -#[async_trait] +// Wasm target need not be `Send` because it is single-threaded +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl AsyncTelegramApi for AsyncApi { type Error = Error; @@ -102,44 +105,58 @@ impl AsyncTelegramApi for AsyncApi { Params: serde::ser::Serialize + std::fmt::Debug + std::marker::Send, Output: serde::de::DeserializeOwned, { - let json_string = crate::json::encode(¶ms)?; - let json_struct: Value = serde_json::from_str(&json_string).unwrap(); - let file_keys: Vec<&str> = files.iter().map(|(key, _)| *key).collect(); - let files_with_paths: Vec<(String, &str, String)> = files - .iter() - .map(|(key, path)| { - ( - (*key).to_string(), - path.to_str().unwrap(), - path.file_name().unwrap().to_str().unwrap().to_string(), - ) - }) - .collect(); - - let mut form = multipart::Form::new(); - for (key, val) in json_struct.as_object().unwrap() { - if !file_keys.contains(&key.as_str()) { - let val = match val { - Value::String(val) => val.to_string(), - other => other.to_string(), - }; - - form = form.text(key.clone(), val); + #[cfg(not(target_arch = "wasm32"))] + { + use reqwest::multipart; + use serde_json::Value; + + let json_string = crate::json::encode(¶ms)?; + let json_struct: Value = serde_json::from_str(&json_string).unwrap(); + + let file_keys: Vec<&str> = files.iter().map(|(key, _)| *key).collect(); + let files_with_paths: Vec<(String, &str, String)> = files + .iter() + .map(|(key, path)| { + ( + (*key).to_string(), + path.to_str().unwrap(), + path.file_name().unwrap().to_str().unwrap().to_string(), + ) + }) + .collect(); + + let mut form = multipart::Form::new(); + for (key, val) in json_struct.as_object().unwrap() { + if !file_keys.contains(&key.as_str()) { + let val = match val { + Value::String(val) => val.to_string(), + other => other.to_string(), + }; + + form = form.text(key.clone(), val); + } } - } - for (parameter_name, file_path, file_name) in files_with_paths { - let file = File::open(file_path) - .await - .map_err(|error| Error::Encode(error.to_string()))?; - let part = multipart::Part::stream(file).file_name(file_name); - form = form.part(parameter_name, part); - } + for (parameter_name, file_path, file_name) in files_with_paths { + let file = tokio::fs::File::open(file_path) + .await + .map_err(|error| Error::Encode(error.to_string()))?; + let part = multipart::Part::stream(file).file_name(file_name); + form = form.part(parameter_name, part); + } - let url = format!("{}/{method}", self.api_url); + let url = format!("{}/{method}", self.api_url); - let response = self.client.post(url).multipart(form).send().await?; - Self::decode_response(response).await + let response = self.client.post(url).multipart(form).send().await?; + Self::decode_response(response).await + } + + #[cfg(target_arch = "wasm32")] + { + Err(Error::Encode(format!( + "calling {method:?} with files is currently unsupported in WASM due to missing form_data / attachment support. Was called with params {params:?} and files {files:?}", + ))) + } } } diff --git a/src/trait_async.rs b/src/trait_async.rs index cf550fa..c1a0fa4 100644 --- a/src/trait_async.rs +++ b/src/trait_async.rs @@ -48,7 +48,9 @@ macro_rules! request_nb { } } -#[async_trait::async_trait] +// Wasm target need not be `Send` because it is single-threaded +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] pub trait AsyncTelegramApi where Self: Sync,