1crate::ix!();
3
4#[derive(Debug, StructOpt, Builder, Getters, Setters, Clone)]
5#[builder(setter(into))]
6#[getset(get = "pub", set = "pub")]
7#[structopt(
8 name = "ManualGrowerFlow",
9 about = "Runs the step-by-step, human-in-the-loop growth flow"
10)]
11pub struct ManualGrowerFlowCliArgs {
12
13 #[structopt(long, help = "Path to partial model JSON directory")]
15 growing_patch_bay: PathBuf,
16
17 #[structopt(
19 long = "toml-inputs-file",
20 help = "If provided, override CLI-based GrowerInputs"
21 )]
22 toml_inputs_file: PathBuf,
23
24 #[structopt(long = "force-override-drift")]
25 force_override_drift: bool,
26
27 #[structopt(long, default_value = "query.txt")]
29 query_file: PathBuf,
30
31 #[structopt(long = "clear-clipboard")]
33 clear_clipboard: bool,
34
35 #[structopt(long = "failed-parse-dir")]
37 failed_parse_dir: Option<PathBuf>,
38}
39
40impl ManualGrowerFlowCliArgs {
47
48 #[instrument(level = "trace", skip_all)]
51 pub async fn run_with_strategy<S: GrowerFlowStrategy>(
52 &self,
53 strategy: &S,
54 ) -> Result<(), ManualGrowerFlowError> {
55 trace!("Starting run_with_strategy() for CLI args: {:?}", self);
56
57 self.maybe_clear_clipboard();
59
60 let raw_clipboard: String = match self.read_raw_clipboard_text() {
62 Ok(txt) => txt,
63 Err(e) => {
64 warn!("Could not read raw clipboard => ignoring snippet. Error: {:?}", e);
65 String::new()
66 }
67 };
68
69 let snippet: Option<ClipboardSnippet> =
71 self.parse_clipboard_snippet_in_topdown_order(&raw_clipboard);
72
73 if let Some(sn) = snippet {
75 self.handle_parsed_snippet(sn, strategy).await
76 } else {
77 self.fallback_toml_flow(strategy).await
78 }
79 }
80
81 fn maybe_clear_clipboard(&self) {
85 if *self.clear_clipboard() {
86 trace!("User requested --clear-clipboard => clearing the clipboard now.");
87 if let Err(e) = system_clear_clipboard() {
88 warn!("Could not clear system clipboard: {:?}", e);
89 }
90 }
91 }
92
93 pub fn read_raw_clipboard_text(&self) -> Result<String, FuzzyClipboardParseError> {
97 let mut ctx = ClipboardContext::new()
99 .map_err(|_| FuzzyClipboardParseError::ClipboardContextCreationFailed)?;
100 ctx.get_contents()
101 .map_err(|_| FuzzyClipboardParseError::ClipboardGetContentsFailed)
102 }
103
104 pub fn parse_clipboard_snippet_in_topdown_order(
108 &self,
109 raw_clipboard: &str,
110 ) -> Option<ClipboardSnippet> {
111 if let Ok(Some(parsed)) = attempt_snippet_parse::<JustifiedGrowerTreeConfiguration>(
113 raw_clipboard,
114 "JustifiedGrowerTreeConfiguration",
115 self.failed_parse_dir().as_ref(),
116 ) {
117 return Some(ClipboardSnippet::JustifiedGrowerTreeConfiguration(parsed));
118 }
119
120 if let Ok(Some(parsed)) = attempt_snippet_parse::<JustifiedStringSkeleton>(
122 raw_clipboard,
123 "JustifiedStringSkeleton",
124 self.failed_parse_dir().as_ref(),
125 ) {
126 return Some(ClipboardSnippet::JustifiedStringSkeleton(parsed));
127 }
128
129 if let Ok(Some(parsed)) = attempt_snippet_parse::<CoreStringSkeleton>(
133 raw_clipboard,
134 "CoreStringSkeleton",
135 self.failed_parse_dir().as_ref(),
136 ) {
137 return Some(ClipboardSnippet::CoreStringSkeleton(parsed));
138 }
139
140 if let Ok(Some(parsed)) = attempt_snippet_parse::<AnnotatedLeafHolderExpansions>(
142 raw_clipboard,
143 "AnnotatedLeafHolderExpansions",
144 self.failed_parse_dir().as_ref(),
145 ) {
146 return Some(ClipboardSnippet::AnnotatedLeafHolderExpansions(parsed));
147 }
148
149 None
150 }
151
152 pub async fn handle_parsed_snippet<S: GrowerFlowStrategy>(
156 &self,
157 snippet: ClipboardSnippet,
158 strategy: &S,
159 ) -> Result<(), ManualGrowerFlowError> {
160 match snippet.target_name() {
161 Some(tn) if !tn.trim().is_empty() => {
162 let file_path = self.growing_patch_bay().join(format!("{}.json", tn));
163 if !file_path.exists() {
164 error!("Partial file for target='{tn}' does not exist => ignoring snippet => done.");
165 return Ok(());
166 }
167
168 let mut snippet_partial = match PartiallyGrownModel::load_from_file_fuzzy(&file_path).await {
169 Ok(pm) => pm,
170 Err(e) => {
171 error!("Could not parse partial file for target='{tn}': {e:?} => skipping snippet => done.");
172 return Ok(());
173 }
174 };
175
176 let missing_step = snippet_partial.validate().err();
177 let partial_rank = missing_field_rank(&missing_step);
178 let snippet_rank = snippet_field_rank(&snippet);
179 trace!("Target='{}': partial missing={:?} => rank={} ; snippet rank={}", tn, missing_step, partial_rank, snippet_rank);
180
181 if partial_rank == 0 {
182 warn!("Partial is already fully valid => ignoring snippet => done.");
183 return Ok(());
184 }
185 else if snippet_rank < partial_rank {
186 warn!("Snippet is for an earlier step => ignoring snippet, continuing from partial as-is...");
188
189 self.post_snippet_partial_loop(snippet_partial, file_path, strategy).await
193 }
194 else if snippet_rank == partial_rank {
195 snippet_partial.apply_clipboard_snippet(snippet);
197 snippet_partial.save_to_file(&file_path).await?;
198 self.post_snippet_partial_loop(snippet_partial, file_path, strategy).await
199 }
200 else {
201 if snippet_rank == partial_rank + 1 {
203 info!("Snippet is exactly one step ahead => applying anyway...");
204 snippet_partial.apply_clipboard_snippet(snippet);
205 snippet_partial.save_to_file(&file_path).await?;
206 self.post_snippet_partial_loop(snippet_partial, file_path, strategy).await
207 } else {
208 error!("Snippet is more than one step ahead => ignoring snippet => done.");
209 Ok(())
210 }
211 }
212 }
213 _ => {
214 warn!("Clipboard snippet has no valid target_name => ignoring => done.");
215 Ok(())
216 }
217 }
218 }
219
220 async fn post_snippet_partial_loop<S: GrowerFlowStrategy>(
223 &self,
224 mut partial: PartiallyGrownModel,
225 file_path: PathBuf,
226 strategy: &S,
227 ) -> Result<(), ManualGrowerFlowError> {
228 loop {
229 match partial.validate() {
230 Ok(_) => {
231 info!("Partial is now fully valid => finalize => done.");
232 let fm = GrowerModel::finalize_from_valid_partial(partial.clone())
233 .map_err(ManualGrowerFlowError::GrowerModelGenError)?;
234 fm.save_to_file(&file_path).await?;
235 return Ok(());
236 }
237 Err(missing) => {
238 let gi = partial.grower_inputs().clone().unwrap();
239 if let Err(e2) = manual_flow_try_to_fix_missing_field(
240 strategy,
241 &mut partial,
242 &gi,
243 &missing,
244 self,
245 )
246 .await
247 {
248 warn!("Could not fix missing field => exiting: {:?}", e2);
249 return Ok(());
250 }
251 partial.save_to_file(&file_path).await?;
252 }
253 }
254 }
255 }
256
257 pub async fn fallback_toml_flow<S: GrowerFlowStrategy>(
261 &self,
262 strategy: &S,
263 ) -> Result<(), ManualGrowerFlowError> {
264 trace!("No parseable snippet => continuing with TOML flow.");
265
266 let grower_inputs = self.load_and_validate_toml()?;
267 let mut partial_model = self.open_or_create_partial(&grower_inputs).await?;
268 partial_model = self.resolve_input_drift(&grower_inputs, partial_model).await?;
269
270 loop {
271 match partial_model.validate() {
272 Ok(()) => {
273 info!("Partial is fully valid => finalize => done.");
274 let final_model = GrowerModel::finalize_from_valid_partial(partial_model.clone())
275 .map_err(ManualGrowerFlowError::GrowerModelGenError)?;
276 let partial_path = self.calculate_partial_path(&grower_inputs);
277 final_model.save_to_file(&partial_path).await?;
278 return Ok(());
279 }
280 Err(missing) => {
281 if let Err(e) = manual_flow_try_to_fix_missing_field(
282 strategy,
283 &mut partial_model,
284 &grower_inputs,
285 &missing,
286 self,
287 )
288 .await
289 {
290 warn!("Could not fill missing field => exiting. Error={:?}", e);
291 return Ok(());
292 }
293 }
294 }
295 }
296 }
297
298
299 pub fn calculate_partial_path(&self, grower_inputs: &GrowerInputs) -> PathBuf {
301 self.growing_patch_bay
302 .join(format!("{}.json", grower_inputs.target()))
303 }
304
305 #[instrument(level = "trace", skip_all)]
306 fn load_and_validate_toml(&self) -> Result<GrowerInputs, ManualGrowerFlowError> {
307 debug!("Loading GrowerInputs from: {:?}", self.toml_inputs_file());
308 let gi = GrowerInputs::from_toml_file(self.toml_inputs_file())?;
309 gi.validate()?;
310 Ok(gi)
311 }
312
313 #[instrument(level = "trace", skip_all)]
314 async fn open_or_create_partial(
315 &self,
316 grower_inputs: &GrowerInputs,
317 ) -> Result<PartiallyGrownModel, ManualGrowerFlowError> {
318 let partial_path = self.calculate_partial_path(grower_inputs);
319 maybe_write_initial_partial_model_file(&partial_path).await?;
320 let partial_model = PartiallyGrownModel::load_from_file_fuzzy(&partial_path).await?;
321 Ok(partial_model)
322 }
323
324 #[instrument(level = "trace", skip_all)]
325 async fn resolve_input_drift(
326 &self,
327 grower_inputs: &GrowerInputs,
328 mut partial_model: PartiallyGrownModel,
329 ) -> Result<PartiallyGrownModel, ManualGrowerFlowError> {
330 let partial_path = self.calculate_partial_path(grower_inputs);
331
332 if let Some(pm_grower_inputs) = partial_model.grower_inputs() {
333 if pm_grower_inputs != grower_inputs {
334 if !self.force_override_drift() {
335 if partial_model.essentially_empty() {
336 warn!("Inputs drift but partial is basically empty => overwriting grower_inputs");
337 partial_model = PartiallyGrownModel::from(grower_inputs.clone());
338 partial_model.save_to_file(&partial_path).await?;
339 } else {
340 warn!("Inputs drift => exiting early unless forced override is used.");
341 return Ok(partial_model);
342 }
343 } else {
344 warn!("Inputs drift => forced override => clobbering partial");
345 partial_model = PartiallyGrownModel::from(grower_inputs.clone());
346 partial_model.save_to_file(&partial_path).await?;
347 }
348 }
349 } else {
350 partial_model.set_grower_inputs(Some(grower_inputs.clone()));
351 partial_model.save_to_file(&partial_path).await?;
352 }
353
354 Ok(partial_model)
355 }
356}