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
        }),
    ])
}