Skip to main content

ddd_toolkit_core/application/
usecase.rs

1/// A single, named application operation that orchestrates domain objects
2/// and ports to do one thing.
3#[trait_variant::make(Send)]
4pub trait UseCase: Send + Sync {
5    /// The input required to run this use case.
6    type Input: Send;
7    /// What this use case produces on success.
8    type Output: Send;
9    /// What this use case produces on failure.
10    type Error: std::error::Error + Send + Sync + 'static;
11
12    /// Runs the use case.
13    async fn execute(&self, input: Self::Input) -> Result<Self::Output, Self::Error>;
14}
15
16#[cfg(test)]
17mod test {
18    use std::fmt::Display;
19
20    use crate::testing::block_on;
21
22    use super::*;
23
24    #[derive(Debug, PartialEq)]
25    struct FooError {
26        message: String,
27    }
28
29    impl Display for FooError {
30        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31            write!(f, "{}", self.message)
32        }
33    }
34
35    impl std::error::Error for FooError {}
36
37    struct FooUseCase;
38
39    impl UseCase for FooUseCase {
40        type Input = String;
41        type Output = String;
42        type Error = FooError;
43
44        async fn execute(&self, input: Self::Input) -> Result<Self::Output, Self::Error> {
45            Ok(format!("hello, {input}"))
46        }
47    }
48
49    struct CountingUseCase {
50        calls: std::sync::Mutex<usize>,
51    }
52
53    impl UseCase for CountingUseCase {
54        type Input = ();
55        type Output = usize;
56        type Error = FooError;
57
58        async fn execute(&self, _input: Self::Input) -> Result<Self::Output, Self::Error> {
59            let mut calls = self.calls.lock().expect("lock should not be poisoned");
60            *calls += 1;
61            Ok(*calls)
62        }
63    }
64
65    struct FailingUseCase;
66
67    impl UseCase for FailingUseCase {
68        type Input = String;
69        type Output = String;
70        type Error = FooError;
71
72        async fn execute(&self, input: Self::Input) -> Result<Self::Output, Self::Error> {
73            Err(FooError {
74                message: format!("failed to execute with {input}"),
75            })
76        }
77    }
78
79    struct UnitUseCase;
80
81    impl UseCase for UnitUseCase {
82        type Input = ();
83        type Output = ();
84        type Error = FooError;
85
86        async fn execute(&self, _input: Self::Input) -> Result<Self::Output, Self::Error> {
87            Ok(())
88        }
89    }
90
91    #[test]
92    fn execute_returns_ok_output() {
93        let use_case = FooUseCase;
94
95        let output =
96            block_on(use_case.execute("world".to_string())).expect("execute should succeed");
97
98        assert_eq!(output, "hello, world");
99    }
100
101    #[test]
102    fn execute_with_shared_reference_allows_multiple_calls() {
103        let use_case = CountingUseCase {
104            calls: std::sync::Mutex::new(0),
105        };
106
107        let first = block_on(use_case.execute(())).expect("first call should succeed");
108        let second = block_on(use_case.execute(())).expect("second call should succeed");
109
110        assert_eq!(first, 1);
111        assert_eq!(second, 2);
112    }
113
114    // Verifies the trait bounds are usable from generic code; the runtime
115    // assertion is secondary to the fact that this compiles.
116    #[test]
117    fn use_case_is_usable_through_generic_bound() {
118        fn run<U: UseCase>(use_case: &U, input: U::Input) -> Result<U::Output, U::Error> {
119            block_on(use_case.execute(input))
120        }
121
122        let output =
123            run(&FooUseCase, "generic".to_string()).expect("generic execute should succeed");
124
125        assert_eq!(output, "hello, generic");
126    }
127
128    // Compile-time only (no runtime assertion): the generic bound accepts the
129    // future as `Send` purely because `#[trait_variant::make(Send)]` promises
130    // it at the trait level, not because of the concrete impl.
131    #[test]
132    fn execute_future_is_send() {
133        fn assert_send(_: impl Send) {}
134
135        fn check<U: UseCase>(use_case: &U, input: U::Input) {
136            assert_send(use_case.execute(input));
137        }
138
139        check(&FooUseCase, "send".to_string());
140    }
141
142    #[test]
143    fn execute_returns_err_from_failing_use_case() {
144        let use_case = FailingUseCase;
145
146        let error = block_on(use_case.execute("bad".to_string())).expect_err("execute should fail");
147
148        assert_eq!(
149            error,
150            FooError {
151                message: "failed to execute with bad".to_string()
152            }
153        );
154        assert_eq!(error.to_string(), "failed to execute with bad");
155    }
156
157    // Verifies the `Error: std::error::Error + Send + Sync + 'static` bound is
158    // strong enough for the usual boxed-error upcast.
159    #[test]
160    fn error_can_be_boxed_as_dyn_error() {
161        fn boxed<U: UseCase>(error: U::Error) -> Box<dyn std::error::Error + Send + Sync> {
162            Box::new(error)
163        }
164
165        let error =
166            block_on(FailingUseCase.execute("bad".to_string())).expect_err("execute should fail");
167
168        let boxed = boxed::<FailingUseCase>(error);
169
170        assert_eq!(boxed.to_string(), "failed to execute with bad");
171    }
172
173    #[test]
174    fn execute_works_with_unit_input_and_output() {
175        let () = block_on(UnitUseCase.execute(())).expect("execute should succeed");
176    }
177}