configs/
validated_types.rs

1//! Config newtypes that encode validation constraints in the type system.
2//!
3//! The generic ones live in `schema_kit` and are re-exported here so config
4//! modules keep a single import path. `WeightMultiplier` stays because its
5//! constraint is a game rule: a gacha weight may only ever be scaled up.
6
7pub use schema_kit::{NonEmptyVec, NonZeroU64, PositiveF64, PositiveI32, PositiveI64};
8
9use std::fmt;
10
11use schemars::JsonSchema;
12use schemars::r#gen::SchemaGenerator;
13use schemars::schema::{InstanceType, Schema, SchemaObject};
14use serde::{Deserialize, Deserializer, Serialize};
15
16// ---------------------------------------------------------------------------
17// WeightMultiplier (f64 >= 1.0)
18// ---------------------------------------------------------------------------
19
20#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize)]
21#[serde(transparent)]
22pub struct WeightMultiplier(f64);
23
24impl WeightMultiplier {
25    /// Creates a new `WeightMultiplier`. Panics if `value < 1.0`.
26    pub fn new(value: f64) -> Self {
27        assert!(
28            value >= 1.0,
29            "WeightMultiplier: expected >= 1.0, got {value}"
30        );
31        Self(value)
32    }
33
34    pub fn get(self) -> f64 {
35        self.0
36    }
37}
38
39impl<'de> Deserialize<'de> for WeightMultiplier {
40    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
41        let v = f64::deserialize(deserializer)?;
42        if v < 1.0 {
43            return Err(serde::de::Error::custom(format!(
44                "expected weight multiplier >= 1.0, got {v}"
45            )));
46        }
47        Ok(Self(v))
48    }
49}
50
51impl JsonSchema for WeightMultiplier {
52    fn schema_name() -> String {
53        "WeightMultiplier".to_owned()
54    }
55
56    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
57        SchemaObject {
58            instance_type: Some(InstanceType::Number.into()),
59            number: Some(Box::new(schemars::schema::NumberValidation {
60                minimum: Some(1.0),
61                ..Default::default()
62            })),
63            ..Default::default()
64        }
65        .into()
66    }
67}
68
69impl fmt::Display for WeightMultiplier {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        self.0.fmt(f)
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn weight_multiplier_rejects_below_one() {
81        let result: Result<WeightMultiplier, _> = serde_json::from_str("0.5");
82        assert!(result.is_err());
83    }
84
85    #[test]
86    fn weight_multiplier_accepts_one() {
87        let v: WeightMultiplier = serde_json::from_str("1.0").unwrap();
88        assert!((v.get() - 1.0).abs() < f64::EPSILON);
89    }
90}