#![forbid(unsafe_code)]
#![warn(missing_docs)]
use serde::{Deserialize, Serialize};
pub type Handle = u32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ty {
Text,
Bytes,
Image,
Document,
Json,
List(Box<Ty>),
Record(std::collections::BTreeMap<String, Ty>),
}
impl Ty {
pub fn assignable_to(&self, expected: &Ty) -> bool {
match (self, expected) {
(_, Ty::Json) => true,
(Ty::List(a), Ty::List(b)) => a.assignable_to(b),
(Ty::Record(have), Ty::Record(need)) => need
.iter()
.all(|(name, want)| have.get(name).is_some_and(|got| got.assignable_to(want))),
(a, b) => a == b,
}
}
pub fn describe(&self) -> String {
match self {
Ty::Text => "text".into(),
Ty::Bytes => "bytes".into(),
Ty::Image => "image".into(),
Ty::Document => "document".into(),
Ty::Json => "json".into(),
Ty::List(inner) => format!("[{}]", inner.describe()),
Ty::Record(fields) => {
let body = fields
.iter()
.map(|(k, v)| format!("{k}: {}", v.describe()))
.collect::<Vec<_>>()
.join(", ");
format!("{{{body}}}")
}
}
}
}
impl std::fmt::Display for Ty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.describe())
}
}
impl std::str::FromStr for Ty {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_ty(s.trim())
}
}
impl Serialize for Ty {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.describe())
}
}
impl<'de> Deserialize<'de> for Ty {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let raw = String::deserialize(d)?;
raw.parse().map_err(serde::de::Error::custom)
}
}
fn parse_ty(s: &str) -> Result<Ty, String> {
let s = s.trim();
match s {
"text" => return Ok(Ty::Text),
"bytes" => return Ok(Ty::Bytes),
"image" => return Ok(Ty::Image),
"document" => return Ok(Ty::Document),
"json" => return Ok(Ty::Json),
_ => {}
}
if let Some(inner) = s.strip_prefix('[').and_then(|r| r.strip_suffix(']')) {
return Ok(Ty::List(Box::new(parse_ty(inner)?)));
}
if let Some(body) = s.strip_prefix('{').and_then(|r| r.strip_suffix('}')) {
let mut fields = std::collections::BTreeMap::new();
if !body.trim().is_empty() {
for part in split_fields(body) {
let (name, ty) = part
.split_once(':')
.ok_or_else(|| format!("expected `name: type` in `{part}`"))?;
fields.insert(name.trim().to_string(), parse_ty(ty)?);
}
}
return Ok(Ty::Record(fields));
}
Err(format!("`{s}` is not a type"))
}
fn split_fields(body: &str) -> Vec<String> {
let (mut out, mut depth, mut current) = (Vec::new(), 0i32, String::new());
for c in body.chars() {
match c {
'[' | '{' => {
depth += 1;
current.push(c);
}
']' | '}' => {
depth -= 1;
current.push(c);
}
',' if depth == 0 => out.push(std::mem::take(&mut current)),
_ => current.push(c),
}
}
if !current.trim().is_empty() {
out.push(current);
}
out
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signature {
pub input: Ty,
pub output: Ty,
}
impl std::fmt::Display for Signature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} -> {}", self.input, self.output)
}
}
impl std::str::FromStr for Signature {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (input, output) = s
.split_once(" -> ")
.ok_or_else(|| format!("`{s}` is not a signature (expected `input -> output`)"))?;
Ok(Signature {
input: input.parse()?,
output: output.parse()?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MediaKind {
Text,
Image {
format: String,
},
Document {
pages: u32,
has_text_layer: bool,
},
Binary,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum Command {
Infer {
prompt: String,
max_tokens: u32,
#[serde(default)]
images: Vec<Handle>,
},
Open {
path: String,
},
Slice {
handle: Handle,
offset: u64,
len: u64,
},
SliceBytes {
handle: Handle,
offset: u64,
len: u64,
},
PageText {
handle: Handle,
page: u32,
},
PageImage {
handle: Handle,
page: u32,
},
Emit {
progress: serde_json::Value,
},
Done {
result: serde_json::Value,
},
Fail {
code: String,
message: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum Event {
InferDone {
text: String,
tokens_out: u32,
},
Opened {
handle: Handle,
len: u64,
#[serde(default)]
kind: MediaKind,
},
Sliced {
text: String,
next_offset: u64,
},
SlicedBytes {
bytes_base64: String,
next_offset: u64,
},
PageTexted {
text: String,
},
PageImaged {
handle: Handle,
len: u64,
},
Emitted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenAction {
Continue,
Stop,
}
impl TokenAction {
pub fn from_i32(v: i32) -> Self {
if v == 0 {
Self::Continue
} else {
Self::Stop
}
}
pub fn as_i32(self) -> i32 {
match self {
Self::Continue => 0,
Self::Stop => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JobStatus {
Queued,
Running,
Completed,
Failed,
Cancelled,
Interrupted,
}
impl JobStatus {
pub fn is_terminal(self) -> bool {
matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Usage {
pub tokens_in: u32,
pub tokens_out: u32,
pub duration_ms: u64,
pub model: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Envelope {
pub status: JobStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JobError>,
pub usage: Usage,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct JobError {
pub code: String,
pub message: String,
}
pub mod error_codes {
pub const MODEL_LOAD_FAILED: &str = "model_load_failed";
pub const CAPABILITY_DENIED: &str = "capability_denied";
pub const SCHEMA_VALIDATION_FAILED: &str = "schema_validation_failed";
pub const WASM_TRAP: &str = "wasm_trap";
pub const TIMEOUT: &str = "timeout";
pub const CANCELLED: &str = "cancelled";
pub const UNSUPPORTED: &str = "unsupported";
}
impl Default for MediaKind {
fn default() -> Self {
Self::Text
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_signature_round_trips_through_its_compact_string() {
let sig = Signature {
input: Ty::Record([("path".to_string(), Ty::Text)].into_iter().collect()),
output: Ty::List(Box::new(Ty::Text)),
};
let s = sig.to_string();
assert_eq!(s, "{path: text} -> [text]");
assert_eq!(s.parse::<Signature>().unwrap(), sig);
}
#[test]
fn a_signature_without_an_arrow_is_rejected() {
assert!("just-a-type".parse::<Signature>().is_err());
}
#[test]
fn a_signature_with_an_unparseable_side_is_rejected() {
assert!("text -> not a type".parse::<Signature>().is_err());
}
}