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