use crate::LlmError;
pub fn parse_schema_dsl(input: &str) -> Result<serde_json::Value, LlmError> {
let input = input.trim();
if input.is_empty() {
return Err(LlmError::Config("empty schema DSL input".into()));
}
let fields: Vec<&str> = if input.contains('\n') {
input.split('\n').collect()
} else {
input.split(',').collect()
};
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
for field in fields {
let field = field.trim();
if field.is_empty() {
continue;
}
let mut parts = field.splitn(2, char::is_whitespace);
let name = parts
.next()
.ok_or_else(|| LlmError::Config(format!("invalid field: {field}")))?
.trim();
let mut json_type = "string".to_string();
let mut description: Option<String> = None;
if let Some(rest) = parts.next() {
let rest = rest.trim();
if !rest.is_empty() {
if let Some((type_part, desc_part)) = rest.split_once(':') {
let type_part = type_part.trim();
if !type_part.is_empty() {
json_type = map_type(type_part)?;
}
let desc_part = desc_part.trim();
if !desc_part.is_empty() {
description = Some(desc_part.to_string());
}
} else {
json_type = map_type(rest)?;
}
}
}
let mut prop = serde_json::Map::new();
prop.insert("type".into(), serde_json::Value::String(json_type));
if let Some(desc) = description {
prop.insert("description".into(), serde_json::Value::String(desc));
}
properties.insert(name.to_string(), serde_json::Value::Object(prop));
required.push(serde_json::Value::String(name.to_string()));
}
if properties.is_empty() {
return Err(LlmError::Config("no valid fields in schema DSL".into()));
}
Ok(serde_json::json!({
"type": "object",
"properties": properties,
"required": required,
}))
}
pub fn multi_schema(schema: serde_json::Value) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"items": {
"type": "array",
"items": schema,
}
},
"required": ["items"],
})
}
fn map_type(t: &str) -> Result<String, LlmError> {
match t {
"str" => Ok("string".into()),
"int" => Ok("integer".into()),
"float" => Ok("number".into()),
"bool" => Ok("boolean".into()),
other => Err(LlmError::Config(format!("unknown type: {other}"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single_string_field() {
let result = parse_schema_dsl("name str").unwrap();
assert_eq!(result["type"], "object");
assert_eq!(result["properties"]["name"]["type"], "string");
assert_eq!(result["required"][0], "name");
}
#[test]
fn parse_each_type() {
let result = parse_schema_dsl("a int, b float, c bool, d str").unwrap();
assert_eq!(result["properties"]["a"]["type"], "integer");
assert_eq!(result["properties"]["b"]["type"], "number");
assert_eq!(result["properties"]["c"]["type"], "boolean");
assert_eq!(result["properties"]["d"]["type"], "string");
}
#[test]
fn parse_multiple_fields() {
let result = parse_schema_dsl("name str, age int, active bool").unwrap();
let props = result["properties"].as_object().unwrap();
assert_eq!(props.len(), 3);
let required = result["required"].as_array().unwrap();
assert_eq!(required.len(), 3);
}
#[test]
fn parse_field_with_description() {
let result = parse_schema_dsl("age int:The person's age").unwrap();
assert_eq!(result["properties"]["age"]["type"], "integer");
assert_eq!(
result["properties"]["age"]["description"],
"The person's age"
);
}
#[test]
fn parse_mixed_descriptions() {
let result = parse_schema_dsl("name str, age int:The age, active bool").unwrap();
assert!(result["properties"]["name"]["description"].is_null());
assert_eq!(result["properties"]["age"]["description"], "The age");
assert!(result["properties"]["active"]["description"].is_null());
}
#[test]
fn parse_default_type_is_string() {
let result = parse_schema_dsl("name").unwrap();
assert_eq!(result["properties"]["name"]["type"], "string");
}
#[test]
fn parse_whitespace_tolerance() {
let result = parse_schema_dsl(" name str , age int ").unwrap();
assert_eq!(result["properties"]["name"]["type"], "string");
assert_eq!(result["properties"]["age"]["type"], "integer");
}
#[test]
fn parse_newline_separated() {
let result = parse_schema_dsl("name str\nage int\nactive bool").unwrap();
let props = result["properties"].as_object().unwrap();
assert_eq!(props.len(), 3);
}
#[test]
fn parse_empty_string_error() {
let result = parse_schema_dsl("");
assert!(result.is_err());
}
#[test]
fn parse_invalid_type_error() {
let result = parse_schema_dsl("name xyz");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("unknown type"));
}
#[test]
fn multi_schema_wraps_in_array() {
let schema = parse_schema_dsl("name str, age int").unwrap();
let multi = multi_schema(schema.clone());
assert_eq!(multi["type"], "object");
assert_eq!(multi["properties"]["items"]["type"], "array");
assert_eq!(multi["properties"]["items"]["items"], schema);
assert_eq!(multi["required"][0], "items");
}
}