use std::io::Write as _;
use std::process::ExitCode;
use schemars::JsonSchema;
use schemars::generate::{Contract, SchemaSettings};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::{Input, Output, Reply, Server, ServerInfo, ToolError};
pub const MANIFEST_FLAG: &str = "--subako-manifest";
pub struct Function {
name: String,
description: String,
input_schema: fn(Contract) -> serde_json::Value,
output_schema: fn(Contract) -> serde_json::Value,
call: Box<dyn Fn(Input) -> Result<Output, ToolError>>,
}
impl Function {
pub fn new<I, O, E>(
name: impl Into<String>,
description: impl Into<String>,
f: impl Fn(I) -> Result<O, E> + 'static,
) -> Self
where
I: DeserializeOwned + JsonSchema,
O: Reply,
E: Into<ToolError>,
{
Function {
name: name.into(),
description: description.into(),
input_schema: schema::<I>,
output_schema: O::schema,
call: Box::new(move |input| f(input.parse()?).map_err(Into::into)?.into_output()),
}
}
pub fn name(&self) -> &str {
&self.name
}
}
pub const ABI: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Manifest {
pub abi: u32,
pub name: String,
pub description: String,
}
pub struct Package {
name: String,
version: String,
description: String,
functions: Vec<Function>,
}
impl Package {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
functions: Vec<Function>,
) -> Self {
Package {
name: name.into(),
version: "0.0.0".to_string(),
description: description.into(),
functions,
}
}
pub fn with_version(self, version: impl Into<String>) -> Self {
Package {
version: version.into(),
..self
}
}
pub fn functions(&self) -> &[Function] {
&self.functions
}
pub fn manifest(&self) -> Manifest {
Manifest {
abi: ABI,
name: self.name.clone(),
description: self.description.clone(),
}
}
pub fn call(&self, tool: &str, input: Input) -> Result<Output, ToolError> {
match self.functions.iter().find(|f| f.name == tool) {
Some(function) => (function.call)(input),
None => Err(ToolError::unknown_tool(tool)),
}
}
pub fn execute(&self, args: &[String], stdin: &[u8]) -> (Vec<u8>, u8) {
match args.get(1).map(String::as_str) {
Some(MANIFEST_FLAG) => (self.manifest_json(), 0),
_ => crate::execute(stdin, self),
}
}
pub fn run(&self) -> ExitCode {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(String::as_str) {
Some(MANIFEST_FLAG) => {
let mut stdout = std::io::stdout().lock();
match stdout
.write_all(&self.manifest_json())
.and_then(|()| stdout.flush())
{
Ok(()) => ExitCode::SUCCESS,
Err(_) => ExitCode::from(2),
}
}
_ => crate::run(self),
}
}
fn manifest_json(&self) -> Vec<u8> {
let mut json = serde_json::to_vec_pretty(&self.manifest()).unwrap_or_default();
json.push(b'\n');
json
}
}
impl Server for Package {
fn info(&self) -> ServerInfo<'_> {
ServerInfo {
name: &self.name,
version: &self.version,
}
}
fn tools(&self) -> Vec<serde_json::Value> {
self.functions
.iter()
.map(|f| {
serde_json::json!({
"name": f.name,
"description": f.description,
"inputSchema": (f.input_schema)(Contract::Deserialize),
"outputSchema": (f.output_schema)(Contract::Serialize),
})
})
.collect()
}
fn call(&self, tool: &str, input: Input) -> Result<Output, ToolError> {
Package::call(self, tool, input)
}
}
pub(crate) fn schema<T: JsonSchema>(contract: Contract) -> serde_json::Value {
let mut schema = SchemaSettings::draft2020_12()
.with(|s| {
s.inline_subschemas = true;
s.meta_schema = None;
s.contract = contract;
})
.into_generator()
.into_root_schema_for::<T>();
schema.remove("title");
schema.to_value()
}