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