Skip to main content

tanzim_validate/
lib.rs

1#![doc = include_str!("../README.md")]
2
3mod error;
4
5#[cfg(feature = "boolean")]
6mod boolean;
7#[cfg(feature = "either")]
8mod either;
9#[cfg(feature = "enumeration")]
10mod enumeration;
11#[cfg(feature = "float")]
12mod float;
13#[cfg(feature = "integer")]
14mod integer;
15#[cfg(feature = "list")]
16mod list;
17#[cfg(feature = "net")]
18mod net;
19#[cfg(feature = "non_empty")]
20mod non_empty;
21#[cfg(feature = "number")]
22mod number;
23#[cfg(feature = "path")]
24mod path;
25#[cfg(feature = "percentage")]
26mod percentage;
27#[cfg(feature = "static_map")]
28mod static_map;
29#[cfg(feature = "string")]
30mod string;
31
32#[cfg(feature = "bytesize")]
33mod bytesize;
34#[cfg(feature = "cidr")]
35mod cidr;
36#[cfg(feature = "datetime")]
37mod datetime;
38#[cfg(feature = "duration")]
39mod duration;
40#[cfg(feature = "dynamic_map")]
41mod dynamic_map;
42#[cfg(feature = "encoding")]
43mod encoding;
44#[cfg(feature = "regex")]
45mod regex;
46#[cfg(feature = "schema")]
47mod schema;
48#[cfg(feature = "semver")]
49mod semver;
50#[cfg(feature = "url")]
51mod url;
52#[cfg(feature = "uuid")]
53mod uuid;
54
55pub use error::{Error, ErrorKind, Segment};
56pub use tanzim_value::{LocatedValue, Location, Map, Value, ValueType};
57
58#[cfg(feature = "boolean")]
59pub use boolean::Bool;
60#[cfg(feature = "dynamic_map")]
61pub use dynamic_map::DynamicMap;
62#[cfg(feature = "either")]
63pub use either::Either;
64#[cfg(feature = "enumeration")]
65pub use enumeration::Enum;
66#[cfg(feature = "float")]
67pub use float::Float;
68#[cfg(feature = "integer")]
69pub use integer::Integer;
70#[cfg(feature = "list")]
71pub use list::List;
72#[cfg(feature = "net")]
73pub use net::{Domain, Email, Host, IpAddr, Port, SocketAddr};
74#[cfg(feature = "non_empty")]
75pub use non_empty::NonEmpty;
76#[cfg(feature = "number")]
77pub use number::Number;
78#[cfg(feature = "path")]
79pub use path::{Path, PathKind};
80#[cfg(feature = "percentage")]
81pub use percentage::Percentage;
82#[cfg(feature = "static_map")]
83pub use static_map::StaticMap;
84#[cfg(feature = "string")]
85pub use string::Str;
86
87#[cfg(feature = "bytesize")]
88pub use bytesize::ByteSize;
89#[cfg(feature = "cidr")]
90pub use cidr::Cidr;
91#[cfg(feature = "datetime")]
92pub use datetime::{Date, DateTime};
93#[cfg(feature = "duration")]
94pub use duration::Duration;
95#[cfg(feature = "encoding")]
96pub use encoding::{Base64, Hex};
97#[cfg(feature = "regex")]
98pub use regex::RegexPattern;
99#[cfg(feature = "schema")]
100pub use schema::{
101    Constructor, Node, Registry, SchemaError, SchemaErrorKind, SchemaValue, build, build_value,
102};
103#[cfg(feature = "semver")]
104pub use semver::Semver;
105#[cfg(feature = "url")]
106pub use url::Url;
107#[cfg(feature = "uuid")]
108pub use uuid::Uuid;
109
110/// Human-facing metadata a validator carries and attaches to its errors.
111///
112/// Set through the [`WithMeta`] builder methods (`with_name`, `with_description`, `with_default`,
113/// `to_int`, …) available on every validator. On a validation failure a validator attaches its own
114/// `Meta` to the [`Error`] (innermost wins), so messages can name the field and offer a description,
115/// examples, and a default. `convert` requests a post-validation output cast (see
116/// [`Validator::validate`]).
117#[derive(Debug, Clone, PartialEq, Default)]
118pub struct Meta {
119    pub name: String,
120    pub description: Option<String>,
121    /// Example values, each with an optional note explaining it.
122    pub examples: Vec<(Value, Option<String>)>,
123    pub default: Option<Value>,
124    /// Target type for the post-validation output cast, if any.
125    pub convert: Option<ValueType>,
126}
127
128impl Meta {
129    /// A metadata block with just the name.
130    pub fn new(name: impl Into<String>) -> Self {
131        Self {
132            name: name.into(),
133            ..Self::default()
134        }
135    }
136}
137
138/// A validator: a rule ([`check`](Validator::check)) plus human-facing [`Meta`].
139///
140/// Each validator stores a [`Meta`] and returns it from [`meta`](Validator::meta). [`check`](Validator::meta) is the
141/// rule; it receives `&mut Value` (not [`LocatedValue`]) so it can coerce in place — e.g. a numeric
142/// string into an integer. [`validate`](Validator::validate) is provided: it runs `check`, attaches
143/// this validator's [`Meta`] to any error (innermost wins), and applies the output conversion in
144/// `meta().convert` on success. Composite validators recurse by calling `validate` on their
145/// children, then attach the child's [`Location`] via [`Error::under_key`]/[`Error::under_index`].
146pub trait Validator {
147    /// This validator's human-facing metadata.
148    fn meta(&self) -> &Meta;
149
150    /// Mutable access to this validator's metadata (backs the [`WithMeta`] builder setters).
151    fn meta_mut(&mut self) -> &mut Meta;
152
153    /// The validation rule: check (and coerce) `value` in place.
154    fn check(&self, value: &mut Value) -> Result<(), Error>;
155
156    /// Run [`check`](Validator::check); on error attach this validator's [`Meta`] (innermost wins);
157    /// on success apply the output conversion in `meta().convert`, if any.
158    fn validate(&self, value: &mut Value) -> Result<(), Error> {
159        if matches!(value, Value::Null)
160            && let Some(default) = self.meta().default.as_ref()
161        {
162            *value = default.clone();
163        }
164        if let Err(error) = self.check(value) {
165            return Err(error.with_meta(self.meta()));
166        }
167        if let Some(target) = self.meta().convert {
168            cast(value, target).map_err(|error| error.with_meta(self.meta()))?;
169        }
170        Ok(())
171    }
172}
173
174impl<V: Validator + 'static> From<V> for Box<dyn Validator> {
175    fn from(validator: V) -> Self {
176        Box::new(validator)
177    }
178}
179
180/// Getters and fluent setters for every validator's [`Meta`].
181///
182/// Blanket-implemented for every [`Validator`], so `Integer::new().with_name("Port").to_int()`
183/// works without each validator repeating these. Getters read `meta()`; setters mutate `meta_mut()`
184/// and return `self` for chaining.
185#[allow(clippy::wrong_self_convention)]
186pub trait WithMeta: Validator + Sized {
187    /// The human-readable name.
188    fn name(&self) -> &str {
189        &self.meta().name
190    }
191
192    /// The description, if any.
193    fn description(&self) -> Option<&str> {
194        self.meta().description.as_deref()
195    }
196
197    /// The example values (each with an optional note).
198    fn examples(&self) -> &[(Value, Option<String>)] {
199        &self.meta().examples
200    }
201
202    /// The default value, if any.
203    fn default_value(&self) -> Option<&Value> {
204        self.meta().default.as_ref()
205    }
206
207    /// The output conversion target, if any.
208    fn convert(&self) -> Option<ValueType> {
209        self.meta().convert
210    }
211
212    /// Set the human-readable name (surfaced in error messages).
213    fn with_name(mut self, name: impl Into<String>) -> Self {
214        self.meta_mut().name = name.into();
215        self
216    }
217
218    /// Attach a human-readable description.
219    fn with_description(mut self, text: impl Into<String>) -> Self {
220        self.meta_mut().description = Some(text.into());
221        self
222    }
223
224    /// Add an example value.
225    fn with_example(mut self, value: impl Into<Value>) -> Self {
226        self.meta_mut().examples.push((value.into(), None));
227        self
228    }
229
230    /// Add an example value with an explanatory note.
231    fn with_example_noted(mut self, value: impl Into<Value>, note: impl Into<String>) -> Self {
232        self.meta_mut()
233            .examples
234            .push((value.into(), Some(note.into())));
235        self
236    }
237
238    /// Set the default value used as an on-error fallback (see the pipeline's validate stage).
239    fn with_default(mut self, value: impl Into<Value>) -> Self {
240        self.meta_mut().default = Some(value.into());
241        self
242    }
243
244    /// After validation succeeds, cast the value to a string.
245    fn to_string(mut self) -> Self {
246        self.meta_mut().convert = Some(ValueType::String);
247        self
248    }
249
250    /// After validation succeeds, cast the value to an integer.
251    fn to_int(mut self) -> Self {
252        self.meta_mut().convert = Some(ValueType::Int);
253        self
254    }
255
256    /// After validation succeeds, cast the value to a float.
257    fn to_float(mut self) -> Self {
258        self.meta_mut().convert = Some(ValueType::Float);
259        self
260    }
261
262    /// After validation succeeds, cast the value to a boolean.
263    fn to_bool(mut self) -> Self {
264        self.meta_mut().convert = Some(ValueType::Bool);
265        self
266    }
267}
268
269impl<T: Validator> WithMeta for T {}
270
271/// Cast a validated [`Value`] to `target`, reusing the same lenient coercions the leaf validators
272/// use. An impossible cast is a [`ErrorKind::NotConvertible`] error.
273fn cast(value: &mut Value, target: ValueType) -> Result<(), Error> {
274    if matches!(value, Value::Null) {
275        return Ok(());
276    }
277    if value.type_name() == target {
278        return Ok(());
279    }
280    let converted = match target {
281        ValueType::String => Value::String(match value {
282            Value::Bool(inner) => inner.to_string(),
283            Value::Int(inner) => inner.to_string(),
284            Value::Float(inner) => inner.to_string(),
285            Value::String(inner) => std::mem::take(inner),
286            _ => {
287                return Err(Error::new(ErrorKind::NotConvertible {
288                    target,
289                    found: value.type_name(),
290                }));
291            }
292        }),
293        ValueType::Int => match value {
294            Value::Int(inner) => Value::Int(*inner),
295            Value::Bool(inner) => Value::Int(*inner as isize),
296            Value::Float(inner) if inner.fract() == 0.0 => Value::Int(*inner as isize),
297            Value::String(inner) if inner.parse::<isize>().is_ok() => {
298                Value::Int(inner.parse::<isize>().unwrap())
299            }
300            _ => {
301                return Err(Error::new(ErrorKind::NotConvertible {
302                    target,
303                    found: value.type_name(),
304                }));
305            }
306        },
307        ValueType::Float => match value {
308            Value::Float(inner) => Value::Float(*inner),
309            Value::Int(inner) => Value::Float(*inner as f64),
310            Value::String(inner) if inner.parse::<f64>().is_ok() => {
311                Value::Float(inner.parse::<f64>().unwrap())
312            }
313            _ => {
314                return Err(Error::new(ErrorKind::NotConvertible {
315                    target,
316                    found: value.type_name(),
317                }));
318            }
319        },
320        ValueType::Bool => match value {
321            Value::Bool(inner) => Value::Bool(*inner),
322            Value::String(inner) if inner.eq_ignore_ascii_case("true") => Value::Bool(true),
323            Value::String(inner) if inner.eq_ignore_ascii_case("false") => Value::Bool(false),
324            _ => {
325                return Err(Error::new(ErrorKind::NotConvertible {
326                    target,
327                    found: value.type_name(),
328                }));
329            }
330        },
331        ValueType::List | ValueType::Map | ValueType::Null => {
332            return Err(Error::new(ErrorKind::NotConvertible {
333                target,
334                found: value.type_name(),
335            }));
336        }
337    };
338    *value = converted;
339    Ok(())
340}
341
342/// Validate a whole node, seeding the root [`Location`] into any error.
343///
344/// ```
345/// use tanzim_validate::{validate, Integer};
346/// use tanzim_value::{LocatedValue, Location, Value};
347///
348/// let mut node = LocatedValue::new(
349///     Value::String("42".into()),
350///     Location::at("file", "config.toml", Some(1), Some(1), None),
351/// );
352/// validate(&Integer::new().range(0, 100), &mut node).unwrap();
353/// assert_eq!(node.value().as_int(), Some(42)); // coerced from string
354/// ```
355pub fn validate(validator: &dyn Validator, value: &mut LocatedValue) -> Result<(), Error> {
356    match validator.validate(value.value_mut()) {
357        Ok(()) => Ok(()),
358        Err(error) => Err(error.with_location(value.location())),
359    }
360}