1use std::{borrow::Cow, fmt, marker::PhantomData, time::Duration};
14
15use bytes::Bytes;
16use http::Method;
17use serde::Serialize;
18
19use crate::{
20 client::Client,
21 codec::{self, EncodeError},
22 config::ZERO_TIMEOUT,
23 de::{self, AnswerSet},
24 error::Error,
25 question::{PreparedQuestions, upsert},
26 response::{Answers, SystemOneResponse},
27 retry::{self, RetryPolicy},
28 text,
29 transport::{self, Exchange, HttpService},
30};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub(crate) enum Deadline {
35 #[default]
37 Client,
38 After(Duration),
40 Never,
42}
43
44impl Deadline {
45 pub(crate) fn resolve(self, client: Option<Duration>) -> Result<Option<Duration>, Error> {
52 match self {
53 Self::Client => Ok(client),
54 Self::After(timeout) if timeout.is_zero() => Err(Error::invalid_request(ZERO_TIMEOUT)),
55 Self::After(timeout) => Ok(Some(timeout)),
56 Self::Never => Ok(None),
57 }
58 }
59}
60
61#[derive(Clone, Default)]
63pub(crate) struct CallHeaders<'a>(Vec<(Cow<'a, str>, Cow<'a, str>)>);
64
65impl<'a> CallHeaders<'a> {
66 pub(crate) fn push(&mut self, name: Cow<'a, str>, value: Cow<'a, str>) {
67 self.0.push((name, value));
68 }
69
70 pub(crate) fn parse(
76 &self,
77 with_body: bool,
78 ) -> Result<Vec<(http::HeaderName, http::HeaderValue)>, Error> {
79 transport::call_headers(
80 self.0.iter().map(|(name, value)| (name.as_ref(), value.as_ref())),
81 with_body,
82 )
83 }
84}
85
86impl fmt::Debug for CallHeaders<'_> {
87 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89 formatter.debug_list().entries(self.0.iter().map(|(name, _)| name)).finish()
90 }
91}
92
93const STATE: &str = "state";
96const MODEL: &str = "model";
97const QUESTIONS: &str = "questions";
98
99type ExtraMember<'a> = (Cow<'a, str>, Result<Vec<u8>, EncodeError>);
102
103#[must_use = "a request does nothing until it is sent"]
113pub struct SystemOne<'a, S, T: ?Sized, A = Answers> {
114 client: &'a Client<S>,
115 state: &'a T,
116 questions: &'a PreparedQuestions,
117 model: Option<Cow<'a, str>>,
118 deadline: Deadline,
119 headers: CallHeaders<'a>,
120 extra: Vec<ExtraMember<'a>>,
121 retry: Option<RetryPolicy>,
123 answers: PhantomData<fn() -> A>,
126}
127
128impl<'a, S, T> SystemOne<'a, S, T>
129where
130 T: ?Sized,
131{
132 pub(crate) fn new(
133 client: &'a Client<S>,
134 state: &'a T,
135 questions: &'a PreparedQuestions,
136 ) -> Self {
137 Self {
138 client,
139 state,
140 questions,
141 model: None,
142 deadline: Deadline::Client,
143 headers: CallHeaders::default(),
144 extra: Vec::new(),
145 retry: None,
146 answers: PhantomData,
147 }
148 }
149}
150
151impl<'a, S, T, A> SystemOne<'a, S, T, A>
152where
153 T: ?Sized,
154{
155 pub fn model(mut self, model: impl Into<Cow<'a, str>>) -> Self {
157 self.model = Some(model.into());
158 self
159 }
160
161 pub fn timeout(mut self, timeout: Duration) -> Self {
163 self.deadline = Deadline::After(timeout);
164 self
165 }
166
167 pub fn no_timeout(mut self) -> Self {
169 self.deadline = Deadline::Never;
170 self
171 }
172
173 pub fn header(mut self, name: impl Into<Cow<'a, str>>, value: impl Into<Cow<'a, str>>) -> Self {
181 self.headers.push(name.into(), value.into());
182 self
183 }
184
185 pub fn retry(mut self, policy: RetryPolicy) -> Self {
188 self.retry = Some(policy);
189 self
190 }
191
192 pub fn extra_body<V>(mut self, name: impl Into<Cow<'a, str>>, value: &V) -> Self
201 where
202 V: Serialize + ?Sized,
203 {
204 let mut encoded = Vec::new();
205 let value = codec::encode_into(&mut encoded, value).map(|()| encoded);
206 upsert(&mut self.extra, name.into(), value);
207 self
208 }
209
210 pub fn typed<B>(self) -> SystemOne<'a, S, T, B>
214 where
215 B: AnswerSet,
216 {
217 SystemOne {
218 client: self.client,
219 state: self.state,
220 questions: self.questions,
221 model: self.model,
222 deadline: self.deadline,
223 headers: self.headers,
224 extra: self.extra,
225 retry: self.retry,
226 answers: PhantomData,
227 }
228 }
229}
230
231impl<S, T, A> SystemOne<'_, S, T, A>
232where
233 S: HttpService,
234 T: Serialize + ?Sized,
235 A: AnswerSet,
236{
237 pub async fn send(self) -> Result<SystemOneResponse<A>, Error> {
259 let shared = self.client.shared();
260 let deadline = self.deadline.resolve(shared.config.timeout())?;
261 let headers = self.headers.parse(true)?;
262 let mut body = self.encode()?;
263
264 let uri = shared.config.endpoints().system_one();
265 let exchange = Exchange {
266 method: &Method::POST,
267 uri,
268 base_headers: &shared.post_headers,
269 call_headers: &headers,
270 deadline,
271 max_response_bytes: shared.config.max_response_bytes(),
272 };
273 let policy = self.retry.as_ref().unwrap_or(&shared.retry);
274 let retain = policy.can_retry();
278 let asked =
279 de::AnswerContext::new(self.questions.len()).with_levels(self.questions.max_levels());
280 retry::run(policy, &Method::POST, uri, move |retry| {
281 let body = if retain { body.clone() } else { std::mem::take(&mut body) };
282 async move {
283 let (status, headers, body) =
284 transport::attempt(&shared.service, exchange, retry, Some(body)).await?;
285 de::decode_system_one_with(body, status, headers, asked, Some((&Method::POST, uri)))
288 }
289 })
290 .await
291 }
292
293 fn encode(&self) -> Result<Bytes, Error> {
296 for (name, value) in &self.extra {
297 if let Err(error) = value {
298 let part = format!("the extra member {}", text::quoted(name));
299 return Err(encode_failure(&part, error));
300 }
301 }
302 let extra = |wanted: &str| {
303 self.extra.iter().find_map(|(name, value)| match value {
304 Ok(bytes) if name == wanted => Some(bytes.as_slice()),
305 _ => None,
306 })
307 };
308
309 let mut state_is_json_content = true;
310 let body = codec::encode_body(|buffer| {
311 buffer.extend_from_slice(br#"{"state":"#);
312 match extra(STATE) {
313 Some(bytes) => buffer.extend_from_slice(bytes),
314 None => {
315 let start = buffer.len();
316 codec::encode_into(buffer, self.state)?;
317 if !matches!(buffer.get(start), Some(b'"' | b'{' | b'[')) {
321 state_is_json_content = false;
322 return Ok(());
323 }
324 }
325 }
326 buffer.extend_from_slice(br#","model":"#);
327 match (extra(MODEL), &self.model) {
328 (Some(bytes), _) => buffer.extend_from_slice(bytes),
329 (None, Some(model)) => codec::write_json_string(buffer, model),
330 (None, None) => buffer.extend_from_slice(&self.client.shared().model_json),
331 }
332 buffer.extend_from_slice(br#","questions":"#);
333 buffer.extend_from_slice(extra(QUESTIONS).unwrap_or(self.questions.as_bytes()));
334 for (name, value) in &self.extra {
335 if let (Ok(bytes), false) = (value, [STATE, MODEL, QUESTIONS].contains(&&**name)) {
336 buffer.push(b',');
337 codec::write_json_string(buffer, name);
338 buffer.push(b':');
339 buffer.extend_from_slice(bytes);
340 }
341 }
342 buffer.push(b'}');
343 Ok(())
344 });
345 let body = body.map_err(|error| encode_failure("the state", &error))?;
346 if !state_is_json_content {
347 return Err(Error::invalid_request(
348 "The state must be a JSON string, object or array; \
349 it encoded as a number, a boolean or null.",
350 ));
351 }
352 Ok(body)
353 }
354}
355
356fn encode_failure(part: &str, error: &EncodeError) -> Error {
358 Error::invalid_request(format!(
361 "The request body could not be encoded as JSON: {part}: {}",
362 text::bounded(&error.message(), text::MAX_MESSAGE_CHARS)
363 ))
364}
365
366impl<S, T, A> fmt::Debug for SystemOne<'_, S, T, A>
367where
368 T: ?Sized,
369{
370 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
374 let mut shown = formatter.debug_struct("SystemOne");
375 shown
376 .field("questions", &self.questions.len())
377 .field("model", &self.model)
378 .field("deadline", &self.deadline)
379 .field("headers", &self.headers)
380 .field("extra_body", &self.extra.iter().map(|(name, _)| name).collect::<Vec<_>>());
381 if let Some(retry) = &self.retry {
382 shown.field("retry", retry);
383 }
384 shown.finish_non_exhaustive()
385 }
386}
387
388#[cfg(test)]
389#[path = "request_tests.rs"]
390mod tests;