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}