Skip to main content

kunobi_decision/
types.rs

1//! Values shared by requests and responses.
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5
6use crate::error::Error;
7
8/// Text, a JSON object, a JSON array, or `null`.
9///
10/// State, instructions and criteria descriptions all take this shape. Numbers
11/// and booleans are not valid on their own; wrap them in an object.
12#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
13#[serde(untagged)]
14pub enum Entry {
15    /// No value. For a criterion, this leaves the label undescribed.
16    #[default]
17    Null,
18    /// Plain text.
19    Text(String),
20    /// A JSON object.
21    Object(Map<String, Value>),
22    /// A JSON array.
23    Array(Vec<Value>),
24}
25
26impl Entry {
27    /// Serialize any value into an entry.
28    ///
29    /// Fails when the value serializes to a number or a boolean.
30    pub fn from_serialize<T: Serialize + ?Sized>(value: &T) -> Result<Self, Error> {
31        let value = serde_json::to_value(value)
32            .map_err(|err| Error::InvalidRequest(format!("Could not serialize entry: {err}")))?;
33        Self::try_from(value)
34    }
35
36    /// Whether this entry is `null`.
37    pub fn is_null(&self) -> bool {
38        matches!(self, Entry::Null)
39    }
40}
41
42impl From<&str> for Entry {
43    fn from(text: &str) -> Self {
44        Entry::Text(text.to_owned())
45    }
46}
47
48impl From<String> for Entry {
49    fn from(text: String) -> Self {
50        Entry::Text(text)
51    }
52}
53
54impl From<&String> for Entry {
55    fn from(text: &String) -> Self {
56        Entry::Text(text.clone())
57    }
58}
59
60impl From<Map<String, Value>> for Entry {
61    fn from(object: Map<String, Value>) -> Self {
62        Entry::Object(object)
63    }
64}
65
66impl From<Vec<Value>> for Entry {
67    fn from(array: Vec<Value>) -> Self {
68        Entry::Array(array)
69    }
70}
71
72impl<T: Into<Entry>> From<Option<T>> for Entry {
73    fn from(value: Option<T>) -> Self {
74        value.map_or(Entry::Null, Into::into)
75    }
76}
77
78impl TryFrom<Value> for Entry {
79    type Error = Error;
80
81    fn try_from(value: Value) -> Result<Self, Error> {
82        match value {
83            Value::Null => Ok(Entry::Null),
84            Value::String(text) => Ok(Entry::Text(text)),
85            Value::Object(object) => Ok(Entry::Object(object)),
86            Value::Array(array) => Ok(Entry::Array(array)),
87            other => Err(Error::InvalidRequest(format!(
88                "An entry must be text, a JSON object, a JSON array, or null, got {other}."
89            ))),
90        }
91    }
92}
93
94impl From<Entry> for Value {
95    fn from(entry: Entry) -> Self {
96        match entry {
97            Entry::Null => Value::Null,
98            Entry::Text(text) => Value::String(text),
99            Entry::Object(object) => Value::Object(object),
100            Entry::Array(array) => Value::Array(array),
101        }
102    }
103}
104
105/// Token usage for a request, when the API reports it.
106///
107/// Both counts are optional because the API does not always send them, and
108/// billing is per input token: a silent `0` would read as a free call.
109#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
110pub struct Usage {
111    /// Input tokens used, or `None` when the API did not report it.
112    #[serde(default)]
113    pub input_tokens: Option<u64>,
114    /// Output tokens used, or `None` when the API did not report it.
115    #[serde(default)]
116    pub output_tokens: Option<u64>,
117}
118
119/// Metadata for an available model.
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct ModelCard {
122    /// Model name to pass as `model`, such as `jev-latest`.
123    pub name: String,
124    /// Human-readable description.
125    #[serde(default)]
126    pub description: String,
127    /// Release date as the API reports it.
128    #[serde(default)]
129    pub release_date: String,
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use serde_json::json;
136
137    #[test]
138    fn entries_serialize_to_their_json_shape() {
139        assert_eq!(serde_json::to_value(Entry::Null).unwrap(), json!(null));
140        assert_eq!(
141            serde_json::to_value(Entry::from("hi")).unwrap(),
142            json!("hi")
143        );
144        let object = Entry::try_from(json!({"a": 1})).unwrap();
145        assert_eq!(serde_json::to_value(object).unwrap(), json!({"a": 1}));
146        let array = Entry::try_from(json!([1, "b"])).unwrap();
147        assert_eq!(serde_json::to_value(array).unwrap(), json!([1, "b"]));
148    }
149
150    #[test]
151    fn entries_reject_bare_numbers_and_booleans() {
152        assert!(Entry::try_from(json!(1)).is_err());
153        assert!(Entry::try_from(json!(true)).is_err());
154        assert!(Entry::from_serialize(&3.5).is_err());
155    }
156
157    #[test]
158    fn entries_accept_serializable_structs() {
159        #[derive(Serialize)]
160        struct Ticket {
161            subject: &'static str,
162        }
163        let entry = Entry::from_serialize(&Ticket { subject: "refund" }).unwrap();
164        assert_eq!(Value::from(entry), json!({"subject": "refund"}));
165    }
166
167    #[test]
168    fn object_entries_keep_key_order() {
169        let entry = Entry::try_from(json!({"zeta": 1, "alpha": 2, "mid": 3})).unwrap();
170        assert_eq!(
171            serde_json::to_string(&entry).unwrap(),
172            r#"{"zeta":1,"alpha":2,"mid":3}"#
173        );
174    }
175
176    #[test]
177    fn options_map_none_to_null() {
178        assert_eq!(Entry::from(None::<&str>), Entry::Null);
179        assert_eq!(Entry::from(Some("x")), Entry::Text("x".into()));
180    }
181
182    #[test]
183    fn entries_deserialize_from_json() {
184        let entries: Vec<Entry> =
185            serde_json::from_value(json!([null, "t", {"k": 1}, [1]])).unwrap();
186        assert!(entries[0].is_null());
187        assert_eq!(entries[1], Entry::Text("t".into()));
188        assert!(matches!(entries[2], Entry::Object(_)));
189        assert!(matches!(entries[3], Entry::Array(_)));
190    }
191}