faux/mock/
stub.rs

1use std::{
2    fmt::{self, Formatter},
3    num::NonZeroUsize,
4};
5
6use crate::matcher::InvocationMatcher;
7
8pub struct Stub<'a, I, O> {
9    matcher: Box<dyn InvocationMatcher<I> + Send>,
10    answer: Answer<'a, I, O>,
11}
12
13pub enum Answer<'a, I, O> {
14    Exhausted,
15    Once(Box<dyn FnOnce(I) -> O + Send + 'a>),
16    Many {
17        stub: Box<dyn FnMut(I) -> O + Send + 'a>,
18        times: Times,
19    },
20}
21
22#[derive(Debug, Clone, Copy)]
23pub enum Times {
24    Always,
25    Times(NonZeroUsize),
26}
27
28#[derive(Debug)]
29pub enum Error {
30    Exhausted,
31    NotMatched(String),
32}
33
34impl std::error::Error for Error {}
35
36impl fmt::Display for Error {
37    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
38        match self {
39            Error::Exhausted => f.write_str("stub was exhausted"),
40            Error::NotMatched(error) => f.write_str(error),
41        }
42    }
43}
44
45impl Times {
46    pub fn decrement(self) -> Option<Self> {
47        match self {
48            Times::Always => Some(self),
49            Times::Times(n) => NonZeroUsize::new(n.get() - 1).map(Times::Times),
50        }
51    }
52}
53
54impl<'a, I, O> Stub<'a, I, O> {
55    pub fn new(
56        stub: Answer<'a, I, O>,
57        matcher: impl InvocationMatcher<I> + Send + 'static,
58    ) -> Self {
59        Stub {
60            matcher: Box::new(matcher),
61            answer: stub,
62        }
63    }
64
65    pub fn call(&mut self, input: I) -> Result<O, (I, Error)> {
66        // TODO: should the error message be different if the stub is also exhausted?
67        if let Err(e) = self.matcher.matches(&input) {
68            return Err((input, Error::NotMatched(e)));
69        }
70
71        self.answer.call(input)
72    }
73}
74
75impl<I, O> Answer<'_, I, O> {
76    fn call(&mut self, input: I) -> Result<O, (I, Error)> {
77        // no need to replace if we can keep decrementing
78        if let Answer::Many { stub, times } = self {
79            if let Some(decremented) = times.decrement() {
80                *times = decremented;
81                return Ok(stub(input));
82            }
83        }
84
85        // otherwise replace it with an exhaust
86        match std::mem::replace(self, Answer::Exhausted) {
87            Answer::Exhausted => Err((input, Error::Exhausted)),
88            Answer::Once(stub) => Ok(stub(input)),
89            Answer::Many { mut stub, .. } => Ok(stub(input)),
90        }
91    }
92}
93
94impl<I, O> fmt::Debug for Stub<'_, I, O> {
95    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
96        f.debug_struct("Stub")
97            // TODO: Add debug information for InvocationMatcher
98            // .field("matcher", &self.matcher)
99            .field(
100                "answer",
101                match &self.answer {
102                    Answer::Exhausted => &"Exhausted",
103                    Answer::Once(_) => &"Once",
104                    Answer::Many { .. } => &"Many",
105                },
106            )
107            .finish()
108    }
109}