Skip to main content

nounsql_core/
template.rs

1/// `"idx_${table}_${columns}"` のような命名テンプレート。
2#[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    /// テンプレートが参照している変数名を列挙する。
40    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}