Skip to main content

gwm/
templating.rs

1use crate::error::{GwmError, Result};
2use serde::Deserialize;
3use std::collections::BTreeMap;
4
5#[derive(Debug, Clone, Default)]
6pub struct TemplateContext {
7  values: BTreeMap<String, String>,
8}
9
10impl TemplateContext {
11  pub fn from_pairs<const N: usize>(pairs: [(&str, &str); N]) -> Self {
12    Self {
13      values: pairs
14        .into_iter()
15        .map(|(key, value)| (key.to_string(), value.to_string()))
16        .collect(),
17    }
18  }
19}
20
21#[derive(Debug, Clone, Default)]
22pub struct FormDefaults {
23  pub fields: BTreeMap<String, String>,
24}
25
26#[derive(Debug, Clone, Default)]
27pub struct FormMetadata {
28  pub title: Option<String>,
29  pub labels: Vec<String>,
30}
31
32pub fn render_template(template: &str, ctx: &TemplateContext) -> String {
33  let mut rendered = template.to_string();
34  for (key, value) in &ctx.values {
35    rendered = rendered.replace(&format!("{{{}}}", key), value);
36  }
37  rendered
38}
39
40pub fn issue_form_metadata(raw: &str) -> Result<FormMetadata> {
41  let form = parse_issue_form(raw)?;
42  Ok(FormMetadata {
43    title: form.title,
44    labels: form.labels,
45  })
46}
47
48pub fn render_form_markdown(raw: &str, ctx: &TemplateContext, defaults: &FormDefaults) -> Result<String> {
49  let form = parse_issue_form(raw)?;
50  let mut out = String::new();
51  for item in form.body {
52    match item.kind.as_str() {
53      "markdown" => {
54        if let Some(value) = item.attributes.value {
55          push_block(&mut out, &render_template(&value, ctx));
56        }
57      }
58      "textarea" => {
59        let label = item
60          .attributes
61          .label
62          .clone()
63          .unwrap_or_else(|| item.id.clone().unwrap_or_else(|| "Field".into()));
64        let value = field_value(&item, defaults).unwrap_or_default();
65        push_block(&mut out, &format!("## {}\n\n{}", label, render_template(&value, ctx)));
66      }
67      "input" | "dropdown" => {
68        let label = item
69          .attributes
70          .label
71          .clone()
72          .unwrap_or_else(|| item.id.clone().unwrap_or_else(|| "Field".into()));
73        let value = field_value(&item, defaults).unwrap_or_default();
74        push_block(&mut out, &format!("**{}:** {}", label, render_template(&value, ctx)));
75      }
76      _ => {}
77    }
78  }
79  Ok(out.trim_end().to_string())
80}
81
82fn parse_issue_form(raw: &str) -> Result<IssueForm> {
83  serde_yaml_ng::from_str(raw).map_err(|e| GwmError::Config(format!("issue template YAML: {}", e)))
84}
85
86fn field_value(item: &IssueFormItem, defaults: &FormDefaults) -> Option<String> {
87  if let Some(id) = &item.id {
88    if let Some(value) = defaults.fields.get(id) {
89      return Some(value.clone());
90    }
91  }
92  item
93    .attributes
94    .value
95    .clone()
96    .or_else(|| item.attributes.placeholder.clone())
97}
98
99fn push_block(out: &mut String, block: &str) {
100  let block = block.trim();
101  if block.is_empty() {
102    return;
103  }
104  if !out.is_empty() {
105    out.push_str("\n\n");
106  }
107  out.push_str(block);
108}
109
110#[derive(Debug, Deserialize)]
111struct IssueForm {
112  #[serde(default)]
113  title: Option<String>,
114  #[serde(default)]
115  labels: Vec<String>,
116  #[serde(default)]
117  body: Vec<IssueFormItem>,
118}
119
120#[derive(Debug, Deserialize)]
121struct IssueFormItem {
122  #[serde(rename = "type")]
123  kind: String,
124  #[serde(default)]
125  id: Option<String>,
126  #[serde(default)]
127  attributes: IssueFormAttributes,
128}
129
130#[derive(Debug, Default, Deserialize)]
131struct IssueFormAttributes {
132  #[serde(default)]
133  label: Option<String>,
134  #[serde(default)]
135  value: Option<String>,
136  #[serde(default)]
137  placeholder: Option<String>,
138}