use crate::grammar::Grammar;
pub trait Task {
type Output;
type Value;
type ParseError: core::error::Error;
fn prompt(&self) -> &str;
fn schema(&self) -> &Self::Value;
fn grammar(&self) -> Grammar;
fn parse(&self, raw: &str) -> Result<Self::Output, Self::ParseError>;
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub use json::JsonParseError;
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
mod json {
use std::vec::Vec;
#[derive(thiserror::Error, Debug)]
pub enum JsonParseError {
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("schema violation: required fields missing or null: {0:?}")]
MissingFields(Vec<&'static str>),
#[error("structured response had no usable fields")]
NoUsableFields,
}
}
#[cfg(all(test, feature = "std", any(feature = "json", feature = "regex")))]
mod tests {
use super::*;
#[cfg(feature = "json")]
use std::sync::OnceLock;
#[cfg(feature = "json")]
#[test]
fn task_is_dyn_compatible() {
struct Dummy;
impl Task for Dummy {
type Output = ();
type Value = serde_json::Value;
type ParseError = JsonParseError;
fn prompt(&self) -> &str {
""
}
fn schema(&self) -> &serde_json::Value {
static V: OnceLock<serde_json::Value> = OnceLock::new();
V.get_or_init(|| serde_json::Value::Null)
}
fn grammar(&self) -> Grammar {
Grammar::JsonSchema(self.schema().clone())
}
fn parse(&self, _raw: &str) -> Result<(), JsonParseError> {
Ok(())
}
}
let _: Box<dyn Task<Output = (), Value = serde_json::Value, ParseError = JsonParseError>> =
Box::new(Dummy);
fn _assert_send_sync(_: &impl ?Sized) {}
_assert_send_sync(&*Box::new(Dummy)
as &dyn Task<Output = (), Value = serde_json::Value, ParseError = JsonParseError>);
}
#[cfg(feature = "json")]
#[test]
fn json_task_default_grammar_wraps_schema() {
struct JsonTask;
impl Task for JsonTask {
type Output = ();
type Value = serde_json::Value;
type ParseError = JsonParseError;
fn prompt(&self) -> &str {
""
}
fn schema(&self) -> &serde_json::Value {
static V: OnceLock<serde_json::Value> = OnceLock::new();
V.get_or_init(|| serde_json::json!({"type": "string"}))
}
fn grammar(&self) -> Grammar {
Grammar::JsonSchema(self.schema().clone())
}
fn parse(&self, _raw: &str) -> Result<(), JsonParseError> {
Ok(())
}
}
let g = JsonTask.grammar();
assert!(g.is_json_schema());
assert_eq!(
g.as_json_schema().unwrap(),
&serde_json::json!({"type": "string"})
);
}
#[cfg(feature = "regex")]
#[test]
fn regex_only_task_compiles_without_json_paths() {
#[derive(Debug)]
struct StringErr(String);
impl std::fmt::Display for StringErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for StringErr {}
struct TimestampTask {
pattern: smol_str::SmolStr,
grammar: Grammar,
}
impl Task for TimestampTask {
type Output = String;
type Value = smol_str::SmolStr;
type ParseError = StringErr;
fn prompt(&self) -> &str {
"Output a date in YYYY-MM-DD format."
}
fn schema(&self) -> &smol_str::SmolStr {
&self.pattern
}
fn grammar(&self) -> Grammar {
self.grammar.clone()
}
fn parse(&self, raw: &str) -> Result<String, StringErr> {
let trimmed = raw.trim();
if self.grammar.is_regex_full_match(trimmed) != Some(true) {
return Err(StringErr(format!(
"output {trimmed:?} does not match pattern {:?}",
self.pattern.as_str()
)));
}
Ok(trimmed.to_string())
}
}
let pattern = smol_str::SmolStr::new(r"[0-9]{4}-[0-9]{2}-[0-9]{2}");
let task = TimestampTask {
grammar: Grammar::regex(&pattern).unwrap(),
pattern,
};
assert_eq!(task.grammar().kind(), "regex");
assert_eq!(task.schema().as_str(), r"[0-9]{4}-[0-9]{2}-[0-9]{2}");
assert_eq!(task.parse("2026-05-09\n").unwrap(), "2026-05-09");
assert!(task.parse("not a date").is_err());
assert!(task.parse("abc2026-05-09xyz").is_err());
}
#[cfg(feature = "json")]
#[test]
fn engine_can_bind_value_to_json_for_typed_access() {
fn json_only_engine<T>(task: &T) -> &serde_json::Value
where
T: Task<Value = serde_json::Value>,
{
task.schema()
}
struct X;
impl Task for X {
type Output = ();
type Value = serde_json::Value;
type ParseError = JsonParseError;
fn prompt(&self) -> &str {
""
}
fn schema(&self) -> &serde_json::Value {
static V: OnceLock<serde_json::Value> = OnceLock::new();
V.get_or_init(|| serde_json::json!({"type": "object"}))
}
fn grammar(&self) -> Grammar {
Grammar::JsonSchema(self.schema().clone())
}
fn parse(&self, _raw: &str) -> Result<(), JsonParseError> {
Ok(())
}
}
let v = json_only_engine(&X);
assert_eq!(v, &serde_json::json!({"type": "object"}));
}
}