sscanf 0.5.0

A sscanf (inverse of format!()) macro with near unlimited parsing capabilities
Documentation
use sscanf::*;

#[test]
fn basic() {
    #[derive(FromScanf, Debug, PartialEq)]
    enum Number {
        #[sscanf(format = "0")]
        Zero,
        #[sscanf(format = "{}")]
        Whole(isize),
        #[sscanf(format = "{numerator}/{denominator}")]
        Fraction {
            numerator: isize,
            denominator: usize,
        },
    }

    let input = "0 5 -1/2.";
    let (zero, whole, fraction) = sscanf!(input, "{Number} {Number} {Number}.").unwrap();

    assert_eq!(zero, Number::Zero);

    assert_eq!(whole, Number::Whole(5));

    assert_eq!(
        fraction,
        Number::Fraction {
            numerator: -1,
            denominator: 2
        }
    );
}

#[test]
fn order() {
    #[derive(FromScanf, Debug, PartialEq)]
    enum Number {
        #[sscanf(format = "{}")]
        Whole(isize),
        #[sscanf(format = "0")]
        Zero,
        #[sscanf(format = "{}/{}")]
        Fraction(isize, usize),
    }

    let input = "0 5 -1/2.";
    let (zero, whole, fraction) = sscanf!(input, "{Number} {Number} {Number}.").unwrap();

    assert_eq!(zero, Number::Whole(0));
    assert_eq!(whole, Number::Whole(5));
    assert_eq!(fraction, Number::Fraction(-1, 2));
}

#[test]
fn not_constructible() {
    #[allow(dead_code)]
    #[derive(FromScanf, Debug, PartialEq)]
    enum Number {
        #[sscanf(format = "0")]
        Zero,
        Whole(isize),
        Fraction(isize, usize),
    }

    assert_eq!(sscanf!("0", "{Number}").unwrap(), Number::Zero);
    assert!(sscanf!("5", "{Number}").is_none());
    assert!(sscanf!("-1/2", "{Number}").is_none());
}

#[test]
fn transparent() {
    #[derive(FromScanf, Debug, PartialEq)]
    enum Dynamic {
        #[sscanf(transparent)]
        Number(isize),
        #[sscanf(transparent)]
        String(String),
    }

    let input = "123 hello 0 hi";
    let (num, string, zero, hi) =
        sscanf!(input, "{Dynamic} {Dynamic} {Dynamic} {Dynamic}").unwrap();
    assert_eq!(num, Dynamic::Number(123));
    assert_eq!(string, Dynamic::String("hello".to_string()));
    assert_eq!(zero, Dynamic::Number(0));
    assert_eq!(hi, Dynamic::String("hi".to_string()));
}

#[test]
fn autogen() {
    #[derive(FromScanf, Debug, PartialEq)]
    #[sscanf(autogen)]
    enum Words {
        Hello,
        World,
        Hi,
    }

    let input = "Hello World Hi";
    let expected = (Words::Hello, Words::World, Words::Hi);
    let parsed = sscanf!(input, "{Words} {Words} {Words}").unwrap();
    assert_eq!(parsed, expected);

    let input_lower = "hello world hi";
    assert!(sscanf!(input_lower, "{Words} {Words} {Words}").is_none());

    #[derive(FromScanf, Debug, PartialEq)]
    #[allow(dead_code)]
    #[sscanf(autogen)]
    enum WordsWithFields {
        Hello,
        #[sscanf(skip)]
        World(usize),
        #[sscanf(transparent)]
        Hi(u8),
    }

    let input = "Hello 5";
    let expected = (WordsWithFields::Hello, WordsWithFields::Hi(5));
    let parsed = sscanf!(input, "{WordsWithFields} {WordsWithFields}").unwrap();
    assert_eq!(parsed, expected);

    let input_world = "World";
    assert!(sscanf!(input_world, "{WordsWithFields}").is_none());
}

#[test]
fn autogen_cases() {
    let cases: std::collections::HashMap<_, _> = [
        ("lowercase", "helloworld"),
        ("UPPERCASE", "HELLOWORLD"),
        ("lower case", "hello world"),
        ("UPPER CASE", "HELLO WORLD"),
        ("PascalCase", "HelloWorld"),
        ("camelCase", "helloWorld"),
        ("snake_case", "hello_world"),
        ("SCREAMING_SNAKE_CASE", "HELLO_WORLD"),
        ("kebab-case", "hello-world"),
        ("SCREAMING-KEBAB-CASE", "HELLO-WORLD"),
        ("rANdOmCasE", "hElLowOrLd"),
    ]
    .iter()
    .copied()
    .collect();

    let mut errors = String::new();

    macro_rules! run_check {
        ($case: literal : $($accepted: literal),+) => {{
            #[derive(FromScanf, Debug, PartialEq)]
            #[sscanf(autogen = $case)]
            enum Word {
                HelloWorld
            }

            let mut accepted = std::collections::HashSet::new();
            $( accepted.insert($accepted); )+

            for (name, input) in &cases {
                let result = sscanf!(input, "{Word}");
                if accepted.contains(name) {
                    if result.is_none() {
                        errors.push_str(&format!(r#"input "{}" should match autogen="{}""#, input, $case));
                    }
                } else {
                    if result.is_some() {
                        errors.push_str(&format!(r#"input "{}" incorrectly matched autogen="{}""#, input, $case));
                    }
                }
            }
        }};
    }

    run_check!("CaseSensitive": "PascalCase");
    run_check!("CAsEiNsenSiTIvE": "PascalCase", "lowercase", "UPPERCASE", "PascalCase", "camelCase", "rANdOmCasE");
    run_check!("lowercase": "lowercase");
    run_check!("UPPERCASE": "UPPERCASE");
    run_check!("lower case": "lower case");
    run_check!("UPPER CASE": "UPPER CASE");
    run_check!("PascalCase": "PascalCase");
    run_check!("camelCase": "camelCase");
    run_check!("snake_case": "snake_case");
    run_check!("SCREAMING_SNAKE_CASE": "SCREAMING_SNAKE_CASE");
    run_check!("kebab-case": "kebab-case");
    run_check!("SCREAMING-KEBAB-CASE": "SCREAMING-KEBAB-CASE");

    assert!(errors.is_empty(), "{}", errors);
}

#[test]
fn variant_attributes() {
    #[derive(FromScanf, Debug, PartialEq)]
    enum Command {
        #[sscanf("SET {}")]
        Set(String),
        #[sscanf(format_regex = "SET_[0-9]+ {}")]
        SetNumbered(String),
        #[sscanf(skip)]
        #[allow(dead_code)]
        Unused(String),
        #[sscanf(transparent)]
        Verbatim(String),
    }

    let input = "SET foo SET_42 bar baz";
    let (cmd1, cmd2, verbatim) = sscanf!(input, "{Command} {Command} {Command}").unwrap();
    assert_eq!(cmd1, Command::Set("foo".to_string()));
    assert_eq!(cmd2, Command::SetNumbered("bar".to_string()));
    assert_eq!(verbatim, Command::Verbatim("baz".to_string()));
}