Skip to main content

typed_openapi/
client.rs

1//! A typed client over a document, and the two steps every call is made of.
2//!
3//! [`Client`] holds one document and hands out [`Call`]s by `operationId`.
4//! `Call` knows what its answer deserialises into, so sending is one expression
5//! in each flavour and a caller never names a response type twice.
6//!
7//! Nothing here is specific to one API. A generated wrapper is the typed door
8//! onto [`Client::call`]: it names the `operationId`, names the arguments under
9//! the document's own names, and holds no path template, no query rule and no
10//! encoder.
11
12use std::fmt;
13use std::marker::PhantomData;
14
15use http::{StatusCode, Uri};
16use serde::Serialize;
17use serde::de::DeserializeOwned;
18use thiserror::Error;
19
20use crate::model::{Document, DocumentError, DriftError, Operation};
21use crate::request::{Invocation, ValueError};
22use crate::transport::{AsyncClient, HttpRequest, HttpResponse, SyncClient};
23use crate::values::Values;
24
25/// A response the document promises no body for.
26///
27/// It deserialises from anything, including an empty body, so an operation that
28/// answers `201` with nothing is not a special case at the call site.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct NoContent;
31
32impl<'de> serde::Deserialize<'de> for NoContent {
33    fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
34        serde::de::IgnoredAny::deserialize(de).map(|_| Self)
35    }
36}
37
38/// Anything that stops a call short of a typed answer.
39#[derive(Debug, Error)]
40pub enum Error {
41    #[error("the document does not load: {0}")]
42    Document(#[from] DocumentError),
43    #[error(transparent)]
44    Drift(#[from] DriftError),
45    #[error(transparent)]
46    Values(#[from] ValueError),
47    #[error("cannot build the request: {0}")]
48    Request(#[from] http::Error),
49    #[error("cannot serialise the request body: {0}")]
50    Encode(#[source] serde_json::Error),
51    /// The client refused the request or never got an answer, carrying the
52    /// error the client itself returned.
53    ///
54    /// Boxed, so this type does not grow a parameter for the client. Whether
55    /// the request left is a reading only an adapter can make, so it belongs
56    /// above the seam, and a failure nobody has classified is one that may have
57    /// arrived. The concrete error is here to be read: `downcast_ref` recovers
58    /// `C::Error`, and [`Call::request`] hands back the bytes for a caller who
59    /// would rather send them through the client's own `send` and box nothing.
60    /// `docs/client.md` shows both.
61    #[error("transport: {0}")]
62    Transport(#[source] Box<dyn std::error::Error + Send + Sync>),
63    #[error("{status}: {body}")]
64    Status { status: StatusCode, body: String },
65    #[error("the response is not the shape the document promises: {source}")]
66    Decode {
67        #[source]
68        source: serde_json::Error,
69        raw: String,
70    },
71}
72
73/// One document, one server, and a typed way to call anything in it.
74#[derive(Debug)]
75pub struct Client {
76    document: Document,
77    base: Uri,
78}
79
80impl Client {
81    /// The one constructor: a reduction that is already in hand.
82    ///
83    /// A shipped binary gets that reduction from `Document::from_blob`, which
84    /// is all it can do — the reader is a cargo feature and the binary does not
85    /// enable it. A bless step, which does, writes
86    /// `Client::over(Document::load(document, overlay)?)` and handles the load
87    /// failure under its own type, so no shape on this page depends on which
88    /// build a reader is looking at.
89    pub fn over(document: Document) -> Result<Self, Error> {
90        let base = document.base().clone();
91        Ok(Self { document, base })
92    }
93
94    /// Point at a different server than the document's first one.
95    #[must_use]
96    pub fn with_base(mut self, base: Uri) -> Self {
97        self.base = base;
98        self
99    }
100
101    #[must_use]
102    pub fn base(&self) -> &Uri {
103        &self.base
104    }
105
106    /// The document itself, for a caller that wants the command tree or an
107    /// operation no wrapper covers.
108    #[must_use]
109    pub fn document(&self) -> &Document {
110        &self.document
111    }
112
113    /// An operation and the values for it.
114    ///
115    /// The operation is a reference into a document rather than a name to look
116    /// up, so there is no "no such operation" to report here: the caller
117    /// already holds one.
118    pub fn call<'a, T>(&'a self, op: &'a Operation, values: Values) -> Result<Call<'a, T>, Error> {
119        Ok(Call {
120            invocation: Invocation::new(op, values)?,
121            base: &self.base,
122            response: PhantomData,
123        })
124    }
125}
126
127/// A request body that does not deserialise into the type the document
128/// describes for it.
129///
130/// This is what a `--json-body` file gets checked against. The source is
131/// serde's own message, which names the field that is missing or ill-typed.
132#[derive(Debug, Error)]
133#[error("{op}: the request body does not fit the schema the document declares")]
134pub struct BodyError {
135    pub op: String,
136    #[source]
137    pub source: serde_json::Error,
138}
139
140/// Does `body` deserialise into `T`?
141///
142/// Generated code calls this with the type its own wrapper takes for the
143/// operation, which is how a body assembled on a command line is held to the
144/// same schema as a body passed from Rust — before a request is built.
145pub fn fits<T: DeserializeOwned>(op: &str, body: &serde_json::Value) -> Result<(), BodyError> {
146    serde_json::from_value::<T>(body.clone())
147        .map(drop)
148        .map_err(|source| BodyError {
149            op: op.to_owned(),
150            source,
151        })
152}
153
154/// One request, built and waiting, that knows what its answer deserialises into.
155pub struct Call<'a, T> {
156    invocation: Invocation<'a>,
157    base: &'a Uri,
158    response: PhantomData<fn() -> T>,
159}
160
161impl<T> fmt::Debug for Call<'_, T> {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        f.debug_struct("Call")
164            .field("operation", &self.invocation.operation().id())
165            .field("response", &std::any::type_name::<T>())
166            .finish()
167    }
168}
169
170impl<T: DeserializeOwned> Call<'_, T> {
171    /// What the document says about the operation this call was built from:
172    /// whether it writes, and which gates it stands behind.
173    ///
174    /// A Rust caller is trusted and is stopped by nothing here. A caller that
175    /// wants the same gate a CLI has hands this to [`Plan::decide`], which is
176    /// what the `finalize-voucher` verb in the example does.
177    ///
178    /// [`Plan::decide`]: crate::Plan::decide
179    #[must_use]
180    pub fn operation(&self) -> &Operation {
181        self.invocation.operation()
182    }
183
184    /// The exact bytes that go on the wire. A CLI prints this for a dry run; a
185    /// test asserts on it.
186    pub fn request(&self) -> Result<HttpRequest, Error> {
187        Ok(self.invocation.request(self.base)?)
188    }
189
190    /// Send it and deserialise the answer.
191    pub fn send<C: SyncClient>(&self, client: &C) -> Result<T, Error> {
192        parse(&client.send(self.request()?).map_err(transport)?)
193    }
194
195    /// The same, inside a runtime.
196    pub async fn send_async<C: AsyncClient>(&self, client: &C) -> Result<T, Error> {
197        parse(&client.send(self.request()?).await.map_err(transport)?)
198    }
199}
200
201fn transport<E: std::error::Error + Send + Sync + 'static>(error: E) -> Error {
202    Error::Transport(Box::new(error))
203}
204
205/// Status first, then the body — never deserialise the success type out of an
206/// error response.
207fn parse<T: DeserializeOwned>(response: &HttpResponse) -> Result<T, Error> {
208    let body = response.body();
209    if !response.status().is_success() {
210        return Err(Error::Status {
211            status: response.status(),
212            body: String::from_utf8_lossy(body).into_owned(),
213        });
214    }
215    // An empty body is how "no content" arrives; `NoContent` accepts `null`.
216    let bytes = if body.is_empty() { b"null" } else { &body[..] };
217    serde_json::from_slice(bytes).map_err(|source| Error::Decode {
218        source,
219        raw: String::from_utf8_lossy(body).into_owned(),
220    })
221}
222
223/// The request body of a typed wrapper, as JSON.
224pub fn to_json<T: Serialize>(value: &T) -> Result<serde_json::Value, Error> {
225    serde_json::to_value(value).map_err(Error::Encode)
226}