crate::ix!();
#[derive(Debug, StructOpt, Builder, Getters, Setters, Clone)]
#[builder(setter(into))]
#[getset(get = "pub", set = "pub")]
#[structopt(
name = "ManualGrowerFlow",
about = "Runs the step-by-step, human-in-the-loop growth flow"
)]
pub struct ManualGrowerFlowCliArgs {
#[structopt(long, help = "Path to partial model JSON directory")]
growing_patch_bay: PathBuf,
#[structopt(
long = "toml-inputs-file",
help = "If provided, override CLI-based GrowerInputs"
)]
toml_inputs_file: PathBuf,
#[structopt(long = "force-override-drift")]
force_override_drift: bool,
#[structopt(long, default_value = "query.txt")]
query_file: PathBuf,
#[structopt(long = "clear-clipboard")]
clear_clipboard: bool,
#[structopt(long = "failed-parse-dir")]
failed_parse_dir: Option<PathBuf>,
}
impl ManualGrowerFlowCliArgs {
#[instrument(level = "trace", skip_all)]
pub async fn run_with_strategy<S: GrowerFlowStrategy>(
&self,
strategy: &S,
) -> Result<(), ManualGrowerFlowError> {
trace!("Starting run_with_strategy() for CLI args: {:?}", self);
self.maybe_clear_clipboard();
let raw_clipboard: String = match self.read_raw_clipboard_text() {
Ok(txt) => txt,
Err(e) => {
warn!("Could not read raw clipboard => ignoring snippet. Error: {:?}", e);
String::new()
}
};
let snippet: Option<ClipboardSnippet> =
self.parse_clipboard_snippet_in_topdown_order(&raw_clipboard);
if let Some(sn) = snippet {
self.handle_parsed_snippet(sn, strategy).await
} else {
self.fallback_toml_flow(strategy).await
}
}
fn maybe_clear_clipboard(&self) {
if *self.clear_clipboard() {
trace!("User requested --clear-clipboard => clearing the clipboard now.");
if let Err(e) = system_clear_clipboard() {
warn!("Could not clear system clipboard: {:?}", e);
}
}
}
pub fn read_raw_clipboard_text(&self) -> Result<String, FuzzyClipboardParseError> {
let mut ctx = ClipboardContext::new()
.map_err(|_| FuzzyClipboardParseError::ClipboardContextCreationFailed)?;
ctx.get_contents()
.map_err(|_| FuzzyClipboardParseError::ClipboardGetContentsFailed)
}
pub fn parse_clipboard_snippet_in_topdown_order(
&self,
raw_clipboard: &str,
) -> Option<ClipboardSnippet> {
if let Ok(Some(parsed)) = attempt_snippet_parse::<JustifiedGrowerTreeConfiguration>(
raw_clipboard,
"JustifiedGrowerTreeConfiguration",
self.failed_parse_dir().as_ref(),
) {
return Some(ClipboardSnippet::JustifiedGrowerTreeConfiguration(parsed));
}
if let Ok(Some(parsed)) = attempt_snippet_parse::<JustifiedStringSkeleton>(
raw_clipboard,
"JustifiedStringSkeleton",
self.failed_parse_dir().as_ref(),
) {
return Some(ClipboardSnippet::JustifiedStringSkeleton(parsed));
}
if let Ok(Some(parsed)) = attempt_snippet_parse::<CoreStringSkeleton>(
raw_clipboard,
"CoreStringSkeleton",
self.failed_parse_dir().as_ref(),
) {
return Some(ClipboardSnippet::CoreStringSkeleton(parsed));
}
if let Ok(Some(parsed)) = attempt_snippet_parse::<AnnotatedLeafHolderExpansions>(
raw_clipboard,
"AnnotatedLeafHolderExpansions",
self.failed_parse_dir().as_ref(),
) {
return Some(ClipboardSnippet::AnnotatedLeafHolderExpansions(parsed));
}
None
}
pub async fn handle_parsed_snippet<S: GrowerFlowStrategy>(
&self,
snippet: ClipboardSnippet,
strategy: &S,
) -> Result<(), ManualGrowerFlowError> {
match snippet.target_name() {
Some(tn) if !tn.trim().is_empty() => {
let file_path = self.growing_patch_bay().join(format!("{}.json", tn));
if !file_path.exists() {
error!("Partial file for target='{tn}' does not exist => ignoring snippet => done.");
return Ok(());
}
let mut snippet_partial = match PartiallyGrownModel::load_from_file_fuzzy(&file_path).await {
Ok(pm) => pm,
Err(e) => {
error!("Could not parse partial file for target='{tn}': {e:?} => skipping snippet => done.");
return Ok(());
}
};
let missing_step = snippet_partial.validate().err();
let partial_rank = missing_field_rank(&missing_step);
let snippet_rank = snippet_field_rank(&snippet);
trace!("Target='{}': partial missing={:?} => rank={} ; snippet rank={}", tn, missing_step, partial_rank, snippet_rank);
if partial_rank == 0 {
warn!("Partial is already fully valid => ignoring snippet => done.");
return Ok(());
}
else if snippet_rank < partial_rank {
warn!("Snippet is for an earlier step => ignoring snippet, continuing from partial as-is...");
self.post_snippet_partial_loop(snippet_partial, file_path, strategy).await
}
else if snippet_rank == partial_rank {
snippet_partial.apply_clipboard_snippet(snippet);
snippet_partial.save_to_file(&file_path).await?;
self.post_snippet_partial_loop(snippet_partial, file_path, strategy).await
}
else {
if snippet_rank == partial_rank + 1 {
info!("Snippet is exactly one step ahead => applying anyway...");
snippet_partial.apply_clipboard_snippet(snippet);
snippet_partial.save_to_file(&file_path).await?;
self.post_snippet_partial_loop(snippet_partial, file_path, strategy).await
} else {
error!("Snippet is more than one step ahead => ignoring snippet => done.");
Ok(())
}
}
}
_ => {
warn!("Clipboard snippet has no valid target_name => ignoring => done.");
Ok(())
}
}
}
async fn post_snippet_partial_loop<S: GrowerFlowStrategy>(
&self,
mut partial: PartiallyGrownModel,
file_path: PathBuf,
strategy: &S,
) -> Result<(), ManualGrowerFlowError> {
loop {
match partial.validate() {
Ok(_) => {
info!("Partial is now fully valid => finalize => done.");
let fm = GrowerModel::finalize_from_valid_partial(partial.clone())
.map_err(ManualGrowerFlowError::GrowerModelGenError)?;
fm.save_to_file(&file_path).await?;
return Ok(());
}
Err(missing) => {
let gi = partial.grower_inputs().clone().unwrap();
if let Err(e2) = manual_flow_try_to_fix_missing_field(
strategy,
&mut partial,
&gi,
&missing,
self,
)
.await
{
warn!("Could not fix missing field => exiting: {:?}", e2);
return Ok(());
}
partial.save_to_file(&file_path).await?;
}
}
}
}
pub async fn fallback_toml_flow<S: GrowerFlowStrategy>(
&self,
strategy: &S,
) -> Result<(), ManualGrowerFlowError> {
trace!("No parseable snippet => continuing with TOML flow.");
let grower_inputs = self.load_and_validate_toml()?;
let mut partial_model = self.open_or_create_partial(&grower_inputs).await?;
partial_model = self.resolve_input_drift(&grower_inputs, partial_model).await?;
loop {
match partial_model.validate() {
Ok(()) => {
info!("Partial is fully valid => finalize => done.");
let final_model = GrowerModel::finalize_from_valid_partial(partial_model.clone())
.map_err(ManualGrowerFlowError::GrowerModelGenError)?;
let partial_path = self.calculate_partial_path(&grower_inputs);
final_model.save_to_file(&partial_path).await?;
return Ok(());
}
Err(missing) => {
if let Err(e) = manual_flow_try_to_fix_missing_field(
strategy,
&mut partial_model,
&grower_inputs,
&missing,
self,
)
.await
{
warn!("Could not fill missing field => exiting. Error={:?}", e);
return Ok(());
}
}
}
}
}
pub fn calculate_partial_path(&self, grower_inputs: &GrowerInputs) -> PathBuf {
self.growing_patch_bay
.join(format!("{}.json", grower_inputs.target()))
}
#[instrument(level = "trace", skip_all)]
fn load_and_validate_toml(&self) -> Result<GrowerInputs, ManualGrowerFlowError> {
debug!("Loading GrowerInputs from: {:?}", self.toml_inputs_file());
let gi = GrowerInputs::from_toml_file(self.toml_inputs_file())?;
gi.validate()?;
Ok(gi)
}
#[instrument(level = "trace", skip_all)]
async fn open_or_create_partial(
&self,
grower_inputs: &GrowerInputs,
) -> Result<PartiallyGrownModel, ManualGrowerFlowError> {
let partial_path = self.calculate_partial_path(grower_inputs);
maybe_write_initial_partial_model_file(&partial_path).await?;
let partial_model = PartiallyGrownModel::load_from_file_fuzzy(&partial_path).await?;
Ok(partial_model)
}
#[instrument(level = "trace", skip_all)]
async fn resolve_input_drift(
&self,
grower_inputs: &GrowerInputs,
mut partial_model: PartiallyGrownModel,
) -> Result<PartiallyGrownModel, ManualGrowerFlowError> {
let partial_path = self.calculate_partial_path(grower_inputs);
if let Some(pm_grower_inputs) = partial_model.grower_inputs() {
if pm_grower_inputs != grower_inputs {
if !self.force_override_drift() {
if partial_model.essentially_empty() {
warn!("Inputs drift but partial is basically empty => overwriting grower_inputs");
partial_model = PartiallyGrownModel::from(grower_inputs.clone());
partial_model.save_to_file(&partial_path).await?;
} else {
warn!("Inputs drift => exiting early unless forced override is used.");
return Ok(partial_model);
}
} else {
warn!("Inputs drift => forced override => clobbering partial");
partial_model = PartiallyGrownModel::from(grower_inputs.clone());
partial_model.save_to_file(&partial_path).await?;
}
}
} else {
partial_model.set_grower_inputs(Some(grower_inputs.clone()));
partial_model.save_to_file(&partial_path).await?;
}
Ok(partial_model)
}
}