Skip to main content

typesafe_sdk/
request.rs

1//! Building one request and sending it.
2//!
3//! The body is assembled by splicing fragments of finished JSON - the encoded
4//! state, the pre-escaped model name, the prepared questions - rather than by
5//! building a value and encoding it, so a call costs one pass over the state
6//! and nothing over anything else.
7//!
8//! The builder ends in a plain `async fn`, not an `IntoFuture`: on the pinned
9//! toolchain an unboxed `IntoFuture` needs an unstable associated type, so the
10//! choice is between a boxed future on every call and a method call the caller
11//! writes. The method call is free.
12
13use 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/// The deadline a call asked for.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub(crate) enum Deadline {
35    /// The client's deadline.
36    #[default]
37    Client,
38    /// This deadline instead.
39    After(Duration),
40    /// No deadline.
41    Never,
42}
43
44impl Deadline {
45    /// The deadline one attempt gets, given the client's.
46    ///
47    /// # Errors
48    ///
49    /// Returns an [`ErrorKind::InvalidRequest`](crate::ErrorKind::InvalidRequest)
50    /// error, with the Python SDK's message, for a deadline of zero.
51    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/// The headers one call adds, as given; they are checked when it is sent.
62#[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    /// Parses them, dropping the ones the SDK owns.
71    ///
72    /// # Errors
73    ///
74    /// See [`transport::call_headers`].
75    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    /// The names only: a header value is where a caller puts a token.
88    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89        formatter.debug_list().entries(self.0.iter().map(|(name, _)| name)).finish()
90    }
91}
92
93/// The built-in members of a System One body, which an `extra_body` entry of
94/// the same name replaces.
95const STATE: &str = "state";
96const MODEL: &str = "model";
97const QUESTIONS: &str = "questions";
98
99/// An extra top-level member of the body: its name, and its value encoded
100/// when it was added - or the encoding error, kept for `send` to report.
101type ExtraMember<'a> = (Cow<'a, str>, Result<Vec<u8>, EncodeError>);
102
103/// A System One request, ready to be configured and sent.
104///
105/// Made by [`Client::system_one`]. `A` is what the answers decode into:
106/// [`Answers`], a lookup by question name, unless [`typed`](Self::typed)
107/// names a type of the caller's own.
108///
109/// Nothing is checked or encoded until [`send`](Self::send): the methods
110/// never fail, and a header or a body member that cannot be sent is reported
111/// by `send`.
112#[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    /// This call's own policy, in place of the client's.
122    retry: Option<RetryPolicy>,
123    /// `fn() -> A` rather than `A`: the request holds no `A`, so it must not
124    /// inherit `A`'s auto traits or drop behaviour.
125    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    /// The model to ask, instead of the client's default.
156    pub fn model(mut self, model: impl Into<Cow<'a, str>>) -> Self {
157        self.model = Some(model.into());
158        self
159    }
160
161    /// The deadline of each attempt of this call, instead of the client's.
162    pub fn timeout(mut self, timeout: Duration) -> Self {
163        self.deadline = Deadline::After(timeout);
164        self
165    }
166
167    /// No deadline on any attempt of this call.
168    pub fn no_timeout(mut self) -> Self {
169        self.deadline = Deadline::Never;
170        self
171    }
172
173    /// A header for this call only. It replaces a client default of the same
174    /// name; the SDK's own headers still win over it, as they do over a
175    /// default, and a later header of the same name replaces an earlier one.
176    /// The headers the SDK or its transport owns are dropped without an
177    /// error, as they are from a default; see
178    /// [`ClientBuilder::default_header`](crate::ClientBuilder::default_header)
179    /// for the list, and for what `Host` does.
180    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    /// The retry policy of this call, in place of the client's; the client
186    /// and its other calls keep theirs.
187    pub fn retry(mut self, policy: RetryPolicy) -> Self {
188        self.retry = Some(policy);
189        self
190    }
191
192    /// A top-level member of the request body beside `state`, `model` and
193    /// `questions`, for a parameter the API has and this SDK does not model.
194    ///
195    /// Merging is last-write-wins, as in the Python SDK: a later member of
196    /// the same name replaces an earlier one, and a member named `state`,
197    /// `model` or `questions` replaces that built-in one in place. The value
198    /// is encoded now; a value that cannot be encoded fails the call when it
199    /// is sent.
200    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    /// Decodes the answers into `B` instead: a struct with one field per
211    /// question, for instance, which reads each answer straight into its
212    /// field.
213    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    /// Sends the request and decodes the answer, retrying a failure as the
238    /// call's retry policy, or else the client's, says.
239    ///
240    /// The body is encoded once: every attempt sends the same bytes. When
241    /// retrying stops, the error is the one the last attempt failed with.
242    ///
243    /// # Errors
244    ///
245    /// - [`ErrorKind::InvalidRequest`](crate::ErrorKind::InvalidRequest),
246    ///   before anything is sent: a header that is not a valid header, a
247    ///   deadline of zero, a `state` that is not a JSON string, object or
248    ///   array, or a value that cannot be encoded as JSON.
249    /// - [`ErrorKind::Api`](crate::ErrorKind::Api) for a status outside 2xx.
250    /// - [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) when the attempt
251    ///   ran past its deadline.
252    /// - [`ErrorKind::Connection`](crate::ErrorKind::Connection) when no
253    ///   response could be read: the connection failed or broke.
254    /// - [`ErrorKind::ResponseTooLarge`](crate::ErrorKind::ResponseTooLarge)
255    ///   when a success response's body was larger than the client's limit.
256    /// - [`ErrorKind::ResponseValidation`](crate::ErrorKind::ResponseValidation)
257    ///   when the body does not decode into `A`.
258    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        // Every attempt after the first shares these bytes; the first clone
275        // allocates the reference count they are shared through, so a call
276        // that cannot retry hands its one attempt the body itself.
277        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                // Decoding is part of the attempt, so a retry predicate sees a
286                // response that did not decode, as the Python SDK's does.
287                de::decode_system_one_with(body, status, headers, asked, Some((&Method::POST, uri)))
288            }
289        })
290        .await
291    }
292
293    /// The body: `{"state":<state>,"model":<model>,"questions":<questions>}` and the extra members,
294    /// spliced out of finished JSON.
295    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                    // The API takes text, an object or an array. The first
318                    // byte of the encoding says which it is, so the check
319                    // costs nothing whatever the state's size.
320                    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
356/// The error for a part of the body that could not be encoded.
357fn encode_failure(part: &str, error: &EncodeError) -> Error {
358    // The encoder's message can be the caller's own `Serialize` error, of any
359    // length and content.
360    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    /// The request's settings: no state, no header value and no body member,
371    /// which are the caller's data. A retry policy is shown when the call has
372    /// one of its own.
373    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;