cute/
opt.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
use std::{fmt::Debug, hash::Hash};

/// A trait representing a state that can be used in command-line options
pub trait State: Debug + Clone + Eq + Hash + Default {}

impl<T: Debug + Clone + Eq + Hash + Default> State for T {}

/// Represents the result of matching a command-line argument
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Match<'a> {
    /// Successful match with optional value
    Okay(Option<&'a str>),
    /// Failed match
    None,
}

/// Trait defining the behavior of a command-line option
pub trait StateOpt {
    /// The associated state type
    type S: State;

    /// Returns the name of the option
    fn name(&self) -> &str;

    /// Returns the associated state
    fn state(&self) -> &Self::S;

    /// Indicates whether this option consumes the next argument
    fn consume(&self) -> bool {
        true
    }

    /// Attempts to match the given argument with this option
    fn r#match<'a>(&self, arg: &'a str) -> Match<'a> {
        if let Some((name, val)) = arg.split_once('=') {
            if name == self.name() {
                Match::Okay(Some(val))
            } else {
                Match::None
            }
        } else if self.name() == arg {
            Match::Okay(None)
        } else {
            Match::None
        }
    }
}

/// Represents a command-line option with its name, state and consume behavior
pub struct Opt<'a, S> {
    name: &'a str,
    state: S,
    consume: bool,
}

impl<'a, S> Opt<'a, S> {
    /// Creates a new Opt instance
    pub fn new(name: &'a str, state: S, consume: bool) -> Self {
        Self {
            name,
            state,
            consume,
        }
    }
}

impl<S: State> StateOpt for Opt<'_, S> {
    type S = S;

    fn name(&self) -> &str {
        self.name
    }

    fn state(&self) -> &Self::S {
        &self.state
    }

    fn consume(&self) -> bool {
        self.consume
    }
}

/// Creates a new switch option that doesn't consume the next argument
pub fn switch<S: State>(name: &str, state: S) -> Opt<'_, S> {
    Opt::new(name, state, false)
}

/// Creates a new option that consumes the next argument
pub fn option<S: State>(name: &str, state: S) -> Opt<'_, S> {
    Opt::new(name, state, true)
}

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

    #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
    enum TestState {
        On,
        #[default]
        Unknown,
    }

    #[test]
    fn test_opt_new() {
        let opt = Opt::new("test", TestState::On, true);
        assert_eq!(opt.name, "test");
        assert!(opt.consume);
    }

    #[test]
    fn test_stateopt_implementation() {
        let opt = Opt::new("test", TestState::On, false);
        assert_eq!(opt.name(), "test");
        assert!(!opt.consume());
    }

    #[test]
    fn test_match_method() {
        let opt = Opt::new("test", TestState::On, true);

        assert_eq!(opt.r#match("test"), Match::Okay(None));
        assert_eq!(opt.r#match("test=value"), Match::Okay(Some("value")));
        assert_eq!(opt.r#match("testing"), Match::None);
        assert_eq!(opt.r#match("other"), Match::None);
    }

    #[test]
    fn test_switch_function() {
        let opt = switch("test", TestState::On);
        assert_eq!(opt.name, "test");
        assert!(!opt.consume);
    }

    #[test]
    fn test_option_function() {
        let opt = option("test", TestState::On);
        assert_eq!(opt.name, "test");
        assert!(opt.consume);
    }
}