Skip to main content

ctl_core/
validate.rs

1//! Garde rules every declared schema shares. A repository's config and a
2//! template's own exports are held to the same rules and report the same
3//! messages.
4//!
5//! Each one is a `#[garde(custom(…))]` rule. It runs at the boundary, on a
6//! fully parsed value, and garde names the field that failed. The rules are
7//! generic over the context. The same rule works in a schema validated
8//! against nothing and in one validated against the whole document.
9//!
10//! ```
11//! use ctl_core::validate::{at_least_one, one_file_name};
12//!
13//! assert!(one_file_name::<()>("notes.md", &()).is_ok());
14//! assert!(one_file_name::<()>("docs/notes.md", &()).is_err());
15//! assert!(at_least_one::<String, ()>("label")(&[], &()).is_err());
16//! ```
17
18use std::path::{Component, Path};
19
20/// A path a repository may write to: relative, and never upward out of the
21/// tree.
22///
23/// # Errors
24///
25/// "must stay inside the repository" for an absolute path or one with
26/// `..`.
27pub fn inside_the_repo<C>(path: &Path, _: &C) -> garde::Result {
28    if path.is_absolute()
29        || path.components().any(|part| {
30            matches!(
31                part,
32                Component::ParentDir | Component::RootDir | Component::Prefix(_)
33            )
34        })
35    {
36        return Err(garde::Error::new("must stay inside the repository"));
37    }
38    Ok(())
39}
40
41/// A rule for a list that must not be empty. The `noun` names what the list
42/// holds, in the message: `at_least_one("label")` reports "must declare at
43/// least one label".
44///
45/// # Errors
46///
47/// The returned rule fails with that message when the list is empty.
48pub fn at_least_one<T, C>(noun: &'static str) -> impl Fn(&[T], &C) -> garde::Result {
49    move |items, _| {
50        if items.is_empty() {
51            return Err(garde::Error::new(format!(
52                "must declare at least one {noun}"
53            )));
54        }
55        Ok(())
56    }
57}
58
59/// A rule for a string that must not be empty. The `because` text gives the
60/// reason, so the message explains itself.
61///
62/// The message is "cannot be empty", a dash, and `because`.
63///
64/// # Errors
65///
66/// The returned rule fails with that message when the string is empty.
67pub fn cannot_be_empty<C>(because: &'static str) -> impl Fn(&str, &C) -> garde::Result {
68    move |value, _| {
69        if value.is_empty() {
70            return Err(garde::Error::new(format!("cannot be empty — {because}")));
71        }
72        Ok(())
73    }
74}
75
76/// One file name, so a name cannot smuggle in the path it was separated from.
77///
78/// # Errors
79///
80/// "must be one file name" for a name with a directory part.
81pub fn one_file_name<C>(name: &str, _: &C) -> garde::Result {
82    let path = Path::new(name);
83    if path.file_name().is_none_or(|only| only != name) {
84        return Err(garde::Error::new("must be one file name"));
85    }
86    Ok(())
87}