1use crate::fetch::{FetchKind, Terminator};
2use crate::middleware::MiddlewareKind;
3
4#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)]
5pub enum Phase {
6 L4Raw,
7 L4Peeked,
8 L7Request,
9 L7Response,
10 Tunnel,
11}
12
13#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
14pub enum PhaseNodeKind {
15 Check,
16 Middleware(MiddlewareKind),
17 Upgrade,
18 Fetch(FetchKind),
19 Terminate(Terminator),
20}
21
22#[derive(Copy, Clone, Eq, PartialEq, Debug)]
23pub enum Transition {
24 PassThrough,
25 Into(Phase),
26 BiOutcome { response: Phase, tunnel: Phase },
27 Terminal,
28}
29
30#[derive(Copy, Clone, Eq, PartialEq, Debug)]
31pub struct PhaseError {
32 pub expected: &'static [Phase],
33 pub got: Phase,
34}
35
36const L4_ANY: &[Phase] = &[Phase::L4Raw, Phase::L4Peeked];
37const L7_REQ: &[Phase] = &[Phase::L7Request];
38const L7_RESP: &[Phase] = &[Phase::L7Response];
39const TUNNEL: &[Phase] = &[Phase::Tunnel];
40const ANY_PHASE: &[Phase] =
41 &[Phase::L4Raw, Phase::L4Peeked, Phase::L7Request, Phase::L7Response, Phase::Tunnel];
42
43#[must_use]
44#[allow(
45 clippy::match_same_arms,
46 reason = "truth table per spec/flow-model.md § Phase state machine: each arm = one PhaseNodeKind row; merging by RHS hides the table structure"
47)]
48pub const fn accepted_in_phases(kind: PhaseNodeKind) -> &'static [Phase] {
49 match kind {
50 PhaseNodeKind::Check => ANY_PHASE,
51 PhaseNodeKind::Middleware(MiddlewareKind::L4Peek) => L4_ANY,
52 PhaseNodeKind::Middleware(MiddlewareKind::L4Bytes) => L4_ANY,
53 PhaseNodeKind::Middleware(MiddlewareKind::L7Request) => L7_REQ,
54 PhaseNodeKind::Middleware(MiddlewareKind::L7Response) => L7_RESP,
55 PhaseNodeKind::Upgrade => L4_ANY,
60 PhaseNodeKind::Fetch(FetchKind::L4Forward) => L4_ANY,
61 PhaseNodeKind::Fetch(FetchKind::HttpProxy) => L7_REQ,
62 PhaseNodeKind::Fetch(FetchKind::HttpSynthesize) => L7_REQ,
63 PhaseNodeKind::Fetch(FetchKind::WebSocketUpgrade) => L7_REQ,
64 PhaseNodeKind::Fetch(FetchKind::AcmeChallenge) => L7_REQ,
65 PhaseNodeKind::Terminate(Terminator::WriteHttpResponse) => L7_RESP,
66 PhaseNodeKind::Terminate(Terminator::ByteTunnel) => TUNNEL,
67 PhaseNodeKind::Terminate(Terminator::Close) => ANY_PHASE,
70 }
71}
72
73#[allow(
79 clippy::match_same_arms,
80 reason = "truth table per spec/flow-model.md § Phase state machine: each arm = one PhaseNodeKind row; merging by RHS hides the table structure"
81)]
82pub fn transition(kind: PhaseNodeKind, cur: Phase) -> Result<Transition, PhaseError> {
83 let accepted = accepted_in_phases(kind);
84 if !accepted.contains(&cur) {
85 return Err(PhaseError { expected: accepted, got: cur });
86 }
87 Ok(match kind {
88 PhaseNodeKind::Check => Transition::PassThrough,
89 PhaseNodeKind::Middleware(MiddlewareKind::L4Peek) => Transition::Into(Phase::L4Peeked),
90 PhaseNodeKind::Middleware(MiddlewareKind::L4Bytes) => Transition::PassThrough,
91 PhaseNodeKind::Middleware(MiddlewareKind::L7Request) => Transition::Into(Phase::L7Request),
92 PhaseNodeKind::Middleware(MiddlewareKind::L7Response) => Transition::Into(Phase::L7Response),
93 PhaseNodeKind::Upgrade => Transition::Into(Phase::L7Request),
94 PhaseNodeKind::Fetch(FetchKind::L4Forward) => Transition::Into(Phase::Tunnel),
95 PhaseNodeKind::Fetch(FetchKind::HttpProxy) => Transition::Into(Phase::L7Response),
96 PhaseNodeKind::Fetch(FetchKind::HttpSynthesize) => Transition::Into(Phase::L7Response),
97 PhaseNodeKind::Fetch(FetchKind::AcmeChallenge) => Transition::Into(Phase::L7Response),
98 PhaseNodeKind::Fetch(FetchKind::WebSocketUpgrade) => {
99 Transition::BiOutcome { response: Phase::L7Response, tunnel: Phase::Tunnel }
100 }
101 PhaseNodeKind::Terminate(_) => Transition::Terminal,
102 })
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 const ALL_PHASES: [Phase; 5] =
110 [Phase::L4Raw, Phase::L4Peeked, Phase::L7Request, Phase::L7Response, Phase::Tunnel];
111
112 #[test]
113 fn phase_serde_round_trip_per_variant() {
114 for p in ALL_PHASES {
115 let encoded = serde_json::to_string(&p).expect("serialize");
116 let decoded: Phase = serde_json::from_str(&encoded).expect("deserialize");
117 assert_eq!(decoded, p);
118 }
119 }
120
121 #[test]
122 fn check_accepts_any_phase() {
123 assert_eq!(accepted_in_phases(PhaseNodeKind::Check), ANY_PHASE);
124 }
125
126 #[test]
127 fn l4_peek_accepts_l4_phases_only() {
128 assert_eq!(
129 accepted_in_phases(PhaseNodeKind::Middleware(MiddlewareKind::L4Peek)),
130 &[Phase::L4Raw, Phase::L4Peeked] as &[Phase],
131 );
132 }
133
134 #[test]
135 fn l4_bytes_accepts_l4_phases_only() {
136 assert_eq!(
137 accepted_in_phases(PhaseNodeKind::Middleware(MiddlewareKind::L4Bytes)),
138 &[Phase::L4Raw, Phase::L4Peeked] as &[Phase],
139 );
140 }
141
142 #[test]
143 fn l7_request_middleware_accepts_only_l7_request() {
144 assert_eq!(
145 accepted_in_phases(PhaseNodeKind::Middleware(MiddlewareKind::L7Request)),
146 &[Phase::L7Request] as &[Phase],
147 );
148 }
149
150 #[test]
151 fn l7_response_middleware_accepts_only_l7_response() {
152 assert_eq!(
153 accepted_in_phases(PhaseNodeKind::Middleware(MiddlewareKind::L7Response)),
154 &[Phase::L7Response] as &[Phase],
155 );
156 }
157
158 #[test]
159 fn upgrade_accepts_both_l4_phases() {
160 assert_eq!(
163 accepted_in_phases(PhaseNodeKind::Upgrade),
164 &[Phase::L4Raw, Phase::L4Peeked] as &[Phase],
165 );
166 }
167
168 #[test]
169 fn l4_forward_fetch_accepts_l4_phases() {
170 assert_eq!(
171 accepted_in_phases(PhaseNodeKind::Fetch(FetchKind::L4Forward)),
172 &[Phase::L4Raw, Phase::L4Peeked] as &[Phase],
173 );
174 }
175
176 #[test]
177 fn http_fetches_accept_only_l7_request() {
178 for f in [
179 FetchKind::HttpProxy,
180 FetchKind::HttpSynthesize,
181 FetchKind::WebSocketUpgrade,
182 FetchKind::AcmeChallenge,
183 ] {
184 assert_eq!(accepted_in_phases(PhaseNodeKind::Fetch(f)), &[Phase::L7Request] as &[Phase],);
185 }
186 }
187
188 #[test]
189 fn write_http_response_accepts_only_l7_response() {
190 assert_eq!(
191 accepted_in_phases(PhaseNodeKind::Terminate(Terminator::WriteHttpResponse)),
192 &[Phase::L7Response] as &[Phase],
193 );
194 }
195
196 #[test]
197 fn byte_tunnel_accepts_only_tunnel() {
198 assert_eq!(
199 accepted_in_phases(PhaseNodeKind::Terminate(Terminator::ByteTunnel)),
200 &[Phase::Tunnel] as &[Phase],
201 );
202 }
203
204 #[test]
205 fn check_is_pass_through_at_every_phase() {
206 for cur in ALL_PHASES {
207 assert_eq!(transition(PhaseNodeKind::Check, cur), Ok(Transition::PassThrough));
208 }
209 }
210
211 #[test]
212 fn l4_peek_forces_out_to_l4_peeked() {
213 for cur in [Phase::L4Raw, Phase::L4Peeked] {
214 assert_eq!(
215 transition(PhaseNodeKind::Middleware(MiddlewareKind::L4Peek), cur),
216 Ok(Transition::Into(Phase::L4Peeked)),
217 );
218 }
219 }
220
221 #[test]
222 fn l4_bytes_is_pass_through_on_l4_phases() {
223 for cur in [Phase::L4Raw, Phase::L4Peeked] {
224 assert_eq!(
225 transition(PhaseNodeKind::Middleware(MiddlewareKind::L4Bytes), cur),
226 Ok(Transition::PassThrough),
227 );
228 }
229 }
230
231 #[test]
232 fn upgrade_transitions_to_l7_request_from_any_l4_phase() {
233 for cur in [Phase::L4Raw, Phase::L4Peeked] {
234 assert_eq!(transition(PhaseNodeKind::Upgrade, cur), Ok(Transition::Into(Phase::L7Request)),);
235 }
236 }
237
238 #[test]
239 fn l7_request_middleware_stays_in_l7_request() {
240 assert_eq!(
241 transition(PhaseNodeKind::Middleware(MiddlewareKind::L7Request), Phase::L7Request),
242 Ok(Transition::Into(Phase::L7Request)),
243 );
244 }
245
246 #[test]
247 fn l7_response_middleware_stays_in_l7_response() {
248 assert_eq!(
249 transition(PhaseNodeKind::Middleware(MiddlewareKind::L7Response), Phase::L7Response),
250 Ok(Transition::Into(Phase::L7Response)),
251 );
252 }
253
254 #[test]
255 fn l4_forward_fetch_goes_to_tunnel_from_any_l4_phase() {
256 for cur in [Phase::L4Raw, Phase::L4Peeked] {
257 assert_eq!(
258 transition(PhaseNodeKind::Fetch(FetchKind::L4Forward), cur),
259 Ok(Transition::Into(Phase::Tunnel)),
260 );
261 }
262 }
263
264 #[test]
265 fn http_fetch_variants_go_to_l7_response() {
266 for f in [FetchKind::HttpProxy, FetchKind::HttpSynthesize, FetchKind::AcmeChallenge] {
267 assert_eq!(
268 transition(PhaseNodeKind::Fetch(f), Phase::L7Request),
269 Ok(Transition::Into(Phase::L7Response)),
270 );
271 }
272 }
273
274 #[test]
275 fn websocket_fetch_is_bi_outcome() {
276 assert_eq!(
277 transition(PhaseNodeKind::Fetch(FetchKind::WebSocketUpgrade), Phase::L7Request),
278 Ok(Transition::BiOutcome { response: Phase::L7Response, tunnel: Phase::Tunnel }),
279 );
280 }
281
282 #[test]
283 fn write_http_response_is_terminal() {
284 assert_eq!(
285 transition(PhaseNodeKind::Terminate(Terminator::WriteHttpResponse), Phase::L7Response),
286 Ok(Transition::Terminal),
287 );
288 }
289
290 #[test]
291 fn byte_tunnel_is_terminal() {
292 assert_eq!(
293 transition(PhaseNodeKind::Terminate(Terminator::ByteTunnel), Phase::Tunnel),
294 Ok(Transition::Terminal),
295 );
296 }
297
298 #[test]
299 fn close_is_terminal_at_every_phase() {
300 for p in ALL_PHASES {
301 assert_eq!(
302 transition(PhaseNodeKind::Terminate(Terminator::Close), p),
303 Ok(Transition::Terminal),
304 );
305 }
306 }
307
308 #[test]
309 fn close_accepts_any_phase() {
310 assert_eq!(accepted_in_phases(PhaseNodeKind::Terminate(Terminator::Close)), ANY_PHASE,);
311 }
312
313 #[test]
314 fn rejects_out_of_phase_attempts() {
315 let cases: &[(PhaseNodeKind, Phase)] = &[
316 (PhaseNodeKind::Upgrade, Phase::L7Request),
317 (PhaseNodeKind::Upgrade, Phase::L7Response),
318 (PhaseNodeKind::Middleware(MiddlewareKind::L7Request), Phase::L4Raw),
319 (PhaseNodeKind::Middleware(MiddlewareKind::L7Response), Phase::L7Request),
320 (PhaseNodeKind::Fetch(FetchKind::HttpProxy), Phase::L7Response),
321 (PhaseNodeKind::Fetch(FetchKind::L4Forward), Phase::L7Request),
322 (PhaseNodeKind::Terminate(Terminator::WriteHttpResponse), Phase::Tunnel),
323 (PhaseNodeKind::Terminate(Terminator::ByteTunnel), Phase::L7Response),
324 ];
325 for (kind, cur) in cases.iter().copied() {
326 let err = transition(kind, cur).expect_err("out-of-phase must error");
327 assert_eq!(err.got, cur);
328 assert_eq!(err.expected, accepted_in_phases(kind));
329 }
330 }
331}