use std::collections::HashMap;
use std::ops::Index;
use rskit_errors::{AppError, AppResult, ErrorCode};
use rskit_schema::Json;
use serde::{Deserialize, Deserializer, Serialize, de};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub struct ToolSchema(Json);
impl ToolSchema {
pub fn new(value: Json) -> AppResult<Self> {
if value.is_object() {
Ok(Self(value))
} else {
Err(AppError::new(
ErrorCode::InvalidInput,
"tool schema must be a JSON object",
))
}
}
#[must_use]
pub fn any_object() -> Self {
Self(serde_json::json!({"type": "object"}))
}
#[must_use]
pub const fn as_json(&self) -> &Json {
&self.0
}
#[must_use]
pub fn as_object(&self) -> Option<&serde_json::Map<String, Json>> {
self.0.as_object()
}
#[must_use]
pub fn is_object(&self) -> bool {
self.0.is_object()
}
#[must_use]
pub fn into_json(self) -> Json {
self.0
}
}
impl Default for ToolSchema {
fn default() -> Self {
Self::any_object()
}
}
impl TryFrom<Json> for ToolSchema {
type Error = AppError;
fn try_from(value: Json) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl<'de> Deserialize<'de> for ToolSchema {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::new(Json::deserialize(deserializer)?).map_err(de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub struct ToolInput(Json);
impl ToolInput {
pub fn new(value: Json) -> AppResult<Self> {
if value.is_object() {
Ok(Self(value))
} else {
Err(AppError::new(
ErrorCode::InvalidInput,
"tool input must be a JSON object",
))
}
}
#[must_use]
pub fn empty() -> Self {
Self(Json::Object(serde_json::Map::new()))
}
#[must_use]
pub const fn from_object(object: serde_json::Map<String, Json>) -> Self {
Self(Json::Object(object))
}
#[must_use]
pub const fn as_json(&self) -> &Json {
&self.0
}
#[must_use]
pub fn into_json(self) -> Json {
self.0
}
}
impl Default for ToolInput {
fn default() -> Self {
Self::empty()
}
}
impl TryFrom<Json> for ToolInput {
type Error = AppError;
fn try_from(value: Json) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl<'de> Deserialize<'de> for ToolInput {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::new(Json::deserialize(deserializer)?).map_err(de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ToolOutput(Json);
impl ToolOutput {
pub fn from_serializable<T: Serialize>(value: &T) -> AppResult<Self> {
serde_json::to_value(value).map(Self).map_err(|err| {
AppError::new(
ErrorCode::Internal,
format!("failed to serialize tool output: {err}"),
)
.with_cause(err)
})
}
#[must_use]
pub const fn as_json(&self) -> &Json {
&self.0
}
#[must_use]
pub fn into_json(self) -> Json {
self.0
}
}
impl From<Json> for ToolOutput {
fn from(value: Json) -> Self {
Self(value)
}
}
impl PartialEq<Json> for ToolOutput {
fn eq(&self, other: &Json) -> bool {
&self.0 == other
}
}
impl Index<&str> for ToolOutput {
type Output = Json;
fn index(&self, index: &str) -> &Self::Output {
&self.0[index]
}
}
pub type ToolMetadata = HashMap<String, ToolOutput>;
#[cfg(test)]
mod tests {
use super::*;
use serde::ser::{Error as _, SerializeStruct};
#[test]
fn schema_deserialization_rejects_non_object() {
let err = serde_json::from_str::<ToolSchema>(r#""not-a-schema""#).unwrap_err();
assert!(
err.to_string()
.contains("tool schema must be a JSON object")
);
}
#[test]
fn input_deserialization_rejects_non_object() {
let err = serde_json::from_str::<ToolInput>("[]").unwrap_err();
assert!(err.to_string().contains("tool input must be a JSON object"));
}
#[test]
fn output_serialization_error_has_context() {
struct FailingOutput;
impl Serialize for FailingOutput {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("FailingOutput", 1)?;
state.serialize_field("bad", &FailingField)?;
state.end()
}
}
struct FailingField;
impl Serialize for FailingField {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Err(S::Error::custom("boom"))
}
}
let err = ToolOutput::from_serializable(&FailingOutput).unwrap_err();
assert!(
err.message()
.contains("failed to serialize tool output: boom")
);
}
#[test]
fn schema_and_input_helpers_preserve_json_object_contract() {
let schema = ToolSchema::any_object();
assert!(schema.is_object());
assert_eq!(schema.as_json()["type"], "object");
assert_eq!(
ToolSchema::default().into_json(),
serde_json::json!({"type": "object"})
);
assert_eq!(
ToolSchema::try_from(serde_json::json!("string"))
.unwrap_err()
.code(),
ErrorCode::InvalidInput
);
let mut object = serde_json::Map::new();
object.insert("query".to_owned(), serde_json::json!("rust"));
let input = ToolInput::from_object(object);
assert_eq!(input.as_json()["query"], "rust");
assert_eq!(ToolInput::empty().into_json(), serde_json::json!({}));
assert_eq!(
ToolInput::try_from(serde_json::json!(["not", "object"]))
.unwrap_err()
.code(),
ErrorCode::InvalidInput
);
}
#[test]
fn output_helpers_expose_and_consume_json() {
let output = ToolOutput::from(serde_json::json!({"ok": true}));
assert_eq!(output.as_json()["ok"], true);
assert_eq!(output["ok"], true);
assert_eq!(output.into_json(), serde_json::json!({"ok": true}));
}
}