1#[derive(Debug, Clone, PartialEq, Eq)]
3pub enum Seg {
4 Text(String),
5 Var(String),
6 Call { func: String, arg: String },
7}
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Template {
11 pub segments: Vec<Seg>,
12}
13
14impl Template {
15 pub fn parse(src: &str) -> Result<Template, String> {
16 let mut segments = Vec::new();
17 let mut text = String::new();
18 let mut rest = src;
19
20 while let Some(i) = rest.find("${") {
21 text.push_str(&rest[..i]);
22 let after = &rest[i + 2..];
23 let Some(close) = after.find('}') else {
24 return Err("`${` が `}` で閉じられていない".into());
25 };
26 if !text.is_empty() {
27 segments.push(Seg::Text(std::mem::take(&mut text)));
28 }
29 segments.push(parse_expr(after[..close].trim())?);
30 rest = &after[close + 1..];
31 }
32 text.push_str(rest);
33 if !text.is_empty() {
34 segments.push(Seg::Text(text));
35 }
36 Ok(Template { segments })
37 }
38
39 pub fn vars(&self) -> Vec<&str> {
41 self.segments
42 .iter()
43 .filter_map(|s| match s {
44 Seg::Text(_) => None,
45 Seg::Var(v) => Some(v.as_str()),
46 Seg::Call { arg, .. } => Some(arg.as_str()),
47 })
48 .collect()
49 }
50}
51
52fn parse_expr(expr: &str) -> Result<Seg, String> {
53 match expr.split_once('(') {
54 Some((func, rest)) => {
55 let arg = rest
56 .strip_suffix(')')
57 .ok_or_else(|| format!("`{expr}` の括弧が閉じられていない"))?;
58 let func = func.trim();
59 if !matches!(func, "singular" | "plural" | "short" | "desc") {
60 return Err(format!(
61 "テンプレートで使える関数は singular / plural / short / desc のみ。`{func}` は使えない"
62 ));
63 }
64 Ok(Seg::Call {
65 func: func.into(),
66 arg: arg.trim().into(),
67 })
68 }
69 None => {
70 if expr.is_empty() {
71 Err("`${}` が空".into())
72 } else {
73 Ok(Seg::Var(expr.into()))
74 }
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn parses_var_and_call() {
85 let t = Template::parse("${singular(table)}_id").expect("parse");
86 assert_eq!(
87 t.segments,
88 vec![
89 Seg::Call {
90 func: "singular".into(),
91 arg: "table".into()
92 },
93 Seg::Text("_id".into())
94 ]
95 );
96 }
97
98 #[test]
99 fn parses_multiple_vars() {
100 let t = Template::parse("idx_${table}_${columns}").expect("parse");
101 assert_eq!(t.vars(), vec!["table", "columns"]);
102 }
103
104 #[test]
105 fn rejects_unknown_function() {
106 assert!(Template::parse("${upper(table)}").is_err());
107 }
108}