url_macro/
lib.rs

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