openfunctions_rs/parser/
javascript.rs

1//! JavaScript parser for extracting tool definitions from JSDoc comments.
2//!
3//! This parser extracts tool definitions from a JSDoc comment block associated
4//! with a function. It expects a single, well-formed JSDoc block in the file.
5//!
6//! The following annotations are supported:
7//! - A multiline description of the tool.
8//! - `@property {type} [name] - description`: Defines a parameter. `[]` make it optional.
9//! - `@env {VAR_NAME} [description]`: Defines a required environment variable.
10//! - `@meta require-tools <tool1> <tool2>`: Lists required external tools.
11
12use crate::models::{EnvVarDefinition, ParameterDefinition, ParameterType, ToolDefinition};
13use anyhow::Result;
14use regex::Regex;
15use std::collections::HashMap;
16
17/// Parses a JavaScript file and extracts a `ToolDefinition` from its JSDoc.
18pub 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}