substrate_core/
skill_port.rs1use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::error::{Result, SubstrateError};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct SkillDescriptor {
14 pub name: String,
16 pub description: String,
18 pub input_schema: Value,
20 pub output_schema: Value,
22}
23
24pub trait SkillHandler: Send + Sync {
26 fn invoke(&self, input: Value) -> Result<Value>;
28}
29
30pub trait SkillPort: Send + Sync {
32 fn invoke(&self, name: &str, input: Value) -> Result<Value>;
35
36 fn list_skills(&self) -> Vec<SkillDescriptor>;
38}
39
40pub trait ToolRegistry: Send + Sync {
42 fn register(
45 &mut self,
46 descriptor: SkillDescriptor,
47 handler: Box<dyn SkillHandler>,
48 ) -> Result<()>;
49
50 fn lookup(&self, name: &str) -> Option<&SkillDescriptor>;
52
53 fn list(&self) -> Vec<SkillDescriptor>;
55
56 fn validate_input(&self, name: &str, input: &Value) -> Result<()>;
58}
59
60pub fn validate_json_schema(value: &Value, schema: &Value) -> Result<()> {
62 let Some(schema_obj) = schema.as_object() else {
63 return Err(SubstrateError::SchemaValidation(
64 "schema must be a JSON object".into(),
65 ));
66 };
67
68 if let Some(expected_type) = schema_obj.get("type").and_then(|t| t.as_str()) {
69 let matches = match expected_type {
70 "object" => value.is_object(),
71 "array" => value.is_array(),
72 "string" => value.is_string(),
73 "number" => value.is_number(),
74 "integer" => value.as_i64().is_some(),
75 "boolean" => value.is_boolean(),
76 "null" => value.is_null(),
77 other => {
78 return Err(SubstrateError::SchemaValidation(format!(
79 "unsupported schema type: {other}"
80 )));
81 }
82 };
83 if !matches {
84 return Err(SubstrateError::SchemaValidation(format!(
85 "expected type {expected_type}, got {value}"
86 )));
87 }
88 }
89
90 if let Some(required) = schema_obj.get("required").and_then(|r| r.as_array()) {
91 let obj = value
92 .as_object()
93 .ok_or_else(|| SubstrateError::SchemaValidation("value must be an object".into()))?;
94 for key in required {
95 let key_str = key.as_str().ok_or_else(|| {
96 SubstrateError::SchemaValidation("required entry must be a string".into())
97 })?;
98 if !obj.contains_key(key_str) {
99 return Err(SubstrateError::SchemaValidation(format!(
100 "missing required field: {key_str}"
101 )));
102 }
103 }
104 }
105
106 if let (Some(props), Some(obj)) = (
107 schema_obj.get("properties").and_then(|p| p.as_object()),
108 value.as_object(),
109 ) {
110 for (key, prop_schema) in props {
111 if let Some(field_value) = obj.get(key) {
112 validate_json_schema(field_value, prop_schema)?;
113 }
114 }
115 }
116
117 Ok(())
118}