use proc_macro::TokenStream;
use quote::quote;
use reqwest::blocking::Client;
use syn::{parse::Parse, parse::ParseStream, parse_macro_input, LitStr, Token, Type};
use url::Url;
pub(crate) fn fetch_url_content(url_str: &str) -> Result<String, String> {
let url = Url::parse(url_str).map_err(|e| format!("Invalid URL: {}", e))?;
if url.scheme() != "http" && url.scheme() != "https" {
return Err("Only HTTP and HTTPS URLs are supported".to_string());
}
let client = Client::new();
let response = client
.get(url)
.header("User-Agent", "include_url_macro")
.send()
.map_err(|e| format!("Failed to fetch URL: {}", e))?;
response
.text()
.map_err(|e| format!("Failed to read response body: {}", e))
}
#[proc_macro]
pub fn include_url(input: TokenStream) -> TokenStream {
let url_str = parse_macro_input!(input as LitStr).value();
match fetch_url_content(&url_str) {
Ok(content) => {
let output = quote! { #content };
output.into()
}
Err(err) => syn::Error::new(proc_macro2::Span::call_site(), err)
.to_compile_error()
.into(),
}
}
struct JsonUrlInput {
url: LitStr,
ty: Option<Type>,
}
impl Parse for JsonUrlInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let url = input.parse()?;
let ty = if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
Some(input.parse()?)
} else {
None
};
Ok(JsonUrlInput { url, ty })
}
}
#[proc_macro]
pub fn include_json_url(input: TokenStream) -> TokenStream {
let JsonUrlInput { url, ty } = parse_macro_input!(input as JsonUrlInput);
let url_str = url.value();
match fetch_url_content(&url_str) {
Ok(content) => {
match serde_json::from_str::<serde_json::Value>(&content) {
Ok(_) => {
let output = match ty {
Some(ty) => quote! {{
let json_str = #content;
serde_json::from_str::<#ty>(&json_str)
.expect("Failed to parse JSON into the specified type")
}},
None => quote! {{
let json_str = #content;
serde_json::from_str::<serde_json::Value>(&json_str)
.expect("Failed to parse JSON")
}},
};
output.into()
}
Err(json_err) => {
syn::Error::new(
proc_macro2::Span::call_site(),
format!("Invalid JSON content from URL: {}", json_err),
)
.to_compile_error()
.into()
}
}
}
Err(err) => syn::Error::new(proc_macro2::Span::call_site(), err)
.to_compile_error()
.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fetch_url_content() {
let result = fetch_url_content("https://example.com");
assert!(result.is_ok());
}
#[test]
fn test_invalid_scheme() {
let result = fetch_url_content("ftp://example.com");
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Only HTTP and HTTPS URLs are supported"));
}
#[test]
fn test_invalid_url() {
let result = fetch_url_content("not-a-url");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid URL"));
}
}