use std::collections::BTreeMap;
use serde_json::Value;
use crate::mcp::{
Gemma4ToolFunctionParamsProp, Gemma4ToolFunctionParamsPropArrayItems,
Gemma4ToolFunctionParamsPropType, error::ParseToolError,
};
pub fn resolve_ref<'a>(root: &'a Value, ref_str: &str) -> Option<&'a Value> {
let path = ref_str.strip_prefix("#/")?;
let mut current: &Value = root;
for segment in path.split('/') {
let key = segment.replace("~1", "/").replace("~0", "~");
current = current.get(&key)?;
}
Some(current)
}
pub fn map_type(type_str: &str) -> Gemma4ToolFunctionParamsPropType {
match type_str {
"string" => Gemma4ToolFunctionParamsPropType::String,
"array" => Gemma4ToolFunctionParamsPropType::Array,
"number" | "integer" => Gemma4ToolFunctionParamsPropType::Number,
"boolean" => Gemma4ToolFunctionParamsPropType::Boolean,
_ => Gemma4ToolFunctionParamsPropType::Object,
}
}
pub fn unwrap_any_of<'a>(variants: &'a [Value], root: &'a Value) -> (bool, Option<&'a Value>) {
let mut nullable = false;
let mut non_null: Vec<&Value> = Vec::new();
for v in variants {
let resolved: &Value = if let Some(r) = v.get("$ref").and_then(|r| r.as_str()) {
match resolve_ref(root, r) {
Some(rv) => rv,
None => v,
}
} else {
v
};
if resolved.get("type").and_then(|t| t.as_str()) == Some("null") {
nullable = true;
} else {
non_null.push(resolved);
}
}
let effective = if non_null.len() == 1 {
Some(non_null[0])
} else {
non_null.first().copied()
};
(nullable, effective)
}
pub fn convert_schema_node(
node: &Value,
root: &Value,
field_path: &str,
) -> Result<Gemma4ToolFunctionParamsProp, ParseToolError> {
if let Some(ref_str) = node.get("$ref").and_then(|r| r.as_str()) {
let resolved = resolve_ref(root, ref_str)
.ok_or_else(|| ParseToolError(format!("{field_path}.$ref({ref_str})").into()))?;
return convert_schema_node(resolved, root, field_path);
}
let description = node
.get("description")
.and_then(|d| d.as_str())
.unwrap_or_default()
.to_string();
if let Some(variants) = node
.get("anyOf")
.or_else(|| node.get("oneOf"))
.and_then(|v| v.as_array())
{
let (nullable, effective) = unwrap_any_of(variants, root);
return if let Some(eff) = effective {
let mut prop = convert_schema_node(eff, root, field_path)?;
if nullable {
prop.nullable = true;
}
if prop.description.is_empty() && !description.is_empty() {
prop.description = description;
}
Ok(prop)
} else {
Ok(Gemma4ToolFunctionParamsProp {
description,
type_: Gemma4ToolFunctionParamsPropType::Object,
nullable,
..Default::default()
})
};
}
let type_str = node
.get("type")
.and_then(|t| t.as_str())
.unwrap_or("object");
match type_str {
"string" => {
let enum_vals: Vec<String> = node
.get("enum")
.and_then(|e| e.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
Ok(Gemma4ToolFunctionParamsProp {
description,
type_: Gemma4ToolFunctionParamsPropType::String,
enum_: enum_vals,
..Default::default()
})
}
"number" | "integer" => Ok(Gemma4ToolFunctionParamsProp {
description,
type_: Gemma4ToolFunctionParamsPropType::Number,
..Default::default()
}),
"boolean" => Ok(Gemma4ToolFunctionParamsProp {
description,
type_: Gemma4ToolFunctionParamsPropType::Boolean,
..Default::default()
}),
"array" => {
let items_node = node.get("items");
let items = if let Some(items_schema) = items_node {
let items_resolved: &Value =
if let Some(r) = items_schema.get("$ref").and_then(|r| r.as_str()) {
resolve_ref(root, r).unwrap_or(items_schema)
} else {
items_schema
};
let item_type_str = items_resolved
.get("type")
.and_then(|t| t.as_str())
.unwrap_or("object");
let item_type = map_type(item_type_str);
let (item_props, item_required) = if item_type_str == "object" {
(
convert_properties(items_resolved, root, &format!("{field_path}.items"))?,
extract_required(items_resolved, &format!("{field_path}.items"))?,
)
} else {
(BTreeMap::new(), vec![])
};
Some(Gemma4ToolFunctionParamsPropArrayItems {
properties: item_props,
required: item_required,
type_: item_type,
})
} else {
None
};
Ok(Gemma4ToolFunctionParamsProp {
description,
type_: Gemma4ToolFunctionParamsPropType::Array,
items,
..Default::default()
})
}
_ => {
let properties = convert_properties(node, root, field_path)?;
let required = extract_required(node, field_path)?;
Ok(Gemma4ToolFunctionParamsProp {
description,
type_: Gemma4ToolFunctionParamsPropType::Object,
properties,
required,
..Default::default()
})
}
}
}
pub fn convert_properties(
node: &Value,
root: &Value,
parent_path: &str,
) -> Result<BTreeMap<String, Gemma4ToolFunctionParamsProp>, ParseToolError> {
let mut out = BTreeMap::new();
let Some(props_map) = node.get("properties").and_then(|p| p.as_object()) else {
return Ok(out);
};
for (key, prop_schema) in props_map {
let field_path = format!("{parent_path}.properties.{key}");
let prop = convert_schema_node(prop_schema, root, &field_path)?;
out.insert(key.clone(), prop);
}
Ok(out)
}
pub fn extract_required(node: &Value, parent_path: &str) -> Result<Vec<String>, ParseToolError> {
let Some(arr) = node.get("required").and_then(|r| r.as_array()) else {
return Ok(vec![]);
};
arr.iter()
.enumerate()
.map(|(i, v)| {
v.as_str()
.map(str::to_string)
.ok_or_else(|| ParseToolError(format!("{parent_path}.required[{i}]").into()))
})
.collect()
}