ast2str 1.4.1

A crate for pretty-printing ASTs and other recursive data structures.
Documentation
use ast2str::{utils::AstToStrExt, AstToStr};
use pretty_assertions::assert_eq;

pub type Ptr<'a, T> = Box<Expr<'a, T>>;

#[derive(Debug)]
pub enum Op {
    Add,
    Mul,
}

pub trait Numeric:
    std::fmt::Debug + Clone + Copy + PartialEq + std::ops::Add + std::ops::Mul
{
}
impl Numeric for f64 {}

#[derive(Debug, AstToStr)]
pub enum LitValue<'a, T: Numeric = f64> {
    Num(#[debug] T),
    Bool(#[display] bool),
    Str(#[quoted] &'a str),
    Null,
}

#[derive(AstToStr)]
#[allow(unused)]
pub struct Literal<'a, T: Numeric> {
    #[debug]
    value: LitValue<'a, T>,
    #[skip]
    lexeme: &'a str,
}

impl<'a, T: Numeric> Literal<'a, T> {
    pub fn new(value: LitValue<'a, T>) -> Self {
        Self {
            value,
            lexeme: "default lexeme",
        }
    }
}

pub struct Letters<'a> {
    letters: &'a str,
}

impl<'a> IntoIterator for &'_ Letters<'a> {
    type Item = &'a u8;

    type IntoIter = std::slice::Iter<'a, u8>;

    fn into_iter(self) -> Self::IntoIter {
        self.letters.as_bytes().iter()
    }
}

#[derive(AstToStr)]
pub struct Token<'a> {
    #[delegate = "lexeme"]
    #[rename = "lexeme"]
    source: &'a str,
    #[skip]
    span: std::ops::Range<usize>,
}

impl<'a> Token<'a> {
    pub fn lexeme(&self) -> &'a str {
        &self.source[self.span.clone()]
    }
}

#[derive(AstToStr)]
pub enum ExprKind<'a, T: Numeric> {
    Literal(#[forward] Literal<'a, T>),
    SpecialValue(#[forward] LitValue<'a, T>),
    Binary(
        #[rename = "left"] Ptr<'a, T>,
        #[debug]
        #[rename = "operator"]
        Op,
        #[rename = "right"] Ptr<'a, T>,
    ),
    Maybe {
        #[default = "Nothing"]
        value: Option<Ptr<'a, T>>,
    },
    SomeIterable(#[list] Letters<'a>),
    Mappable(#[list(|byte| byte.wrapping_mul(*byte))] Letters<'a>),
    Summable {
        #[callback(|x: &Vec<_>| x.iter().sum::<i32>())]
        #[rename = "sum"]
        values: Vec<i32>,
    },
    List(#[rename = "elements"] Vec<Expr<'a, T>>),
    Variable(#[rename = "name"] Token<'a>),
    Conditional {
        #[skip]
        skip_me_always: (),
        #[skip_if = "Option::is_none"]
        condition: Option<Ptr<'a, T>>,

        #[rename = "values"]
        #[skip_if = "Vec::is_empty"]
        my_values: Vec<Expr<'a, T>>,
    },
}

#[derive(AstToStr)]
pub struct Expr<'a, T>
where
    T: Numeric,
{
    #[forward]
    pub kind: ExprKind<'a, T>,
    pub some_other_field: (),
}

impl<'a, T: Numeric> Expr<'a, T> {
    pub fn new(kind: ExprKind<'a, T>) -> Self {
        Self {
            kind,
            some_other_field: (),
        }
    }
}

fn get_ast() -> Expr<'static, f64> {
    macro_rules! expr {
        (unboxed $name:ident { $($arg:ident : $value:expr),* }) => {
            Expr::new(ExprKind::$name { $($arg : $value),* })
        };
        (unboxed $name:ident $($arg:expr),*) => {
            Expr::new(ExprKind::$name($($arg),*))
        };
        ($name:ident $($arg:expr),*) => {
            Ptr::new(expr!(unboxed $name $($arg),*))
        };
    }
    macro_rules! lit {
        ($name:ident $value:expr) => {
            Literal::new(LitValue::$name($value))
        };
        ($name:ident) => {
            Literal::new(LitValue::$name)
        };
    }

    expr!(unboxed List vec![
        expr!(unboxed Binary
            expr!(Literal lit!(Num 1.0)),
            Op::Add,
            expr!(Binary
                expr!(Literal lit!(Bool true)),
                Op::Mul,
                expr!(Literal lit!(Str "a string"))
            )
        ),
        expr!(unboxed Maybe { value: None }),
        expr!(unboxed Maybe { value: Some(expr!(Literal lit!(Null))) }),
        expr!(unboxed SomeIterable Letters { letters: "abc" }),
        expr!(unboxed Mappable Letters { letters: "def" }),
        expr!(unboxed Summable { values: vec![1, 2, 3, 4] }),
        expr!(unboxed SpecialValue LitValue::Num(2.0)),
        expr!(unboxed SpecialValue LitValue::Bool(false)),
        expr!(unboxed SpecialValue LitValue::Str("another string")),
        expr!(unboxed Variable Token { source: "a variable  ", span: 2..10 }),
        expr!(unboxed Conditional { skip_me_always: (), condition: None, my_values: vec![] }),
        expr!(unboxed Conditional { skip_me_always: (), condition: None, my_values: vec![expr!(unboxed Literal lit!(Bool false))] }),
        expr!(unboxed Conditional { skip_me_always: (), condition: Some(expr!(Literal lit!(Bool true))), my_values: vec![] }),
        expr!(unboxed Conditional {
            skip_me_always: (),
            condition: Some(expr!(Literal lit!(Bool true))),
            my_values: vec![expr!(unboxed Literal lit!(Bool false))]
        }),
    ])
}

#[test]
fn test_ast_to_str() {
    let ast = get_ast();
    assert_eq!(
        ast.ast_to_str().trim().with_display_as_debug_wrapper(),
        r#"
ExprKind::List
╰─elements=↓
  ├─ExprKind::Binary
  │ ├─left: Literal
  │ │ ╰─value: Num(1.0)
  │ ├─operator: Add
  │ ╰─right: ExprKind::Binary
  │   ├─left: Literal
  │   │ ╰─value: Bool(true)
  │   ├─operator: Mul
  │   ╰─right: Literal
  │     ╰─value: Str("a string")
  ├─ExprKind::Maybe
  │ ╰─value: Nothing
  ├─ExprKind::Maybe
  │ ╰─value: Literal
  │   ╰─value: Null
  ├─ExprKind::SomeIterable
  │ ╰─field0=↓
  │   ├─97
  │   ├─98
  │   ╰─99
  ├─ExprKind::Mappable
  │ ╰─field0=↓
  │   ├─16
  │   ├─217
  │   ╰─164
  ├─ExprKind::Summable
  │ ╰─sum: 10
  ├─LitValue::Num
  │ ╰─field0: 2.0
  ├─LitValue::Bool
  │ ╰─field0: false
  ├─LitValue::Str
  │ ╰─field0: `another string`
  ├─ExprKind::Variable
  │ ╰─name: Token
  │   ╰─lexeme: "variable"
  ├─ExprKind::Conditional
  ├─ExprKind::Conditional
  │ ╰─values=↓
  │   ╰─Literal
  │     ╰─value: Bool(false)
  ├─ExprKind::Conditional
  │ ╰─condition: Literal
  │   ╰─value: Bool(true)
  ╰─ExprKind::Conditional
    ├─condition: Literal
    │ ╰─value: Bool(true)
    ╰─values=↓
      ╰─Literal
        ╰─value: Bool(false)"#
            .trim()
            .with_display_as_debug_wrapper()
    );
}

#[test]
fn test_ast_to_str_with_custom_symbols() {
    let ast = get_ast();
    assert_eq!(
        ast.ast_to_str_impl(&ast2str::TestSymbols)
            .trim()
            .with_display_as_debug_wrapper(),
        r#"
ExprKind::List
  elements=
    ExprKind::Binary
      left: Literal
        value: Num(1.0)
      operator: Add
      right: ExprKind::Binary
        left: Literal
          value: Bool(true)
        operator: Mul
        right: Literal
          value: Str("a string")
    ExprKind::Maybe
      value: Nothing
    ExprKind::Maybe
      value: Literal
        value: Null
    ExprKind::SomeIterable
      field0=
        97
        98
        99
    ExprKind::Mappable
      field0=
        16
        217
        164
    ExprKind::Summable
      sum: 10
    LitValue::Num
      field0: 2.0
    LitValue::Bool
      field0: false
    LitValue::Str
      field0: `another string`
    ExprKind::Variable
      name: Token
        lexeme: "variable"
    ExprKind::Conditional
    ExprKind::Conditional
      values=
        Literal
          value: Bool(false)
    ExprKind::Conditional
      condition: Literal
        value: Bool(true)
    ExprKind::Conditional
      condition: Literal
        value: Bool(true)
      values=
        Literal
          value: Bool(false)
    "#
        .trim()
        .with_display_as_debug_wrapper()
    );
}