diff --git a/sdk/core/azure_core/CHANGELOG.md b/sdk/core/azure_core/CHANGELOG.md index 4e23dd4293..eebb7ce271 100644 --- a/sdk/core/azure_core/CHANGELOG.md +++ b/sdk/core/azure_core/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added `UrlExt::append_path()`. + ### Breaking Changes - Moved deserializers and serializers for optional base64-encoded bytes to `base64::option` module. `base64` module now deserializes or serializes non-optional fields congruent with the `time` module. diff --git a/sdk/core/azure_core/src/http/mod.rs b/sdk/core/azure_core/src/http/mod.rs index 035017810a..e4740f1b07 100644 --- a/sdk/core/azure_core/src/http/mod.rs +++ b/sdk/core/azure_core/src/http/mod.rs @@ -23,7 +23,7 @@ pub use response::{AsyncResponse, BufResponse, RawResponse, Response}; pub use typespec_client_core::http::response; pub use typespec_client_core::http::{ new_http_client, AppendToUrlQuery, Context, DeserializeWith, Format, HttpClient, JsonFormat, - Method, NoFormat, StatusCode, Url, + Method, NoFormat, StatusCode, Url, UrlExt, }; pub use crate::error::check_success; diff --git a/sdk/core/typespec_client_core/CHANGELOG.md b/sdk/core/typespec_client_core/CHANGELOG.md index cfc5d5df1d..6673f37cdb 100644 --- a/sdk/core/typespec_client_core/CHANGELOG.md +++ b/sdk/core/typespec_client_core/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added `UrlExt::append_path()`. + ### Breaking Changes - Moved deserializers and serializers for optional base64-encoded bytes to `base64::option` module. `base64` module now deserializes or serializes non-optional fields congruent with the `time` module. diff --git a/sdk/core/typespec_client_core/src/http/mod.rs b/sdk/core/typespec_client_core/src/http/mod.rs index 6f1d39d1a3..27f351b1d2 100644 --- a/sdk/core/typespec_client_core/src/http/mod.rs +++ b/sdk/core/typespec_client_core/src/http/mod.rs @@ -59,3 +59,153 @@ where } } } + +/// Extension trait for [`Url`] to provide additional URL manipulation methods. +pub trait UrlExt: crate::private::Sealed { + /// Appends a path segment to the URL's path, handling slashes appropriately and preserving query parameters. + /// + /// This always assumes the existing URL terminates with a directory, and the `path` you pass in is a separate directory or file segment. + /// + /// # Examples + /// + /// ``` + /// use typespec_client_core::http::{Url, UrlExt as _}; + /// + /// let mut url: Url = "https://contoso.com/foo?a=1".parse().unwrap(); + /// url.append_path("bar"); + /// assert_eq!(url.as_str(), "https://contoso.com/foo/bar?a=1"); + /// ``` + fn append_path(&mut self, path: impl AsRef); +} + +impl UrlExt for Url { + fn append_path(&mut self, p: impl AsRef) { + let path = p.as_ref().trim_start_matches('/'); + if self.path() == "/" { + self.set_path(path); + return; + } + if path.is_empty() { + return; + } + let needs_separator = !self.path().ends_with('/'); + let mut new_len = self.path().len() + path.len(); + if needs_separator { + new_len += 1; + } + let mut new_path = String::with_capacity(new_len); + debug_assert_eq!(new_path.capacity(), new_len); + new_path.push_str(self.path()); + if needs_separator { + new_path.push('/'); + } + new_path.push_str(path); + debug_assert_eq!(new_path.capacity(), new_len); + + self.set_path(&new_path); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn url_append_path() { + let mut url = Url::parse("https://www.microsoft.com?q=q").unwrap(); + url.append_path("foo"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo?q=q"); + + url = Url::parse("https://www.microsoft.com/?q=q").unwrap(); + url.append_path("foo"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo?q=q"); + + url = Url::parse("https://www.microsoft.com?q=q").unwrap(); + url.append_path("/foo"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo?q=q"); + + url = Url::parse("https://www.microsoft.com/?q=q").unwrap(); + url.append_path("/foo"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo?q=q"); + + url = Url::parse("https://www.microsoft.com?q=q").unwrap(); + url.append_path("foo/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/?q=q"); + + url = Url::parse("https://www.microsoft.com/?q=q").unwrap(); + url.append_path("foo/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/?q=q"); + + url = Url::parse("https://www.microsoft.com?q=q").unwrap(); + url.append_path("/foo/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/?q=q"); + + url = Url::parse("https://www.microsoft.com/?q=q").unwrap(); + url.append_path("/foo/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/?q=q"); + + url = Url::parse("https://www.microsoft.com/foo?q=q").unwrap(); + url.append_path("bar"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/bar?q=q"); + + url = Url::parse("https://www.microsoft.com/foo/?q=q").unwrap(); + url.append_path("bar"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/bar?q=q"); + + url = Url::parse("https://www.microsoft.com/foo?q=q").unwrap(); + url.append_path("/bar"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/bar?q=q"); + + url = Url::parse("https://www.microsoft.com/foo/?q=q").unwrap(); + url.append_path("/bar"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/bar?q=q"); + + url = Url::parse("https://www.microsoft.com/foo?q=q").unwrap(); + url.append_path("bar/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/bar/?q=q"); + + url = Url::parse("https://www.microsoft.com/foo/?q=q").unwrap(); + url.append_path("bar/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/bar/?q=q"); + + url = Url::parse("https://www.microsoft.com/foo?q=q").unwrap(); + url.append_path("/bar/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/bar/?q=q"); + + url = Url::parse("https://www.microsoft.com/foo/?q=q").unwrap(); + url.append_path("/bar/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/bar/?q=q"); + + url = Url::parse("https://www.microsoft.com?q=q").unwrap(); + url.append_path("/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/?q=q"); + + url = Url::parse("https://www.microsoft.com/?q=q").unwrap(); + url.append_path("/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/?q=q"); + + url = Url::parse("https://www.microsoft.com?q=q").unwrap(); + url.append_path(""); + assert_eq!(url.as_str(), "https://www.microsoft.com/?q=q"); + + url = Url::parse("https://www.microsoft.com?q=q").unwrap(); + url.append_path(""); + assert_eq!(url.as_str(), "https://www.microsoft.com/?q=q"); + + url = Url::parse("https://www.microsoft.com/foo?q=q").unwrap(); + url.append_path("/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo?q=q"); + + url = Url::parse("https://www.microsoft.com/foo/?q=q").unwrap(); + url.append_path("/"); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/?q=q"); + + url = Url::parse("https://www.microsoft.com/foo?q=q").unwrap(); + url.append_path(""); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo?q=q"); + + url = Url::parse("https://www.microsoft.com/foo/?q=q").unwrap(); + url.append_path(""); + assert_eq!(url.as_str(), "https://www.microsoft.com/foo/?q=q"); + } +} diff --git a/sdk/core/typespec_client_core/src/lib.rs b/sdk/core/typespec_client_core/src/lib.rs index ab087ea5a6..2b43b46787 100644 --- a/sdk/core/typespec_client_core/src/lib.rs +++ b/sdk/core/typespec_client_core/src/lib.rs @@ -26,3 +26,8 @@ pub use typespec::Bytes; pub use uuid::Uuid; pub use sleep::sleep; + +mod private { + pub trait Sealed {} + impl Sealed for crate::http::Url {} +}