extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; use serde_yaml::Value; use syn::LitStr; use syn::parse_macro_input; #[proc_macro] pub fn ip(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as LitStr); let ip_str = input.value(); if ip_str.parse::().is_ok() { let expanded = quote! { std::net::IpAddr::V4(#ip_str.parse::().unwrap()) }; return TokenStream::from(expanded); } if ip_str.parse::().is_ok() { let expanded = quote! { std::net::IpAddr::V6(#ip_str.parse::().unwrap()) }; return TokenStream::from(expanded); } panic!("Invalid IP address: {}", ip_str); } #[proc_macro] pub fn ipv4(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as LitStr); let ip_str = input.value(); if ip_str.parse::().is_ok() { let expanded = quote! { #ip_str.parse::().unwrap() }; return TokenStream::from(expanded); } panic!("Invalid IPv4 address: {}", ip_str); } #[proc_macro] pub fn mac_address(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as LitStr); let mac_str = input.value(); match parse_mac_address(&mac_str) { Ok(bytes) => { let b0 = bytes[0]; let b1 = bytes[1]; let b2 = bytes[2]; let b3 = bytes[3]; let b4 = bytes[4]; let b5 = bytes[5]; quote! { harmony_types::net::MacAddress( [#b0, #b1, #b2, #b3, #b4, #b5] ) } .into() } Err(err) => syn::Error::new(input.span(), err).to_compile_error().into(), } } fn parse_mac_address(mac: &str) -> Result<[u8; 6], String> { let parts: Vec<&str> = mac.split(':').collect(); if parts.len() != 6 { return Err("MAC address must contain exactly six octets separated by colons".to_string()); } let mut bytes = [0u8; 6]; for (i, part) in parts.iter().enumerate() { match u8::from_str_radix(part, 16) { Ok(byte) => bytes[i] = byte, Err(_) => return Err(format!("Invalid MAC address octet: {}", part)), } } Ok(bytes) } /// Verify that input is valid yaml by trying to deserialize it using /// serde_yaml::from_str::(input) /// /// panics: If yaml is not valid #[proc_macro] pub fn yaml(input: TokenStream) -> TokenStream { // TODO, accept a second argument that is the type to be deserialized to and validate at // compile-time that deserialization is possible // // It does not seem to be doable with the way macros are designed : we may pass an ident to the // macro, but the macro only has access to the ident, not the type itself. // // I also tried to create a restricted version of this macro, but this time serde got in the // way : my use case is to make sure that the yaml I am given can deserialize strictly (with // deny_unknown_attributes option on) to k8s-openapi types. But the k8s-openapi types are not // annotated with deny_unknown_attributes and there does not seem to be a way to tell serde to // deserialize in "strict mode" when calling the deserialization function itself. I tried // wrapping the types in something like : // // ```rust // #[derive(Deserialize, Debug)] // #[serde(deny_unknown_fields)] // struct Strict(pub T); // ``` // // But it still does actually deserialize T strictly. I gave up for now at this point. Will // find a solution some day! let yaml = parse_macro_input!(input as LitStr); serde_yaml::from_str::(yaml.value().as_str()).expect("Should be valid yaml"); quote! { serde_yaml::from_str(#yaml) } .into() } /// Verify that a string is a valid(ish) ingress path /// Panics if path does not start with `/` #[proc_macro] pub fn ingress_path(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as LitStr); let path_str = input.value(); match path_str.starts_with("/") { true => { let expanded = quote! {(#path_str.to_string()) }; TokenStream::from(expanded) } false => panic!("Invalid ingress path"), } } #[proc_macro] pub fn cidrv4(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as LitStr); let cidr_str = input.value(); if cidr_str.parse::().is_ok() { let expanded = quote! { #cidr_str.parse::().unwrap() }; return TokenStream::from(expanded); } 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::hurl; /// /// let url = hurl!("https://example.com/path"); /// /// let expected_url = url::Url::parse("https://example.com/path").unwrap(); /// assert!(matches!(url, Url::Url(expected_url))); /// ``` /// /// The following example will fail to compile: /// /// ```rust,compile_fail /// use harmony_macros::hurl; /// /// // This is not a valid URL and will cause a compilation error. /// let _invalid = hurl!("not a valid url"); /// ``` #[proc_macro] pub fn hurl(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) }