facet_miette/
lib.rs

1//! # facet-miette
2//!
3//! Derive [`miette::Diagnostic`] for your error types using facet's plugin system.
4//!
5//! ## Usage
6//!
7//! Since the crate is named `facet-miette` but the derive is `Diagnostic`, you need
8//! to use the explicit path syntax:
9//!
10//! ```ignore
11//! use facet::Facet;
12//! use facet_miette as diagnostic; // for attribute namespace
13//! use miette::SourceSpan;
14//!
15//! #[derive(Facet, Debug)]
16//! #[facet(derive(Error, facet_miette::Diagnostic))]
17//! pub enum ParseError {
18//!     /// Unexpected token in input
19//!     #[facet(diagnostic::code = "parse::unexpected_token")]
20//!     #[facet(diagnostic::help = "Check for typos or missing delimiters")]
21//!     UnexpectedToken {
22//!         #[facet(diagnostic::source_code)]
23//!         src: String,
24//!         #[facet(diagnostic::label = "this token was unexpected")]
25//!         span: SourceSpan,
26//!     },
27//!
28//!     /// End of file reached unexpectedly
29//!     #[facet(diagnostic::code = "parse::unexpected_eof")]
30//!     UnexpectedEof,
31//! }
32//! ```
33//!
34//! ## Attributes
35//!
36//! ### Container/Variant Level
37//!
38//! - `#[facet(diagnostic::code = "my_lib::error_code")]` - Error code for this diagnostic
39//! - `#[facet(diagnostic::help = "Helpful message")]` - Help text shown to user
40//! - `#[facet(diagnostic::url = "https://...")]` - URL for more information
41//! - `#[facet(diagnostic::severity = "warning")]` - Severity: "error", "warning", or "advice"
42//!
43//! ### Field Level
44//!
45//! - `#[facet(diagnostic::source_code)]` - Field containing the source text (impl `SourceCode`)
46//! - `#[facet(diagnostic::label = "description")]` - Field is a span to highlight with label
47//! - `#[facet(diagnostic::related)]` - Field contains related diagnostics (iterator)
48//!
49//! ## Integration with facet-error
50//!
51//! You'll typically use both `Error` and `Diagnostic` together:
52//!
53//! ```ignore
54//! #[derive(Facet, Debug)]
55//! #[facet(derive(Error, facet_miette::Diagnostic))]
56//! pub enum MyError {
57//!     /// Something went wrong
58//!     #[facet(diagnostic::code = "my_error")]
59//!     SomeError,
60//! }
61//! ```
62
63// Re-export miette types for convenience
64pub use miette::{Diagnostic, LabeledSpan, Severity, SourceCode, SourceSpan};
65
66// ============================================================================
67// ATTRIBUTE GRAMMAR
68// ============================================================================
69
70facet::define_attr_grammar! {
71    ns "diagnostic";
72    crate_path ::facet_miette;
73
74    /// Diagnostic attribute types for configuring miette::Diagnostic implementation.
75    pub enum Attr {
76        /// Error code for this diagnostic.
77        ///
78        /// Usage: `#[facet(diagnostic::code = "my_lib::error")]`
79        Code(&'static str),
80
81        /// Help message for this diagnostic.
82        ///
83        /// Usage: `#[facet(diagnostic::help = "Try doing X instead")]`
84        Help(&'static str),
85
86        /// URL for more information.
87        ///
88        /// Usage: `#[facet(diagnostic::url = "https://example.com/errors/E001")]`
89        Url(&'static str),
90
91        /// Severity level: "error", "warning", or "advice".
92        ///
93        /// Usage: `#[facet(diagnostic::severity = "warning")]`
94        Severity(&'static str),
95
96        /// Marks a field as containing the source code to display.
97        ///
98        /// Usage: `#[facet(diagnostic::source_code)]`
99        SourceCode,
100
101        /// Marks a field as a span to highlight with an optional label.
102        ///
103        /// Usage: `#[facet(diagnostic::label = "this is the problem")]`
104        Label(&'static str),
105
106        /// Marks a field as containing related diagnostics.
107        ///
108        /// Usage: `#[facet(diagnostic::related)]`
109        Related,
110    }
111}
112
113// ============================================================================
114// PLUGIN TEMPLATE
115// ============================================================================
116
117/// Plugin chain entry point.
118///
119/// Called by `#[derive(Facet)]` when `#[facet(derive(Diagnostic))]` is present.
120#[macro_export]
121macro_rules! __facet_invoke {
122    (
123        @tokens { $($tokens:tt)* }
124        @remaining { $($remaining:tt)* }
125        @plugins { $($plugins:tt)* }
126        @facet_crate { $($facet_crate:tt)* }
127    ) => {
128        $crate::__facet_invoke_internal! {
129            @tokens { $($tokens)* }
130            @remaining { $($remaining)* }
131            @plugins {
132                $($plugins)*
133                @plugin {
134                    @name { "Diagnostic" }
135                    @template {
136                        impl ::miette::Diagnostic for @Self {
137                            fn code<'__facet_a>(&'__facet_a self) -> ::core::option::Option<::std::boxed::Box<dyn ::core::fmt::Display + '__facet_a>> {
138                                match self {
139                                    @for_variant {
140                                        @if_attr(diagnostic::code) {
141                                            Self::@variant_name { .. } => ::core::option::Option::Some(::std::boxed::Box::new(@attr_args)),
142                                        }
143                                    }
144                                    _ => ::core::option::Option::None,
145                                }
146                            }
147
148                            fn severity(&self) -> ::core::option::Option<::miette::Severity> {
149                                match self {
150                                    @for_variant {
151                                        @if_attr(diagnostic::severity) {
152                                            Self::@variant_name { .. } => {
153                                                let s: &str = @attr_args;
154                                                ::core::option::Option::Some(match s {
155                                                    "error" => ::miette::Severity::Error,
156                                                    "warning" => ::miette::Severity::Warning,
157                                                    "advice" => ::miette::Severity::Advice,
158                                                    _ => ::miette::Severity::Error,
159                                                })
160                                            }
161                                        }
162                                    }
163                                    _ => ::core::option::Option::None,
164                                }
165                            }
166
167                            fn help<'__facet_a>(&'__facet_a self) -> ::core::option::Option<::std::boxed::Box<dyn ::core::fmt::Display + '__facet_a>> {
168                                match self {
169                                    @for_variant {
170                                        @if_attr(diagnostic::help) {
171                                            Self::@variant_name { .. } => ::core::option::Option::Some(::std::boxed::Box::new(@attr_args)),
172                                        }
173                                    }
174                                    _ => ::core::option::Option::None,
175                                }
176                            }
177
178                            fn url<'__facet_a>(&'__facet_a self) -> ::core::option::Option<::std::boxed::Box<dyn ::core::fmt::Display + '__facet_a>> {
179                                match self {
180                                    @for_variant {
181                                        @if_attr(diagnostic::url) {
182                                            Self::@variant_name { .. } => ::core::option::Option::Some(::std::boxed::Box::new(@attr_args)),
183                                        }
184                                    }
185                                    _ => ::core::option::Option::None,
186                                }
187                            }
188
189                            fn source_code(&self) -> ::core::option::Option<&dyn ::miette::SourceCode> {
190                                match self {
191                                    @for_variant {
192                                        @if_field_attr(diagnostic::source_code) {
193                                            Self::@variant_name { @field_name, .. } => ::core::option::Option::Some(@field_name),
194                                        }
195                                    }
196                                    _ => ::core::option::Option::None,
197                                }
198                            }
199
200                            fn labels(&self) -> ::core::option::Option<::std::boxed::Box<dyn ::core::iter::Iterator<Item = ::miette::LabeledSpan> + '_>> {
201                                match self {
202                                    @for_variant {
203                                        @if_any_field_attr(diagnostic::label) {
204                                            Self::@variant_name @variant_pattern => {
205                                                let mut __facet_labels = ::std::vec::Vec::new();
206                                                @for_field {
207                                                    @if_attr(diagnostic::label) {
208                                                        __facet_labels.push(::miette::LabeledSpan::at(
209                                                            @field_name.clone(),
210                                                            @attr_args
211                                                        ));
212                                                    }
213                                                }
214                                                ::core::option::Option::Some(::std::boxed::Box::new(__facet_labels.into_iter()))
215                                            }
216                                        }
217                                    }
218                                    _ => ::core::option::Option::None,
219                                }
220                            }
221
222                            fn related<'__facet_a>(&'__facet_a self) -> ::core::option::Option<::std::boxed::Box<dyn ::core::iter::Iterator<Item = &'__facet_a dyn ::miette::Diagnostic> + '__facet_a>> {
223                                match self {
224                                    @for_variant {
225                                        @if_field_attr(diagnostic::related) {
226                                            Self::@variant_name { @field_name, .. } => {
227                                                ::core::option::Option::Some(::std::boxed::Box::new(
228                                                    @field_name.iter().map(|__facet_e| __facet_e as &dyn ::miette::Diagnostic)
229                                                ))
230                                            }
231                                        }
232                                    }
233                                    _ => ::core::option::Option::None,
234                                }
235                            }
236                        }
237                    }
238                }
239            }
240            @facet_crate { $($facet_crate)* }
241        }
242    };
243}
244
245/// Internal macro that either chains to next plugin or calls finalize
246#[doc(hidden)]
247#[macro_export]
248macro_rules! __facet_invoke_internal {
249    // No more plugins - call finalize
250    (
251        @tokens { $($tokens:tt)* }
252        @remaining { }
253        @plugins { $($plugins:tt)* }
254        @facet_crate { $($facet_crate:tt)* }
255    ) => {
256        $($facet_crate)*::__facet_finalize! {
257            @tokens { $($tokens)* }
258            @plugins { $($plugins)* }
259            @facet_crate { $($facet_crate)* }
260        }
261    };
262
263    // More plugins - chain to next
264    (
265        @tokens { $($tokens:tt)* }
266        @remaining { $next:path $(, $rest:path)* $(,)? }
267        @plugins { $($plugins:tt)* }
268        @facet_crate { $($facet_crate:tt)* }
269    ) => {
270        $next! {
271            @tokens { $($tokens)* }
272            @remaining { $($rest),* }
273            @plugins { $($plugins)* }
274            @facet_crate { $($facet_crate)* }
275        }
276    };
277}