From f925c6caf07fd2961e7c484c7b6a70bfa35fd94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 27 May 2025 11:30:13 +0200 Subject: [PATCH] QoL: RequestPath trait for http-api-client (#5788) * qol: RequestPath trait for http-api-client * additional test case * applied the change to other trait methods --- common/http-api-client/src/lib.rs | 116 +++++++++++++++++------------ common/http-api-client/src/path.rs | 59 +++++++++++++++ nym-wallet/Cargo.lock | 43 +++++++++++ 3 files changed, 170 insertions(+), 48 deletions(-) create mode 100644 common/http-api-client/src/path.rs diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index f1068bb562..50930463da 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -138,7 +138,12 @@ pub use reqwest::{IntoUrl, StatusCode}; +use crate::path::RequestPath; use async_trait::async_trait; +use bytes::Bytes; +use http::header::CONTENT_TYPE; +use http::HeaderMap; +use mime::Mime; use reqwest::header::HeaderValue; use reqwest::{RequestBuilder, Response}; use serde::de::DeserializeOwned; @@ -149,10 +154,6 @@ use thiserror::Error; use tracing::{debug, instrument, warn}; use url::Url; -use bytes::Bytes; -use http::header::CONTENT_TYPE; -use http::HeaderMap; -use mime::Mime; #[cfg(not(target_arch = "wasm32"))] use std::net::SocketAddr; #[cfg(not(target_arch = "wasm32"))] @@ -163,6 +164,8 @@ pub use user_agent::UserAgent; #[cfg(not(target_arch = "wasm32"))] mod dns; +mod path; + #[cfg(not(target_arch = "wasm32"))] pub use dns::{HickoryDnsError, HickoryDnsResolver}; @@ -454,14 +457,15 @@ impl Client { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait ApiClientCore { /// Create an HTTP request using the host configured in this client. - fn create_request( + fn create_request( &self, method: reqwest::Method, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: Option<&B>, ) -> RequestBuilder where + P: RequestPath, B: Serialize + ?Sized, K: AsRef, V: AsRef; @@ -512,7 +516,7 @@ pub trait ApiClientCore { }; let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect(); - self.create_request(method, &path, ¶ms, json_body) + self.create_request(method, path.as_slice(), ¶ms, json_body) } /// Send a created HTTP request. @@ -525,14 +529,15 @@ pub trait ApiClientCore { E: Display; /// Create and send a created HTTP request. - async fn send_request( + async fn send_request( &self, method: reqwest::Method, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: Option<&B>, ) -> Result> where + P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, @@ -547,14 +552,15 @@ pub trait ApiClientCore { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl ApiClientCore for Client { #[instrument(level = "debug", skip_all, fields(path=?path))] - fn create_request( + fn create_request( &self, method: reqwest::Method, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: Option<&B>, ) -> RequestBuilder where + P: RequestPath, B: Serialize + ?Sized, K: AsRef, V: AsRef, @@ -597,12 +603,9 @@ impl ApiClientCore for Client { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait ApiClient: ApiClientCore { /// Create an HTTP GET Request with the provided path and parameters - fn create_get_request( - &self, - path: PathSegments<'_>, - params: Params<'_, K, V>, - ) -> RequestBuilder + fn create_get_request(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder where + P: RequestPath, K: AsRef, V: AsRef, { @@ -610,13 +613,14 @@ pub trait ApiClient: ApiClientCore { } /// Create an HTTP POST Request with the provided path, parameters, and json body - fn create_post_request( + fn create_post_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> RequestBuilder where + P: RequestPath, B: Serialize + ?Sized, K: AsRef, V: AsRef, @@ -625,12 +629,9 @@ pub trait ApiClient: ApiClientCore { } /// Create an HTTP DELETE Request with the provided path and parameters - fn create_delete_request( - &self, - path: PathSegments<'_>, - params: Params<'_, K, V>, - ) -> RequestBuilder + fn create_delete_request(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder where + P: RequestPath, K: AsRef, V: AsRef, { @@ -638,13 +639,14 @@ pub trait ApiClient: ApiClientCore { } /// Create an HTTP PATCH Request with the provided path, parameters, and json body - fn create_patch_request( + fn create_patch_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> RequestBuilder where + P: RequestPath, B: Serialize + ?Sized, K: AsRef, V: AsRef, @@ -654,12 +656,13 @@ pub trait ApiClient: ApiClientCore { /// Create and send an HTTP GET Request with the provided path and parameters #[instrument(level = "debug", skip_all, fields(path=?path))] - async fn send_get_request( + async fn send_get_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, ) -> Result> where + P: RequestPath + Send + Sync, K: AsRef + Sync, V: AsRef + Sync, E: Display, @@ -669,13 +672,14 @@ pub trait ApiClient: ApiClientCore { } /// Create and send an HTTP POST Request with the provided path, parameters, and json data - async fn send_post_request( + async fn send_post_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> Result> where + P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, @@ -686,12 +690,13 @@ pub trait ApiClient: ApiClientCore { } /// Create and send an HTTP DELETE Request with the provided path and parameters - async fn send_delete_request( + async fn send_delete_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, ) -> Result> where + P: RequestPath + Send + Sync, K: AsRef + Sync, V: AsRef + Sync, E: Display, @@ -701,13 +706,14 @@ pub trait ApiClient: ApiClientCore { } /// Create and send an HTTP PATCH Request with the provided path, parameters, and json data - async fn send_patch_request( + async fn send_patch_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> Result> where + P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, @@ -722,12 +728,13 @@ pub trait ApiClient: ApiClientCore { /// into the provided type `T`. #[instrument(level = "debug", skip_all, fields(path=?path))] // TODO: deprecate in favour of get_response that works based on mime type in the response - async fn get_json( + async fn get_json( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, ) -> Result> where + P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, @@ -739,12 +746,13 @@ pub trait ApiClient: ApiClientCore { /// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T` based on the content type header - async fn get_response( + async fn get_response( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, ) -> Result> where + P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, @@ -759,13 +767,14 @@ pub trait ApiClient: ApiClientCore { /// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T`. - async fn post_json( + async fn post_json( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> Result> where + P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, @@ -781,12 +790,13 @@ pub trait ApiClient: ApiClientCore { /// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with /// tuple defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the /// response into the provided type `T`. - async fn delete_json( + async fn delete_json( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, ) -> Result> where + P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, @@ -801,13 +811,14 @@ pub trait ApiClient: ApiClientCore { /// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T`. - async fn patch_json( + async fn patch_json( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> Result> where + P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, @@ -890,7 +901,7 @@ impl ApiClient for C where C: ApiClientCore + Sync {} /// utility function that should solve the double slash problem in API urls forever. fn sanitize_url, V: AsRef>( base: &Url, - segments: PathSegments<'_>, + request_path: impl RequestPath, params: Params<'_, K, V>, ) -> Url { let mut url = base.clone(); @@ -900,10 +911,7 @@ fn sanitize_url, V: AsRef>( path_segments.pop_if_empty(); - for segment in segments { - let segment = segment.strip_prefix('/').unwrap_or(segment); - let segment = segment.strip_suffix('/').unwrap_or(segment); - + for segment in request_path.to_sanitized_segments() { path_segments.push(segment); } @@ -1048,6 +1056,18 @@ mod tests { fn sanitizing_urls() { let base_url: Url = "http://foomp.com".parse().unwrap(); + // works with a full string + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, "/foo//bar/", NO_PARAMS).as_str() + ); + + // (and leading slash doesn't matter) + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, "foo//bar/", NO_PARAMS).as_str() + ); + // works with 1 segment assert_eq!( "http://foomp.com/foo", diff --git a/common/http-api-client/src/path.rs b/common/http-api-client/src/path.rs new file mode 100644 index 0000000000..b7aeea5e47 --- /dev/null +++ b/common/http-api-client/src/path.rs @@ -0,0 +1,59 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Debug; + +/// Collection of URL Path Segments +pub type PathSegments<'a> = &'a [&'a str]; + +fn sanitize_fragment(segment: &str) -> &str { + segment.trim_matches(|c: char| c.is_whitespace() || c == '/') +} + +pub trait RequestPath: Debug { + fn to_sanitized_segments(&self) -> Vec<&str>; +} + +macro_rules! impl_stringified_sanitized_segments { + ($frag_iter:expr) => {{ + let mut path_segments = Vec::new(); + + for segment in $frag_iter { + if !segment.is_empty() { + path_segments.push(sanitize_fragment(segment)); + } + } + + path_segments + }}; +} + +impl RequestPath for PathSegments<'_> { + fn to_sanitized_segments(&self) -> Vec<&str> { + impl_stringified_sanitized_segments!(self.iter()) + } +} + +impl RequestPath for &[&str; N] { + fn to_sanitized_segments(&self) -> Vec<&str> { + impl_stringified_sanitized_segments!(self.iter()) + } +} + +impl RequestPath for &str { + fn to_sanitized_segments(&self) -> Vec<&str> { + impl_stringified_sanitized_segments!(self.split('/')) + } +} + +impl RequestPath for String { + fn to_sanitized_segments(&self) -> Vec<&str> { + impl_stringified_sanitized_segments!(self.split('/')) + } +} + +impl RequestPath for &String { + fn to_sanitized_segments(&self) -> Vec<&str> { + impl_stringified_sanitized_segments!(self.split('/')) + } +} diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 211d475f05..0cdfe77705 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -415,11 +415,14 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" dependencies = [ + "brotli", "flate2", "futures-core", "memchr", "pin-project-lite", "tokio", + "zstd", + "zstd-safe", ] [[package]] @@ -940,6 +943,8 @@ version = "1.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -3529,6 +3534,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.3.1" @@ -9270,6 +9285,34 @@ dependencies = [ "memchr", ] +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "5.4.0"