Skip to main content

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