1use crate::model::OrderSide;
8
9const CLIENT_REQUEST_ID_MAX_LEN: usize = 32;
10#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12#[non_exhaustive]
13pub enum RequestValidationError {
14 #[error("request field `{field}` must not be empty")]
16 EmptyField {
17 field: &'static str,
19 },
20
21 #[error("request field `{field}` must be at most {max} characters")]
23 TooLong {
24 field: &'static str,
26
27 max: usize,
29 },
30
31 #[error("request field `{field}` must contain between {min} and {max} characters")]
33 LengthOutOfRange {
34 field: &'static str,
36
37 min: usize,
39
40 max: usize,
42 },
43
44 #[error("request field `{field}` has invalid format: {expected}")]
46 InvalidFormat {
47 field: &'static str,
49
50 expected: &'static str,
52 },
53
54 #[error("request field `{field}` must be between {min} and {max}")]
56 OutOfRange {
57 field: &'static str,
59
60 min: u64,
62
63 max: u64,
65 },
66
67 #[error("request field `{field}` must be between {min} and {max}")]
69 DecimalOutOfRange {
70 field: &'static str,
72
73 min: &'static str,
75
76 max: &'static str,
78 },
79
80 #[error("request field `{field}` is required when {condition}")]
82 RequiredWhen {
83 field: &'static str,
85
86 condition: &'static str,
88 },
89
90 #[error("request field `{field}` is not applicable when {condition}")]
92 NotApplicable {
93 field: &'static str,
95
96 condition: &'static str,
98 },
99
100 #[error("at least one of these request fields is required: {fields}")]
102 AtLeastOneRequired {
103 fields: &'static str,
105 },
106
107 #[error("request fields are mutually exclusive: {fields}")]
109 MutuallyExclusive {
110 fields: &'static str,
112 },
113}
114
115pub trait ValidateRequest {
120 fn validate(&self) -> Result<(), RequestValidationError>;
122}
123
124pub(crate) fn non_empty(field: &'static str, value: &str) -> Result<(), RequestValidationError> {
129 if value.trim().is_empty() {
130 return Err(RequestValidationError::EmptyField { field });
131 }
132
133 Ok(())
134}
135
136pub(crate) fn optional_non_empty(
140 field: &'static str,
141 value: Option<&str>,
142) -> Result<(), RequestValidationError> {
143 if value.is_some_and(|value| value.trim().is_empty()) {
144 return Err(RequestValidationError::EmptyField { field });
145 }
146
147 Ok(())
148}
149
150pub(crate) fn require_when(
155 field: &'static str,
156 value: Option<&str>,
157 condition: &'static str,
158) -> Result<(), RequestValidationError> {
159 match value {
160 Some(value) => non_empty(field, value),
161
162 None => Err(RequestValidationError::RequiredWhen { field, condition }),
163 }
164}
165
166pub(crate) fn reject_when_present<T>(
171 field: &'static str,
172 value: Option<&T>,
173 condition: &'static str,
174) -> Result<(), RequestValidationError> {
175 if value.is_some() {
176 return Err(RequestValidationError::NotApplicable { field, condition });
177 }
178
179 Ok(())
180}
181
182pub(crate) fn max_length(
187 field: &'static str,
188 value: &str,
189 max: usize,
190) -> Result<(), RequestValidationError> {
191 if value.chars().count() > max {
192 return Err(RequestValidationError::TooLong { field, max });
193 }
194
195 Ok(())
196}
197
198pub(crate) fn length_range(
203 field: &'static str,
204 value: &str,
205 min: usize,
206 max: usize,
207) -> Result<(), RequestValidationError> {
208 let length = value.chars().count();
209
210 if !(min..=max).contains(&length) {
211 return Err(RequestValidationError::LengthOutOfRange { field, min, max });
212 }
213
214 Ok(())
215}
216
217pub(crate) fn range_u64(
222 field: &'static str,
223 value: u64,
224 min: u64,
225 max: u64,
226) -> Result<(), RequestValidationError> {
227 if !(min..=max).contains(&value) {
228 return Err(RequestValidationError::OutOfRange { field, min, max });
229 }
230
231 Ok(())
232}
233
234pub(crate) fn one_of(
236 field: &'static str,
237 value: &str,
238 allowed: &[&str],
239 expected: &'static str,
240) -> Result<(), RequestValidationError> {
241 non_empty(field, value)?;
242 if !allowed.contains(&value) {
243 return Err(RequestValidationError::InvalidFormat { field, expected });
244 }
245 Ok(())
246}
247
248pub(crate) fn optional_one_of(
250 field: &'static str,
251 value: Option<&str>,
252 allowed: &[&str],
253 expected: &'static str,
254) -> Result<(), RequestValidationError> {
255 if let Some(value) = value {
256 one_of(field, value, allowed, expected)?;
257 }
258 Ok(())
259}
260
261pub(crate) fn unsigned_integer_string(
263 field: &'static str,
264 value: &str,
265) -> Result<(), RequestValidationError> {
266 non_empty(field, value)?;
267 if !value.bytes().all(|byte| byte.is_ascii_digit()) {
268 return Err(RequestValidationError::InvalidFormat {
269 field,
270 expected: "an unsigned integer encoded with ASCII digits",
271 });
272 }
273 Ok(())
274}
275
276pub(crate) fn optional_unsigned_integer_string(
278 field: &'static str,
279 value: Option<&str>,
280) -> Result<(), RequestValidationError> {
281 if let Some(value) = value {
282 unsigned_integer_string(field, value)?;
283 }
284 Ok(())
285}
286
287pub(crate) fn positive_decimal_string(
292 field: &'static str,
293 value: &str,
294) -> Result<(), RequestValidationError> {
295 non_empty(field, value)?;
296 let mut dot_seen = false;
297 let mut digit_seen = false;
298 let mut non_zero_seen = false;
299
300 for byte in value.bytes() {
301 match byte {
302 b'0'..=b'9' => {
303 digit_seen = true;
304 non_zero_seen |= byte != b'0';
305 }
306 b'.' if !dot_seen => dot_seen = true,
307 _ => {
308 return Err(RequestValidationError::InvalidFormat {
309 field,
310 expected: "a positive decimal string",
311 });
312 }
313 }
314 }
315
316 if !digit_seen || !non_zero_seen || value.starts_with('.') || value.ends_with('.') {
317 return Err(RequestValidationError::InvalidFormat {
318 field,
319 expected: "a positive decimal string",
320 });
321 }
322 Ok(())
323}
324
325pub(crate) fn non_negative_decimal_string(
330 field: &'static str,
331 value: &str,
332) -> Result<(), RequestValidationError> {
333 non_empty(field, value)?;
334 let mut dot_seen = false;
335 let mut digit_seen = false;
336
337 for byte in value.bytes() {
338 match byte {
339 b'0'..=b'9' => digit_seen = true,
340 b'.' if !dot_seen => dot_seen = true,
341 _ => {
342 return Err(RequestValidationError::InvalidFormat {
343 field,
344 expected: "a non-negative decimal string",
345 });
346 }
347 }
348 }
349
350 if !digit_seen || value.starts_with('.') || value.ends_with('.') {
351 return Err(RequestValidationError::InvalidFormat {
352 field,
353 expected: "a non-negative decimal string",
354 });
355 }
356 Ok(())
357}
358
359pub(crate) fn positive_unsigned_integer_string(
361 field: &'static str,
362 value: &str,
363) -> Result<(), RequestValidationError> {
364 unsigned_integer_string(field, value)?;
365 if value.bytes().all(|byte| byte == b'0') {
366 return Err(RequestValidationError::InvalidFormat {
367 field,
368 expected: "a positive integer encoded with ASCII digits",
369 });
370 }
371 Ok(())
372}
373
374pub(crate) fn optional_positive_decimal_string(
376 field: &'static str,
377 value: Option<&str>,
378) -> Result<(), RequestValidationError> {
379 if let Some(value) = value {
380 positive_decimal_string(field, value)?;
381 }
382 Ok(())
383}
384
385pub(crate) fn decimal_string_range(
387 field: &'static str,
388 value: &str,
389 min: f64,
390 max: f64,
391 min_display: &'static str,
392 max_display: &'static str,
393) -> Result<(), RequestValidationError> {
394 positive_decimal_string(field, value)?;
395 let parsed = value
396 .parse::<f64>()
397 .map_err(|_| RequestValidationError::InvalidFormat {
398 field,
399 expected: "a finite positive decimal string",
400 })?;
401 if !parsed.is_finite() || !(min..=max).contains(&parsed) {
402 return Err(RequestValidationError::DecimalOutOfRange {
403 field,
404 min: min_display,
405 max: max_display,
406 });
407 }
408 Ok(())
409}
410
411pub(crate) fn collection_length(
413 field: &'static str,
414 length: usize,
415 min: usize,
416 max: usize,
417) -> Result<(), RequestValidationError> {
418 if !(min..=max).contains(&length) {
419 return Err(RequestValidationError::LengthOutOfRange { field, min, max });
420 }
421 Ok(())
422}
423
424pub(crate) fn non_empty_items<'a>(
426 field: &'static str,
427 values: impl IntoIterator<Item = &'a str>,
428) -> Result<(), RequestValidationError> {
429 for value in values {
430 non_empty(field, value)?;
431 }
432 Ok(())
433}
434
435pub(crate) fn at_least_one(
440 fields: &'static str,
441 present: &[bool],
442) -> Result<(), RequestValidationError> {
443 if !present.iter().copied().any(|present| present) {
444 return Err(RequestValidationError::AtLeastOneRequired { fields });
445 }
446
447 Ok(())
448}
449
450pub(crate) fn at_most_one(
455 fields: &'static str,
456 present: &[bool],
457) -> Result<(), RequestValidationError> {
458 let count = present.iter().copied().filter(|present| *present).count();
459
460 if count > 1 {
461 return Err(RequestValidationError::MutuallyExclusive { fields });
462 }
463
464 Ok(())
465}
466
467pub(crate) fn exactly_one(
471 fields: &'static str,
472 present: &[bool],
473) -> Result<(), RequestValidationError> {
474 at_least_one(fields, present)?;
475 at_most_one(fields, present)
476}
477
478pub(crate) fn validate_side(side: &OrderSide) -> Result<(), RequestValidationError> {
482 match side {
483 OrderSide::Buy | OrderSide::Sell => Ok(()),
484 _ => Err(RequestValidationError::InvalidFormat {
485 field: "side",
486 expected: "buy or sell",
487 }),
488 }
489}
490
491pub(crate) fn validate_client_request_id(
496 field: &'static str,
497 value: Option<&str>,
498) -> Result<(), RequestValidationError> {
499 let Some(value) = value else {
500 return Ok(());
501 };
502
503 non_empty(field, value)?;
504 if value.chars().count() > CLIENT_REQUEST_ID_MAX_LEN {
505 return Err(RequestValidationError::TooLong {
506 field,
507 max: CLIENT_REQUEST_ID_MAX_LEN,
508 });
509 }
510 if !value.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
511 return Err(RequestValidationError::InvalidFormat {
512 field,
513 expected: "1-32 ASCII alphanumeric characters",
514 });
515 }
516
517 Ok(())
518}
519
520#[cfg(test)]
521mod tests;