typesafe_system_one/
request.rs1use std::collections::BTreeMap;
4
5use serde_json::Value;
6
7use crate::errors::Error;
8use crate::questions::{validate, Question};
9
10#[derive(Debug, Clone, PartialEq)]
28pub struct SystemOneRequest {
29 pub state: Value,
31 pub model: Option<String>,
33 pub questions: BTreeMap<String, Question>,
35}
36
37impl SystemOneRequest {
38 pub fn new(state: impl Into<Value>) -> Self {
45 Self {
46 state: state.into(),
47 model: None,
48 questions: BTreeMap::new(),
49 }
50 }
51
52 pub fn with_state_serialize<T: serde::Serialize>(state: &T) -> Result<Self, Error> {
56 let value = serde_json::to_value(state).map_err(|error| {
57 Error::InvalidRequest(format!("state failed to serialize: {error}"))
58 })?;
59 if !matches!(value, Value::String(_) | Value::Object(_) | Value::Array(_)) {
60 return Err(Error::InvalidRequest(
61 "state must serialize to a JSON string, object, or array.".into(),
62 ));
63 }
64 Ok(Self::new(value))
65 }
66
67 pub fn model(mut self, model: impl Into<String>) -> Self {
69 self.model = Some(model.into());
70 self
71 }
72
73 pub fn question(mut self, id: impl Into<String>, question: impl Into<Question>) -> Self {
75 self.questions.insert(id.into(), question.into());
76 self
77 }
78
79 pub fn questions<I, S>(mut self, questions: I) -> Self
81 where
82 I: IntoIterator<Item = (S, Question)>,
83 S: Into<String>,
84 {
85 for (id, question) in questions {
86 self.questions.insert(id.into(), question);
87 }
88 self
89 }
90
91 pub(crate) fn validate(&self) -> Result<(), Error> {
93 validate(&self.state, &self.questions)
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::questions::{Choice, Noul, Score};
101 use serde_json::json;
102
103 #[derive(serde::Serialize)]
104 struct Ticket {
105 subject: String,
106 body: String,
107 }
108
109 #[test]
110 fn with_state_serialize_accepts_objects() {
111 let ticket = Ticket {
112 subject: "Refund".into(),
113 body: "Charged twice".into(),
114 };
115 let request = SystemOneRequest::with_state_serialize(&ticket).unwrap();
116 assert_eq!(
117 request.state,
118 json!({"subject": "Refund", "body": "Charged twice"})
119 );
120 }
121
122 #[test]
123 fn with_state_serialize_rejects_primitives() {
124 let err = SystemOneRequest::with_state_serialize(&42).unwrap_err();
125 assert!(matches!(err, Error::InvalidRequest(_)));
126 let err = SystemOneRequest::with_state_serialize(&true).unwrap_err();
127 assert!(matches!(err, Error::InvalidRequest(_)));
128 let err = SystemOneRequest::with_state_serialize::<Option<u8>>(&None).unwrap_err();
129 assert!(matches!(err, Error::InvalidRequest(_)));
130 let err = SystemOneRequest::with_state_serialize(&"string is fine").unwrap();
131 assert_eq!(err.state, json!("string is fine"));
132 }
133
134 #[test]
135 fn builder_methods() {
136 let request = SystemOneRequest::new("text")
137 .model("jev-1.13.0")
138 .question("a", Noul::new("q1"))
139 .question("b", Choice::new("q2").option("x", "y"))
140 .question("c", Score::new("q3", ["1", "2"]));
141 assert_eq!(request.model.as_deref(), Some("jev-1.13.0"));
142 assert_eq!(request.questions.len(), 3);
143 assert_eq!(request.questions["a"], Question::Noul(Noul::new("q1")));
144 assert!(request.validate().is_ok());
145 }
146}