loopsmith_core/config/
guidelines.rs1use schemars::JsonSchema;
30use serde::{Deserialize, Serialize};
31
32#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
33#[serde(deny_unknown_fields)]
34pub struct ExecutionGuidelines {
35 #[serde(default)]
36 pub items: Vec<Guideline>,
37 #[serde(default)]
43 pub dependency: Vec<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
47#[serde(deny_unknown_fields)]
48pub struct Guideline {
49 pub name: String,
50 pub guideline: String,
53 #[serde(default)]
55 pub note: Option<String>,
56}
57
58#[derive(Debug, Clone, PartialEq)]
61pub struct Phase {
62 pub name: String,
63 pub guideline: String,
64 pub depends_on: Vec<String>,
65}
66
67impl ExecutionGuidelines {
68 pub fn is_empty(&self) -> bool {
69 self.items.is_empty()
70 }
71
72 pub fn get(&self, name: &str) -> Option<&Guideline> {
73 self.items.iter().find(|g| g.name == name)
74 }
75
76 pub fn names(&self) -> Vec<&str> {
77 self.items.iter().map(|g| g.name.as_str()).collect()
78 }
79
80 pub fn edges(&self) -> Result<Vec<(String, String)>, String> {
85 let mut out = Vec::new();
86 for line in &self.dependency {
87 out.extend(parse_chain(line)?);
88 }
89 Ok(out)
90 }
91
92 pub fn phases(&self) -> Result<Vec<Phase>, String> {
97 let edges = self.edges()?;
98 Ok(self
99 .items
100 .iter()
101 .map(|g| Phase {
102 name: g.name.clone(),
103 guideline: g.guideline.clone(),
104 depends_on: edges
105 .iter()
106 .filter(|(_, to)| *to == g.name)
107 .map(|(from, _)| from.clone())
108 .collect(),
109 })
110 .collect())
111 }
112}
113
114pub fn parse_chain(line: &str) -> Result<Vec<(String, String)>, String> {
116 let parts: Vec<&str> = line.split("->").map(str::trim).collect();
117 if parts.len() < 2 {
118 return Err(format!(
119 "`{line}` is not an ordering; write it as `earlier -> later`"
120 ));
121 }
122 if let Some(blank) = parts.iter().position(|p| p.is_empty()) {
123 return Err(format!(
124 "`{line}` has an empty name at position {}; every `->` needs a guideline on both sides",
125 blank + 1
126 ));
127 }
128 Ok(parts
129 .windows(2)
130 .map(|w| (w[0].to_string(), w[1].to_string()))
131 .collect())
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 fn guidelines(items: &[&str], dependency: &[&str]) -> ExecutionGuidelines {
139 ExecutionGuidelines {
140 items: items
141 .iter()
142 .map(|n| Guideline {
143 name: n.to_string(),
144 guideline: format!("do the {n} work"),
145 note: None,
146 })
147 .collect(),
148 dependency: dependency.iter().map(|s| s.to_string()).collect(),
149 }
150 }
151
152 #[test]
153 fn a_two_name_arrow_is_one_edge() {
154 assert_eq!(
155 parse_chain("gather -> draft").unwrap(),
156 vec![("gather".into(), "draft".into())]
157 );
158 }
159
160 #[test]
161 fn a_chain_becomes_consecutive_edges() {
162 assert_eq!(
163 parse_chain("a -> b -> c").unwrap(),
164 vec![("a".into(), "b".into()), ("b".into(), "c".into())]
165 );
166 }
167
168 #[test]
169 fn spacing_around_the_arrow_does_not_matter() {
170 assert_eq!(parse_chain("a->b").unwrap(), parse_chain("a -> b").unwrap());
171 }
172
173 #[test]
174 fn a_line_without_an_arrow_is_refused() {
175 let err = parse_chain("gather").unwrap_err();
176 assert!(err.contains("earlier -> later"), "got: {err}");
177 }
178
179 #[test]
180 fn a_dangling_arrow_is_refused() {
181 for line in ["a ->", "-> b", "a -> -> c"] {
182 let err = parse_chain(line).unwrap_err();
183 assert!(err.contains("empty name"), "for `{line}`, got: {err}");
184 }
185 }
186
187 #[test]
188 fn phases_carry_the_predecessors_the_arrows_named() {
189 let g = guidelines(&["gather", "draft", "review"], &["gather -> draft -> review"]);
190 let phases = g.phases().unwrap();
191 assert_eq!(phases[0].depends_on, Vec::<String>::new());
192 assert_eq!(phases[1].depends_on, vec!["gather"]);
193 assert_eq!(phases[2].depends_on, vec!["draft"]);
194 }
195
196 #[test]
197 fn guidelines_with_no_arrow_between_them_are_independent() {
198 let g = guidelines(&["seo", "social"], &[]);
201 let phases = g.phases().unwrap();
202 assert!(phases.iter().all(|p| p.depends_on.is_empty()));
203 }
204
205 #[test]
206 fn one_phase_can_wait_on_several() {
207 let g = guidelines(
208 &["a", "b", "publish"],
209 &["a -> publish", "b -> publish"],
210 );
211 let publish = &g.phases().unwrap()[2];
212 assert_eq!(publish.depends_on, vec!["a", "b"]);
213 }
214}