1use std::fmt;
2
3use serde::{Deserialize, Serialize, de::DeserializeOwned};
4
5pub trait MutationForm: DeserializeOwned {
12 type Request: DeserializeOwned;
14
15 fn into_request(self) -> Result<Self::Request, String>;
18}
19
20fn config_entries_from_form(value: &str) -> Result<Vec<ConfigEntryDto>, String> {
25 value
26 .split(',')
27 .map(str::trim)
28 .filter(|entry| !entry.is_empty())
29 .map(|entry| {
30 let Some((name, value)) = entry.split_once('=') else {
31 return Err(format!("config entry \"{entry}\" must read name=value"));
32 };
33
34 Ok(ConfigEntryDto {
35 name: name.trim().to_string(),
36 value: value.trim().to_string(),
37 })
38 })
39 .collect()
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct ConfigEntryDto {
44 pub name: String,
45 pub value: String,
46}
47
48impl ConfigEntryDto {
49 pub fn validate(&self) -> Result<(), String> {
52 ensure_not_blank("config name", &self.name)
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct CreateTopicRequestDto {
58 pub name: String,
59 pub partitions: i32,
60 pub replicas: i32,
61 pub configs: Vec<ConfigEntryDto>,
62}
63
64impl CreateTopicRequestDto {
65 pub fn validate(&self) -> Result<(), String> {
68 ensure_not_blank("topic name", &self.name)?;
69 ensure_positive("partition count", self.partitions)?;
70 ensure_positive("replica count", self.replicas)?;
71
72 for config in &self.configs {
73 config.validate()?;
74 }
75
76 Ok(())
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct DeleteTopicRequestDto {
82 pub name: String,
83}
84
85impl DeleteTopicRequestDto {
86 pub fn validate(&self) -> Result<(), String> {
89 ensure_not_blank("topic name", &self.name)
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct CreatePartitionsRequestDto {
95 pub topic: String,
96 pub total_count: i32,
97}
98
99impl CreatePartitionsRequestDto {
100 pub fn validate(&self) -> Result<(), String> {
103 ensure_not_blank("topic name", &self.topic)?;
104 ensure_positive("partition count", self.total_count)
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct AlterConfigRequestDto {
110 pub resource_type: String,
111 pub resource_name: String,
112 pub configs: Vec<ConfigEntryDto>,
113}
114
115impl AlterConfigRequestDto {
116 pub fn validate(&self) -> Result<(), String> {
119 ensure_not_blank("config resource type", &self.resource_type)?;
120 ensure_not_blank("config resource name", &self.resource_name)?;
121
122 for config in &self.configs {
123 config.validate()?;
124 }
125
126 Ok(())
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct AclRequestDto {
132 pub resource_type: String,
133 pub resource_name: String,
134 pub pattern_type: String,
137 pub principal: String,
138 pub operation: String,
139 pub permission: String,
140 pub host: String,
141}
142
143impl AclRequestDto {
144 pub fn validate(&self) -> Result<(), String> {
147 ensure_not_blank("ACL resource type", &self.resource_type)?;
148 ensure_not_blank("ACL resource name", &self.resource_name)?;
149 ensure_not_blank("ACL pattern type", &self.pattern_type)?;
150 ensure_not_blank("ACL principal", &self.principal)?;
151 ensure_not_blank("ACL operation", &self.operation)?;
152 ensure_not_blank("ACL permission", &self.permission)?;
153 ensure_not_blank("ACL host", &self.host)
154 }
155}
156
157#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub struct ScramUserUpsertDto {
159 pub username: String,
160 pub password: String,
161 pub iterations: i32,
162}
163
164impl ScramUserUpsertDto {
165 pub fn validate(&self) -> Result<(), String> {
168 ensure_not_blank("SCRAM username", &self.username)?;
169 ensure_not_blank("SCRAM password", &self.password)?;
170 ensure_positive("SCRAM iterations", self.iterations)
171 }
172}
173
174impl fmt::Debug for ScramUserUpsertDto {
175 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 formatter
177 .debug_struct("ScramUserUpsertDto")
178 .field("username", &self.username)
179 .field("password", &"<redacted>")
180 .field("iterations", &self.iterations)
181 .finish()
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct ScramUserDeleteDto {
187 pub username: String,
188}
189
190impl ScramUserDeleteDto {
191 pub fn validate(&self) -> Result<(), String> {
194 ensure_not_blank("SCRAM username", &self.username)
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199pub struct QuotaUpsertDto {
200 pub entity: String,
201 pub quota_type: String,
202 pub value: f64,
203}
204
205impl QuotaUpsertDto {
206 pub fn validate(&self) -> Result<(), String> {
209 ensure_not_blank("quota entity", &self.entity)?;
210 ensure_not_blank("quota type", &self.quota_type)?;
211 ensure_finite("quota value", self.value)
212 }
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub struct QuotaDeleteDto {
217 pub entity: String,
218 pub quota_type: String,
219}
220
221impl QuotaDeleteDto {
222 pub fn validate(&self) -> Result<(), String> {
225 ensure_not_blank("quota entity", &self.entity)?;
226 ensure_not_blank("quota type", &self.quota_type)
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231pub struct LogDirMoveRequestDto {
232 pub topic: String,
233 pub partition: i32,
234 pub broker_id: i32,
236 pub destination_log_dir: String,
237}
238
239impl LogDirMoveRequestDto {
240 pub fn validate(&self) -> Result<(), String> {
243 ensure_not_blank("log-dir move topic", &self.topic)?;
244 ensure_nonnegative("log-dir move partition", self.partition)?;
245 ensure_nonnegative("log-dir move broker id", self.broker_id)?;
246 ensure_not_blank("destination log dir", &self.destination_log_dir)
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct CreateTopicFormDto {
253 pub name: String,
254 pub partitions: i32,
255 pub replicas: i32,
256 pub configs: String,
258}
259
260impl MutationForm for CreateTopicFormDto {
261 type Request = CreateTopicRequestDto;
262
263 fn into_request(self) -> Result<Self::Request, String> {
264 Ok(CreateTopicRequestDto {
265 name: self.name,
266 partitions: self.partitions,
267 replicas: self.replicas,
268 configs: config_entries_from_form(&self.configs)?,
269 })
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
275pub struct AlterConfigFormDto {
276 pub resource_type: String,
277 pub resource_name: String,
278 pub configs: String,
280}
281
282impl MutationForm for AlterConfigFormDto {
283 type Request = AlterConfigRequestDto;
284
285 fn into_request(self) -> Result<Self::Request, String> {
286 Ok(AlterConfigRequestDto {
287 resource_type: self.resource_type,
288 resource_name: self.resource_name,
289 configs: config_entries_from_form(&self.configs)?,
290 })
291 }
292}
293
294macro_rules! flat_mutation_form {
295 ($($request:ty),+ $(,)?) => {
296 $(
297 impl MutationForm for $request {
298 type Request = Self;
299
300 fn into_request(self) -> Result<Self::Request, String> {
301 Ok(self)
302 }
303 }
304 )+
305 };
306}
307
308flat_mutation_form!(
309 DeleteTopicRequestDto,
310 CreatePartitionsRequestDto,
311 AclRequestDto,
312 ScramUserUpsertDto,
313 ScramUserDeleteDto,
314 QuotaUpsertDto,
315 QuotaDeleteDto,
316 LogDirMoveRequestDto,
317);
318
319fn ensure_not_blank(field: &str, value: &str) -> Result<(), String> {
320 if value.trim().is_empty() {
321 return Err(format!("{field} must not be blank"));
322 }
323
324 Ok(())
325}
326
327fn ensure_positive(field: &str, value: i32) -> Result<(), String> {
328 if value <= 0 {
329 return Err(format!("{field} must be positive"));
330 }
331
332 Ok(())
333}
334
335fn ensure_nonnegative(field: &str, value: i32) -> Result<(), String> {
336 if value < 0 {
337 return Err(format!("{field} must be nonnegative"));
338 }
339
340 Ok(())
341}
342
343fn ensure_finite(field: &str, value: f64) -> Result<(), String> {
344 if !value.is_finite() {
345 return Err(format!("{field} must be finite"));
346 }
347
348 Ok(())
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352pub struct KafkaErrorDto {
353 pub code: i16,
354 pub name: String,
355 pub message: Option<String>,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
359pub struct ResourceOutcome {
360 pub resource: String,
361 pub error: Option<KafkaErrorDto>,
362}
363
364impl ResourceOutcome {
365 #[must_use]
366 pub fn ok(resource: impl Into<String>) -> Self {
367 Self {
368 resource: resource.into(),
369 error: None,
370 }
371 }
372
373 #[must_use]
374 pub fn failed(resource: impl Into<String>, error: KafkaErrorDto) -> Self {
375 Self {
376 resource: resource.into(),
377 error: Some(error),
378 }
379 }
380
381 #[must_use]
382 pub const fn has_error(&self) -> bool {
383 self.error.is_some()
384 }
385}
386
387#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub struct TopicRow {
389 pub name: String,
390 pub topic_id: Option<String>,
391 pub partition_count: i32,
392 pub replication_factor: i32,
393 pub error: Option<KafkaErrorDto>,
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397pub struct GroupRow {
398 pub group_id: String,
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402pub struct AclRow {
403 pub resource: String,
404 pub pattern_type: String,
405 pub principal: String,
406 pub host: String,
409 pub operation: String,
410 pub permission: String,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
414pub struct UserRow {
415 pub username: String,
416 pub principal: String,
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
420pub struct QuotaRow {
421 pub entity: String,
422 pub quota_type: String,
423 pub value: String,
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427pub struct LogDirRow {
428 pub broker_id: i32,
429 pub log_dir: String,
430 pub topic: String,
431 pub partition: i32,
432 pub partition_size: i64,
433 pub offset_lag: i64,
434 pub is_future_key: bool,
435 pub error: Option<KafkaErrorDto>,
436}