Skip to main content

Crate mynt

Crate mynt 

Source
Expand description

§mynt 🍬 - a refreshing error handling crate for proc macros

Crates.io Version docs.rs

mynt provides a straightforward way to handle errors based on the nightly diagnostic interface. It takes a different approach compared to manyhow.

Instead of returning results, we return token streams and instead emit errors externally and continue, only quitting if the error is fatal.

There is no passing around emitters or writing to dummy streams if an error occurs.

It is inspired by proc-macro-error3, but with some changes:

  • Uses declarative macros instead of procedural macros for lower compile times
  • All dependencies are optional
  • Removes dummy streams (instead opting for the developer to emit as much as possible)
  • Support for syn and proc-macro2, as well as support for venial and darling
  • Macros for emitting diagnostics, bailing, quitting, and writing assertions
  • Terminal colors for the stable channel fallback with yansi

§API

The following are the utilities mynt provides to make error handling easier with proc-macros:

§Entry points

Macros need to be wrapped in an entry point to use mynt’s features (needed for quit + fallback support).

§Helpers

Helpers make emitting diagnostics easy. They can be called in two ways:

  • helper!(item); for emitting a diagnostic from an item that implements Emittable (like strings or error types).
  • helper!(spans => message); for emitting a diagnostic with a custom span and message.

You can also call helper!("message"); to use the call site span.

  • emit!(): Emit a diagnostic for a given Level
  • help!(): Emit a help message (written to stderr on stable)
  • note!(): Emit a note (written to stderr on stable)
  • warn!(): Emit a warning (written to stderr on stable)
  • error!(): Emit an error
  • bail!(): Emit an error and return with the default value
  • fatal!(): Emit an error and quit

§Assertions

mynt provides equivalents to assert_*! macros that instead call fatal! instead of panic! for cleaner error output.

§Low-level API

mynt exposes some of its internals just in case.

  • Diagnostic: Manually write diagnostics
  • Level: The level of diagnostic (Error/Warning/Note/Help)
  • quit(): Quit the proc-macro and let mynt clean-up

§Feature Flags

  • default: proc-macro2, syn
  • darling: support for darling error conversion
  • nightly: support for nightly Rust’s proc_macro_diagnostic feature
  • proc-macro2: support for proc-macro2 span conversion
  • syn: support for syn error conversion
  • venial: support for venial error conversion
  • yansi: support for fallback (stable Rust) terminal coloring via yansi

§Example

This example (/examples/attribute/src/lib.rs) demonstrates the outer macro pattern, a technique where we want to share information between invocations of macros (like when we want to get information about items in a database schema), which is not currently possible with macros (without risking determinism).

Instead, we can wrap macros in an outer macro, which can then find those macros, collect data from them, and then proceed with their implementations.

This pattern shows how mynt can shine, by allowing inner macros to produce output, even if other inner macros run into errors.

use std::collections::HashMap;

use mynt::*;
use ordered_multimap::ListOrderedMultimap;
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote, quote_spanned};

// declare the attribute macro
// (we could also wrap the macro with mynt!, but this allows for defining the macro in another file)
mynt_macro_attribute!(schema => schema_impl);

fn schema_impl(attr: TokenStream, input: TokenStream) -> TokenStream {
    mynt_assert!(attr.is_empty()); // ensure the outer macro doesn't have any arguments

    let syn::ItemMod {
        attrs,
        vis,
        unsafety,
        mod_token,
        ident,
        content,
        semi,
    } = syn::parse2(input).unwrap_or_quit(); // parse with syn or quit with an error

    let Some((_, items)) = content else {
        fatal!(ident.span() => "not a inline module"); // quit if the module is to a file
    };

    let mut item_store = SchemaStore::new();
    let mut output = HashMap::new();

    for item in items {
        let syn::Item::Struct(mut item_struct) = item else {
            item_store.insert(None, ModItem::Not(item.to_token_stream()));
            continue;
        };

        let attr: InnerAttr = match try_extract_attributes(&mut item_struct) {
            Ok(Some(attr)) => attr,
            Ok(None) => continue,
            Err(err) => {
                error!(err); // emit an error, but then continue with the rest
                continue;
            }
        };

        let ident = item_struct.ident.clone();
        let schema_item = SchemaItem { attr, item_struct };

        item_store.insert(Some(ident), ModItem::Schema(schema_item));
    }

    for item in &item_store {
        let (Some(ident), ModItem::Schema(schema_item)) = item else {
            continue;
        };

        output.insert(
            ident.clone(),
            match &schema_item.attr {
                InnerAttr::Row => TokenStream::new(),
                InnerAttr::Table(args) => table_impl(args, &schema_item.item_struct, &item_store),
            },
        );
    }

    let new_module: TokenStream = item_store
        .into_iter()
        .map(|(_, v)| match v {
            ModItem::Schema(schema_item) => {
                let original = schema_item.item_struct;
                let output = output.get(&original.ident);

                quote! {
                    #original

                    #output
                }
            }
            ModItem::Not(token_stream) => token_stream,
        })
        .collect();

    quote! {
        #(#attrs)*
        #vis
        #unsafety
        #mod_token
        #ident
        {
            #new_module
        }
        #semi
    }
}

// in a production proc macro, it would probably be better to use a custom implementation for compile time
type SchemaStore = ListOrderedMultimap<Option<syn::Ident>, ModItem>;

fn try_extract_attributes<T: deluxe::HasAttributes, R: deluxe::ExtractAttributes<T>>(
    obj: &mut T,
) -> deluxe::Result<Option<R>> {
    if obj.attrs().iter().any(|a| R::path_matches(a.path())) {
        return R::extract_attributes(obj).map(Some);
    }

    Ok(None)
}

enum ModItem {
    Schema(SchemaItem),
    Not(TokenStream),
}

struct SchemaItem {
    attr: InnerAttr,
    item_struct: syn::ItemStruct,
}

#[derive(deluxe::ExtractAttributes)]
#[deluxe(attributes(schema))]
enum InnerAttr {
    Row,
    #[deluxe(transparent)]
    Table(TableArgs),
}

#[derive(deluxe::ParseMetaItem)]
struct TableArgs {
    row: syn::Ident,
    pk: syn::Ident,
}

fn table_impl(args: &TableArgs, item_struct: &syn::ItemStruct, store: &SchemaStore) -> TokenStream {
    let ident = item_struct.ident.clone();
    let span = ident.span();

    let TableArgs { row, pk } = args;

    let getter_ident = format_ident!("find_by_{pk}");
    let Some(ModItem::Schema(row_item)) = store.get(&Some(row.clone())) else {
        bail!(row.span() => "row type not found in schema scope"); // emit an error and return the default
    };

    let syn::Fields::Named(row_fields) = &row_item.item_struct.fields else {
        bail!(row.span() => "row struct needs named fields"); // in this function, the default is an empty stream
    };

    let Some(pk_field) = row_fields
        .named
        .iter()
        .find(|f| f.ident.as_ref() == Some(pk))
    else {
        bail!(pk.span() => "primary key not found in row struct");
    };

    let getter_ty = &pk_field.ty;

    quote_spanned! {span=>
        impl #ident {
            #[allow(unused_variables)]
            pub fn #getter_ident(#pk: #getter_ty) -> #row {
                #row {
                    #pk: 32,
                    ..Default::default()
                }
            }
        }
    }
}

Check out /examples on the repository to see how this macro is used and other examples.

Modules§

fallback
Fallback implementation of the diagnostic interface for the stable channel.

Macros§

bail
Emit an error diagnostic and then exit the current function with the Default::default value.
emit
Helper for emitting a diagnostic.
error
Helper for emitting a Level::Error diagnostic.
fatal
Emit an error diagnostic and then quit.
help
Helper for emitting a Level::Help diagnostic.
mynt
General use entrypoint that wraps any proc-macro.
mynt_assert
Asserts that a boolean expression is true at runtime.
mynt_assert_eq
Asserts that two expressions are equal to each other (using std::cmp::PartialEq).
mynt_assert_ne
Asserts that two expressions are not equal to each other (using std::cmp::PartialEq).
mynt_macro
Declares a function-like proc-macro entrypoint.
mynt_macro_attribute
Declares an attribute proc-macro entrypoint.
mynt_macro_derive
Declares a derive proc-macro entrypoint.
note
Helper for emitting a Level::Note diagnostic.
warn
Helper for emitting a Level::Warning diagnostic.

Traits§

Emittable
Trait implemented by types that can be emitted through diagnostics.
MyntResultExt
Extension trait for Result.
ToSpans
Helper trait implemented by types that can be converted to a multispan.

Functions§

quit
Panics with a marker that allows mynt to safely emit any errors accumulated before returning an empty token stream.

Type Aliases§

Diagnostic
Type alias to the available diagnostic struct.
Level
Type alias to the available diagnostic level enum.