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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//! Ordinary Rust control flow with explicit uncertainty and retained traces.
use crate::{
Answer, Answers, Error, ErrorKind, NamedQuestion, Operator, Provider, Request, State,
TypedQuestion, ask, operator,
};
/// One attempted request and its result, including failed attempts.
#[derive(Clone, Debug)]
pub struct Step {
/// Request whose Debug redacts its input and text.
pub request: Request,
/// Answers, validation/provider failure, or interrupted local execution.
pub result: crate::Result<Answers>,
}
/// A structured early exit; handling uncertainty is explicit.
#[derive(Clone, Debug)]
pub enum FlowExit {
/// An operator was read while undecided.
Undecided {
/// Operator identity.
name: String,
/// Full retained answer.
answer: Box<Answer>,
},
/// An input or provider failure, separate from uncertainty.
Error(Error),
}
impl From<Error> for FlowExit {
fn from(error: Error) -> Self {
Self::Error(error)
}
}
/// Results from one ask, read through typed operators.
#[derive(Clone, Debug)]
pub struct FlowAnswers {
answers: Answers,
}
impl FlowAnswers {
/// Inspect all answers without requiring decisions.
pub fn full(&self) -> &Answers {
&self.answers
}
/// Inspect a typed answer without stopping on uncertainty.
///
/// # Errors
/// Returns an error if the operator is absent, its question does not match
/// the retained answer, or conversion to its output type fails.
pub fn peek<Q: TypedQuestion>(
&self,
operator: &Operator<Q>,
) -> crate::Result<Answer<Q::Output>> {
operator.answer(&self.answers)
}
/// Read a typed value or return an uncertainty stop.
///
/// # Errors
/// Returns [`FlowExit::Undecided`] when the answer has no decision. Missing
/// operators, incompatible answers, and conversion failures become
/// [`FlowExit::Error`].
pub fn require<Q: TypedQuestion>(&self, operator: &Operator<Q>) -> Result<Q::Output, FlowExit> {
let answer = self.peek(operator)?;
match answer.value() {
Some(value) => Ok(value.clone()),
None => Err(FlowExit::Undecided {
name: operator.name().into(),
answer: Box::new(
self.answers
.get(operator.name())
.ok_or_else(|| {
Error::new(ErrorKind::InvalidArgument, "operator not found")
})?
.revalidate(&operator.question().clone().into())?,
),
}),
}
}
}
/// Completed application result or uncertainty stop, with a trace.
#[derive(Clone, Debug)]
pub struct FlowOutcome<T> {
/// Present for completion, even if T itself is an application Option::None.
pub value: Option<T>,
/// Operator that stopped the flow, if any.
pub stopped_at: Option<String>,
/// Full answer responsible for a stop.
pub stopped_answer: Option<Answer>,
/// Every flow request attempt, including failures before provider dispatch.
pub trace: Vec<Step>,
}
impl<T> FlowOutcome<T> {
/// Whether the application completed without an uncertainty stop.
pub fn decided(&self) -> bool {
self.stopped_at.is_none()
}
/// Number of flow request attempts, including failures before dispatch.
pub fn calls(&self) -> usize {
self.trace.len()
}
}
/// Failed flow retaining its complete trace.
#[derive(Clone, Debug)]
pub struct FlowFailure {
/// First latched failure.
pub error: Error,
/// Every flow request attempt through this context, including interruption.
pub trace: Vec<Step>,
}
impl std::fmt::Display for FlowFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.error.fmt(f)
}
}
impl std::error::Error for FlowFailure {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
/// Records calls and latches failures while application code controls sequencing.
pub struct FlowContext<'a> {
provider: &'a dyn Provider,
trace: Vec<Step>,
failure: Option<Error>,
}
impl<'a> FlowContext<'a> {
/// Begin an empty trace using a borrowed provider.
pub fn new(provider: &'a dyn Provider) -> Self {
Self {
provider,
trace: Vec::new(),
failure: None,
}
}
/// Ask independent operators together about one state.
///
/// # Errors
/// Returns [`FlowExit::Error`] for an empty batch, duplicate operator names,
/// a previously latched failure, or request/provider/response validation
/// failures. Successful undecided answers remain available for inspection.
pub async fn ask(
&mut self,
state: &State,
operators: &[NamedQuestion],
) -> Result<FlowAnswers, FlowExit> {
let request = Request::new(state.clone(), operator::questions(operators)?)?;
self.ask_request(&request).await
}
/// Ask a request that may carry image assets.
///
/// # Errors
/// Returns [`FlowExit::Error`] for a previously latched failure or any error
/// from the checked provider call. A new call failure is retained in the
/// trace and latched so later calls cannot silently proceed.
///
/// Dropping this future after it starts retains the attempt and latches
/// [`ErrorKind::Runtime`] for interrupted local execution. This does not
/// establish whether remote work stopped or incurred charges. An unpolled
/// future records no attempt.
pub async fn ask_request(&mut self, request: &Request) -> Result<FlowAnswers, FlowExit> {
if let Some(error) = &self.failure {
return Err(FlowExit::Error(error.clone()));
}
// The exclusive context borrow prevents observing this provisional
// failure while the future is running. If it is dropped at the await,
// the retained failure prevents a partial execution appearing complete.
let interrupted = Error::new(
ErrorKind::Runtime,
"flow request ended before local completion",
);
let position = self.trace.len();
self.trace.push(Step {
request: request.clone(),
result: Err(interrupted.clone()),
});
self.failure = Some(interrupted);
let result = ask(self.provider, request).await;
self.trace[position].result = result.clone();
self.failure = result.as_ref().err().cloned();
match result {
Ok(answers) => Ok(FlowAnswers { answers }),
Err(error) => Err(FlowExit::Error(error)),
}
}
/// Finish. A caught provider error cannot become successful completion.
///
/// # Errors
/// Returns [`FlowFailure`] with the trace when an earlier call failed,
/// a polled request future was dropped before completing, or
/// `result` contains [`FlowExit::Error`]. An uncertainty exit instead becomes
/// a successful [`FlowOutcome`] with `stopped_at` set.
pub fn finish<T>(self, result: Result<T, FlowExit>) -> Result<FlowOutcome<T>, FlowFailure> {
if let Some(error) = self.failure {
return Err(FlowFailure {
error,
trace: self.trace,
});
}
match result {
Ok(value) => Ok(FlowOutcome {
value: Some(value),
stopped_at: None,
stopped_answer: None,
trace: self.trace,
}),
Err(FlowExit::Undecided { name, answer }) => Ok(FlowOutcome {
value: None,
stopped_at: Some(name),
stopped_answer: Some(*answer),
trace: self.trace,
}),
Err(FlowExit::Error(error)) => Err(FlowFailure {
error,
trace: self.trace,
}),
}
}
}