openfunctions_rs/parser/
javascript.rs1use crate::models::{EnvVarDefinition, ParameterDefinition, ParameterType, ToolDefinition};
13use anyhow::Result;
14use regex::Regex;
15use std::collections::HashMap;
16
17pub fn parse(source: &str) -> Result<ToolDefinition> {
19 let jsdoc_re = Regex::new(r"/\*\*([\s\S]*?)\*/")?;
20 let jsdoc = jsdoc_re
21 .captures(source)
22 .ok_or_else(|| anyhow::anyhow!("No JSDoc comment found"))?
23 .get(1)
24 .unwrap()
25 .as_str();
26
27 let mut description = String::new();
28 let mut parameters = Vec::new();
29 let mut env_vars = Vec::new();
30 let mut required_tools = Vec::new();
31 let metadata = HashMap::new();
32
33 let property_re =
34 Regex::new(r"@property\s+\{([^}]+)\}\s+(\[?)([a-zA-Z0-9_]+)\]?\s*-?\s*(.*)$")?;
35 let env_re = Regex::new(r"@env\s+\{([A-Z_][A-Z0-9_]*)\}\s*(.*)")?;
36 let meta_re = Regex::new(r"@meta\s+require-tools\s+(.+)$")?;
37
38 for line in jsdoc.lines() {
39 let line = line.trim().trim_start_matches('*').trim();
40
41 if line.is_empty() || line.starts_with('@') {
42 if description.is_empty() && !line.starts_with('@') {
43 continue;
44 }
45 } else if description.is_empty() {
46 description = line.to_string();
47 }
48
49 if let Some(caps) = property_re.captures(line) {
50 let type_str = caps.get(1).unwrap().as_str();
51 let optional_bracket = caps.get(2).unwrap().as_str();
52 let name = caps.get(3).unwrap().as_str().to_string();
53 let desc = caps.get(4).unwrap().as_str().to_string();
54
55 let required = optional_bracket.is_empty();
56 let param_type = parse_js_type(type_str)?;
57
58 parameters.push(ParameterDefinition {
59 name,
60 param_type,
61 description: desc,
62 required,
63 default: None,
64 enum_values: None,
65 });
66 } else if let Some(caps) = env_re.captures(line) {
67 let name = caps.get(1).unwrap().as_str().to_string();
68 let desc = caps.get(2).unwrap().as_str().to_string();
69 let required = !desc.contains("[optional]");
70
71 env_vars.push(EnvVarDefinition {
72 name,
73 description: desc.replace("[optional]", "").trim().to_string(),
74 required,
75 default: None,
76 });
77 } else if let Some(caps) = meta_re.captures(line) {
78 required_tools = caps
79 .get(1)
80 .unwrap()
81 .as_str()
82 .split_whitespace()
83 .map(|s| s.to_string())
84 .collect();
85 }
86 }
87
88 if description.is_empty() {
89 anyhow::bail!("No description found in JSDoc");
90 }
91
92 Ok(ToolDefinition {
93 description,
94 parameters,
95 env_vars,
96 required_tools,
97 metadata,
98 })
99}
100
101fn parse_js_type(type_str: &str) -> Result<ParameterType> {
102 let type_str = type_str.trim();
103
104 if type_str.contains('|') {
105 let values: Vec<String> = type_str
106 .split('|')
107 .map(|s| s.trim().trim_matches('\'').trim_matches('"').to_string())
108 .collect();
109 return Ok(ParameterType::Enum(values));
110 }
111
112 if type_str.ends_with("[]") {
113 return Ok(ParameterType::Array);
114 }
115
116 match type_str.to_lowercase().as_str() {
117 "string" => Ok(ParameterType::String),
118 "number" => Ok(ParameterType::Number),
119 "integer" => Ok(ParameterType::Integer),
120 "boolean" => Ok(ParameterType::Boolean),
121 "array" => Ok(ParameterType::Array),
122 "object" => Ok(ParameterType::Object),
123 _ => Ok(ParameterType::String),
124 }
125}