Repository: zkat/miette (rust)

Issue:
SourceCode on Option<impl SourceCode>? 
I would find it useful if there were such an implementation:

```rust
impl miette::SourceCode for Option<NamedSource> {
    fn read_span<'a>(
        &'a self,
        span: &miette::SourceSpan,
        context_lines_before: usize,
        context_lines_after: usize,
    ) -> std::result::Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
        self.expect("no source code was set in the error").read_span(span, context_lines_before, context_lines_after)     
    }
}
```

or perhaps a more general implementation:

```rust
impl<T> miette::SourceCode for Option<T> where T: miette::SourceCode {
    fn read_span<'a>(
        &'a self,
        span: &miette::SourceSpan,
        context_lines_before: usize,
        context_lines_after: usize,
    ) -> std::result::Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
        self.expect("no source code was set in the error").read_span(span, context_lines_before, context_lines_after)     
    }
}
```

this way I can have an optional source up until I set it:

```rust
#[source_code]
src: Option<NamedSource>
```

BTW I also noticed that I can't call this field `source` otherwise I get a weird error. Might be good to have a better error when I call the field `source` instead of `src`

Pull request description:
Allow optional sources in derive
Resolves #217 

Patch:
diff --git a/miette-derive/src/source_code.rs b/miette-derive/src/source_code.rs
index 62f28e7..e1b85ab 100644
--- a/miette-derive/src/source_code.rs
+++ b/miette-derive/src/source_code.rs
@@ -10,6 +10,7 @@ use crate::{
 
 pub struct SourceCode {
     source_code: syn::Member,
+    is_option: bool,
 }
 
 impl SourceCode {
@@ -27,6 +28,19 @@ impl SourceCode {
         for (i, field) in fields.iter().enumerate() {
             for attr in &field.attrs {
                 if attr.path().is_ident("source_code") {
+                    let is_option = if let syn::Type::Path(syn::TypePath {
+                        path: syn::Path { segments, .. },
+                        ..
+                    }) = &field.ty
+                    {
+                        segments
+                            .last()
+                            .map(|seg| seg.ident == "Option")
+                            .unwrap_or(false)
+                    } else {
+                        false
+                    };
+
                     let source_code = if let Some(ident) = field.ident.clone() {
                         syn::Member::Named(ident)
                     } else {
@@ -35,7 +49,10 @@ impl SourceCode {
                             span: field.span(),
                         })
                     };
-                    return Ok(Some(SourceCode { source_code }));
+                    return Ok(Some(SourceCode {
+                        source_code,
+                        is_option,
+                    }));
                 }
             }
         }
@@ -45,11 +62,21 @@ impl SourceCode {
     pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
         let (display_pat, _display_members) = display_pat_members(fields);
         let src = &self.source_code;
+        let ret = if self.is_option {
+            quote! {
+                self.#src.as_ref().map(|s| s as _)
+            }
+        } else {
+            quote! {
+                Some(&self.#src)
+            }
+        };
+
         Some(quote! {
             #[allow(unused_variables)]
             fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> {
                 let Self #display_pat = self;
-                Some(&self.#src)
+                #ret
             }
         })
     }
@@ -68,10 +95,19 @@ impl SourceCode {
                         }
                     };
                     let variant_name = ident.clone();
+                    let ret = if source_code.is_option {
+                        quote! {
+                            #field.as_ref().map(|s| s as _)
+                        }
+                    } else {
+                        quote! {
+                            std::option::Option::Some(#field)
+                        }
+                    };
                     match &fields {
                         syn::Fields::Unit => None,
                         _ => Some(quote! {
-                            Self::#variant_name #display_pat => std::option::Option::Some(#field),
+                            Self::#variant_name #display_pat => #ret,
                         }),
                     }
                 })


Test patch:
diff --git a/tests/derive.rs b/tests/derive.rs
index f4739a7..d1da1bb 100644
--- a/tests/derive.rs
+++ b/tests/derive.rs
@@ -584,3 +584,45 @@ fn test_unit_enum_display() {
         "hello from unit help"
     );
 }
+
+#[test]
+fn test_optional_source_code() {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("struct with optional source")]
+    struct Struct {
+        #[source_code]
+        src: Option<String>,
+    }
+    assert!(Struct { src: None }.source_code().is_none());
+    assert!(Struct {
+        src: Some("".to_string())
+    }
+    .source_code()
+    .is_some());
+
+    #[derive(Debug, Diagnostic, Error)]
+    enum Enum {
+        #[error("variant1 with optional source")]
+        Variant1 {
+            #[source_code]
+            src: Option<String>,
+        },
+        #[error("variant2 with optional source")]
+        Variant2 {
+            #[source_code]
+            src: Option<String>,
+        },
+    }
+    assert!(Enum::Variant1 { src: None }.source_code().is_none());
+    assert!(Enum::Variant1 {
+        src: Some("".to_string())
+    }
+    .source_code()
+    .is_some());
+    assert!(Enum::Variant2 { src: None }.source_code().is_none());
+    assert!(Enum::Variant2 {
+        src: Some("".to_string())
+    }
+    .source_code()
+    .is_some());
+}