forked from NationTech/harmony
* it was named `hurl!` instead of just `url!` because it was clashing with the crate `url` so we would have been forced to use it with `harmony_macros::url!` which is less sexy Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/135
216 lines
6.6 KiB
Rust
216 lines
6.6 KiB
Rust
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::<std::net::Ipv4Addr>().is_ok() {
|
|
let expanded =
|
|
quote! { std::net::IpAddr::V4(#ip_str.parse::<std::net::Ipv4Addr>().unwrap()) };
|
|
return TokenStream::from(expanded);
|
|
}
|
|
|
|
if ip_str.parse::<std::net::Ipv6Addr>().is_ok() {
|
|
let expanded =
|
|
quote! { std::net::IpAddr::V6(#ip_str.parse::<std::net::Ipv6Addr>().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::<std::net::Ipv4Addr>().is_ok() {
|
|
let expanded = quote! { #ip_str.parse::<std::net::Ipv4Addr>().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::<serde_yaml::value::Value>(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<T>(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::<Value>(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::<cidr::Ipv4Cidr>().is_ok() {
|
|
let expanded = quote! { #cidr_str.parse::<cidr::Ipv4Cidr>().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)
|
|
}
|