mprocs 0.7.2

TUI for running multiple processes
use std::{env::consts::OS, rc::Rc};

use anyhow::bail;
use indexmap::IndexMap;
use serde_yaml::Value;

#[derive(Clone)]
struct Trace(Option<Rc<Box<(String, Trace)>>>);

impl Trace {
  pub fn empty() -> Self {
    Trace(None)
  }

  pub fn add<T: ToString>(&self, seg: T) -> Self {
    Trace(Some(Rc::new(Box::new((seg.to_string(), self.clone())))))
  }

  pub fn to_string(&self) -> String {
    let mut str = String::new();
    fn add(buf: &mut String, trace: &Trace) {
      match &trace.0 {
        Some(part) => {
          add(buf, &part.1);
          buf.push('.');
          buf.push_str(&part.0);
        }
        None => buf.push_str("<config>"),
      }
    }
    add(&mut str, self);

    str
  }
}

pub struct Val<'a>(&'a Value, Trace);

impl<'a> Val<'a> {
  pub fn new(value: &'a Value) -> anyhow::Result<Self> {
    Self::create(value, Trace::empty())
  }

  fn create(value: &'a Value, trace: Trace) -> anyhow::Result<Self> {
    match value {
      Value::Mapping(map) => {
        if map
          .into_iter()
          .next()
          .map_or(false, |(k, _)| k.eq("$select"))
        {
          let (v, t) = Self::select(map, trace.clone())?;
          return Self::create(v, t);
        }
      }
      _ => (),
    }
    Ok(Val(value, trace))
  }

  pub fn raw(&self) -> &Value {
    self.0
  }

  fn select(
    map: &'a serde_yaml::Mapping,
    trace: Trace,
  ) -> anyhow::Result<(&'a Value, Trace)> {
    if map.get(&Value::from("$select")).unwrap() == "os" {
      if let Some(v) = map.get(&Value::from(OS)) {
        return Ok((v, trace.add(OS)));
      }

      if let Some(v) = map.get(&Value::from("$else")) {
        return Ok((v, trace.add("$else")));
      }

      anyhow::bail!(
        "No matching condition found at {}. Use \"$else\" for default value.",
        trace.to_string(),
      )
    } else {
      anyhow::bail!("Expected \"os\" at {}", trace.add("$select").to_string())
    }
  }

  pub fn error_at<T: AsRef<str>>(&self, msg: T) -> anyhow::Error {
    anyhow::format_err!("{} at {}", msg.as_ref(), self.1.to_string())
  }

  pub fn as_bool(&self) -> anyhow::Result<bool> {
    self.0.as_bool().ok_or_else(|| {
      anyhow::format_err!("Expected bool at {}", self.1.to_string())
    })
  }

  pub fn as_usize(&self) -> anyhow::Result<usize> {
    self
      .0
      .as_u64()
      .ok_or_else(|| {
        anyhow::format_err!("Expected int at {}", self.1.to_string())
      })
      .map(|x| x as usize)
  }

  pub fn as_str(&self) -> anyhow::Result<&str> {
    self.0.as_str().ok_or_else(|| {
      anyhow::format_err!("Expected string at {}", self.1.to_string())
    })
  }

  pub fn as_array(&self) -> anyhow::Result<Vec<Val>> {
    self
      .0
      .as_sequence()
      .ok_or_else(|| {
        anyhow::format_err!("Expected array at {}", self.1.to_string())
      })?
      .iter()
      .enumerate()
      .map(|(i, item)| Val::create(item, self.1.add(i)))
      .collect::<anyhow::Result<Vec<_>>>()
  }

  pub fn as_object(&self) -> anyhow::Result<IndexMap<Value, Val>> {
    self
      .0
      .as_mapping()
      .ok_or_else(|| {
        anyhow::format_err!("Expected object at {}", self.1.to_string())
      })?
      .iter()
      .map(|(k, item)| {
        #[inline]
        fn mk_val<'a>(
          k: &'a Value,
          item: &'a Value,
          trace: &'a Trace,
        ) -> anyhow::Result<Val<'a>> {
          Ok(Val::create(item, trace.add(value_to_string(k)?))?)
        }
        Ok((k.to_owned(), mk_val(k, item, &self.1)?))
      })
      .collect::<anyhow::Result<IndexMap<_, _>>>()
  }
}

pub fn value_to_string(value: &Value) -> anyhow::Result<String> {
  match value {
    Value::Null => Ok("null".to_string()),
    Value::Bool(v) => Ok(v.to_string()),
    Value::Number(v) => Ok(v.to_string()),
    Value::String(v) => Ok(v.to_string()),
    Value::Sequence(_v) => {
      bail!("`primitive_to_string` is not implemented for arrays.")
    }
    Value::Mapping(_v) => {
      bail!("`primitive_to_string` is not implemented for objects.")
    }
    Value::Tagged(_) => anyhow::bail!("Yaml tags are not supported"),
  }
}