Skip to main content

ctl_core/
validate.rs

1//! Garde rules every declared schema shares, so a repository's config and a
2//! template's own exports are held to the same rules and complain the same
3//! way.
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. They are generic
7//! over the context, so the same rule works in a schema validated against
8//! nothing and in one validated against the whole document.
9
10use std::path::{Component, Path};
11
12/// Somewhere a repository may write: relative, and never upward out of the
13/// tree. A rooted path without a drive (`\x` on Windows) or a drive-relative
14/// one (`C:x`) is not absolute, yet joining it replaces the repository root,
15/// so any root or prefix component is refused too.
16///
17/// # Errors
18///
19/// "must stay inside the repository" for an absolute or rooted path, one
20/// with a drive prefix, or one with `..`.
21pub fn inside_the_repo<C>(path: &Path, _: &C) -> garde::Result {
22    if path.is_absolute()
23        || path.components().any(|part| {
24            matches!(
25                part,
26                Component::ParentDir | Component::RootDir | Component::Prefix(_)
27            )
28        })
29    {
30        return Err(garde::Error::new("must stay inside the repository"));
31    }
32    Ok(())
33}
34
35/// A list a schema needs something in, complaining in the repository's own
36/// words: `at_least_one("label")` says "must declare at least one label".
37pub fn at_least_one<T, C>(noun: &'static str) -> impl Fn(&[T], &C) -> garde::Result {
38    move |items, _| {
39        if items.is_empty() {
40            return Err(garde::Error::new(format!(
41                "must declare at least one {noun}"
42            )));
43        }
44        Ok(())
45    }
46}
47
48/// A value a schema cannot do without, told what it is for, so the complaint
49/// reads like a sentence: "cannot be empty — it is what marks a template".
50pub fn cannot_be_empty<C>(because: &'static str) -> impl Fn(&str, &C) -> garde::Result {
51    move |value, _| {
52        if value.is_empty() {
53            return Err(garde::Error::new(format!("cannot be empty — {because}")));
54        }
55        Ok(())
56    }
57}
58
59/// One file name, so a name cannot smuggle in the path it was separated from.
60///
61/// # Errors
62///
63/// "must be one file name" for a name with a directory part.
64pub fn one_file_name<C>(name: &str, _: &C) -> garde::Result {
65    let path = Path::new(name);
66    if path.file_name().is_none_or(|only| only != name) {
67        return Err(garde::Error::new("must be one file name"));
68    }
69    Ok(())
70}