smcp_computer/mcp_clients/
render.rs1use async_recursion::async_recursion;
11use regex::Regex;
12use serde_json::Value;
13use thiserror::Error;
14
15#[derive(Error, Debug)]
16pub enum RenderError {
17 #[error("Input not found: {0}")]
18 InputNotFound(String),
19 #[error("Render depth exceeded")]
20 DepthExceeded,
21 #[error("Invalid placeholder format")]
22 InvalidPlaceholder,
23}
24
25pub struct ConfigRender {
27 placeholder_regex: Regex,
28 max_depth: usize,
29}
30
31impl ConfigRender {
32 pub fn new(max_depth: usize) -> Self {
34 Self {
35 placeholder_regex: Regex::new(r"\$\{input:([^}]+)}").unwrap(),
36 max_depth,
37 }
38 }
39
40 pub async fn render<F, Fut>(&self, data: Value, resolver: F) -> Result<Value, RenderError>
42 where
43 F: Fn(String) -> Fut + Copy + Send + Sync,
44 Fut: std::future::Future<Output = Result<Value, RenderError>> + Send,
45 {
46 self.render_with_depth(data, resolver, 0).await
47 }
48
49 #[async_recursion]
50 async fn render_with_depth<F, Fut>(
51 &self,
52 data: Value,
53 resolver: F,
54 depth: usize,
55 ) -> Result<Value, RenderError>
56 where
57 F: Fn(String) -> Fut + Copy + Send + Sync,
58 Fut: std::future::Future<Output = Result<Value, RenderError>> + Send,
59 {
60 if depth > self.max_depth {
61 return Err(RenderError::DepthExceeded);
62 }
63
64 match data {
65 Value::String(s) => self.render_string(s, resolver, depth).await,
66 Value::Object(mut map) => {
67 for (k, v) in map.clone() {
68 map.insert(k, self.render_with_depth(v, resolver, depth + 1).await?);
69 }
70 Ok(Value::Object(map))
71 }
72 Value::Array(arr) => {
73 let mut new_arr = Vec::with_capacity(arr.len());
74 for item in arr {
75 new_arr.push(self.render_with_depth(item, resolver, depth + 1).await?);
76 }
77 Ok(Value::Array(new_arr))
78 }
79 _ => Ok(data),
80 }
81 }
82
83 async fn render_string<F, Fut>(
84 &self,
85 s: String,
86 resolver: F,
87 _depth: usize,
88 ) -> Result<Value, RenderError>
89 where
90 F: Fn(String) -> Fut + Copy + Send + Sync,
91 Fut: std::future::Future<Output = Result<Value, RenderError>> + Send,
92 {
93 let matches: Vec<_> = self.placeholder_regex.find_iter(&s).collect();
94
95 if matches.is_empty() {
96 return Ok(Value::String(s));
97 }
98
99 if matches.len() == 1 && matches[0].start() == 0 && matches[0].end() == s.len() {
101 let input_id = matches[0]
102 .as_str()
103 .strip_prefix("${input:")
104 .unwrap()
105 .strip_suffix('}')
106 .unwrap();
107 return match resolver(input_id.to_string()).await {
108 Ok(value) => Ok(value),
109 Err(RenderError::InputNotFound(_)) => {
110 Ok(Value::String(s))
112 }
113 Err(e) => Err(e),
114 };
115 }
116
117 let mut result = s.clone();
119 let mut offset: isize = 0;
120
121 for m in matches {
122 let input_id = m
123 .as_str()
124 .strip_prefix("${input:")
125 .unwrap()
126 .strip_suffix('}')
127 .unwrap();
128
129 let replacement = match resolver(input_id.to_string()).await {
130 Ok(value) => match value {
131 Value::String(s) => s,
132 other => other.to_string(),
133 },
134 Err(RenderError::InputNotFound(_)) => {
135 m.as_str().to_string()
137 }
138 Err(e) => return Err(e),
139 };
140
141 let start = (m.start() as isize + offset) as usize;
142 let end = (m.end() as isize + offset) as usize;
143 result.replace_range(start..end, &replacement);
144 offset += replacement.len() as isize - (m.end() - m.start()) as isize;
145 }
146
147 Ok(Value::String(result))
148 }
149}
150
151impl Default for ConfigRender {
152 fn default() -> Self {
153 Self::new(10)
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 async fn mock_resolver(id: String) -> Result<Value, RenderError> {
162 match id.as_str() {
163 "test" => Ok(Value::String("resolved".to_string())),
164 "number" => Ok(Value::Number(serde_json::Number::from(42))),
165 "missing" => Err(RenderError::InputNotFound(id)),
166 _ => Ok(Value::String(format!("resolved_{}", id))),
167 }
168 }
169
170 #[tokio::test]
171 async fn test_simple_placeholder() {
172 let render = ConfigRender::default();
173 let input = Value::String("${input:test}".to_string());
174 let result = render.render(input, mock_resolver).await.unwrap();
175 assert_eq!(result, Value::String("resolved".to_string()));
176 }
177
178 #[tokio::test]
179 async fn test_multiple_placeholders() {
180 let render = ConfigRender::default();
181 let input = Value::String("Hello ${input:test} and ${input:world}".to_string());
182 let result = render.render(input, mock_resolver).await.unwrap();
183 assert_eq!(
184 result,
185 Value::String("Hello resolved and resolved_world".to_string())
186 );
187 }
188
189 #[tokio::test]
190 async fn test_missing_input() {
191 let render = ConfigRender::default();
192 let input = Value::String("${input:missing}".to_string());
193 let result = render.render(input, mock_resolver).await.unwrap();
194 assert_eq!(result, Value::String("${input:missing}".to_string()));
195 }
196
197 #[tokio::test]
198 async fn test_object_render() {
199 let render = ConfigRender::default();
200 let mut obj = serde_json::Map::new();
201 obj.insert(
202 "key".to_string(),
203 Value::String("${input:test}".to_string()),
204 );
205 obj.insert("nested".to_string(), Value::String("value".to_string()));
206 let input = Value::Object(obj);
207 let result = render.render(input, mock_resolver).await.unwrap();
208
209 if let Value::Object(map) = result {
210 assert_eq!(
211 map.get("key").unwrap(),
212 &Value::String("resolved".to_string())
213 );
214 assert_eq!(
215 map.get("nested").unwrap(),
216 &Value::String("value".to_string())
217 );
218 } else {
219 panic!("Expected object");
220 }
221 }
222}