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}