use serde::Serialize;
use crate::domain::ir::{JsonObject, JsonObjectError};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum OutcomeError {
#[error("the arguments do not match this tool: {0}")]
Arguments(String),
#[error("the handler's answer cannot be encoded: {0}")]
Answer(String),
#[error("the assembled result is not a JSON object: {0}")]
NotAnObject(JsonObjectError),
#[error("the result cannot be expressed at this revision: {0}")]
NotLowerable(crate::domain::ir::LowerError),
}
pub fn success(answer: &impl Serialize) -> Result<JsonObject, OutcomeError> {
let value =
serde_json::to_value(answer).map_err(|error| OutcomeError::Answer(error.to_string()))?;
let (structured, text) = match &value {
serde_json::Value::Object(_) => (Some(value.clone()), value.to_string()),
serde_json::Value::String(text) => (None, text.clone()),
other => (None, other.to_string()),
};
let mut document = serde_json::json!({
"content": [{ "type": "text", "text": text }],
"isError": false,
});
if let (serde_json::Value::Object(map), Some(structured)) = (&mut document, structured) {
map.insert("structuredContent".to_string(), structured);
}
JsonObject::from_value(document).map_err(OutcomeError::NotAnObject)
}
#[must_use]
pub fn failure(message: impl core::fmt::Display) -> JsonObject {
let document = serde_json::json!({
"content": [{ "type": "text", "text": message.to_string() }],
"isError": true,
});
JsonObject::from_value(document).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use serde::Serialize;
use super::{OutcomeError, failure, success};
use crate::domain::ir::JsonObject;
#[derive(Serialize)]
struct ObjectAnswer {
total: i32,
}
#[derive(Serialize)]
struct TextAnswer(String);
fn text_blocks(document: &JsonObject) -> Result<Vec<String>, String> {
let parsed: serde_json::Value =
serde_json::from_str(document.as_str()).map_err(|error| error.to_string())?;
parsed
.get("content")
.and_then(serde_json::Value::as_array)
.ok_or("the result has no content array")?
.iter()
.map(|block| {
block
.get("text")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
.ok_or_else(|| "a content block is not text".to_string())
})
.collect()
}
#[test]
fn an_object_answer_becomes_structured_content_and_a_text_copy() -> Result<(), String> {
let document = success(&ObjectAnswer { total: 7 }).map_err(|e| e.to_string())?;
let parsed: serde_json::Value =
serde_json::from_str(document.as_str()).map_err(|error| error.to_string())?;
assert_eq!(
parsed
.get("structuredContent")
.and_then(|value| value.get("total")),
Some(&serde_json::Value::from(7)),
"the object answer did not reach structuredContent"
);
let blocks = text_blocks(&document)?;
if blocks.first() != Some(&"{\"total\":7}".to_string()) {
return Err(format!("the text copy is not the object: {blocks:?}"));
}
Ok(())
}
#[test]
fn a_text_answer_becomes_a_text_block_and_nothing_else() -> Result<(), String> {
let document = success(&TextAnswer("hello".to_string())).map_err(|e| e.to_string())?;
let parsed: serde_json::Value =
serde_json::from_str(document.as_str()).map_err(|error| error.to_string())?;
if parsed.get("structuredContent").is_some() {
return Err("a text answer produced structuredContent".to_string());
}
if text_blocks(&document)? != ["hello".to_string()] {
return Err("the text answer did not survive".to_string());
}
Ok(())
}
#[test]
fn a_success_is_never_flagged_as_an_error() -> Result<(), String> {
let document = success(&ObjectAnswer { total: 0 }).map_err(|e| e.to_string())?;
let parsed: serde_json::Value =
serde_json::from_str(document.as_str()).map_err(|error| error.to_string())?;
assert_eq!(parsed.get("isError"), Some(&serde_json::Value::Bool(false)));
Ok(())
}
#[test]
fn a_failure_carries_its_message_and_is_flagged() -> Result<(), String> {
let document = failure("the widget is missing");
let parsed: serde_json::Value =
serde_json::from_str(document.as_str()).map_err(|error| error.to_string())?;
assert_eq!(parsed.get("isError"), Some(&serde_json::Value::Bool(true)));
if text_blocks(&document)? != ["the widget is missing".to_string()] {
return Err("the failure lost its message".to_string());
}
Ok(())
}
#[test]
fn an_unserialisable_answer_is_reported_as_the_handlers_fault() -> Result<(), String> {
match success(&NotSerializable) {
Err(OutcomeError::Answer(_)) => Ok(()),
Err(other) => Err(format!("wrong error for a bad answer: {other}")),
Ok(document) => Err(format!("a bad answer produced {document}")),
}
}
struct NotSerializable;
impl Serialize for NotSerializable {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Err(serde::ser::Error::custom("this value has no JSON form"))
}
}
}