gwm-cli 1.6.0

git worktree manager — TUI + CLI, native libgit2, per-repo bootstrap
Documentation
use crate::error::{GwmError, Result};
use serde::Deserialize;
use std::collections::BTreeMap;

#[derive(Debug, Clone, Default)]
pub struct TemplateContext {
  values: BTreeMap<String, String>,
}

impl TemplateContext {
  pub fn from_pairs<const N: usize>(pairs: [(&str, &str); N]) -> Self {
    Self {
      values: pairs
        .into_iter()
        .map(|(key, value)| (key.to_string(), value.to_string()))
        .collect(),
    }
  }
}

#[derive(Debug, Clone, Default)]
pub struct FormDefaults {
  pub fields: BTreeMap<String, String>,
}

#[derive(Debug, Clone, Default)]
pub struct FormMetadata {
  pub title: Option<String>,
  pub labels: Vec<String>,
}

pub fn render_template(template: &str, ctx: &TemplateContext) -> String {
  let mut rendered = template.to_string();
  for (key, value) in &ctx.values {
    rendered = rendered.replace(&format!("{{{}}}", key), value);
  }
  rendered
}

pub fn issue_form_metadata(raw: &str) -> Result<FormMetadata> {
  let form = parse_issue_form(raw)?;
  Ok(FormMetadata {
    title: form.title,
    labels: form.labels,
  })
}

pub fn render_form_markdown(raw: &str, ctx: &TemplateContext, defaults: &FormDefaults) -> Result<String> {
  let form = parse_issue_form(raw)?;
  let mut out = String::new();
  for item in form.body {
    match item.kind.as_str() {
      "markdown" => {
        if let Some(value) = item.attributes.value {
          push_block(&mut out, &render_template(&value, ctx));
        }
      }
      "textarea" => {
        let label = item
          .attributes
          .label
          .clone()
          .unwrap_or_else(|| item.id.clone().unwrap_or_else(|| "Field".into()));
        let value = field_value(&item, defaults).unwrap_or_default();
        push_block(&mut out, &format!("## {}\n\n{}", label, render_template(&value, ctx)));
      }
      "input" | "dropdown" => {
        let label = item
          .attributes
          .label
          .clone()
          .unwrap_or_else(|| item.id.clone().unwrap_or_else(|| "Field".into()));
        let value = field_value(&item, defaults).unwrap_or_default();
        push_block(&mut out, &format!("**{}:** {}", label, render_template(&value, ctx)));
      }
      _ => {}
    }
  }
  Ok(out.trim_end().to_string())
}

fn parse_issue_form(raw: &str) -> Result<IssueForm> {
  serde_yaml_ng::from_str(raw).map_err(|e| GwmError::Config(format!("issue template YAML: {}", e)))
}

fn field_value(item: &IssueFormItem, defaults: &FormDefaults) -> Option<String> {
  if let Some(id) = &item.id {
    if let Some(value) = defaults.fields.get(id) {
      return Some(value.clone());
    }
  }
  item
    .attributes
    .value
    .clone()
    .or_else(|| item.attributes.placeholder.clone())
}

fn push_block(out: &mut String, block: &str) {
  let block = block.trim();
  if block.is_empty() {
    return;
  }
  if !out.is_empty() {
    out.push_str("\n\n");
  }
  out.push_str(block);
}

#[derive(Debug, Deserialize)]
struct IssueForm {
  #[serde(default)]
  title: Option<String>,
  #[serde(default)]
  labels: Vec<String>,
  #[serde(default)]
  body: Vec<IssueFormItem>,
}

#[derive(Debug, Deserialize)]
struct IssueFormItem {
  #[serde(rename = "type")]
  kind: String,
  #[serde(default)]
  id: Option<String>,
  #[serde(default)]
  attributes: IssueFormAttributes,
}

#[derive(Debug, Default, Deserialize)]
struct IssueFormAttributes {
  #[serde(default)]
  label: Option<String>,
  #[serde(default)]
  value: Option<String>,
  #[serde(default)]
  placeholder: Option<String>,
}