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}