Skip to main content

typesafe_systemone/
request.rs

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/// A `POST /v1/systemone` call under construction. Created by [`Client::system_one`].
18///
19/// Set the state once with [`state`](Self::state) (any `Serialize`, typically your own
20/// struct) or build it field by field with [`field`](Self::field), add questions, then
21/// [`send`](Self::send). Errors in the inputs (unserializable value, fields added to a
22/// non-object state, no state, no questions) surface from `send`, so the chain stays clean.
23#[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    /// Model for this call only. Defaults to the client's model.
44    pub fn model(mut self, model: impl Into<String>) -> Self {
45        self.model = Some(model.into());
46        self
47    }
48
49    /// The whole state: a string, or any `Serialize` value (your own struct, a `Vec`, a
50    /// `serde_json::Value`). Replaces anything set before.
51    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    /// One named field of an object state. Call it once per field; a later
60    /// [`state`](Self::state) replaces them all. Fails at `send` if the state was already
61    /// set to something that is not an object.
62    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) | State::Whole(Value::Object(mut map)) => {
74                map.insert(name, value);
75                State::Fields(map)
76            }
77            other @ State::Whole(_) => {
78                self.fail(Error::InvalidRequest(format!(
79                    "cannot add field `{name}`: state is not an object"
80                )));
81                other
82            }
83        };
84        self
85    }
86
87    /// Any question under `id`. Answers come back under the same id.
88    pub fn question(mut self, id: impl Into<String>, question: Question) -> Self {
89        let id = id.into();
90        if self.questions.insert(id.clone(), question).is_some() {
91            self.fail(Error::InvalidRequest(format!("duplicate question id `{id}`")));
92        }
93        self
94    }
95
96    /// Yes/no question.
97    pub fn noul(self, id: impl Into<String>, instructions: impl Into<Value>) -> Self {
98        self.question(id, Question::noul(instructions))
99    }
100
101    /// Yes/no question with descriptions of what yes and no mean.
102    pub fn noul_with_criteria(
103        self,
104        id: impl Into<String>,
105        instructions: impl Into<Value>,
106        yes: impl Into<String>,
107        no: impl Into<String>,
108    ) -> Self {
109        self.question(id, Question::noul_with_criteria(instructions, yes, no))
110    }
111
112    /// Pick one option; build the option set in the closure. Repeating an option name is an
113    /// error at `send`, as is an empty set or more than [`MAX_CHOICE_OPTIONS`](crate::MAX_CHOICE_OPTIONS) options.
114    pub fn choice(
115        mut self,
116        id: impl Into<String>,
117        instructions: impl Into<Value>,
118        options: impl FnOnce(ChoiceBuilder) -> ChoiceBuilder,
119    ) -> Self {
120        let id = id.into();
121        let built = options(ChoiceBuilder::default());
122        if let Some(dup) = built.duplicates.first() {
123            self.fail(Error::InvalidRequest(format!("choice `{id}` repeats option `{dup}`")));
124            return self;
125        }
126        self.question(id, Question::choice(instructions, built.options))
127    }
128
129    /// Rate along `levels`, lowest first. Fewer than two levels is an error at `send`.
130    pub fn score<L: Into<String>>(
131        self,
132        id: impl Into<String>,
133        instructions: impl Into<Value>,
134        levels: impl IntoIterator<Item = L>,
135    ) -> Self {
136        self.question(id, Question::score(instructions, levels))
137    }
138
139    /// Send the request.
140    ///
141    /// # Errors
142    ///
143    /// [`Error::InvalidRequest`] for a request that is not sendable as built: no state, no
144    /// questions, a Choice without options or with more than [`MAX_CHOICE_OPTIONS`](crate::MAX_CHOICE_OPTIONS), a
145    /// repeated option name, a Score with fewer than two levels, a duplicate question id,
146    /// or a field added to a non-object state. [`Error::RequestSerialization`]
147    /// if a `state`/`field` value failed to serialise. Otherwise as [`Client::evaluate`].
148    pub async fn send(self) -> Result<SystemOneResponse> {
149        if let Some(e) = self.error {
150            return Err(e);
151        }
152        let state = match self.state {
153            State::Empty => {
154                return Err(Error::InvalidRequest(
155                    "no state: call `.state(..)` or `.field(..)`".into(),
156                ));
157            }
158            State::Whole(v) => v,
159            State::Fields(map) => Value::Object(map),
160        };
161        if self.questions.is_empty() {
162            return Err(Error::InvalidRequest("no questions".into()));
163        }
164        if let Some((id, problem)) = self
165            .questions
166            .iter()
167            .find_map(|(id, q)| q.structural_problem().map(|p| (id, p)))
168        {
169            return Err(Error::InvalidRequest(format!("question `{id}` {problem}")));
170        }
171        match self.model {
172            Some(model) => self.client.evaluate_with_model(&model, state, self.questions).await,
173            None => self.client.evaluate(state, self.questions).await,
174        }
175    }
176
177    fn fail(&mut self, e: Error) {
178        if self.error.is_none() {
179            self.error = Some(e);
180        }
181    }
182}
183
184/// Option set of a Choice, built inside [`SystemOneRequest::choice`].
185#[derive(Default)]
186#[must_use = "return the builder from the `choice` closure"]
187pub struct ChoiceBuilder {
188    options: BTreeMap<String, Option<String>>,
189    duplicates: Vec<String>,
190}
191
192impl ChoiceBuilder {
193    fn insert(&mut self, name: String, description: Option<String>) {
194        if self.options.insert(name.clone(), description).is_some() {
195            self.duplicates.push(name);
196        }
197    }
198
199    /// An option with a rubric description.
200    pub fn option(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
201        self.insert(name.into(), Some(description.into()));
202        self
203    }
204
205    /// An option that needs no description.
206    pub fn option_plain(mut self, name: impl Into<String>) -> Self {
207        self.insert(name.into(), None);
208        self
209    }
210
211    /// Many undescribed options at once.
212    pub fn options_plain<K: Into<String>>(mut self, names: impl IntoIterator<Item = K>) -> Self {
213        for name in names {
214            self.insert(name.into(), None);
215        }
216        self
217    }
218
219    /// Many described options at once.
220    pub fn options<K, V>(mut self, pairs: impl IntoIterator<Item = (K, V)>) -> Self
221    where
222        K: Into<String>,
223        V: Into<String>,
224    {
225        for (name, description) in pairs {
226            self.insert(name.into(), Some(description.into()));
227        }
228        self
229    }
230
231    /// The abstain option (`none_of_the_above`). A Choice always picks *something*; this is
232    /// how the model says nothing in the list fits.
233    pub fn none_of_the_above(self, description: impl Into<String>) -> Self {
234        self.option(NONE_OF_THE_ABOVE, description)
235    }
236}