Skip to main content

cerno_sdk/
builder.rs

1//! Assembling a request.
2
3use crate::{Client, Error, response::Answers};
4use cerno_types::{
5    Calibration, ChoiceSpec, LevelSpec, Question, QuestionKind, ScoreSpec, SystemOneRequest,
6};
7
8/// A score rubric: either a plain count or the text of each level.
9///
10/// The conversions exist so `.score("sev", "How bad?", 5)` and
11/// `.score("sev", "How bad?", ["low", "high"])` both read naturally at the call site.
12pub struct Levels(pub(crate) LevelSpec);
13
14impl From<u8> for Levels {
15    fn from(count: u8) -> Self {
16        Self::count(count.into())
17    }
18}
19
20impl Levels {
21    /// A rubric of `count` generated levels. `From<u8>` covers a literal at the call site; this
22    /// takes a count read from somewhere else, so a value too large for any rubric still reaches
23    /// the service and is refused there with the bounds, rather than wrapping on the way.
24    pub fn count(count: u32) -> Self {
25        Self(LevelSpec::Count(count))
26    }
27
28    /// A rubric from level texts, lowest first.
29    pub fn labels<S: AsRef<str>>(labels: impl IntoIterator<Item = S>) -> Self {
30        Self(LevelSpec::Labels(collect(labels)))
31    }
32}
33
34// Implemented for concrete container types rather than a blanket `IntoIterator`: a blanket impl
35// overlaps `From<u8>`, because nothing stops a future `u8: IntoIterator`. These cover the shapes
36// that actually appear at a call site, and `Levels::labels` covers the rest.
37macro_rules! levels_from_labels {
38    ($($t:ty),* $(,)?) => {
39        $(impl From<$t> for Levels {
40            fn from(labels: $t) -> Self {
41                Self::labels(labels)
42            }
43        })*
44    };
45}
46
47levels_from_labels!(Vec<String>, Vec<&str>, &[&str], &[String]);
48
49impl<const N: usize> From<[&str; N]> for Levels {
50    fn from(labels: [&str; N]) -> Self {
51        Self::labels(labels)
52    }
53}
54
55impl<const N: usize> From<[String; N]> for Levels {
56    fn from(labels: [String; N]) -> Self {
57        Self::labels(labels)
58    }
59}
60
61/// A request under construction.
62#[must_use = "a builder does nothing until send() is called"]
63pub struct SystemOne<'a> {
64    client: &'a Client,
65    request: SystemOneRequest,
66}
67
68impl<'a> SystemOne<'a> {
69    pub(crate) fn new(client: &'a Client, state: String) -> Self {
70        Self {
71            client,
72            request: SystemOneRequest {
73                state,
74                model: None,
75                calibration: None,
76                questions: Vec::new(),
77            },
78        }
79    }
80
81    /// Name a model or a configured alias. The service's default applies otherwise.
82    pub fn model(mut self, model: impl Into<String>) -> Self {
83        self.request.model = Some(model.into());
84        self
85    }
86
87    /// Scale the label logits before they are normalised. Above 1 flattens, below 1 sharpens.
88    pub fn calibration(mut self, temperature: f64) -> Self {
89        self.request.calibration = Some(Calibration { temperature });
90        self
91    }
92
93    /// How likely the answer to `question` is yes.
94    pub fn noul(self, id: impl Into<String>, question: impl Into<String>) -> Self {
95        self.push(id, QuestionKind::Noul(question.into()))
96    }
97
98    /// One of `options`.
99    pub fn choice<S: AsRef<str>>(
100        self,
101        id: impl Into<String>,
102        question: impl Into<String>,
103        options: impl IntoIterator<Item = S>,
104    ) -> Self {
105        let spec = ChoiceSpec {
106            question: Some(question.into()),
107            options: collect(options),
108        };
109        self.push(id, QuestionKind::Choice(spec))
110    }
111
112    /// One of `options`, where the options speak for themselves.
113    pub fn choice_of<S: AsRef<str>>(
114        self,
115        id: impl Into<String>,
116        options: impl IntoIterator<Item = S>,
117    ) -> Self {
118        let spec = ChoiceSpec {
119            question: None,
120            options: collect(options),
121        };
122        self.push(id, QuestionKind::Choice(spec))
123    }
124
125    /// A position on a rubric: `5` for five generated levels, or a list of level texts.
126    pub fn score(
127        self,
128        id: impl Into<String>,
129        question: impl Into<String>,
130        levels: impl Into<Levels>,
131    ) -> Self {
132        let spec = ScoreSpec {
133            question: Some(question.into()),
134            levels: levels.into().0,
135        };
136        self.push(id, QuestionKind::Score(spec))
137    }
138
139    /// The request as it will be sent. Useful for logging and for testing a builder chain
140    /// without a server.
141    pub fn body(&self) -> &SystemOneRequest {
142        &self.request
143    }
144
145    pub async fn send(self) -> Result<Answers, Error> {
146        let response = self.client.post_systemone(&self.request).await?;
147        Ok(Answers::new(response))
148    }
149
150    fn push(mut self, id: impl Into<String>, kind: QuestionKind) -> Self {
151        self.request.questions.push(Question {
152            id: id.into(),
153            kind,
154        });
155        self
156    }
157}
158
159fn collect<S: AsRef<str>>(items: impl IntoIterator<Item = S>) -> Vec<String> {
160    items.into_iter().map(|s| s.as_ref().to_string()).collect()
161}