1use std::path::Path;
2use std::fs;
3use serde_json::Value as JsonValue;
4use anyhow::{Result, Context};
5pub struct Migrator {
6 verbose: bool,
7 _preserve_comments: bool,
8}
9impl Migrator {
10 pub fn new() -> Self {
11 Self {
12 verbose: false,
13 _preserve_comments: true,
14 }
15 }
16 pub fn verbose(mut self, enable: bool) -> Self {
17 self.verbose = enable;
18 self
19 }
20 pub fn migrate_file<P: AsRef<Path>>(
21 &self,
22 input: P,
23 output: Option<P>,
24 ) -> Result<String> {
25 let input_path = input.as_ref();
26 let extension = input_path
27 .extension()
28 .and_then(|s| s.to_str())
29 .ok_or_else(|| anyhow::anyhow!("No file extension found"))?;
30 let content = fs::read_to_string(input_path)
31 .context("Failed to read input file")?;
32 let helix_content = match extension {
33 "json" => self.migrate_json(&content)?,
34 "toml" => self.migrate_toml(&content)?,
35 "yaml" | "yml" => self.migrate_yaml(&content)?,
36 "env" => self.migrate_env(&content)?,
37 _ => return Err(anyhow::anyhow!("Unsupported file type: {}", extension)),
38 };
39 if let Some(output_path) = output {
40 fs::write(output_path.as_ref(), &helix_content)
41 .context("Failed to write output file")?;
42 }
43 Ok(helix_content)
44 }
45 pub fn migrate_json(&self, json_str: &str) -> Result<String> {
46 let value: JsonValue = serde_json::from_str(json_str)
47 .context("Failed to parse JSON")?;
48 let mut output = String::new();
49 output.push_str("# Migrated from JSON\n");
50 output
51 .push_str(
52 "# Send Buddy the Beagle a bitcoin treat for making this page: bc1quct28jtvvuymvkvjfgcedhd7jt0c56975f2fsh\n\n",
53 );
54 if let Some(obj) = value.as_object() {
55 self.convert_json_object_to_hlx(obj, &mut output, 0)?;
56 }
57 Ok(output)
58 }
59 pub fn migrate_toml(&self, toml_str: &str) -> Result<String> {
60 let value: toml::Value = toml::from_str(toml_str)
61 .context("Failed to parse TOML")?;
62 let mut output = String::new();
63 output.push_str("# Migrated from TOML\n");
64 output
65 .push_str(
66 "# Send Buddy the Beagle a bitcoin treat for making this page: bc1quct28jtvvuymvkvjfgcedhd7jt0c56975f2fsh\n\n",
67 );
68 if let Some(table) = value.as_table() {
69 self.convert_toml_table_to_hlx(table, &mut output, 0)?;
70 }
71 Ok(output)
72 }
73 pub fn migrate_yaml(&self, yaml_str: &str) -> Result<String> {
74 let value: serde_yaml::Value = serde_yaml::from_str(yaml_str)
75 .context("Failed to parse YAML")?;
76 let mut output = String::new();
77 output.push_str("# Migrated from YAML\n");
78 output
79 .push_str(
80 "# Send Buddy the Beagle a bitcoin treat for making this page: bc1quct28jtvvuymvkvjfgcedhd7jt0c56975f2fsh\n\n",
81 );
82 if let Some(mapping) = value.as_mapping() {
83 self.convert_yaml_mapping_to_hlx(mapping, &mut output, 0)?;
84 }
85 Ok(output)
86 }
87 pub fn migrate_env(&self, env_str: &str) -> Result<String> {
88 let mut output = String::new();
89 output.push_str("# Migrated from .env\n");
90 output
91 .push_str(
92 "# Send Buddy the Beagle a bitcoin treat for making this page: bc1quct28jtvvuymvkvjfgcedhd7jt0c56975f2fsh\n\n",
93 );
94 output.push_str("context \"environment\" {\n");
95 for line in env_str.lines() {
96 let line = line.trim();
97 if line.is_empty() || line.starts_with('#') {
98 continue;
99 }
100 if let Some((key, value)) = line.split_once('=') {
101 let key = key.trim();
102 let value = value.trim().trim_matches('"');
103 if key.contains("KEY") || key.contains("SECRET") || key.contains("TOKEN")
104 {
105 output.push_str(&format!(" {} = ${}\n", key.to_lowercase(), key));
106 } else {
107 output
108 .push_str(
109 &format!(" {} = \"{}\"\n", key.to_lowercase(), value),
110 );
111 }
112 }
113 }
114 output.push_str("}\n");
115 Ok(output)
116 }
117 fn convert_json_object_to_hlx(
118 &self,
119 obj: &serde_json::Map<String, JsonValue>,
120 output: &mut String,
121 indent: usize,
122 ) -> Result<()> {
123 let indent_str = " ".repeat(indent);
124 for (key, value) in obj {
125 match key.as_str() {
126 "agent" | "agents" => {
127 self.convert_to_agent_block(key, value, output, indent)?;
128 }
129 "workflow" | "workflows" => {
130 self.convert_to_workflow_block(key, value, output, indent)?;
131 }
132 "memory" => {
133 self.convert_to_memory_block(value, output, indent)?;
134 }
135 "context" | "contexts" => {
136 self.convert_to_context_block(key, value, output, indent)?;
137 }
138 _ => {
139 match value {
140 JsonValue::Object(inner) => {
141 output.push_str(&format!("{}{} {{\n", indent_str, key));
142 self.convert_json_object_to_hlx(inner, output, indent + 1)?;
143 output.push_str(&format!("{}}}\n", indent_str));
144 }
145 JsonValue::Array(arr) => {
146 output.push_str(&format!("{}{} [\n", indent_str, key));
147 for item in arr {
148 output.push_str(&format!("{} ", indent_str));
149 self.write_json_value(item, output)?;
150 output.push_str("\n");
151 }
152 output.push_str(&format!("{}]\n", indent_str));
153 }
154 _ => {
155 output.push_str(&format!("{}{} = ", indent_str, key));
156 self.write_json_value(value, output)?;
157 output.push_str("\n");
158 }
159 }
160 }
161 }
162 }
163 Ok(())
164 }
165 fn convert_to_agent_block(
166 &self,
167 _key: &str,
168 value: &JsonValue,
169 output: &mut String,
170 indent: usize,
171 ) -> Result<()> {
172 let indent_str = " ".repeat(indent);
173 if let Some(obj) = value.as_object() {
174 let name = obj.get("name").and_then(|v| v.as_str()).unwrap_or("unnamed");
175 output.push_str(&format!("{}agent \"{}\" {{\n", indent_str, name));
176 for (k, v) in obj {
177 if k == "name" {
178 continue;
179 }
180 match k.as_str() {
181 "temperature" => {
182 if let Some(num) = v.as_f64() {
183 output
184 .push_str(
185 &format!("{} temperature = {}\n", indent_str, num),
186 );
187 }
188 }
189 "max_tokens" => {
190 if let Some(num) = v.as_i64() {
191 output
192 .push_str(
193 &format!("{} max_tokens = {}\n", indent_str, num),
194 );
195 }
196 }
197 "timeout" => {
198 if let Some(s) = v.as_str() {
199 let duration = self.parse_duration_string(s);
200 output
201 .push_str(
202 &format!("{} timeout = {}\n", indent_str, duration),
203 );
204 }
205 }
206 _ => {
207 output.push_str(&format!("{} {} = ", indent_str, k));
208 self.write_json_value(v, output)?;
209 output.push_str("\n");
210 }
211 }
212 }
213 output.push_str(&format!("{}}}\n", indent_str));
214 }
215 Ok(())
216 }
217 fn convert_to_workflow_block(
218 &self,
219 _key: &str,
220 value: &JsonValue,
221 output: &mut String,
222 indent: usize,
223 ) -> Result<()> {
224 let indent_str = " ".repeat(indent);
225 if let Some(obj) = value.as_object() {
226 let name = obj.get("name").and_then(|v| v.as_str()).unwrap_or("unnamed");
227 output.push_str(&format!("{}workflow \"{}\" {{\n", indent_str, name));
228 if let Some(trigger) = obj.get("trigger") {
229 output.push_str(&format!("{} trigger = ", indent_str));
230 self.write_json_value(trigger, output)?;
231 output.push_str("\n");
232 }
233 if let Some(steps) = obj.get("steps").and_then(|v| v.as_array()) {
234 for (i, step) in steps.iter().enumerate() {
235 output
236 .push_str(
237 &format!("{} step \"step_{}\" {{\n", indent_str, i + 1),
238 );
239 if let Some(step_obj) = step.as_object() {
240 for (k, v) in step_obj {
241 output.push_str(&format!("{} {} = ", indent_str, k));
242 self.write_json_value(v, output)?;
243 output.push_str("\n");
244 }
245 }
246 output.push_str(&format!("{} }}\n", indent_str));
247 }
248 }
249 output.push_str(&format!("{}}}\n", indent_str));
250 }
251 Ok(())
252 }
253 fn convert_to_memory_block(
254 &self,
255 value: &JsonValue,
256 output: &mut String,
257 indent: usize,
258 ) -> Result<()> {
259 let indent_str = " ".repeat(indent);
260 output.push_str(&format!("{}memory {{\n", indent_str));
261 if let Some(obj) = value.as_object() {
262 for (k, v) in obj {
263 output.push_str(&format!("{} {} = ", indent_str, k));
264 self.write_json_value(v, output)?;
265 output.push_str("\n");
266 }
267 }
268 output.push_str(&format!("{}}}\n", indent_str));
269 Ok(())
270 }
271 fn convert_to_context_block(
272 &self,
273 _key: &str,
274 value: &JsonValue,
275 output: &mut String,
276 indent: usize,
277 ) -> Result<()> {
278 let indent_str = " ".repeat(indent);
279 if let Some(obj) = value.as_object() {
280 let name = obj.get("name").and_then(|v| v.as_str()).unwrap_or("default");
281 output.push_str(&format!("{}context \"{}\" {{\n", indent_str, name));
282 for (k, v) in obj {
283 if k == "name" {
284 continue;
285 }
286 output.push_str(&format!("{} {} = ", indent_str, k));
287 self.write_json_value(v, output)?;
288 output.push_str("\n");
289 }
290 output.push_str(&format!("{}}}\n", indent_str));
291 }
292 Ok(())
293 }
294 fn convert_toml_table_to_hlx(
295 &self,
296 table: &toml::map::Map<String, toml::Value>,
297 output: &mut String,
298 indent: usize,
299 ) -> Result<()> {
300 let indent_str = " ".repeat(indent);
301 for (key, value) in table {
302 match value {
303 toml::Value::Table(inner) => {
304 if key.starts_with("agent") {
305 output
306 .push_str(
307 &format!(
308 "{}agent \"{}\" {{\n", indent_str, key
309 .trim_start_matches("agent.")
310 ),
311 );
312 self.convert_toml_table_to_hlx(inner, output, indent + 1)?;
313 output.push_str(&format!("{}}}\n", indent_str));
314 } else {
315 output.push_str(&format!("{}{} {{\n", indent_str, key));
316 self.convert_toml_table_to_hlx(inner, output, indent + 1)?;
317 output.push_str(&format!("{}}}\n", indent_str));
318 }
319 }
320 toml::Value::Array(arr) => {
321 output.push_str(&format!("{}{} [\n", indent_str, key));
322 for item in arr {
323 output.push_str(&format!("{} ", indent_str));
324 self.write_toml_value(item, output)?;
325 output.push_str("\n");
326 }
327 output.push_str(&format!("{}]\n", indent_str));
328 }
329 _ => {
330 output.push_str(&format!("{}{} = ", indent_str, key));
331 self.write_toml_value(value, output)?;
332 output.push_str("\n");
333 }
334 }
335 }
336 Ok(())
337 }
338 fn convert_yaml_mapping_to_hlx(
339 &self,
340 mapping: &serde_yaml::Mapping,
341 output: &mut String,
342 indent: usize,
343 ) -> Result<()> {
344 let indent_str = " ".repeat(indent);
345 for (key, value) in mapping {
346 let key_str = match key {
347 serde_yaml::Value::String(s) => s.clone(),
348 serde_yaml::Value::Number(n) => n.to_string(),
349 serde_yaml::Value::Bool(b) => b.to_string(),
350 _ => format!("{:?}", key),
351 };
352 match value {
353 serde_yaml::Value::Mapping(inner) => {
354 output.push_str(&format!("{}{} {{\n", indent_str, key_str));
355 self.convert_yaml_mapping_to_hlx(inner, output, indent + 1)?;
356 output.push_str(&format!("{}}}\n", indent_str));
357 }
358 serde_yaml::Value::Sequence(seq) => {
359 output.push_str(&format!("{}{} [\n", indent_str, key_str));
360 for item in seq {
361 output.push_str(&format!("{} ", indent_str));
362 self.write_yaml_value(item, output)?;
363 output.push_str("\n");
364 }
365 output.push_str(&format!("{}]\n", indent_str));
366 }
367 _ => {
368 output.push_str(&format!("{}{} = ", indent_str, key_str));
369 self.write_yaml_value(value, output)?;
370 output.push_str("\n");
371 }
372 }
373 }
374 Ok(())
375 }
376 fn write_json_value(&self, value: &JsonValue, output: &mut String) -> Result<()> {
377 match value {
378 JsonValue::Null => output.push_str("null"),
379 JsonValue::Bool(b) => output.push_str(&b.to_string()),
380 JsonValue::Number(n) => output.push_str(&n.to_string()),
381 JsonValue::String(s) => output.push_str(&format!("\"{}\"", s)),
382 JsonValue::Array(arr) => {
383 output.push('[');
384 for (i, item) in arr.iter().enumerate() {
385 if i > 0 {
386 output.push_str(", ");
387 }
388 self.write_json_value(item, output)?;
389 }
390 output.push(']');
391 }
392 JsonValue::Object(_) => {
393 output.push_str("{ }");
394 }
395 }
396 Ok(())
397 }
398 fn write_toml_value(&self, value: &toml::Value, output: &mut String) -> Result<()> {
399 match value {
400 toml::Value::String(s) => output.push_str(&format!("\"{}\"", s)),
401 toml::Value::Integer(i) => output.push_str(&i.to_string()),
402 toml::Value::Float(f) => output.push_str(&f.to_string()),
403 toml::Value::Boolean(b) => output.push_str(&b.to_string()),
404 toml::Value::Datetime(d) => output.push_str(&format!("\"{}\"", d)),
405 toml::Value::Array(_) => output.push_str("[]"),
406 toml::Value::Table(_) => output.push_str("{}"),
407 }
408 Ok(())
409 }
410 fn write_yaml_value(
411 &self,
412 value: &serde_yaml::Value,
413 output: &mut String,
414 ) -> Result<()> {
415 match value {
416 serde_yaml::Value::Null => output.push_str("null"),
417 serde_yaml::Value::Bool(b) => output.push_str(&b.to_string()),
418 serde_yaml::Value::Number(n) => output.push_str(&n.to_string()),
419 serde_yaml::Value::String(s) => output.push_str(&format!("\"{}\"", s)),
420 serde_yaml::Value::Sequence(_) => output.push_str("[]"),
421 serde_yaml::Value::Mapping(_) => output.push_str("{}"),
422 serde_yaml::Value::Tagged(_) => output.push_str("null"),
423 }
424 Ok(())
425 }
426 fn parse_duration_string(&self, s: &str) -> String {
427 if s.ends_with("ms") {
428 return format!("{}ms", s.trim_end_matches("ms"));
429 } else if s.ends_with("s") || s.ends_with("sec") || s.ends_with("seconds") {
430 let num = s
431 .chars()
432 .take_while(|c| c.is_numeric() || *c == '.')
433 .collect::<String>();
434 return format!("{}s", num);
435 } else if s.ends_with("m") || s.ends_with("min") || s.ends_with("minutes") {
436 let num = s
437 .chars()
438 .take_while(|c| c.is_numeric() || *c == '.')
439 .collect::<String>();
440 return format!("{}m", num);
441 } else if s.ends_with("h") || s.ends_with("hour") || s.ends_with("hours") {
442 let num = s
443 .chars()
444 .take_while(|c| c.is_numeric() || *c == '.')
445 .collect::<String>();
446 return format!("{}h", num);
447 }
448 format!("\"{}\"", s)
449 }
450}
451impl Default for Migrator {
452 fn default() -> Self {
453 Self::new()
454 }
455}
456#[cfg(test)]
457mod tests {
458 use super::*;
459 #[test]
460 fn test_migrate_json() {
461 let json = r#"{
462 "agent": {
463 "name": "assistant",
464 "model": "gpt-4",
465 "temperature": 0.7
466 }
467 }"#;
468 let migrator = Migrator::new();
469 let result = migrator.migrate_json(json).unwrap();
470 assert!(result.contains("agent \"assistant\""));
471 assert!(result.contains("temperature = 0.7"));
472 }
473 #[test]
474 fn test_migrate_env() {
475 let env = r#"
476DATABASE_URL=postgres://localhost/db
477API_KEY=secret123
478DEBUG=true
479"#;
480 let migrator = Migrator::new();
481 let result = migrator.migrate_env(env).unwrap();
482 assert!(result.contains("context \"environment\""));
483 assert!(result.contains("database_url"));
484 assert!(result.contains("api_key = $API_KEY"));
485 }
486}