1use std::fmt;
17
18use regress::Regex;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub enum Scalar {
27 Text(Text),
29 Integer(Bounds<i64>),
31 Number(Bounds<f64>),
33 Boolean,
35 Choice(Vec<String>),
37}
38
39#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct Text {
46 pub pattern: Option<String>,
48 pub min_length: Option<usize>,
50 pub max_length: Option<usize>,
52}
53
54#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
56pub struct Bounds<T> {
57 pub low: Option<Limit<T>>,
59 pub high: Option<Limit<T>>,
61 pub multiple_of: Option<T>,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub enum Limit<T> {
72 Inclusive(T),
74 Exclusive(T),
76}
77
78#[derive(Debug, Clone, Error, PartialEq, Eq)]
80pub enum ScalarError {
81 #[error("`{raw}` is not {wanted}")]
83 Kind { raw: String, wanted: &'static str },
84 #[error("`{raw}` is not one of {}", .allowed.join(", "))]
87 Choice { raw: String, allowed: Vec<String> },
88 #[error("`{raw}` {rule}")]
93 Rule { raw: String, rule: String },
94 #[error("`{pattern}` is not a regular expression: {message}")]
98 Pattern { pattern: String, message: String },
99}
100
101impl ScalarError {
102 fn rule(raw: &str, rule: String) -> Self {
104 Self::Rule {
105 raw: raw.to_owned(),
106 rule,
107 }
108 }
109}
110
111impl Scalar {
112 pub fn parse(&self, raw: &str) -> Result<serde_json::Value, ScalarError> {
115 match self {
116 Self::Text(text) => text.check(raw).map(|()| raw.into()),
117 Self::Integer(bounds) => bounded(bounds, raw),
118 Self::Number(bounds) => bounded(bounds, raw),
119 Self::Boolean => raw
120 .parse::<bool>()
121 .map(Into::into)
122 .map_err(|_| ScalarError::Kind {
123 raw: raw.to_owned(),
124 wanted: "`true` or `false`",
125 }),
126 Self::Choice(allowed) => {
127 if allowed.iter().any(|value| value == raw) {
128 Ok(raw.into())
129 } else {
130 Err(ScalarError::Choice {
131 raw: raw.to_owned(),
132 allowed: allowed.clone(),
133 })
134 }
135 }
136 }
137 }
138
139 pub fn runnable(&self) -> Result<(), ScalarError> {
146 match self {
147 Self::Text(text) => text.pattern.as_deref().map(compiled).transpose().map(drop),
148 Self::Integer(_) | Self::Number(_) | Self::Boolean | Self::Choice(_) => Ok(()),
149 }
150 }
151
152 #[must_use]
155 pub fn value_name(&self) -> &'static str {
156 match self {
157 Self::Text(_) | Self::Choice(_) => "STRING",
158 Self::Integer(_) => "INT",
159 Self::Number(_) => "NUMBER",
160 Self::Boolean => "BOOL",
161 }
162 }
163
164 #[must_use]
169 pub fn note(&self) -> Option<String> {
170 let notes = match self {
171 Self::Text(text) => text.notes(),
172 Self::Integer(bounds) => bounds.notes(),
173 Self::Number(bounds) => bounds.notes(),
174 Self::Boolean | Self::Choice(_) => Vec::new(),
175 };
176 (!notes.is_empty()).then(|| notes.join("; "))
177 }
178}
179
180impl Text {
181 fn check(&self, raw: &str) -> Result<(), ScalarError> {
183 let length = raw.chars().count();
185 if let Some(least) = self.min_length
186 && length < least
187 {
188 return Err(ScalarError::rule(
189 raw,
190 format!("is shorter than {least} characters"),
191 ));
192 }
193 if let Some(most) = self.max_length
194 && length > most
195 {
196 return Err(ScalarError::rule(
197 raw,
198 format!("is longer than {most} characters"),
199 ));
200 }
201 let Some(pattern) = self.pattern.as_deref() else {
202 return Ok(());
203 };
204 if compiled(pattern)?.find(raw).is_none() {
208 return Err(ScalarError::rule(raw, format!("does not match {pattern}")));
209 }
210 Ok(())
211 }
212
213 fn notes(&self) -> Vec<String> {
214 let mut notes = Vec::new();
215 if let Some(least) = self.min_length {
216 notes.push(format!("at least {least} characters"));
217 }
218 if let Some(most) = self.max_length {
219 notes.push(format!("at most {most} characters"));
220 }
221 if let Some(pattern) = &self.pattern {
222 notes.push(format!("matches {pattern}"));
223 }
224 notes
225 }
226}
227
228impl<T: Numeric> Bounds<T> {
229 fn check(&self, value: T, raw: &str) -> Result<(), ScalarError> {
231 let refuse = |rule: String| ScalarError::rule(raw, format!("is not {rule}"));
232 if let Some(low) = self.low {
233 low.floor(value).map_err(&refuse)?;
234 }
235 if let Some(high) = self.high {
236 high.ceiling(value).map_err(&refuse)?;
237 }
238 if let Some(step) = self.multiple_of
239 && !value.divisible_by(step)
240 {
241 return Err(refuse(format!("a multiple of {step}")));
242 }
243 Ok(())
244 }
245
246 fn notes(&self) -> Vec<String> {
247 let mut notes = Vec::new();
248 if let Some(low) = self.low {
249 notes.push(low.note("at least", "more than"));
250 }
251 if let Some(high) = self.high {
252 notes.push(high.note("at most", "less than"));
253 }
254 if let Some(step) = self.multiple_of {
255 notes.push(format!("a multiple of {step}"));
256 }
257 notes
258 }
259}
260
261impl<T: Numeric> Limit<T> {
262 fn floor(self, value: T) -> Result<(), String> {
266 let admits = match self {
267 Self::Inclusive(limit) => value >= limit,
268 Self::Exclusive(limit) => value > limit,
269 };
270 if admits {
271 Ok(())
272 } else {
273 Err(self.note("at least", "more than"))
274 }
275 }
276
277 fn ceiling(self, value: T) -> Result<(), String> {
279 let admits = match self {
280 Self::Inclusive(limit) => value <= limit,
281 Self::Exclusive(limit) => value < limit,
282 };
283 if admits {
284 Ok(())
285 } else {
286 Err(self.note("at most", "less than"))
287 }
288 }
289
290 fn note(self, inclusive: &str, exclusive: &str) -> String {
291 match self {
292 Self::Inclusive(limit) => format!("{inclusive} {limit}"),
293 Self::Exclusive(limit) => format!("{exclusive} {limit}"),
294 }
295 }
296}
297
298fn compiled(pattern: &str) -> Result<Regex, ScalarError> {
300 Regex::new(pattern).map_err(|error| ScalarError::Pattern {
301 pattern: pattern.to_owned(),
302 message: error.to_string(),
303 })
304}
305
306fn bounded<T: Numeric>(bounds: &Bounds<T>, raw: &str) -> Result<serde_json::Value, ScalarError> {
308 let (value, json) = T::read(raw).ok_or_else(|| ScalarError::Kind {
309 raw: raw.to_owned(),
310 wanted: T::KIND,
311 })?;
312 bounds.check(value, raw)?;
313 Ok(json)
314}
315
316pub trait Numeric: Copy + PartialOrd + fmt::Display {
324 const KIND: &'static str;
326
327 fn read(raw: &str) -> Option<(Self, serde_json::Value)>;
333
334 fn divisible_by(self, step: Self) -> bool;
336}
337
338impl Numeric for i64 {
339 const KIND: &'static str = "an integer";
340
341 fn read(raw: &str) -> Option<(Self, serde_json::Value)> {
342 raw.parse::<Self>().ok().map(|value| (value, value.into()))
343 }
344
345 fn divisible_by(self, step: Self) -> bool {
346 step != 0 && self % step == 0
347 }
348}
349
350impl Numeric for f64 {
351 const KIND: &'static str = "a number";
352
353 fn read(raw: &str) -> Option<(Self, serde_json::Value)> {
354 let value = raw.parse::<Self>().ok()?;
355 Some((value, serde_json::Number::from_f64(value)?.into()))
356 }
357
358 fn divisible_by(self, step: Self) -> bool {
361 step != 0.0 && (self / step).fract() == 0.0
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 fn text(pattern: &str) -> Scalar {
370 Scalar::Text(Text {
371 pattern: Some(pattern.to_owned()),
372 ..Text::default()
373 })
374 }
375
376 #[test]
377 fn a_pattern_is_enforced_and_reads_back_in_the_documents_own_spelling() {
378 let amount = text(r"^-?[0-9]+(\.[0-9]{1,2})?$");
379 for raw in ["12.50", "0", "0.0", "-3.07", "1000000.00"] {
380 assert_eq!(amount.parse(raw), Ok(raw.into()), "{raw}");
381 }
382 assert_eq!(
383 amount.parse("1,50"),
384 Err(ScalarError::Rule {
385 raw: "1,50".to_owned(),
386 rule: r"does not match ^-?[0-9]+(\.[0-9]{1,2})?$".to_owned(),
387 })
388 );
389 for raw in ["12.5x", "", "12.505", ".5", "-", "1e3"] {
390 assert!(amount.parse(raw).is_err(), "{raw}");
391 }
392 }
393
394 #[test]
397 fn an_unanchored_pattern_matches_anywhere_in_the_value() {
398 let digits = text("[0-9]+");
399 assert!(digits.parse("ab12cd").is_ok());
400 assert!(digits.parse("abcd").is_err());
401 assert!(text("^[A-Z]+$").parse("xyZ").is_err());
402 }
403
404 #[test]
406 fn a_pattern_the_engine_cannot_read_refuses_every_value() {
407 let broken = text("[unterminated");
408 assert!(matches!(
409 broken.runnable(),
410 Err(ScalarError::Pattern { .. })
411 ));
412 assert!(matches!(
413 broken.parse("anything"),
414 Err(ScalarError::Pattern { .. })
415 ));
416 assert_eq!(text("^ok$").runnable(), Ok(()));
417 }
418
419 #[test]
420 fn lengths_are_counted_in_characters_and_refused_with_the_documents_number() {
421 let code = Scalar::Text(Text {
422 min_length: Some(3),
423 max_length: Some(3),
424 ..Text::default()
425 });
426 assert!(code.parse("EUR").is_ok());
427 assert!(code.parse("€€€").is_ok());
429 assert_eq!(
430 code.parse("EU"),
431 Err(ScalarError::Rule {
432 raw: "EU".to_owned(),
433 rule: "is shorter than 3 characters".to_owned(),
434 })
435 );
436 assert_eq!(
437 code.parse("EURO"),
438 Err(ScalarError::Rule {
439 raw: "EURO".to_owned(),
440 rule: "is longer than 3 characters".to_owned(),
441 })
442 );
443 }
444
445 #[test]
446 fn an_inclusive_bound_admits_its_own_value_and_an_exclusive_one_does_not() {
447 let inclusive = Scalar::Integer(Bounds {
448 low: Some(Limit::Inclusive(1)),
449 high: Some(Limit::Inclusive(100)),
450 multiple_of: None,
451 });
452 assert!(inclusive.parse("1").is_ok());
453 assert!(inclusive.parse("100").is_ok());
454 assert_eq!(
455 inclusive.parse("0"),
456 Err(ScalarError::Rule {
457 raw: "0".to_owned(),
458 rule: "is not at least 1".to_owned(),
459 })
460 );
461 assert_eq!(
462 inclusive.parse("101"),
463 Err(ScalarError::Rule {
464 raw: "101".to_owned(),
465 rule: "is not at most 100".to_owned(),
466 })
467 );
468
469 let exclusive = Scalar::Number(Bounds {
470 low: Some(Limit::Exclusive(0.0)),
471 high: Some(Limit::Exclusive(1.0)),
472 multiple_of: None,
473 });
474 assert!(exclusive.parse("0.5").is_ok());
475 assert_eq!(
476 exclusive.parse("0"),
477 Err(ScalarError::Rule {
478 raw: "0".to_owned(),
479 rule: "is not more than 0".to_owned(),
480 })
481 );
482 assert_eq!(
483 exclusive.parse("1"),
484 Err(ScalarError::Rule {
485 raw: "1".to_owned(),
486 rule: "is not less than 1".to_owned(),
487 })
488 );
489 }
490
491 #[test]
492 fn a_step_is_enforced_for_both_number_kinds() {
493 let by_five = Scalar::Integer(Bounds {
494 multiple_of: Some(5),
495 ..Bounds::default()
496 });
497 assert!(by_five.parse("15").is_ok());
498 assert_eq!(
499 by_five.parse("7"),
500 Err(ScalarError::Rule {
501 raw: "7".to_owned(),
502 rule: "is not a multiple of 5".to_owned(),
503 })
504 );
505
506 let by_quarter = Scalar::Number(Bounds {
507 multiple_of: Some(0.25),
508 ..Bounds::default()
509 });
510 assert!(by_quarter.parse("1.75").is_ok());
511 assert!(by_quarter.parse("1.3").is_err());
512 }
513
514 #[test]
515 fn a_choice_outside_the_enum_is_rejected_with_the_alternatives() {
516 let status = Scalar::Choice(vec!["draft".to_owned(), "paid".to_owned()]);
517 assert_eq!(
518 status.parse("void"),
519 Err(ScalarError::Choice {
520 raw: "void".to_owned(),
521 allowed: vec!["draft".to_owned(), "paid".to_owned()],
522 })
523 );
524 assert!(status.parse("paid").is_ok());
525 }
526
527 #[test]
528 fn integers_stay_integers_in_json() {
529 let plain = Scalar::Integer(Bounds::default());
530 assert_eq!(plain.parse("5"), Ok(serde_json::json!(5)));
531 assert_eq!(
532 plain.parse("5.0"),
533 Err(ScalarError::Kind {
534 raw: "5.0".to_owned(),
535 wanted: "an integer",
536 })
537 );
538 }
539
540 #[test]
543 fn a_number_json_cannot_carry_is_refused_rather_than_nulled() {
544 let plain = Scalar::Number(Bounds::default());
545 assert_eq!(plain.parse("1.5"), Ok(serde_json::json!(1.5)));
546 for raw in ["inf", "-inf", "NaN"] {
547 assert_eq!(
548 plain.parse(raw),
549 Err(ScalarError::Kind {
550 raw: raw.to_owned(),
551 wanted: "a number",
552 }),
553 "{raw}"
554 );
555 }
556 }
557
558 #[test]
561 fn every_note_is_a_rule_that_is_enforced() {
562 let code = Scalar::Text(Text {
563 pattern: Some("^[A-Z]+$".to_owned()),
564 min_length: Some(2),
565 max_length: Some(4),
566 });
567 assert_eq!(
568 code.note().as_deref(),
569 Some("at least 2 characters; at most 4 characters; matches ^[A-Z]+$")
570 );
571 assert!(code.parse("A").is_err());
572 assert!(code.parse("ABCDE").is_err());
573 assert!(code.parse("ab").is_err());
574 assert!(code.parse("AB").is_ok());
575
576 let count = Scalar::Integer(Bounds {
577 low: Some(Limit::Inclusive(1)),
578 high: Some(Limit::Exclusive(10)),
579 multiple_of: Some(3),
580 });
581 assert_eq!(
582 count.note().as_deref(),
583 Some("at least 1; less than 10; a multiple of 3")
584 );
585 assert_eq!(Scalar::Boolean.note(), None);
586 assert_eq!(Scalar::Text(Text::default()).note(), None);
587 }
588}