1use std::collections::BTreeMap;
2
3use serde::Serialize;
4use serde_json::{Map, Value};
5
6use crate::answer::SystemOneResponse;
7use crate::client::Client;
8use crate::error::{Error, Result};
9use crate::question::{NONE_OF_THE_ABOVE, Question};
10
11enum State {
12 Empty,
13 Whole(Value),
14 Fields(Map<String, Value>),
15}
16
17#[must_use = "a request does nothing until `.send().await`"]
24pub struct SystemOneRequest<'a> {
25 client: &'a Client,
26 model: Option<String>,
27 state: State,
28 questions: BTreeMap<String, Question>,
29 error: Option<Error>,
30}
31
32impl<'a> SystemOneRequest<'a> {
33 pub(crate) fn new(client: &'a Client) -> Self {
34 Self {
35 client,
36 model: None,
37 state: State::Empty,
38 questions: BTreeMap::new(),
39 error: None,
40 }
41 }
42
43 pub fn model(mut self, model: impl Into<String>) -> Self {
45 self.model = Some(model.into());
46 self
47 }
48
49 pub fn state(mut self, state: impl Serialize) -> Self {
52 match serde_json::to_value(state) {
53 Ok(v) => self.state = State::Whole(v),
54 Err(e) => self.fail(Error::RequestSerialization(e)),
55 }
56 self
57 }
58
59 pub fn field(mut self, name: impl Into<String>, value: impl Serialize) -> Self {
63 let value = match serde_json::to_value(value) {
64 Ok(v) => v,
65 Err(e) => {
66 self.fail(Error::RequestSerialization(e));
67 return self;
68 }
69 };
70 let name = name.into();
71 self.state = match std::mem::replace(&mut self.state, State::Empty) {
72 State::Empty => State::Fields(Map::from_iter([(name, value)])),
73 State::Fields(mut map) => {
74 map.insert(name, value);
75 State::Fields(map)
76 }
77 State::Whole(Value::Object(mut map)) => {
78 map.insert(name, value);
79 State::Fields(map)
80 }
81 other @ State::Whole(_) => {
82 self.fail(Error::InvalidRequest(format!(
83 "cannot add field `{name}`: state is not an object"
84 )));
85 other
86 }
87 };
88 self
89 }
90
91 pub fn question(mut self, id: impl Into<String>, question: Question) -> Self {
93 let id = id.into();
94 if self.questions.insert(id.clone(), question).is_some() {
95 self.fail(Error::InvalidRequest(format!("duplicate question id `{id}`")));
96 }
97 self
98 }
99
100 pub fn noul(self, id: impl Into<String>, instructions: impl Into<Value>) -> Self {
102 self.question(id, Question::noul(instructions))
103 }
104
105 pub fn noul_with_criteria(
107 self,
108 id: impl Into<String>,
109 instructions: impl Into<Value>,
110 yes: impl Into<String>,
111 no: impl Into<String>,
112 ) -> Self {
113 self.question(id, Question::noul_with_criteria(instructions, yes, no))
114 }
115
116 pub fn choice(
118 mut self,
119 id: impl Into<String>,
120 instructions: impl Into<Value>,
121 options: impl FnOnce(ChoiceBuilder) -> ChoiceBuilder,
122 ) -> Self {
123 let id = id.into();
124 let built = options(ChoiceBuilder::default());
125 if built.options.is_empty() {
126 self.fail(Error::InvalidRequest(format!("choice `{id}` has no options")));
127 return self;
128 }
129 self.question(
130 id,
131 Question::Choice {
132 instructions: instructions.into(),
133 criteria: built.options,
134 },
135 )
136 }
137
138 pub fn score<L: Into<String>>(
140 mut self,
141 id: impl Into<String>,
142 instructions: impl Into<Value>,
143 levels: impl IntoIterator<Item = L>,
144 ) -> Self {
145 let id = id.into();
146 let levels: Vec<String> = levels.into_iter().map(Into::into).collect();
147 if levels.len() < 2 {
148 self.fail(Error::InvalidRequest(format!("score `{id}` needs at least two levels")));
149 return self;
150 }
151 self.question(
152 id,
153 Question::Score {
154 instructions: instructions.into(),
155 criteria: levels,
156 },
157 )
158 }
159
160 pub async fn send(self) -> Result<SystemOneResponse> {
162 if let Some(e) = self.error {
163 return Err(e);
164 }
165 let state = match self.state {
166 State::Empty => {
167 return Err(Error::InvalidRequest(
168 "no state: call `.state(..)` or `.field(..)`".into(),
169 ));
170 }
171 State::Whole(v) => v,
172 State::Fields(map) => Value::Object(map),
173 };
174 if self.questions.is_empty() {
175 return Err(Error::InvalidRequest("no questions".into()));
176 }
177 match self.model {
178 Some(model) => self.client.evaluate_with_model(&model, state, self.questions).await,
179 None => self.client.evaluate(state, self.questions).await,
180 }
181 }
182
183 fn fail(&mut self, e: Error) {
184 if self.error.is_none() {
185 self.error = Some(e);
186 }
187 }
188}
189
190#[derive(Default)]
192pub struct ChoiceBuilder {
193 options: BTreeMap<String, Option<String>>,
194}
195
196impl ChoiceBuilder {
197 pub fn option(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
199 self.options.insert(name.into(), Some(description.into()));
200 self
201 }
202
203 pub fn option_plain(mut self, name: impl Into<String>) -> Self {
205 self.options.insert(name.into(), None);
206 self
207 }
208
209 pub fn options_plain<K: Into<String>>(mut self, names: impl IntoIterator<Item = K>) -> Self {
211 self.options.extend(names.into_iter().map(|n| (n.into(), None)));
212 self
213 }
214
215 pub fn options<K, V>(mut self, pairs: impl IntoIterator<Item = (K, V)>) -> Self
217 where
218 K: Into<String>,
219 V: Into<String>,
220 {
221 self.options
222 .extend(pairs.into_iter().map(|(k, v)| (k.into(), Some(v.into()))));
223 self
224 }
225
226 pub fn none_of_the_above(self, description: impl Into<String>) -> Self {
229 self.option(NONE_OF_THE_ABOVE, description)
230 }
231}