git-ents.gitmain
⌘K
foforge
definition.rs105 lines · 3.6 KB · rusthistorycomment on this file
1//! Write-time validation of an effect definition (`effect.validation`).
2//!
3//! `ents-receive` cannot call this (`arch.query-effect-split`: no push path
4//! may link executor code), so a future frontend that builds an effect
5//! definition's commit (`git effect add`, `git-ents` bin, phase 6) calls
6//! [`validate`] itself before ever proposing the write — `receive` still
7//! admits or refuses the *push* on its own terms (the gate, `receive.unit`);
8//! this only keeps a frontend from proposing a definition that could never
9//! usefully run.
10
11use ents_model::{Effect, namespace};
12use ents_query::Query;
13
14use crate::error::{Error, Result};
15
16/// Reject `effect` before it is ever proposed to `receive`, per
17/// `effect.validation`: every name in `toolchains` must be a valid
18/// ref-path segment, and `trigger` must parse as a `CommitQuery`
19/// (`query.grammar`) — which already rejects a `rev(expr)` naming a
20/// `refs/meta/*` pattern (`query.rev`) and a `meta(glob)` naming an
21/// effect-written namespace (`query.meta`), since the parser enforces
22/// both.
23///
24/// # Errors
25///
26/// [`Error::Trigger`] if `trigger` does not parse; [`Error::InvalidToolchainName`]
27/// for the first toolchain name that is not a valid ref-path segment.
28///
29/// # Examples
30///
31/// ```
32/// use ents_effect::definition::validate;
33/// use ents_model::Effect;
34///
35/// let good = Effect {
36/// name: "unit".into(),
37/// trigger: "rev(refs/heads/main)".into(),
38/// toolchains: vec!["rust-stable".into()],
39/// run: "cargo test".into(),
40/// };
41/// assert!(validate(&good).is_ok());
42///
43/// let bad_trigger = Effect { trigger: "not a query".into(), ..good.clone() };
44/// assert!(validate(&bad_trigger).is_err());
45///
46/// let bad_toolchain = Effect { toolchains: vec!["../escape".into()], ..good };
47/// assert!(validate(&bad_toolchain).is_err());
48/// ```
49// @relation(effect.validation, scope=function)
50pub fn validate(effect: &Effect) -> Result<()> {
51 effect.trigger.parse::<Query>().map_err(Error::from)?;
52 for name in &effect.toolchains {
53 namespace::toolchain_ref(name)
54 .map_err(|_invalid| Error::InvalidToolchainName(name.clone()))?;
55 }
56 Ok(())
57}
58
59#[cfg(test)]
60mod tests {
61 #![allow(clippy::expect_used, reason = "unit test")]
62
63 use rstest::rstest;
64
65 use super::*;
66
67 fn effect(trigger: &str, toolchains: &[&str]) -> Effect {
68 Effect {
69 name: "unit".to_owned(),
70 trigger: trigger.to_owned(),
71 toolchains: toolchains.iter().map(|s| (*s).to_owned()).collect(),
72 run: "true".to_owned(),
73 }
74 }
75
76 #[rstest]
77 // @relation(effect.validation, scope=function, role=Verifies)
78 fn validate_accepts_a_well_formed_definition() {
79 validate(&effect("rev(refs/heads/main)", &["rust-stable"])).expect("well-formed");
80 }
81
82 #[rstest]
83 // @relation(effect.validation, scope=function, role=Verifies)
84 fn validate_rejects_an_unparsable_trigger() {
85 assert!(validate(&effect("not a query", &[])).is_err());
86 }
87
88 #[rstest]
89 // @relation(effect.validation, query.rev, scope=function, role=Verifies)
90 fn validate_rejects_a_rev_naming_a_meta_pattern() {
91 assert!(validate(&effect("rev(refs/meta/effects/*)", &[])).is_err());
92 }
93
94 #[rstest]
95 // @relation(effect.validation, query.meta, scope=function, role=Verifies)
96 fn validate_rejects_a_meta_glob_naming_an_effect_written_namespace() {
97 assert!(validate(&effect("meta(refs/meta/results/*)", &[])).is_err());
98 }
99
100 #[rstest]
101 // @relation(effect.validation, scope=function, role=Verifies)
102 fn validate_rejects_an_invalid_toolchain_name() {
103 assert!(validate(&effect("rev(refs/heads/main)", &["../escape"])).is_err());
104 }
105}