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 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 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/// Invoked via [`impl_meta_methods!`] on each concrete validator so
183/// `Integer::new().with_name("Port").to_int()` works without importing a trait.
184/// Getters read `meta()`; setters mutate `meta_mut()` and return `self` for chaining.
185#[macro_export]
186macro_rules! impl_meta_methods {
187    ($ty:ty) => {
188        #[allow(clippy::wrong_self_convention)]
189        impl $ty {
190            /// The human-readable name.
191            pub fn name(&self) -> &str {
192                &<$ty as $crate::Validator>::meta(self).name
193            }
194
195            /// The description, if any.
196            pub fn description(&self) -> Option<&str> {
197                <$ty as $crate::Validator>::meta(self)
198                    .description
199                    .as_deref()
200            }
201
202            /// The example values (each with an optional note).
203            pub fn examples(&self) -> &[(tanzim_value::Value, Option<String>)] {
204                &<$ty as $crate::Validator>::meta(self).examples
205            }
206
207            /// The default value, if any.
208            pub fn default_value(&self) -> Option<&tanzim_value::Value> {
209                <$ty as $crate::Validator>::meta(self).default.as_ref()
210            }
211
212            /// The output conversion target, if any.
213            pub fn convert(&self) -> Option<tanzim_value::ValueType> {
214                <$ty as $crate::Validator>::meta(self).convert
215            }
216
217            /// Set the human-readable name (surfaced in error messages).
218            pub fn with_name(mut self, name: impl Into<String>) -> Self {
219                <$ty as $crate::Validator>::meta_mut(&mut self).name = name.into();
220                self
221            }
222
223            /// Attach a human-readable description.
224            pub fn with_description(mut self, text: impl Into<String>) -> Self {
225                <$ty as $crate::Validator>::meta_mut(&mut self).description = Some(text.into());
226                self
227            }
228
229            /// Add an example value.
230            pub fn with_example(mut self, value: impl Into<tanzim_value::Value>) -> Self {
231                <$ty as $crate::Validator>::meta_mut(&mut self)
232                    .examples
233                    .push((value.into(), None));
234                self
235            }
236
237            /// Add an example value with an explanatory note.
238            pub fn with_example_noted(
239                mut self,
240                value: impl Into<tanzim_value::Value>,
241                note: impl Into<String>,
242            ) -> Self {
243                <$ty as $crate::Validator>::meta_mut(&mut self)
244                    .examples
245                    .push((value.into(), Some(note.into())));
246                self
247            }
248
249            /// Set the default value used as an on-error fallback (see the pipeline's validate stage).
250            pub fn with_default(mut self, value: impl Into<tanzim_value::Value>) -> Self {
251                <$ty as $crate::Validator>::meta_mut(&mut self).default = Some(value.into());
252                self
253            }
254
255            /// After validation succeeds, cast the value to a string.
256            pub fn to_string(mut self) -> Self {
257                <$ty as $crate::Validator>::meta_mut(&mut self).convert =
258                    Some(tanzim_value::ValueType::String);
259                self
260            }
261
262            /// After validation succeeds, cast the value to an integer.
263            pub fn to_int(mut self) -> Self {
264                <$ty as $crate::Validator>::meta_mut(&mut self).convert =
265                    Some(tanzim_value::ValueType::Int);
266                self
267            }
268
269            /// After validation succeeds, cast the value to a float.
270            pub fn to_float(mut self) -> Self {
271                <$ty as $crate::Validator>::meta_mut(&mut self).convert =
272                    Some(tanzim_value::ValueType::Float);
273                self
274            }
275
276            /// After validation succeeds, cast the value to a boolean.
277            pub fn to_bool(mut self) -> Self {
278                <$ty as $crate::Validator>::meta_mut(&mut self).convert =
279                    Some(tanzim_value::ValueType::Bool);
280                self
281            }
282        }
283    };
284}
285
286/// Cast a validated [`Value`] to `target`, reusing the same lenient coercions the leaf validators
287/// use. An impossible cast is a [`ErrorKind::NotConvertible`] error.
288fn cast(value: &mut Value, target: ValueType) -> Result<(), Error> {
289    if matches!(value, Value::Null) {
290        return Ok(());
291    }
292    if value.type_name() == target {
293        return Ok(());
294    }
295    let converted = match target {
296        ValueType::String => Value::String(match value {
297            Value::Bool(inner) => inner.to_string(),
298            Value::Int(inner) => inner.to_string(),
299            Value::Float(inner) => inner.to_string(),
300            Value::String(inner) => std::mem::take(inner),
301            _ => {
302                return Err(Error::new(ErrorKind::NotConvertible {
303                    target,
304                    found: value.type_name(),
305                }));
306            }
307        }),
308        ValueType::Int => match value {
309            Value::Int(inner) => Value::Int(*inner),
310            Value::Bool(inner) => Value::Int(*inner as isize),
311            Value::Float(inner) if inner.fract() == 0.0 => Value::Int(*inner as isize),
312            Value::String(inner) if inner.parse::<isize>().is_ok() => {
313                Value::Int(inner.parse::<isize>().unwrap())
314            }
315            _ => {
316                return Err(Error::new(ErrorKind::NotConvertible {
317                    target,
318                    found: value.type_name(),
319                }));
320            }
321        },
322        ValueType::Float => match value {
323            Value::Float(inner) => Value::Float(*inner),
324            Value::Int(inner) => Value::Float(*inner as f64),
325            Value::String(inner) if inner.parse::<f64>().is_ok() => {
326                Value::Float(inner.parse::<f64>().unwrap())
327            }
328            _ => {
329                return Err(Error::new(ErrorKind::NotConvertible {
330                    target,
331                    found: value.type_name(),
332                }));
333            }
334        },
335        ValueType::Bool => match value {
336            Value::Bool(inner) => Value::Bool(*inner),
337            Value::String(inner) if inner.eq_ignore_ascii_case("true") => Value::Bool(true),
338            Value::String(inner) if inner.eq_ignore_ascii_case("false") => Value::Bool(false),
339            _ => {
340                return Err(Error::new(ErrorKind::NotConvertible {
341                    target,
342                    found: value.type_name(),
343                }));
344            }
345        },
346        ValueType::List | ValueType::Map | ValueType::Null => {
347            return Err(Error::new(ErrorKind::NotConvertible {
348                target,
349                found: value.type_name(),
350            }));
351        }
352    };
353    *value = converted;
354    Ok(())
355}
356
357/// Validate a whole node, seeding the root [`Location`] into any error.
358///
359/// ```
360/// use tanzim_validate::{validate, Integer};
361/// use tanzim_value::{LocatedValue, Location, Value};
362///
363/// let mut node = LocatedValue::new(
364///     Value::String("42".into()),
365///     Location::at("file", "config.toml", Some(1), Some(1), None),
366/// );
367/// validate(&Integer::new().range(0, 100), &mut node).unwrap();
368/// assert_eq!(node.value().as_int(), Some(42)); // coerced from string
369/// ```
370pub fn validate(validator: &dyn Validator, value: &mut LocatedValue) -> Result<(), Error> {
371    match validator.validate(value.value_mut()) {
372        Ok(()) => Ok(()),
373        Err(error) => Err(error.with_location(value.location())),
374    }
375}