git-ents.gitmain
⌘K
foforge
effect.rs99 lines · 3.8 KB · rusthistorycomment on this file
1//! The Effect entity: a declarative subscription to a commit-set query.
2//!
3//! Spec coverage: `model.effect-definition`.
4
5use ents_attrs as ents;
6use facet::Facet;
7
8/// A declarative effect definition, living at `refs/meta/effects/<name>`
9/// (`namespace::effect_ref`).
10///
11/// `trigger` is the raw `CommitQuery` text (`query.grammar`): the algebra
12/// itself — parsing, footprint extraction, incremental evaluation — is
13/// `ents-query`'s domain (phase 3). Storing it as `String` here rather than
14/// a parsed AST keeps the dependency edge the crate graph already states:
15/// `ents-query` depends on `ents-model`, never the reverse.
16///
17/// `model.effect-definition` explicitly forbids executor, sandbox, or
18/// retry fields — how an effect runs is a deployment property
19/// (`effect.deployment-property`), decided by `ents-effect` (phase 5) and
20/// the composition root, never stored on the entity itself.
21///
22/// # Examples
23///
24/// ```
25/// use ents_model::Effect;
26///
27/// let effect = Effect {
28/// name: "unit".to_owned(),
29/// trigger: "rev(refs/heads/main)".to_owned(),
30/// toolchains: vec!["rust-stable".to_owned()],
31/// run: "cargo nextest run".to_owned(),
32/// };
33/// let (id, store) = facet_git_tree::serialize(&effect).expect("serialize");
34/// let back: Effect = facet_git_tree::deserialize(&id, &store).expect("deserialize");
35/// assert_eq!(back, effect);
36/// ```
37// @relation(model.effect-definition, effect.definition, effect.deployment-property, meta-ref.identity-binding, meta-ref.typed-tree, model.extensibility, scope=file)
38#[derive(Debug, Clone, PartialEq, Eq, Facet)]
39pub struct Effect {
40 /// The effect's own name — the natural key the refname's final segment
41 /// binds to (`model.effect-definition`, `meta-ref.identity-binding`):
42 /// the gate recomputes `refs/meta/effects/<name>` from this field.
43 #[facet(ents::skip)]
44 pub name: String,
45 /// The raw `CommitQuery` text denoting the commit set this effect
46 /// fires for (`query.grammar`).
47 #[facet(ents::col)]
48 pub trigger: String,
49 /// The names of the toolchains this effect's run requires, each a
50 /// `refs/meta/toolchains/<name>` reference (`model.toolchain`).
51 #[facet(ents::skip_empty)]
52 pub toolchains: Vec<String>,
53 /// The run command.
54 pub run: String,
55}
56
57#[cfg(test)]
58mod tests {
59 #![allow(
60 clippy::expect_used,
61 clippy::panic,
62 reason = "unit test; the panic is an assertion the type reflects as a struct at all"
63 )]
64
65 use facet::{Facet as _, Type, UserType};
66 use facet_git_tree::{deserialize, serialize};
67 use rstest::rstest;
68
69 use super::*;
70
71 #[rstest]
72 // @relation(model.effect-definition, effect.definition, meta-ref.typed-tree, scope=function, role=Verifies)
73 fn effect_round_trips_through_a_tree() {
74 let effect = Effect {
75 name: "unit".to_owned(),
76 trigger: "rev(refs/heads/main) & results(unit, pass)".to_owned(),
77 toolchains: vec!["rust-stable".to_owned(), "node-lts".to_owned()],
78 run: "cargo nextest run".to_owned(),
79 };
80 let (id, store) = serialize(&effect).expect("serialize");
81 let back: Effect = deserialize(&id, &store).expect("deserialize");
82 assert_eq!(back, effect);
83 }
84
85 #[rstest]
86 #[case::executor("executor")]
87 #[case::sandbox("sandbox")]
88 #[case::retry("retry")]
89 // @relation(model.effect-definition, effect.deployment-property, scope=function, role=Verifies)
90 fn effect_never_carries_a_deployment_field(#[case] forbidden: &str) {
91 let Type::User(UserType::Struct(struct_ty)) = Effect::SHAPE.ty else {
92 panic!("Effect must reflect as a struct");
93 };
94 assert!(
95 struct_ty.fields.iter().all(|f| f.name != forbidden),
96 "Effect must not carry a {forbidden:?} field: how it runs is a deployment property"
97 );
98 }
99}