Skip to main content

rorpc_parse/
errors.rs

1//! Structured error type for all orpc-parse parsing and validation failures.
2//!
3//! Every error carries a `Span` so the Rust compiler points at the exact
4//! token that caused the problem, plus a `kind` that produces an actionable
5//! `help:` message alongside the main diagnostic.
6
7use proc_macro2::Span;
8use quote::quote;
9
10pub type Result<T> = std::result::Result<T, Error>;
11
12// ---------------------------------------------------------------------------
13// Public error type
14// ---------------------------------------------------------------------------
15
16/// A parse or validation error with span information and a user-facing suggestion.
17///
18/// Convert to a compiler diagnostic with [`Error::to_compile_error`].
19#[derive(Debug)]
20pub struct Error {
21    span: Span,
22    kind: ErrorKind,
23}
24
25// SRP: each variant owns exactly one failure mode and its associated message data.
26#[derive(Debug)]
27pub enum ErrorKind {
28    /// A type was found where a specific wrapper was expected.
29    MissingWrapper {
30        expected: &'static str,
31        found: String,
32        suggestion: String,
33    },
34    /// A wrapper type has no generic arguments (e.g. bare `Json` instead of `Json<T>`).
35    EmptyGenericArgs { wrapper: &'static str },
36    /// A type that orpc does not know how to handle appears in a handler signature.
37    UnsupportedType {
38        name: String,
39        reason: &'static str,
40        suggestion: &'static str,
41    },
42    /// An attribute key has a value of the wrong kind (e.g. non-string literal).
43    InvalidAttrValue {
44        attr: String,
45        expected: &'static str,
46        found: String,
47    },
48    /// A required attribute key is absent.
49    MissingRequiredAttr {
50        attr: &'static str,
51        context: &'static str,
52    },
53    /// Two attribute keys that cannot coexist were both provided.
54    ConflictingAttrs {
55        first: String,
56        second: String,
57        suggestion: String,
58    },
59    /// A handler function has no return type annotation.
60    MissingReturnType { fn_name: String },
61    /// A handler function's signature does not match the expected shape.
62    InvalidHandlerSig {
63        fn_name: String,
64        reason: &'static str,
65    },
66    /// An unrecognised key was provided in a macro attribute.
67    UnknownKey {
68        key: String,
69        valid_keys: &'static [&'static str],
70    },
71    /// A `syn` parse error forwarded directly.
72    SynError(syn::Error),
73}
74
75// ---------------------------------------------------------------------------
76// Constructors
77// ---------------------------------------------------------------------------
78
79impl Error {
80    pub fn missing_wrapper(span: Span, expected: &'static str, found: &syn::Type) -> Self {
81        let found_str = type_display(found);
82        Self {
83            span,
84            kind: ErrorKind::MissingWrapper {
85                suggestion: format!("wrap the type: `{}<{}>`", expected, found_str),
86                expected,
87                found: found_str,
88            },
89        }
90    }
91
92    pub fn empty_generic_args(span: Span, wrapper: &'static str) -> Self {
93        Self {
94            span,
95            kind: ErrorKind::EmptyGenericArgs { wrapper },
96        }
97    }
98
99    pub fn unsupported_type(
100        span: Span,
101        ty: &syn::Type,
102        reason: &'static str,
103        suggestion: &'static str,
104    ) -> Self {
105        Self {
106            span,
107            kind: ErrorKind::UnsupportedType {
108                name: type_display(ty),
109                reason,
110                suggestion,
111            },
112        }
113    }
114
115    pub fn invalid_attr_value(span: Span, attr: &str, expected: &'static str, found: &str) -> Self {
116        Self {
117            span,
118            kind: ErrorKind::InvalidAttrValue {
119                attr: attr.to_string(),
120                expected,
121                found: found.to_string(),
122            },
123        }
124    }
125
126    pub fn missing_required_attr(span: Span, attr: &'static str, context: &'static str) -> Self {
127        Self {
128            span,
129            kind: ErrorKind::MissingRequiredAttr { attr, context },
130        }
131    }
132
133    pub fn conflicting_attrs(span: Span, first: &str, second: &str) -> Self {
134        Self {
135            span,
136            kind: ErrorKind::ConflictingAttrs {
137                suggestion: format!("remove either `{}` or `{}`", first, second),
138                first: first.to_string(),
139                second: second.to_string(),
140            },
141        }
142    }
143
144    pub fn missing_return_type(span: Span, fn_name: &str) -> Self {
145        Self {
146            span,
147            kind: ErrorKind::MissingReturnType {
148                fn_name: fn_name.to_string(),
149            },
150        }
151    }
152
153    pub fn invalid_handler_sig(span: Span, fn_name: &str, reason: &'static str) -> Self {
154        Self {
155            span,
156            kind: ErrorKind::InvalidHandlerSig {
157                fn_name: fn_name.to_string(),
158                reason,
159            },
160        }
161    }
162
163    pub fn unknown_key(span: Span, key: &str, valid_keys: &'static [&'static str]) -> Self {
164        Self {
165            span,
166            kind: ErrorKind::UnknownKey {
167                key: key.to_string(),
168                valid_keys,
169            },
170        }
171    }
172
173    /// Emit a `compile_error!` token stream pointing at the offending span.
174    pub fn to_compile_error(&self) -> proc_macro2::TokenStream {
175        syn::Error::new(self.span, self.to_string()).to_compile_error()
176    }
177}
178
179// ---------------------------------------------------------------------------
180// Display — the text the compiler shows after "error:"
181// ---------------------------------------------------------------------------
182
183impl std::fmt::Display for Error {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        use ErrorKind::*;
186        match &self.kind {
187            MissingWrapper {
188                expected,
189                found,
190                suggestion,
191            } => write!(
192                f,
193                "expected `{}` wrapper, found `{}`\n  = help: {}",
194                expected, found, suggestion
195            ),
196            EmptyGenericArgs { wrapper } => {
197                write!(f, "`{}` requires at least one type argument", wrapper)
198            }
199            UnsupportedType {
200                name,
201                reason,
202                suggestion,
203            } => write!(
204                f,
205                "unsupported type `{}`\n  = note: {}\n  = help: {}",
206                name, reason, suggestion
207            ),
208            InvalidAttrValue {
209                attr,
210                expected,
211                found,
212            } => write!(
213                f,
214                "invalid value for `{}`\n  = expected: {}\n  = found: {}",
215                attr, expected, found
216            ),
217            MissingRequiredAttr { attr, context } => write!(
218                f,
219                "missing required attribute `{}`\n  = help: {}",
220                attr, context
221            ),
222            ConflictingAttrs {
223                first,
224                second,
225                suggestion,
226            } => write!(
227                f,
228                "conflicting attributes `{}` and `{}`\n  = help: {}",
229                first, second, suggestion
230            ),
231            MissingReturnType { fn_name } => write!(
232                f,
233                "`{}` has no return type\n  = help: add `-> Json<T>` or `-> Result<Json<T>, E>`",
234                fn_name
235            ),
236            InvalidHandlerSig { fn_name, reason } => write!(
237                f,
238                "invalid handler signature for `{}`\n  = note: {}\n  = help: return `Json<T>` or `Result<Json<T>, E>`",
239                fn_name, reason
240            ),
241            UnknownKey { key, valid_keys } => write!(
242                f,
243                "unknown key `{}`\n  = valid keys: {}",
244                key,
245                valid_keys.join(", ")
246            ),
247            SynError(e) => write!(f, "{}", e),
248        }
249    }
250}
251
252impl std::error::Error for Error {}
253
254impl From<syn::Error> for Error {
255    fn from(e: syn::Error) -> Self {
256        Self {
257            span: e.span(),
258            kind: ErrorKind::SynError(e),
259        }
260    }
261}
262
263// ---------------------------------------------------------------------------
264// Internal helper — the only place a Type is rendered to a String
265// ---------------------------------------------------------------------------
266
267/// Render a `syn::Type` as a display string for use in error messages only.
268/// Never use this for type matching — compare AST idents directly.
269pub(crate) fn type_display(ty: &syn::Type) -> String {
270    quote!(#ty).to_string().replace(' ', "")
271}
272
273// ---------------------------------------------------------------------------
274// Tests
275// ---------------------------------------------------------------------------
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use proc_macro2::Span;
281
282    fn span() -> Span {
283        Span::call_site()
284    }
285
286    #[test]
287    fn missing_wrapper_message() {
288        let ty: syn::Type = syn::parse_str("Vec<Planet>").unwrap();
289        let err = Error::missing_wrapper(span(), "Json", &ty);
290        let msg = err.to_string();
291        assert!(msg.contains("expected `Json` wrapper"));
292        assert!(msg.contains("Vec<Planet>"));
293        assert!(msg.contains("help:"));
294        assert!(msg.contains("Json<Vec<Planet>>"));
295    }
296
297    #[test]
298    fn empty_generic_args_message() {
299        let err = Error::empty_generic_args(span(), "Result");
300        let msg = err.to_string();
301        assert!(msg.contains("`Result` requires at least one type argument"));
302    }
303
304    #[test]
305    fn missing_required_attr_message() {
306        let err =
307            Error::missing_required_attr(span(), "method", "add `method = \"GET\"` to #[orpc]");
308        let msg = err.to_string();
309        assert!(msg.contains("missing required attribute `method`"));
310        assert!(msg.contains("help:"));
311    }
312
313    #[test]
314    fn unknown_key_message() {
315        let err = Error::unknown_key(span(), "routes", &["method", "path", "data"]);
316        let msg = err.to_string();
317        assert!(msg.contains("unknown key `routes`"));
318        assert!(msg.contains("method"));
319        assert!(msg.contains("path"));
320        assert!(msg.contains("data"));
321    }
322
323    #[test]
324    fn conflicting_attrs_message() {
325        let err = Error::conflicting_attrs(span(), "method", "methods");
326        let msg = err.to_string();
327        assert!(msg.contains("conflicting attributes"));
328        assert!(msg.contains("help: remove either"));
329    }
330
331    #[test]
332    fn missing_return_type_message() {
333        let err = Error::missing_return_type(span(), "handle_ping");
334        let msg = err.to_string();
335        assert!(msg.contains("`handle_ping` has no return type"));
336        assert!(msg.contains("Json<T>"));
337    }
338
339    #[test]
340    fn invalid_handler_sig_message() {
341        let err = Error::invalid_handler_sig(span(), "my_handler", "return type is not Json<T>");
342        let msg = err.to_string();
343        assert!(msg.contains("invalid handler signature for `my_handler`"));
344        assert!(msg.contains("return type is not Json<T>"));
345    }
346
347    #[test]
348    fn syn_error_forwarded() {
349        let syn_err = syn::Error::new(span(), "raw syn error");
350        let err = Error::from(syn_err);
351        assert!(err.to_string().contains("raw syn error"));
352    }
353
354    #[test]
355    fn to_compile_error_is_nonempty() {
356        let err = Error::empty_generic_args(span(), "Json");
357        let tokens = err.to_compile_error();
358        assert!(!tokens.is_empty());
359    }
360}