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
142
143
144
use core::ops::Range;
use thiserror::Error;

/// error that may occur when creating a new `Options` using
/// the `new_length` function.
///
/// This function will mark all `Options` with a length of `0` options
/// as invalid.
#[derive(Debug, Error)]
#[error("Invalid multi choice option {num_choices}")]
pub struct InvalidOptionsLength {
    num_choices: u8,
}

/// options for the vote
///
/// currently this is a 4bits structure, allowing up to 16 choices
/// however we may allow more complex object to be set in
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Options {
    options_range: Range<u8>,
}

/// a choice
///
/// A `Choice` is a representation of a choice that has been made and must
/// be compliant with the `Options`. A way to validate it is with `Options::validate`.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct Choice(u8);

impl Options {
    const NUM_CHOICES_MAX: u8 = 0b0001_0000;

    /// create a new `Options` with the given number of available choices
    ///
    /// available choices will go from `0` to `num_choices` not included.
    pub fn new_length(num_choices: u8) -> Result<Self, InvalidOptionsLength> {
        if num_choices > 0 && num_choices <= Self::NUM_CHOICES_MAX {
            let options_range = Range {
                start: 0,
                end: num_choices,
            };
            Ok(Self { options_range })
        } else {
            Err(InvalidOptionsLength { num_choices })
        }
    }

    /// get the byte representation of the `Options`
    pub(crate) fn as_byte(&self) -> u8 {
        self.options_range.end
    }

    /// validate the given `Choice` against the available `Options`
    ///
    /// returns `true` if the choice is valid, `false` otherwise. By _valid_
    /// it is meant as in the context of the available `Options`. There is
    /// obviously no wrong choices to make, only lessons to learn.
    pub fn validate(&self, choice: Choice) -> bool {
        self.options_range.contains(&choice.0)
    }

    pub fn choice_range(&self) -> &core::ops::Range<u8> {
        &self.options_range
    }
}

impl Choice {
    pub fn new(choice: u8) -> Self {
        Choice(choice)
    }

    pub fn as_byte(self) -> u8 {
        self.0
    }
}

#[cfg(any(test, feature = "property-test-api"))]
mod property {
    use super::*;
    use quickcheck::{Arbitrary, Gen};

    impl Arbitrary for Options {
        fn arbitrary<G: Gen>(g: &mut G) -> Self {
            Self::new_length(u8::arbitrary(g)).unwrap_or_else(|_| Options::new_length(1).unwrap())
        }
    }

    impl Arbitrary for Choice {
        fn arbitrary<G: Gen>(g: &mut G) -> Self {
            Self::new(u8::arbitrary(g))
        }
    }
}

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

    fn validate_choices<I>(options: &Options, choices: I, expected: bool)
    where
        I: IntoIterator<Item = Choice>,
    {
        for (index, choice) in choices.into_iter().enumerate() {
            if options.validate(choice) != expected {
                panic!(
                    "Choice 0b{choice:08b} ({index}) is not validated properly against 0b{options:08b}",
                    choice = choice.as_byte(),
                    index = index,
                    options = options.as_byte()
                )
            }
        }
    }

    fn test_with_length(num_choices: u8) {
        let options = Options::new_length(num_choices).unwrap();

        validate_choices(&options, (0..num_choices).map(Choice::new), true);

        validate_choices(&options, (num_choices..=u8::MAX).map(Choice::new), false);
    }

    #[test]
    fn check_validations() {
        for length in 1..=Options::NUM_CHOICES_MAX {
            test_with_length(length)
        }
    }

    #[proptest]
    fn vote_options_max(#[strategy(any::<u8>())] num_choices: u8) {
        let options = Options::new_length(num_choices);

        if num_choices == 0 || num_choices > Options::NUM_CHOICES_MAX {
            assert!(options.is_err())
        } else {
            let options = options.expect("non `0` options should always be valid");
            assert!(options.as_byte() == num_choices)
        }
    }
}