enum-path 0.1.0

Derive FromStr and Display impls for enums that follow a hierarchical path-like serialization scheme
Documentation
use enum_path::EnumPath;

#[derive(EnumPath, Clone, Debug, PartialEq, Eq)]
#[enum_path(FromStr, Display, rename_all = "snake_case")]
enum Action
{
    Exit,
    SendMessage(String),
    SetState(State),
    #[enum_path(rename = "quit")]
    Terminate,
    GetID,
}

#[derive(EnumPath, Clone, Debug, PartialEq, Eq)]
#[enum_path(FromStr, Display)]
enum State
{
    Idle,
    Ready,
    Running(Phase),
}

#[derive(EnumPath, Clone, Debug, PartialEq, Eq)]
#[enum_path(FromStr, Display)]
enum Phase
{
    Init,
    Execute,
}

#[derive(EnumPath, Clone, Debug, PartialEq, Eq)]
#[enum_path(FromStr, Display, case_insensitive)]
enum Command
{
    Start,
    Stop,
    Reload(String),
}

mod from_str
{
    use super::*;

    #[test]
    fn unit()
    {
        let action: Action = "exit".parse().unwrap();
        assert_eq!(action, Action::Exit);
    }

    #[test]
    fn consecutive_capitals()
    {
        let action: Action = "get_id".parse().unwrap();
        assert_eq!(action, Action::GetID);
    }

    #[test]
    fn string_payload()
    {
        let action: Action = "send_message.hello world".parse().unwrap();
        assert_eq!(action, Action::SendMessage("hello world".to_owned()));
    }

    #[test]
    fn string_payload_with_delimiter()
    {
        let action: Action = "send_message.with.dot".parse().unwrap();
        assert_eq!(action, Action::SendMessage("with.dot".to_owned()));
    }

    #[test]
    fn nested_depth_0()
    {
        let action: Action = "set_state.Ready".parse().unwrap();
        assert_eq!(action, Action::SetState(State::Ready));
    }

    #[test]
    fn nested_depth_1()
    {
        let action: Action = "set_state.Running.Execute".parse().unwrap();
        assert_eq!(action, Action::SetState(State::Running(Phase::Execute)));
    }

    #[test]
    fn inner_enum()
    {
        let phase: Phase = "Init".parse().unwrap();
        assert_eq!(phase, Phase::Init);
    }

    #[test]
    fn variant_rename()
    {
        let action: Action = "quit".parse().unwrap();
        assert_eq!(action, Action::Terminate);
    }

    #[test]
    fn error()
    {
        let err = "nonexistent".parse::<Action>().unwrap_err();
        assert_eq!(err.input, "nonexistent");
        assert_eq!(err.expected, "Action");
    }
}

mod display
{
    use super::*;

    #[test]
    fn unit()
    {
        assert_eq!(Action::Exit.to_string(), "exit");
    }

    #[test]
    fn consecutive_capitals()
    {
        assert_eq!(Action::GetID.to_string(), "get_id");
    }

    #[test]
    fn string_payload()
    {
        assert_eq!(
            Action::SendMessage("hello world".to_owned()).to_string(),
            "send_message.hello world"
        );
    }

    #[test]
    fn nested_depth_0()
    {
        assert_eq!(
            Action::SetState(State::Ready).to_string(),
            "set_state.Ready"
        );
    }

    #[test]
    fn nested_depth_1()
    {
        assert_eq!(
            Action::SetState(State::Running(Phase::Execute)).to_string(),
            "set_state.Running.Execute"
        );
    }

    #[test]
    fn variant_rename()
    {
        assert_eq!(Action::Terminate.to_string(), "quit");
    }
}

mod case_insensitive
{
    use super::*;

    #[test]
    fn exact_case()
    {
        let cmd: Command = "Start".parse().unwrap();
        assert_eq!(cmd, Command::Start);
    }

    #[test]
    fn lower_case()
    {
        let cmd: Command = "start".parse().unwrap();
        assert_eq!(cmd, Command::Start);
    }

    #[test]
    fn upper_case()
    {
        let cmd: Command = "STOP".parse().unwrap();
        assert_eq!(cmd, Command::Stop);
    }

    #[test]
    fn mixed_case()
    {
        let cmd: Command = "sToP".parse().unwrap();
        assert_eq!(cmd, Command::Stop);
    }

    #[test]
    fn payload_case_insensitive_prefix()
    {
        let cmd: Command = "rELOAD.config.yaml".parse().unwrap();
        assert_eq!(cmd, Command::Reload("config.yaml".to_owned()));
    }

    #[test]
    fn display_preserves_canonical_case()
    {
        assert_eq!(Command::Start.to_string(), "Start");
        assert_eq!(Command::Stop.to_string(), "Stop");
    }
}

mod roundtrip
{
    use super::*;

    #[test]
    fn all_variants()
    {
        let cases = [
            Action::Exit,
            Action::SendMessage("payload".to_owned()),
            Action::SetState(State::Idle),
            Action::SetState(State::Running(Phase::Init)),
            Action::Terminate,
        ];
        for original in &cases {
            let serialized = original.to_string();
            let parsed: Action = serialized.parse().unwrap();
            assert_eq!(&parsed, original);
        }
    }
}

mod custom_error
{
    use super::*;

    #[derive(Debug)]
    struct MyError(enum_path::Error);

    impl core::fmt::Display for MyError
    {
        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
        {
            write!(f, "custom: {}", self.0)
        }
    }

    impl From<enum_path::Error> for MyError
    {
        fn from(e: enum_path::Error) -> Self
        {
            Self(e)
        }
    }

    #[derive(EnumPath, Clone, Debug, PartialEq, Eq)]
    #[enum_path(FromStr, error = MyError)]
    enum Thing
    {
        Foo,
    }

    #[test]
    fn uses_custom_error()
    {
        let err = "bar".parse::<Thing>().unwrap_err();
        assert_eq!(err.0.expected, "Thing");
    }
}