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_yml::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}