1use serde::Serialize;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "lowercase")]
15pub enum PortKind {
16 Success,
18 Outcome,
21 Error,
24}
25
26#[derive(Debug, Clone, Copy, Serialize)]
28pub struct PortDecl {
29 pub name: &'static str,
30 pub kind: PortKind,
31 pub description: &'static str,
32}
33
34#[derive(Debug, Clone, Copy, Serialize)]
36pub struct PortSpec {
37 pub input: Option<&'static str>,
40 pub outputs: &'static [PortDecl],
41}
42
43const SUCCESS: PortDecl = PortDecl {
44 name: "success",
45 kind: PortKind::Success,
46 description: "The node completed normally; the request continues.",
47};
48const ERROR: PortDecl = PortDecl {
49 name: "error",
50 kind: PortKind::Error,
51 description: "The node failed (configuration, parse, or infrastructure error).",
52};
53
54pub const DEFAULT_SPEC: PortSpec = PortSpec {
56 input: Some("Request context from the previous node."),
57 outputs: &[SUCCESS, ERROR],
58};
59
60pub const LISTENER_SPEC: PortSpec = PortSpec {
62 input: None,
63 outputs: &[PortDecl {
64 name: "success",
65 kind: PortKind::Success,
66 description: "Entry into the policy pipeline.",
67 }],
68};
69
70pub const CLIENT_SPEC: PortSpec = PortSpec {
72 input: Some("Final context; the response is sent to the client."),
73 outputs: &[],
74};
75
76pub const CORS_SPEC: PortSpec = PortSpec {
78 input: Some("Request context from the previous node."),
79 outputs: &[
80 SUCCESS,
81 PortDecl {
82 name: "preflight",
83 kind: PortKind::Outcome,
84 description: "OPTIONS preflight answered with a prepared 204; wire to client.",
85 },
86 ERROR,
87 ],
88};
89
90pub const REDIRECT_SPEC: PortSpec = PortSpec {
92 input: Some("Request context from the previous node."),
93 outputs: &[
94 SUCCESS,
95 PortDecl {
96 name: "redirect",
97 kind: PortKind::Outcome,
98 description: "A 3xx redirect response is prepared; wire to client.",
99 },
100 ERROR,
101 ],
102};
103
104pub const FAULT_INJECTION_SPEC: PortSpec = PortSpec {
106 input: Some("Request context from the previous node."),
107 outputs: &[
108 SUCCESS,
109 PortDecl {
110 name: "abort",
111 kind: PortKind::Outcome,
112 description: "An injected fault response is prepared; wire to client.",
113 },
114 ERROR,
115 ],
116};
117
118pub const SCRIPT_SPEC: PortSpec = PortSpec {
123 input: Some("Request context from the previous node."),
124 outputs: &[
125 SUCCESS,
126 PortDecl {
127 name: "respond",
128 kind: PortKind::Outcome,
129 description: "The script prepared ctx.response and returned it with \"respond\"; wire to client (or a custom handler).",
130 },
131 ERROR,
132 ],
133};
134
135pub const AUTH_SPEC: PortSpec = PortSpec {
139 input: Some("Request context from the previous node."),
140 outputs: &[
141 SUCCESS,
142 PortDecl {
143 name: "denied",
144 kind: PortKind::Outcome,
145 description: "Authentication or authorization was denied; a 4xx response is prepared. Wire to client (or a custom denial handler).",
146 },
147 ERROR,
148 ],
149};
150
151pub const INTERACTIVE_AUTH_SPEC: PortSpec = PortSpec {
153 input: Some("Request context from the previous node."),
154 outputs: &[
155 SUCCESS,
156 PortDecl {
157 name: "denied",
158 kind: PortKind::Outcome,
159 description: "Authentication was denied; a 4xx response is prepared. Wire to client.",
160 },
161 PortDecl {
162 name: "redirect",
163 kind: PortKind::Outcome,
164 description: "The browser must move (login/logout/callback 3xx); response is prepared. Wire to client.",
165 },
166 ERROR,
167 ],
168};
169
170pub const DENY_SPEC: PortSpec = PortSpec {
176 input: Some("Request context from the previous node."),
177 outputs: &[
178 SUCCESS,
179 PortDecl {
180 name: "denied",
181 kind: PortKind::Outcome,
182 description: "The request was denied by a policy rule; a 4xx response is prepared. Wire to client.",
183 },
184 ERROR,
185 ],
186};
187
188pub const LIMIT_SPEC: PortSpec = PortSpec {
191 input: Some("Request context from the previous node."),
192 outputs: &[
193 SUCCESS,
194 PortDecl {
195 name: "limited",
196 kind: PortKind::Outcome,
197 description:
198 "The request exceeded a traffic limit; a 429 response is prepared. Wire to client.",
199 },
200 ERROR,
201 ],
202};
203
204pub const BREAKER_SPEC: PortSpec = PortSpec {
207 input: Some("Request context from the previous node."),
208 outputs: &[
209 SUCCESS,
210 PortDecl {
211 name: "broken",
212 kind: PortKind::Outcome,
213 description:
214 "The circuit breaker is open; the break response is prepared. Wire to client.",
215 },
216 ERROR,
217 ],
218};
219
220pub const WORKFLOW_SPEC: PortSpec = PortSpec {
223 input: Some("Request context from the previous node."),
224 outputs: &[
225 SUCCESS,
226 PortDecl {
227 name: "denied",
228 kind: PortKind::Outcome,
229 description: "A 'return' rule rejected the request; the configured response is prepared. Wire to client.",
230 },
231 PortDecl {
232 name: "limited",
233 kind: PortKind::Outcome,
234 description: "A 'limit-count' rule's quota was exceeded; a rejection response is prepared. Wire to client.",
235 },
236 ERROR,
237 ],
238};
239
240pub const TRAFFIC_SPLIT_SPEC: PortSpec = PortSpec {
245 input: Some("Request context from the previous node."),
246 outputs: &[
247 SUCCESS,
248 PortDecl {
249 name: "routed",
250 kind: PortKind::Outcome,
251 description:
252 "The request was steered to and served by a weighted split target; wire to client.",
253 },
254 ERROR,
255 ],
256};
257
258pub const CONDITION_SPEC: PortSpec = PortSpec {
264 input: Some("Request context from the previous node."),
265 outputs: &[
266 PortDecl {
267 name: "true",
268 kind: PortKind::Outcome,
269 description: "The conditions evaluated to true.",
270 },
271 PortDecl {
272 name: "false",
273 kind: PortKind::Outcome,
274 description: "The conditions evaluated to false.",
275 },
276 ERROR,
277 ],
278};
279
280pub const PROXY_CACHE_SPEC: PortSpec = PortSpec {
284 input: Some("Request context from the previous node."),
285 outputs: &[
286 SUCCESS,
287 PortDecl {
288 name: "hit",
289 kind: PortKind::Outcome,
290 description: "The response was served from cache; wire to client.",
291 },
292 ERROR,
293 ],
294};
295
296pub const STORE_GET_SPEC: PortSpec = PortSpec {
300 input: Some("Request context from the previous node."),
301 outputs: &[
302 SUCCESS,
303 PortDecl {
304 name: "miss",
305 kind: PortKind::Outcome,
306 description: "The key does not exist; nothing was written to context.message. Wire to whatever should happen on first sight.",
307 },
308 ERROR,
309 ],
310};
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 const RESERVED_PORT_NAMES: &[&str] = &["in", "out", "success", "error"];
320
321 #[test]
324 fn test_every_known_type_has_a_valid_spec() {
325 for ty in crate::plugins::KNOWN_PLUGIN_TYPES {
326 let spec =
327 crate::plugins::port_spec(ty).unwrap_or_else(|| panic!("no port spec for '{ty}'"));
328 for p in spec.outputs {
329 if p.name != "success" && p.name != "error" {
330 assert!(
331 !RESERVED_PORT_NAMES.contains(&p.name),
332 "'{ty}' declares reserved port '{}'",
333 p.name
334 );
335 assert!(
336 p.name.chars().all(|c| c.is_ascii_lowercase() || c == '-'),
337 "'{ty}' port '{}' is not lowercase-kebab",
338 p.name
339 );
340 assert!(
341 matches!(p.kind, PortKind::Outcome),
342 "'{ty}' custom port '{}' must be kind outcome",
343 p.name
344 );
345 }
346 assert!(
347 !p.description.is_empty(),
348 "'{ty}' port '{}' lacks description",
349 p.name
350 );
351 }
352 }
353 }
354
355 #[test]
356 fn test_structural_specs() {
357 let l = crate::plugins::port_spec("listener").unwrap();
358 assert!(l.input.is_none());
359 assert_eq!(l.outputs.len(), 1);
360 assert_eq!(l.outputs[0].name, "success");
361
362 let c = crate::plugins::port_spec("client").unwrap();
363 assert!(c.input.is_some());
364 assert!(c.outputs.is_empty());
365
366 assert!(crate::plugins::port_spec("no-such-type").is_none());
367 }
368}