1use serde::{Deserialize, Deserializer, Serialize};
5use uuid::Uuid;
6
7pub const MAX_OBJECTIVE_BYTES: usize = 16 * 1024;
8pub const MAX_ITERATIONS: u32 = 100;
9pub const MAX_TOTAL_ROUNDS: u32 = 1000;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
17#[serde(tag = "action", rename_all = "snake_case")]
18pub enum LoopControlRequest {
19 Status,
20 Start {
21 operation_id: String,
22 expected_generation: Option<String>,
23 expected_revision: u64,
24 objective: String,
25 max_iterations: u32,
26 max_total_rounds: u32,
27 },
28 Update {
29 operation_id: String,
30 generation: String,
31 expected_revision: u64,
32 objective: Option<String>,
33 max_iterations: Option<u32>,
34 max_total_rounds: Option<u32>,
35 },
36 Stop {
37 operation_id: String,
38 generation: String,
39 expected_revision: u64,
40 },
41}
42
43#[derive(Deserialize)]
47#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
48enum LoopControlWireRequest {
49 Status {},
50 Start {
51 operation_id: String,
52 #[serde(default)]
53 expected_generation: Option<String>,
54 expected_revision: u64,
55 objective: String,
56 max_iterations: u32,
57 max_total_rounds: u32,
58 },
59 Update {
60 operation_id: String,
61 generation: String,
62 expected_revision: u64,
63 #[serde(default)]
64 objective: Option<String>,
65 #[serde(default)]
66 max_iterations: Option<u32>,
67 #[serde(default)]
68 max_total_rounds: Option<u32>,
69 },
70 Stop {
71 operation_id: String,
72 generation: String,
73 expected_revision: u64,
74 },
75}
76
77impl<'de> Deserialize<'de> for LoopControlRequest {
78 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
79 where
80 D: Deserializer<'de>,
81 {
82 Ok(match LoopControlWireRequest::deserialize(deserializer)? {
83 LoopControlWireRequest::Status {} => Self::Status,
84 LoopControlWireRequest::Start {
85 operation_id,
86 expected_generation,
87 expected_revision,
88 objective,
89 max_iterations,
90 max_total_rounds,
91 } => Self::Start {
92 operation_id,
93 expected_generation,
94 expected_revision,
95 objective,
96 max_iterations,
97 max_total_rounds,
98 },
99 LoopControlWireRequest::Update {
100 operation_id,
101 generation,
102 expected_revision,
103 objective,
104 max_iterations,
105 max_total_rounds,
106 } => Self::Update {
107 operation_id,
108 generation,
109 expected_revision,
110 objective,
111 max_iterations,
112 max_total_rounds,
113 },
114 LoopControlWireRequest::Stop {
115 operation_id,
116 generation,
117 expected_revision,
118 } => Self::Stop {
119 operation_id,
120 generation,
121 expected_revision,
122 },
123 })
124 }
125}
126
127impl LoopControlRequest {
128 pub fn action(&self) -> &'static str {
129 match self {
130 Self::Status => "status",
131 Self::Start { .. } => "start",
132 Self::Update { .. } => "update",
133 Self::Stop { .. } => "stop",
134 }
135 }
136
137 pub fn validate(&self) -> Result<(), String> {
138 match self {
139 Self::Status => Ok(()),
140 Self::Start {
141 operation_id,
142 expected_generation,
143 objective,
144 max_iterations,
145 max_total_rounds,
146 ..
147 } => {
148 validate_uuid("operation_id", operation_id)?;
149 if let Some(generation) = expected_generation {
150 validate_uuid("expected_generation", generation)?;
151 }
152 validate_objective(objective)?;
153 validate_bound("max_iterations", *max_iterations, MAX_ITERATIONS)?;
154 validate_bound("max_total_rounds", *max_total_rounds, MAX_TOTAL_ROUNDS)
155 }
156 Self::Update {
157 operation_id,
158 generation,
159 objective,
160 max_iterations,
161 max_total_rounds,
162 ..
163 } => {
164 validate_uuid("operation_id", operation_id)?;
165 validate_uuid("generation", generation)?;
166 if objective.is_none() && max_iterations.is_none() && max_total_rounds.is_none() {
167 return Err("loop_control.update requires at least one patch field".into());
168 }
169 if let Some(objective) = objective {
170 validate_objective(objective)?;
171 }
172 if let Some(max_iterations) = max_iterations {
173 validate_bound("max_iterations", *max_iterations, MAX_ITERATIONS)?;
174 }
175 if let Some(max_total_rounds) = max_total_rounds {
176 validate_bound("max_total_rounds", *max_total_rounds, MAX_TOTAL_ROUNDS)?;
177 }
178 Ok(())
179 }
180 Self::Stop {
181 operation_id,
182 generation,
183 ..
184 } => {
185 validate_uuid("operation_id", operation_id)?;
186 validate_uuid("generation", generation)
187 }
188 }
189 }
190}
191
192fn validate_uuid(field: &str, value: &str) -> Result<(), String> {
193 Uuid::parse_str(value)
194 .map(|_| ())
195 .map_err(|_| format!("loop_control.{field} must be a valid UUID"))
196}
197
198fn validate_objective(objective: &str) -> Result<(), String> {
199 if objective.trim().is_empty() || objective.len() > MAX_OBJECTIVE_BYTES {
200 return Err(format!(
201 "loop_control.objective must contain non-whitespace text and be at most {MAX_OBJECTIVE_BYTES} UTF-8 bytes"
202 ));
203 }
204 Ok(())
205}
206
207fn validate_bound(field: &str, value: u32, ceiling: u32) -> Result<(), String> {
208 if !(1..=ceiling).contains(&value) {
209 return Err(format!(
210 "loop_control.{field} must be between 1 and {ceiling}"
211 ));
212 }
213 Ok(())
214}
215
216pub fn parse_loop_control_request(arguments: &str) -> Result<LoopControlRequest, String> {
218 let request: LoopControlRequest = serde_json::from_str(arguments)
219 .map_err(|error| format!("invalid loop_control arguments: {error}"))?;
220 request.validate()?;
221 Ok(request)
222}