From 61193556810afbd719e8dc2858705da468871c72 Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Fri, 5 Sep 2025 15:10:45 -0400 Subject: [PATCH] feat: add remote_url! and local_folder! macros to make Url easier to create --- Cargo.lock | 1 + examples/rust/src/main.rs | 8 ++--- harmony_macros/Cargo.toml | 1 + harmony_macros/src/lib.rs | 68 +++++++++++++++++++++++++++++++++++++++ harmony_types/src/net.rs | 34 ++++++++++++++++++++ 5 files changed, 108 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62d8aee..dd78084 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2389,6 +2389,7 @@ dependencies = [ "serde", "serde_yaml", "syn 2.0.105", + "url", ] [[package]] diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs index b361edd..457e144 100644 --- a/examples/rust/src/main.rs +++ b/examples/rust/src/main.rs @@ -13,25 +13,25 @@ use harmony::{ }, topology::K8sAnywhereTopology, }; -use harmony_types::net::Url; +use harmony_macros::remote_url; #[tokio::main] async fn main() { let application = Arc::new(RustWebapp { name: "harmony-example-rust-webapp".to_string(), - domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()), + domain: remote_url!("https://rustapp.harmony.example.com"), project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param framework: Some(RustWebFramework::Leptos), }); let discord_receiver = DiscordWebhook { name: "test-discord".to_string(), - url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()), + url: remote_url!("https://discord.doesnt.exist.com"), }; let webhook_receiver = WebhookReceiver { name: "sample-webhook-receiver".to_string(), - url: Url::Url(url::Url::parse("https://webhook-doesnt-exist.com").unwrap()), + url: remote_url!("https://webhook-doesnt-exist.com"), }; let app = ApplicationScore { diff --git a/harmony_macros/Cargo.toml b/harmony_macros/Cargo.toml index 7185d0b..aed8808 100644 --- a/harmony_macros/Cargo.toml +++ b/harmony_macros/Cargo.toml @@ -15,6 +15,7 @@ serde = "1.0.217" serde_yaml = "0.9.34" syn = "2.0.90" cidr.workspace = true +url.workspace = true [dev-dependencies] serde = { version = "1.0.217", features = ["derive"] } diff --git a/harmony_macros/src/lib.rs b/harmony_macros/src/lib.rs index 2f77d1a..adb99a9 100644 --- a/harmony_macros/src/lib.rs +++ b/harmony_macros/src/lib.rs @@ -145,3 +145,71 @@ pub fn cidrv4(input: TokenStream) -> TokenStream { panic!("Invalid IPv4 CIDR : {}", cidr_str); } + +/// Creates a `harmony_types::net::Url::Url` from a string literal. +/// +/// This macro parses the input string as a URL at compile time and will cause a +/// compilation error if the string is not a valid URL. +/// +/// # Example +/// +/// ``` +/// use harmony_types::net::Url; +/// use harmony_macros::remote_url; +/// +/// let remote_url = remote_url!("https://example.com/path"); +/// +/// let expected_url = url::Url::parse("https://example.com/path").unwrap(); +/// assert!(matches!(remote_url, Url::Url(expected_url))); +/// ``` +/// +/// The following example will fail to compile: +/// +/// ```rust,compile_fail +/// use harmony_macros::remote_url; +/// +/// // This is not a valid URL and will cause a compilation error. +/// let _invalid = remote_url!("not a valid url"); +/// ``` +#[proc_macro] +pub fn remote_url(input: TokenStream) -> TokenStream { + let input_lit = parse_macro_input!(input as LitStr); + let url_str = input_lit.value(); + + match ::url::Url::parse(&url_str) { + Ok(_) => { + let expanded = quote! { + ::harmony_types::net::Url::Url(::url::Url::parse(#input_lit).unwrap()) + }; + TokenStream::from(expanded) + } + Err(e) => { + let err_msg = format!("Invalid URL: {e}"); + syn::Error::new(input_lit.span(), err_msg) + .to_compile_error() + .into() + } + } +} + +/// Creates a `harmony_types::net::Url::LocalFolder` from a string literal. +/// +/// # Example +/// +/// ``` +/// use harmony_types::net::Url; +/// use harmony_macros::local_folder; +/// +/// let local_path = local_folder!("/var/data/files"); +/// +/// let expected_path = String::from("/var/data/files"); +/// assert!(matches!(local_path, Url::LocalFolder(expected_path))); +/// ``` +#[proc_macro] +pub fn local_folder(input: TokenStream) -> TokenStream { + let input_lit = parse_macro_input!(input as LitStr); + let expanded = quote! { + ::harmony_types::net::Url::LocalFolder(#input_lit.to_string()) + }; + TokenStream::from(expanded) +} diff --git a/harmony_types/src/net.rs b/harmony_types/src/net.rs index caf023f..801e297 100644 --- a/harmony_types/src/net.rs +++ b/harmony_types/src/net.rs @@ -51,6 +51,40 @@ impl TryFrom for MacAddress { pub type IpAddress = std::net::IpAddr; +/// Represents a URL, which can either be a remote URL or a local file path. +/// +/// For convenience, the `harmony_macros` crate provides `remote_url!` and `local_folder!` +/// macros to construct `Url` variants from string literals. +/// +/// # Examples +/// +/// ### Manual Construction +/// +/// The following example demonstrates how to build `Url` variants directly. This is +/// the standard approach if you are not using the `harmony_macros` crate. +/// +/// ``` +/// // The `use` statement below is for the doc test. In a real project, +/// // you would use `use harmony_types::Url;` +/// # use harmony_types::net::Url; +/// let remote_url = Url::Url(url::Url::parse("https://example.com").unwrap()); +/// let local_path = Url::LocalFolder("/var/data".to_string()); +/// +/// assert!(matches!(remote_url, Url::Url(_))); +/// assert!(matches!(local_path, Url::LocalFolder(_))); +/// ``` +/// +/// ### Usage with `harmony_macros` +/// +/// If `harmony_macros` is a dependency, you can create `Url`s more concisely. +/// +/// ```rust,ignore +/// use harmony_macros::{remote_url, local_folder}; +/// use harmony_types::Url; +/// +/// let remote_url = remote_url!("https://example.com"); +/// let local_path = local_folder!("/var/data"); +/// ``` #[derive(Debug, Clone)] pub enum Url { LocalFolder(String), -- 2.39.5