1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
//! This crate provides a [url!](crate::url) macro for compile-time URL validation.
//!
//! # Examples
//!
//! ```
//! # use url_macro::url;
//! // This compiles correctly
//! let valid = url!("https://www.rust-lang.org/");
//! ```
//!
//! ```compile_fail
//! // This triggers a compiler error
//! let invalid = url!("foo");
//! ```
use proc_macro::{Delimiter, Group, Ident, LexError, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
use std::convert::identity;
use url::Url;
/// A compile-time URL validation macro.
///
/// This macro takes a string literal representing a URL and validates it at compile-time.
/// If the URL is valid, it generates the code to create a `url::Url` object.
/// If the URL is invalid, it produces a compile-time error with a descriptive message.
///
/// # Usage
///
/// ```rust
/// use url_macro::url;
///
/// let valid_url = url!("https://www.example.com");
/// let another_valid_url = url!("http://localhost:8080/path?query=value");
///
/// // The following would cause a compile-time error:
/// // let invalid_url = url!("not a valid url");
/// ```
///
/// # Features
///
/// - Validates URLs at compile-time, preventing runtime errors from malformed URLs.
/// - Provides early error detection in the development process.
/// - Automatically converts valid URL strings into `url::Url` objects.
/// - Preserves the original span information for precise error reporting.
///
/// # Limitations
///
/// - The macro only accepts string literals. Variables or expressions that evaluate to strings
/// at runtime cannot be used with this macro.
///
/// # Dependencies
///
/// This macro relies on the `url` crate for URL parsing and validation. Ensure that your
/// project includes this dependency.
///
/// # Performance
///
/// Since the URL validation occurs at compile-time, there is no runtime performance cost
/// associated with using this macro beyond the cost of creating a `url::Url` object.
///
/// # Examples
///
/// Basic usage:
/// ```rust
/// use url_macro::url;
///
/// let github_url = url!("https://github.com");
/// assert_eq!(github_url.scheme(), "https");
/// assert_eq!(github_url.host_str(), Some("github.com"));
///
/// let complex_url = url!("https://user:pass@example.com:8080/path/to/resource?query=value#fragment");
/// assert_eq!(complex_url.username(), "user");
/// assert_eq!(complex_url.path(), "/path/to/resource");
/// ```
///
/// Compile-time error example:
///
/// ```compile_fail
/// use url_macro::url;
///
/// let invalid_url = url!("ftp://invalid url with spaces");
/// // This will produce a compile-time error
/// ```
///
/// # See Also
///
/// - The [`url`](https://docs.rs/url) crate documentation for more information on URL parsing and manipulation.
#[proc_macro]
pub fn url(input: TokenStream) -> TokenStream {
url_result(input).unwrap_or_else(identity)
}
fn url_result(input: TokenStream) -> Result<TokenStream, TokenStream> {
// Get the first token
let token = input
.into_iter()
.next()
.ok_or_else(|| to_compile_error_stream("Expected a string literal", Span::call_site()))?;
// Ensure it's a string literal
let literal = match token {
TokenTree::Literal(lit) => Ok(lit),
_ => Err(to_compile_error_stream("Expected a string literal", Span::call_site())),
}?;
let span = literal.span();
// Extract the string value
let url_str = literal.to_string();
// Remove the surrounding quotes
let url_str = url_str.trim_matches('"');
// Parse the URL
match Url::parse(url_str) {
Ok(_) => {
// If parsing succeeds, output the unwrap code
let result = format!("::url::Url::parse({}).unwrap()", literal);
result
.parse()
.map_err(|err: LexError| to_compile_error_stream(&err.to_string(), span))
}
Err(err) => Err(to_compile_error_stream(&err.to_string(), span)),
}
}
fn to_compile_error_stream(message: &str, span: Span) -> TokenStream {
TokenStream::from_iter([
TokenTree::Punct({
let mut punct = Punct::new(':', Spacing::Joint);
punct.set_span(span);
punct
}),
TokenTree::Punct({
let mut punct = Punct::new(':', Spacing::Alone);
punct.set_span(span);
punct
}),
TokenTree::Ident(Ident::new("core", span)),
TokenTree::Punct({
let mut punct = Punct::new(':', Spacing::Joint);
punct.set_span(span);
punct
}),
TokenTree::Punct({
let mut punct = Punct::new(':', Spacing::Alone);
punct.set_span(span);
punct
}),
TokenTree::Ident(Ident::new("compile_error", span)),
TokenTree::Punct({
let mut punct = Punct::new('!', Spacing::Alone);
punct.set_span(span);
punct
}),
TokenTree::Group({
let mut group = Group::new(Delimiter::Brace, {
TokenStream::from_iter([TokenTree::Literal({
let mut string = Literal::string(message);
string.set_span(span);
string
})])
});
group.set_span(span);
group
}),
])
}