Skip to main content

vld/primitives/
literal.rs

1use serde_json::Value;
2
3use crate::error::{IssueCode, VldError};
4use crate::schema::VldSchema;
5
6/// Trait for types that can be used as literal values.
7pub trait IntoLiteral: Clone + 'static {
8    type Output: Clone + PartialEq + std::fmt::Debug;
9    fn to_json_value(&self) -> Value;
10    fn extract(value: &Value) -> Option<Self::Output>;
11    fn display(&self) -> String;
12}
13
14impl IntoLiteral for &'static str {
15    type Output = String;
16    fn to_json_value(&self) -> Value {
17        Value::String(self.to_string())
18    }
19    fn extract(value: &Value) -> Option<String> {
20        value.as_str().map(|s| s.to_string())
21    }
22    fn display(&self) -> String {
23        format!("\"{}\"", self)
24    }
25}
26
27impl IntoLiteral for String {
28    type Output = String;
29    fn to_json_value(&self) -> Value {
30        Value::String(self.clone())
31    }
32    fn extract(value: &Value) -> Option<String> {
33        value.as_str().map(|s| s.to_string())
34    }
35    fn display(&self) -> String {
36        format!("\"{}\"", self)
37    }
38}
39
40impl IntoLiteral for i64 {
41    type Output = i64;
42    fn to_json_value(&self) -> Value {
43        Value::Number((*self).into())
44    }
45    fn extract(value: &Value) -> Option<i64> {
46        // Accept integral floats (`5.0`) as well as integers.
47        value.as_i64().or_else(|| {
48            value
49                .as_f64()
50                .filter(|f| f.fract() == 0.0 && *f >= i64::MIN as f64 && *f < i64::MAX as f64)
51                .map(|f| f as i64)
52        })
53    }
54    fn display(&self) -> String {
55        self.to_string()
56    }
57}
58
59/// The other integer types: exact `i64` / `u64` JSON integers (or integral
60/// floats) that fit the type.
61macro_rules! int_literals {
62    ($($t:ty),+) => {$(
63        impl IntoLiteral for $t {
64            type Output = $t;
65            fn to_json_value(&self) -> Value {
66                Value::Number((*self).into())
67            }
68            fn extract(value: &Value) -> Option<$t> {
69                let exact = value
70                    .as_i64()
71                    .and_then(|i| <$t>::try_from(i).ok())
72                    .or_else(|| value.as_u64().and_then(|u| <$t>::try_from(u).ok()));
73                exact.or_else(|| i64::extract(value).and_then(|i| <$t>::try_from(i).ok()))
74            }
75            fn display(&self) -> String {
76                self.to_string()
77            }
78        }
79    )+};
80}
81
82int_literals!(i8, i16, i32, u8, u16, u32, u64, isize, usize);
83
84impl IntoLiteral for f32 {
85    type Output = f32;
86    fn to_json_value(&self) -> Value {
87        // Shortest decimal form: `0.1f32` is the JSON number `0.1`.
88        self.to_string()
89            .parse::<f64>()
90            .ok()
91            .and_then(serde_json::Number::from_f64)
92            .map(Value::Number)
93            .unwrap_or(Value::Null)
94    }
95    fn extract(value: &Value) -> Option<f32> {
96        value.as_f64().map(|f| f as f32)
97    }
98    fn display(&self) -> String {
99        self.to_string()
100    }
101}
102
103impl IntoLiteral for f64 {
104    type Output = f64;
105    fn to_json_value(&self) -> Value {
106        serde_json::Number::from_f64(*self)
107            .map(Value::Number)
108            .unwrap_or(Value::Null)
109    }
110    fn extract(value: &Value) -> Option<f64> {
111        value.as_f64()
112    }
113    fn display(&self) -> String {
114        self.to_string()
115    }
116}
117
118impl IntoLiteral for bool {
119    type Output = bool;
120    fn to_json_value(&self) -> Value {
121        Value::Bool(*self)
122    }
123    fn extract(value: &Value) -> Option<bool> {
124        value.as_bool()
125    }
126    fn display(&self) -> String {
127        self.to_string()
128    }
129}
130
131/// Schema for exact value matching. Created via [`vld::literal()`](crate::literal).
132///
133/// Numbers compare by value: `literal(1.0)` accepts `1`, `literal(5i64)` accepts `5.0`.
134pub struct ZLiteral<T: IntoLiteral> {
135    expected_value: Value,
136    _phantom: std::marker::PhantomData<T>,
137}
138
139impl<T: IntoLiteral> ZLiteral<T> {
140    pub fn new(expected: T) -> Self {
141        Self {
142            expected_value: expected.to_json_value(),
143            _phantom: std::marker::PhantomData,
144        }
145    }
146
147    /// The literal as a JSON value (for JSON Schema `const`).
148    pub fn expected_value(&self) -> &Value {
149        &self.expected_value
150    }
151}
152
153impl<T: IntoLiteral> VldSchema for ZLiteral<T> {
154    type Output = T::Output;
155
156    fn parse_value(&self, value: &Value) -> Result<T::Output, VldError> {
157        super::json_eq(value, &self.expected_value)
158            .then(|| T::extract(value))
159            .flatten()
160            .ok_or_else(|| {
161                VldError::from_code_with_value(
162                    IssueCode::InvalidLiteral {
163                        expected: self.expected_value.clone(),
164                    },
165                    value,
166                )
167            })
168    }
169}
170
171/// `{"const": <literal>}`.
172#[cfg(feature = "openapi")]
173impl<T: IntoLiteral> crate::openapi::JsonSchema for ZLiteral<T> {
174    fn json_schema(&self) -> Value {
175        serde_json::json!({ "const": self.expected_value })
176    }
177}
178
179#[cfg(feature = "openapi")]
180impl<T: IntoLiteral> crate::openapi::CollectNestedSchemas for ZLiteral<T> {}