polka 0.1.2

A dot language parser for Rust; based on Parser Expression Grammar (PEG) using the excellent pest crate as the underpinning.
Documentation
use crate::dot::{Attribute, IdPair};
use crate::parser::symbols;
use pom::char_class::{alphanum, digit};

use pom::parser::{is_a, one_of, sym, Parser};
use std::str::FromStr;

// Any string of alphabetic ([a-zA-Z\200-\377]) characters
// https://www.graphviz.org/doc/info/lang.html
#[allow(clippy::absurd_extreme_comparisons)]
pub fn acceptable_char(term: char) -> bool {
  let term = term as u8;
  alphanum(term) || (term >= 0x80 && term <= 0xFF) || term == 0x5F
}

pub fn id_pair<'a>() -> Parser<'a, char, IdPair> {
  let p = ident() - symbols::space() - sym('=') - symbols::space() + ident();
  p.map(|(key, value)| Attribute { key, value })
}

fn digit_char(c: char) -> bool {
  digit(c as u8)
}

pub fn number<'a>() -> Parser<'a, char, f64> {
  let integer = (one_of("123456789") - one_of("0123456789").repeat(0..)) | sym('0');
  let frac = sym('.') + one_of("0123456789").repeat(1..);
  let number = sym('-').opt() + integer + frac.opt();
  number.collect().convert(|s| {
    let st: String = s.iter().collect();
    f64::from_str(&st)
  })
}

pub fn naked_ident<'a>() -> Parser<'a, char, String> {
  let alpha_num = !is_a(digit_char)
    * (is_a(acceptable_char))
      .repeat(1..)
      .collect()
      .map(|c| c.iter().collect());
  let num = number().map(|e| e.to_string());

  num | alpha_num
}

pub fn quoted_ident<'a>() -> Parser<'a, char, String> {
  sym('"') * naked_ident() - sym('"')
}

pub fn ident<'a>() -> Parser<'a, char, String> {
  quoted_ident() | naked_ident()
}

#[cfg(test)]
mod test {
  use super::*;


  use assert_approx_eq::assert_approx_eq;
  use pretty_assertions::assert_eq;
  use pom::parser::end;

  #[test]
  fn ident_with_correct_input() {
    let inputs = vec![
      (r#"aa"#, "aa"),
      (r#"ab"#, "ab"),
      (r#""a""#, "a"),
      (r#""a"b"#, "a"),
      (r#""ab""#, "ab"),
      (r#"_b"#, "_b"),
      (r#"çåt"#, "çåt"),
      (r#"_çåt"#, "_çåt"),
      (r#"cool_çåt"#, "cool_çåt"),
    ];

    for (input, res) in inputs {
      let input: Vec<char> = input.chars().collect();

      match ident().parse(&input) {
        Ok(a) => assert_eq!(a, res.to_string()),
        Err(e) => panic!(format!("Should have passed. Failed instead with: {:?}", e)),
      };
    }
  }

  #[test]
  fn number_with_correct_input() {
    let inputs: Vec<(&str, f64)> = vec![
      (r#"90"#, 90.0),
      (r#"0.9"#, 0.9),
      (r#"0"#, 0.0),
      (r#"-98.9"#, -98.9),
    ];

    for (input, res) in inputs {
      let input: Vec<char> = input.chars().collect();

      match number().parse(&input) {
        Ok(a) => assert_approx_eq!(a, res),
        Err(e) => panic!(format!("Should have passed. Failed instead with: {:?}", e)),
      };
    }
  }

  #[test]
  fn number_with_incorrect_input() {
    let inputs = vec![r#"a"#, r#"a.9"#, r#"-b"#];

    for input in inputs {
      let i: Vec<char> = input.chars().collect();

      match number().parse(&i) {
        Err(_) => (),
        Ok(r) => panic!(format!("Should have failed. Passed instead with: {:?}", r)),
      };
    }
  }

  #[test]
  fn ident_with_incorrect_input() {
    let inputs = vec![
      r#""aa"#, r#" b a"#, r#"9ab"#,
      // r#"©"#,
    ];

    for input in inputs {
      let input: Vec<char> = input.chars().collect();

      match (ident() - end()).parse(&input) {
        Err(_) => (),
        Ok(r) => panic!(format!("Should have failed. Passed instead with: {:?}", r)),
      };
    }
  }

  #[test]
  fn id_pair_with_correct_input() {
    let inputs = vec![
      r#"a=b"#,
      r#""a"="b"",
      r#""a"=b"#,
      r#"a="b""#,
    ];

    for input in inputs {
      let input: Vec<char> = input.chars().collect();

      match id_pair().parse(&input) {
        Ok(a) => assert_eq!(
          a,
          Attribute {
            key: "a".to_string(),
            value: "b".to_string()
          }
        ),
        Err(e) => panic!(format!("Should have passed. Failed instead with: {:?}", e)),
      };
    }
  }

  #[test]
  fn id_pair_with_incorrect_input() {
    let inputs = vec![r#"a:b"#, r#""c"~"d""#, r#""e"f"#, r#""g=h"#];

    for input in inputs {
      let input: Vec<char> = input.chars().collect();

      match id_pair().parse(&input) {
        Err(_) => (),
        Ok(r) => panic!(format!("Should have failed. Passed instead with: {:?}", r)),
      };
    }
  }
}