use std::collections::BTreeMap;
use serde::Serialize;
use serde_json::{Map, Value};
use crate::answer::SystemOneResponse;
use crate::client::Client;
use crate::error::{Error, Result};
use crate::question::{NONE_OF_THE_ABOVE, Question};
enum State {
Empty,
Whole(Value),
Fields(Map<String, Value>),
}
#[must_use = "a request does nothing until `.send().await`"]
pub struct SystemOneRequest<'a> {
client: &'a Client,
model: Option<String>,
state: State,
questions: BTreeMap<String, Question>,
error: Option<Error>,
}
impl<'a> SystemOneRequest<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self {
client,
model: None,
state: State::Empty,
questions: BTreeMap::new(),
error: None,
}
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn state(mut self, state: impl Serialize) -> Self {
match serde_json::to_value(state) {
Ok(v) => self.state = State::Whole(v),
Err(e) => self.fail(Error::RequestSerialization(e)),
}
self
}
pub fn field(mut self, name: impl Into<String>, value: impl Serialize) -> Self {
let value = match serde_json::to_value(value) {
Ok(v) => v,
Err(e) => {
self.fail(Error::RequestSerialization(e));
return self;
}
};
let name = name.into();
self.state = match std::mem::replace(&mut self.state, State::Empty) {
State::Empty => State::Fields(Map::from_iter([(name, value)])),
State::Fields(mut map) => {
map.insert(name, value);
State::Fields(map)
}
State::Whole(Value::Object(mut map)) => {
map.insert(name, value);
State::Fields(map)
}
other @ State::Whole(_) => {
self.fail(Error::InvalidRequest(format!(
"cannot add field `{name}`: state is not an object"
)));
other
}
};
self
}
pub fn question(mut self, id: impl Into<String>, question: Question) -> Self {
let id = id.into();
if self.questions.insert(id.clone(), question).is_some() {
self.fail(Error::InvalidRequest(format!("duplicate question id `{id}`")));
}
self
}
pub fn noul(self, id: impl Into<String>, instructions: impl Into<Value>) -> Self {
self.question(id, Question::noul(instructions))
}
pub fn noul_with_criteria(
self,
id: impl Into<String>,
instructions: impl Into<Value>,
yes: impl Into<String>,
no: impl Into<String>,
) -> Self {
self.question(id, Question::noul_with_criteria(instructions, yes, no))
}
pub fn choice(
mut self,
id: impl Into<String>,
instructions: impl Into<Value>,
options: impl FnOnce(ChoiceBuilder) -> ChoiceBuilder,
) -> Self {
let id = id.into();
let built = options(ChoiceBuilder::default());
if built.options.is_empty() {
self.fail(Error::InvalidRequest(format!("choice `{id}` has no options")));
return self;
}
self.question(
id,
Question::Choice {
instructions: instructions.into(),
criteria: built.options,
},
)
}
pub fn score<L: Into<String>>(
mut self,
id: impl Into<String>,
instructions: impl Into<Value>,
levels: impl IntoIterator<Item = L>,
) -> Self {
let id = id.into();
let levels: Vec<String> = levels.into_iter().map(Into::into).collect();
if levels.len() < 2 {
self.fail(Error::InvalidRequest(format!("score `{id}` needs at least two levels")));
return self;
}
self.question(
id,
Question::Score {
instructions: instructions.into(),
criteria: levels,
},
)
}
pub async fn send(self) -> Result<SystemOneResponse> {
if let Some(e) = self.error {
return Err(e);
}
let state = match self.state {
State::Empty => {
return Err(Error::InvalidRequest(
"no state: call `.state(..)` or `.field(..)`".into(),
));
}
State::Whole(v) => v,
State::Fields(map) => Value::Object(map),
};
if self.questions.is_empty() {
return Err(Error::InvalidRequest("no questions".into()));
}
match self.model {
Some(model) => self.client.evaluate_with_model(&model, state, self.questions).await,
None => self.client.evaluate(state, self.questions).await,
}
}
fn fail(&mut self, e: Error) {
if self.error.is_none() {
self.error = Some(e);
}
}
}
#[derive(Default)]
pub struct ChoiceBuilder {
options: BTreeMap<String, Option<String>>,
}
impl ChoiceBuilder {
pub fn option(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
self.options.insert(name.into(), Some(description.into()));
self
}
pub fn option_plain(mut self, name: impl Into<String>) -> Self {
self.options.insert(name.into(), None);
self
}
pub fn options_plain<K: Into<String>>(mut self, names: impl IntoIterator<Item = K>) -> Self {
self.options.extend(names.into_iter().map(|n| (n.into(), None)));
self
}
pub fn options<K, V>(mut self, pairs: impl IntoIterator<Item = (K, V)>) -> Self
where
K: Into<String>,
V: Into<String>,
{
self.options
.extend(pairs.into_iter().map(|(k, v)| (k.into(), Some(v.into()))));
self
}
pub fn none_of_the_above(self, description: impl Into<String>) -> Self {
self.option(NONE_OF_THE_ABOVE, description)
}
}