crates/kernel/ents-effect/src/definition.rs
definition.rshistorycomment 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 | |
| 11 | use ents_model::{Effect, namespace}; |
| 12 | use ents_query::Query; |
| 13 | |
| 14 | use 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) |
| 50 | pub 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)] |
| 60 | mod 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 | } |