behavior_contracts/
template.rs1use crate::canonical::py_float_repr;
15use crate::value::Value;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TemplateFailureCode {
19 UnboundParam,
20 ResultScopeInCore,
21 InvalidStringify,
22}
23
24impl TemplateFailureCode {
25 pub fn as_str(self) -> &'static str {
26 match self {
27 TemplateFailureCode::UnboundParam => "UNBOUND_PARAM",
28 TemplateFailureCode::ResultScopeInCore => "RESULT_SCOPE_IN_CORE",
29 TemplateFailureCode::InvalidStringify => "INVALID_STRINGIFY",
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
35pub struct TemplateFailure {
36 pub code: TemplateFailureCode,
37 pub message: String,
38}
39
40impl std::fmt::Display for TemplateFailure {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(f, "{}: {}", self.code.as_str(), self.message)
43 }
44}
45impl std::error::Error for TemplateFailure {}
46
47fn fail<T>(code: TemplateFailureCode, message: impl Into<String>) -> Result<T, TemplateFailure> {
48 Err(TemplateFailure {
49 code,
50 message: message.into(),
51 })
52}
53
54pub fn render_template(
56 template: &str,
57 params: &[(String, Value)],
58) -> Result<String, TemplateFailure> {
59 let mut out = String::with_capacity(template.len());
60 let chars: Vec<char> = template.chars().collect();
61 let mut i = 0;
62 while i < chars.len() {
63 let c = chars[i];
64 if c == '{' {
65 if let Some(close) = find_placeholder_close(&chars, i) {
67 let name: String = chars[i + 1..close].iter().collect();
68 out.push_str(&resolve(template, &name, params)?);
70 i = close + 1;
71 continue;
72 }
73 out.push(c);
75 i += 1;
76 } else {
77 out.push(c);
78 i += 1;
79 }
80 }
81 Ok(out)
82}
83
84fn find_placeholder_close(chars: &[char], open: usize) -> Option<usize> {
88 let mut j = open + 1;
89 let mut count = 0;
90 while j < chars.len() {
91 match chars[j] {
92 '{' => return None, '}' => {
94 if count == 0 {
95 return None; }
97 return Some(j);
98 }
99 _ => {
100 count += 1;
101 j += 1;
102 }
103 }
104 }
105 None
106}
107
108fn resolve(
109 template: &str,
110 name: &str,
111 params: &[(String, Value)],
112) -> Result<String, TemplateFailure> {
113 if name.starts_with("result.") || name.starts_with("item.") {
114 return fail(
115 TemplateFailureCode::ResultScopeInCore,
116 format!("template references '{{{name}}}' resolved by a different layer, not core render_template"),
117 );
118 }
119 match params.iter().find(|(k, _)| k == name) {
120 Some((_, Value::Null)) | None => fail(
121 TemplateFailureCode::UnboundParam,
122 format!("template '{template}' references unbound parameter '{name}'"),
123 ),
124 Some((_, v)) => stringify(v),
125 }
126}
127
128fn stringify(v: &Value) -> Result<String, TemplateFailure> {
130 match v {
131 Value::Str(s) => Ok(s.clone()),
132 Value::Bool(b) => Ok(if *b { "True".into() } else { "False".into() }),
133 Value::Int(i) => Ok(i.to_string()),
134 Value::Float(f) => py_float_repr(*f).map_err(|_| TemplateFailure {
135 code: TemplateFailureCode::InvalidStringify,
136 message: format!("cannot stringify non-finite float {f}"),
137 }),
138 other => fail(
139 TemplateFailureCode::InvalidStringify,
140 format!("cannot stringify {} into a template", other.type_name()),
141 ),
142 }
143}
144
145pub fn resolve_partial(template: &str, params: &[(String, Value)]) -> String {
148 let chars: Vec<char> = template.chars().collect();
149 let mut out = String::with_capacity(template.len());
150 let mut i = 0;
151 while i < chars.len() {
152 if chars[i] == '{' {
153 if let Some(close) = find_placeholder_close(&chars, i) {
154 let name: String = chars[i + 1..close].iter().collect();
155 if name.starts_with("result.") || name.starts_with("item.") {
156 out.push_str(&chars[i..=close].iter().collect::<String>());
157 i = close + 1;
158 continue;
159 }
160 match params.iter().find(|(k, _)| k == &name) {
161 Some((_, v)) if !matches!(v, Value::Null) => {
162 if let Ok(s) = stringify(v) {
163 out.push_str(&s);
164 } else {
165 out.push_str(&chars[i..=close].iter().collect::<String>());
166 }
167 }
168 _ => out.push_str(&chars[i..=close].iter().collect::<String>()),
169 }
170 i = close + 1;
171 continue;
172 }
173 }
174 out.push(chars[i]);
175 i += 1;
176 }
177 out
178}