hecate/codegen/input_schema/
quantity.rs1#![allow(unused)]
2use super::RawRepr;
3use super::unit::{FormatUnitError, format_unit};
4use crate::StdError;
5use const_format::{concatcp, formatcp};
6use derive_more::{Deref, DerefMut};
7use dyn_clone::DynClone;
8use itertools::Itertools;
9use lazy_static::lazy_static;
10use regex::Regex;
11use schemars::{JsonSchema, Schema, json_schema};
12use serde::de::Visitor;
13use serde::{Deserialize, Serialize, de::Error};
14use serde_yaml::Value;
15use std::borrow::Cow;
16use std::collections::HashMap;
17use std::fmt::{Debug, Display};
18use std::ops::Deref;
19use std::str::FromStr;
20use std::sync::LazyLock;
21use thiserror::Error;
22use ucfirst::ucfirst;
23
24#[derive(Debug, PartialEq, PartialOrd, Clone, Deref, DerefMut)]
25pub struct WithRawRepr<L> {
26 raw: String,
27 #[deref_mut]
28 #[deref]
29 parsed: L,
30}
31
32pub trait SchemaId {
33 fn schema_id() -> Cow<'static, str>;
34}
35
36impl<T: JsonSchema> JsonSchema for WithRawRepr<T> {
37 fn schema_name() -> Cow<'static, str> {
38 T::schema_name()
39 }
40
41 fn schema_id() -> Cow<'static, str> {
42 T::schema_id()
43 }
44
45 fn json_schema(r#gen: &mut schemars::SchemaGenerator) -> Schema {
46 T::json_schema(r#gen)
47 }
48}
49
50impl<T> Display for WithRawRepr<T> {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(f, "{}", self.raw)
53 }
54}
55
56impl<L> RawRepr for WithRawRepr<L> {
57 fn raw(&self) -> &str {
58 &self.raw
59 }
60}
61
62impl<T: FromStr> FromStr for WithRawRepr<T> {
63 type Err = <T as FromStr>::Err;
64
65 fn from_str(s: &str) -> Result<Self, Self::Err> {
66 let parsed: T = s.parse()?;
67 Ok(WithRawRepr {
68 raw: s.trim().to_string(),
69 parsed,
70 })
71 }
72}
73
74impl<T> Serialize for WithRawRepr<T> {
75 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
76 where
77 S: serde::Serializer,
78 {
79 serializer.serialize_str(&self.raw)
81 }
82}
83
84struct WithRawReprVisitor<T> {
85 _marker: std::marker::PhantomData<T>,
86}
87
88impl<'de, T: FromStr> Visitor<'de> for WithRawReprVisitor<T>
89where
90 <T as FromStr>::Err: std::fmt::Display,
91{
92 type Value = WithRawRepr<T>;
93
94 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
95 formatter.write_str("a properly formatted quantity with 'value [unit]'")
96 }
97
98 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
99 where
100 E: Error,
101 {
102 Ok(v.parse::<WithRawRepr<T>>().map_err(|e| E::custom(e))?)
103 }
104}
105
106impl<'de, T> Deserialize<'de> for WithRawRepr<T>
107where
108 T: FromStr,
109 <T as FromStr>::Err: std::fmt::Display,
110{
111 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
112 where
113 D: serde::Deserializer<'de>,
114 {
115 deserializer.deserialize_str(WithRawReprVisitor {
116 _marker: std::marker::PhantomData,
117 })
118 }
119}
120
121pub trait QuantityTrait: Clone + Debug + FromStr {
159 fn description() -> Cow<'static, str> {
163 format!("A {}.", Self::name().to_lowercase()).into()
164 }
165
166 fn name() -> Cow<'static, str> {
167 Self::type_id().split('_').map(ucfirst).join(" ").into()
168 }
169
170 fn type_id() -> Cow<'static, str>;
171
172 fn si_value(&self) -> f64;
173}
174
175const PARTIAL_QUANTITY_PATTERN: &str =
176 r"\s*([+-]?[\d_ ]*\.?[\d_ ]+?(?:e(?:\+|-)?[.\d]+)?)[ \t]*([^\d\s.](?:.*?[^.])?)?\s*";
177
178pub const NO_REF_QUANTITY_PATTERN: &str = formatcp!("^{PARTIAL_QUANTITY_PATTERN}$");
179
180const PARTIAL_REFERENCE_PATTERN: &str = concatcp!(r"\s*(reference|ref)?", PARTIAL_QUANTITY_PATTERN);
181pub const QUANTITY_PATTERN: &str = formatcp!("^{PARTIAL_REFERENCE_PATTERN}$");
182
183pub const RANGE_PATTERN: &str =
184 formatcp!(r"^{PARTIAL_QUANTITY_PATTERN}\s*..\s*{PARTIAL_QUANTITY_PATTERN}$");
185
186lazy_static! {
187 pub static ref QUANTITY_RE: Regex = Regex::new(QUANTITY_PATTERN).unwrap();
188}
189
190pub fn get_unit(quantity: &str) -> Option<&str> {
191 Some(QUANTITY_RE.captures(quantity)?.get(3)?.as_str())
192}
193
194#[derive(Debug, Error)]
195pub enum ParseQuantityError {
196 #[error("invalid quantity format : '{0}', should be 'value [unit]'")]
197 InvalidFormat(String),
198 #[error("this quantity can't be a reference, please remove the 'ref' or 'reference' keyword")]
199 NoReference,
200 #[error("invalid unit format: {0}")]
201 InvalidUnitFormat(#[from] FormatUnitError),
202 #[error("quantity not recognized: '{0}'")]
203 Unrecognized(String),
204}
205
206#[derive(Debug, DerefMut, Deref, Clone, PartialEq, PartialOrd)]
207pub struct Quantity<T: QuantityTrait>(T);
208
209pub trait QuantitySchema {
210 fn type_id() -> Cow<'static, str>;
211
212 fn description() -> Cow<'static, str> {
213 format!("A {}.", Self::type_id()).into()
214 }
215}
216
217impl<T: QuantityTrait> JsonSchema for Quantity<T> {
218 fn schema_name() -> Cow<'static, str> {
219 T::type_id()
220 }
221
222 fn json_schema(generator: &mut schemars::SchemaGenerator) -> Schema {
223 json_schema!({
224 "title": ucfirst(&T::name()),
225 "description": T::description(),
226 "oneOf": [
227 {
228 "type": "string",
229 "pattern": QUANTITY_PATTERN
230 },
231 {
232 "type": "number"
233 }
234 ]
235 })
236 }
237}
238
239impl<T: QuantityTrait + PartialEq> PartialEq<T> for Quantity<T> {
240 fn eq(&self, other: &T) -> bool {
241 self.0 == *other
242 }
243}
244
245static UNITS: LazyLock<HashMap<Cow<'static, str>, Cow<'static, str>>> =
246 LazyLock::new(|| [("length".into(), "m".into())].into());
247
248impl<T: QuantityTrait> FromStr for Quantity<T>
249where
250 <T as FromStr>::Err: std::fmt::Display,
251{
252 type Err = ParseQuantityError;
253 fn from_str(raw: &str) -> Result<Self, Self::Err> {
254 if let Some(captures) = QUANTITY_RE.captures(raw) {
255 if captures.get(1).is_some() {
256 return Err(ParseQuantityError::NoReference);
257 }
258 let mut unit: String = UNITS
259 .get(&T::type_id())
260 .cloned()
261 .unwrap_or_else(|| "".into())
262 .to_string();
263 if let Some(u) = captures.get(3) {
264 unit = format_unit(u.as_str())?;
265 }
266
267 let value = &captures[2];
268 let mut pretty_value = String::with_capacity(value.len());
269 let mut prepped_value = String::with_capacity(value.len());
270
271 for c in value.chars() {
272 match c {
273 ' ' => pretty_value.push(' '),
274 '_' => pretty_value.push(' '),
275 _ => {
276 pretty_value.push(c);
277 prepped_value.push(c);
278 }
279 }
280 }
281
282 let prepped_raw = format!("{} {}", prepped_value, &unit);
283
284 Ok(Quantity(prepped_raw.parse().map_err(
285 |e: <T as FromStr>::Err| ParseQuantityError::Unrecognized(e.to_string()),
286 )?))
287 } else {
295 Err(ParseQuantityError::InvalidFormat(raw.to_string()))
296 }
297 }
298}
299
300use uom::si::f64 as si;
378
379pub trait DefaultUnit {
380 const DEFAULT_UNIT: &str;
381}
382
383pub trait QuantityId {
384 const QUANTITY_ID: &str;
385}
386
387pub type Ratio = WithRawRepr<si::Ratio>;
389
390impl DefaultUnit for si::Ratio {
391 const DEFAULT_UNIT: &str = "";
392}
393
394pub type Area = WithRawRepr<Quantity<si::Area>>;
396
397impl QuantitySchema for si::Area {
398 fn type_id() -> Cow<'static, str> {
399 "area".into()
400 }
401}
402
403impl QuantityTrait for si::Area {
404 fn type_id() -> Cow<'static, str> {
405 "area".into()
406 }
407 fn si_value(&self) -> f64 {
408 self.get::<uom::si::area::square_meter>()
409 }
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
416pub struct CustomQuantity {
417 #[serde(default)]
418 pub length: isize,
419 #[serde(default)]
420 pub time: isize,
421 #[serde(default)]
422 pub mass: isize,
423 #[serde(default)]
424 pub current: isize,
425 #[serde(default)]
426 pub temperature: isize,
427 #[serde(default)]
428 pub amount: isize,
429 #[serde(default)]
430 pub luminous_intensity: isize,
431 #[serde(default)]
432 pub value: f64,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
437#[serde(tag = "type", content = "value")]
438#[serde(rename_all = "snake_case")]
439pub enum QuantityEnum {
440 Speed(Speed),
441 Length(Length),
442 Area(Area),
443 Volume(Volume),
444 Mass(Mass),
445 Temperature(Temperature),
446 DiffusionCoefficient(DiffusionCoefficient),
447 Custom(CustomQuantity),
448}
449
450impl QuantityEnum {
451 pub fn si_value(&self) -> f64 {
452 match self {
453 QuantityEnum::Speed(q) => q.value,
454 QuantityEnum::Length(q) => q.value,
455 QuantityEnum::DiffusionCoefficient(q) => q.value,
456 QuantityEnum::Area(q) => q.value,
457 QuantityEnum::Mass(q) => q.value,
458 QuantityEnum::Volume(q) => q.value,
459 QuantityEnum::Temperature(q) => q.value,
460 QuantityEnum::Custom(q) => q.value,
461 }
462 }
463}
464pub type DiffusionCoefficient = WithRawRepr<Quantity<si::DiffusionCoefficient>>;
483impl QuantityTrait for si::DiffusionCoefficient {
489 fn type_id() -> Cow<'static, str> {
490 "diffusion_coefficient".into()
491 }
492 fn si_value(&self) -> f64 {
493 self.get::<uom::si::diffusion_coefficient::square_meter_per_second>()
494 }
495}
496
497pub type Length = WithRawRepr<Quantity<si::Length>>;
513
514impl QuantityTrait for si::Length {
518 fn type_id() -> Cow<'static, str> {
519 "length".into()
520 }
521 fn si_value(&self) -> f64 {
522 self.get::<uom::si::length::meter>()
523 }
524}
525
526impl QuantitySchema for si::Length {
527 fn type_id() -> Cow<'static, str> {
528 "length".into()
529 }
530}
531
532impl Quantity<si::Length> {
533 pub fn meters(&self) -> f64 {
534 self.get::<uom::si::length::meter>()
535 }
536}
537
538pub type Speed = WithRawRepr<Quantity<si::Velocity>>;
539
540impl<T: SchemaId> SchemaId for WithRawRepr<T> {
550 fn schema_id() -> Cow<'static, str> {
551 T::schema_id()
552 }
553}
554
555impl QuantityTrait for si::Velocity {
556 fn si_value(&self) -> f64 {
557 self.get::<uom::si::velocity::meter_per_second>()
558 }
559
560 fn type_id() -> Cow<'static, str> {
561 "speed".into()
562 }
563}
564
565impl QuantitySchema for si::Velocity {
566 fn type_id() -> Cow<'static, str> {
567 "speed".into()
568 }
569}
570
571pub type Mass = WithRawRepr<Quantity<si::Mass>>;
573
574impl QuantityTrait for si::Mass {
575 fn type_id() -> Cow<'static, str> {
576 "mass".into()
577 }
578 fn si_value(&self) -> f64 {
579 self.get::<uom::si::mass::gram>()
580 }
581}
582
583impl DefaultUnit for si::Mass {
584 const DEFAULT_UNIT: &str = "g";
585}
586
587pub type Time = WithRawRepr<Quantity<si::Time>>;
589impl Quantity<si::Time> {
590 pub fn seconds(&self) -> f64 {
591 self.si_value()
592 }
593}
594impl QuantitySchema for si::Time {
595 fn type_id() -> Cow<'static, str> {
596 "time".into()
597 }
598}
599impl QuantityTrait for si::Time {
600 fn type_id() -> Cow<'static, str> {
601 "time".into()
602 }
603 fn si_value(&self) -> f64 {
604 self.get::<uom::si::time::second>()
605 }
606}
607
608pub type Temperature = WithRawRepr<Quantity<si::ThermodynamicTemperature>>;
610
611impl DefaultUnit for si::ThermodynamicTemperature {
612 const DEFAULT_UNIT: &'static str = "°C";
613}
614
615impl QuantityTrait for si::ThermodynamicTemperature {
616 fn type_id() -> Cow<'static, str> {
617 "temperature".into()
618 }
619 fn si_value(&self) -> f64 {
620 self.get::<uom::si::thermodynamic_temperature::kelvin>()
621 }
622}
623
624pub type Pressure = WithRawRepr<Quantity<si::Pressure>>;
626
627impl DefaultUnit for si::Pressure {
628 const DEFAULT_UNIT: &'static str = "Pa";
629}
630
631impl QuantityTrait for si::Pressure {
632 fn type_id() -> Cow<'static, str> {
633 "pressure".into()
634 }
635 fn si_value(&self) -> f64 {
636 self.get::<uom::si::pressure::pascal>()
637 }
638}
639
640pub type Volume = WithRawRepr<Quantity<si::Volume>>;
642
643impl DefaultUnit for si::Volume {
644 const DEFAULT_UNIT: &'static str = "m³";
645}
646
647impl QuantityTrait for si::Volume {
648 fn type_id() -> Cow<'static, str> {
649 "volume".into()
650 }
651 fn si_value(&self) -> f64 {
652 self.get::<uom::si::volume::cubic_meter>()
653 }
654}
655
656pub type MolarMass = WithRawRepr<Quantity<si::MolarMass>>;
658
659impl DefaultUnit for si::MolarMass {
660 const DEFAULT_UNIT: &'static str = "g/mol";
661}
662
663impl QuantityTrait for si::MolarMass {
664 fn type_id() -> Cow<'static, str> {
665 "molar_mass".into()
666 }
667 fn si_value(&self) -> f64 {
668 self.get::<uom::si::molar_mass::gram_per_mole>()
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675 use std::str::FromStr;
676
677 use crate::codegen::input_schema::quantity::Time;
678
679 use super::{DefaultUnit, Pressure, QUANTITY_RE, WithRawRepr};
680 use std::fmt::Debug;
681 use uom::si::{f64::Length, length::*};
682
683 fn make_parsed<L>(raw: &str, parsed: L) -> WithRawRepr<L>
684 where
685 L: FromStr,
686 {
687 WithRawRepr {
688 raw: raw.to_string(),
689 parsed,
690 }
691 }
692
693 #[test]
694 fn parse_length_with_valid_input() {
695 fn make_length(raw: &str) -> super::Length {
696 raw.parse().unwrap()
697 }
698 assert_eq!(
699 make_length("10 m"),
700 make_parsed("10 m", Quantity(Length::new::<meter>(10.)))
701 );
702 assert_eq!(
715 make_length("100 000 m"),
716 make_parsed("100 000 m", Quantity(Length::new::<kilometer>(100.)))
717 );
718 assert_eq!(
719 make_length("1 meter"),
720 make_parsed("1 meter", Quantity(Length::new::<meter>(1.)))
721 );
722 assert_eq!(
723 make_length("2 meters"),
724 make_parsed("2 meters", Quantity(Length::new::<meter>(2.)))
725 );
726 assert_eq!(
727 make_length("-1"),
728 make_parsed("-1", Quantity(Length::new::<meter>(-1.)))
729 );
730 }
740 #[test]
741 fn parse_length_with_invalid_input() {
742 fn attempt_length_parse(raw: &str) {
743 let result = raw.parse::<super::Length>();
744 assert!(result.is_err(), "Expected error for input '{}'", raw);
745 }
746
747 attempt_length_parse("ten m"); attempt_length_parse("10 xyz"); attempt_length_parse(""); attempt_length_parse("reference m"); }
753
754 #[test]
755 fn parse_reference_should_err() {
756 let raw = "reference 5 000 000";
757 assert!(raw.parse::<Pressure>().is_err());
758 }
759
760 #[test]
761 fn parse_real_number() {
762 let raw = "0.1 s";
763 let time: Time = raw.parse().unwrap();
764 assert_eq!(time.parsed.get::<uom::si::time::second>(), 0.1)
765 }
766
767 #[test]
768 fn test_re() {
769 assert!(QUANTITY_RE.captures("0.1 s").is_some())
770 }
771}