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}