Skip to content

Commit be93e43

Browse files
author
lif
committed
Generate a Client method for Dropshot websocket channels
rel: oxidecomputer/dropshot#403 Generated methods return a `WebsocketReactants` type that at present can only be unwrapped into its inner `http::Request` and `tokio::net::TcpStream` for the purpose of implementing against the raw websocket connection, but may later be extended as a generic to allow higher-level channel message definitions (rel: oxidecomputer/dropshot#429) The returned `Request` is an HTTP request containing the client's part of the handshake for establishing a Websocket, and the `TcpStream` is a raw TCP (non-Web-) socket. The consumer of the raw `into_request_and_tcp_stream` interface is expected to send the HTTP request over the TCP socket, i.e. by providing them to a websocket implementation such as `tokio_tungstenite::client_async(Request, TcpStream)`.
1 parent 5aecfd0 commit be93e43

23 files changed

+1219
-70
lines changed

CHANGELOG.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ https://github.com/oxidecomputer/progenitor/compare/v0.1.1\...HEAD[Full list of
2020
* Derive `Debug` for `Client` and builders for the various operations (#145)
2121
* Builders for `struct` types (#171)
2222
* Add a prelude that include the `Client` and any extension traits (#176)
23+
* Added `Error::IoError` variant for channel connection failures (breaks `match` exhaustivity)
2324

2425
== 0.1.1 (released 2022-05-13)
2526

Cargo.lock

+42
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

progenitor-client/Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ description = "An OpenAPI client generator - client support"
99
[dependencies]
1010
bytes = "1.2.1"
1111
futures-core = "0.3.23"
12+
http = "0.2"
1213
percent-encoding = "2.1"
1314
reqwest = { version = "0.11", default-features = false, features = ["json", "stream"] }
1415
serde = "1.0"
1516
serde_json = "1.0"
1617
serde_urlencoded = "0.7.1"
18+
19+
# deps for websocket support
20+
base64 = "0.13"
21+
rand = "0.8"
22+
# reqwest wraps hyper, but does not expose a read/write socket for us to work with,
23+
# so we must make direct socket connections.
24+
tokio = { version = "1.0", features = ["net"] }

progenitor-client/src/progenitor_client.rs

+82
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! Support code for generated clients.
66
77
use std::ops::{Deref, DerefMut};
8+
use std::str::FromStr;
89

910
use bytes::Bytes;
1011
use futures_core::Stream;
@@ -14,6 +15,9 @@ use serde::{de::DeserializeOwned, Serialize};
1415
type InnerByteStream =
1516
std::pin::Pin<Box<dyn Stream<Item = reqwest::Result<Bytes>> + Send + Sync>>;
1617

18+
// used for Websockets requests
19+
type HttpBareRequest = http::Request<()>;
20+
1721
/// Untyped byte stream used for both success and error responses.
1822
pub struct ByteStream(InnerByteStream);
1923

@@ -195,6 +199,65 @@ impl<T: std::fmt::Debug> std::fmt::Debug for ResponseValue<T> {
195199
}
196200
}
197201

202+
/// Value returned by generated client methods for Websocket channels.
203+
///
204+
/// Currently, the only interface available for Dropshot websocket channels is
205+
/// providing their raw, unmodeled form (that is, Dropshot does not yet define
206+
/// higher-level constructs for modeling the structure of Websocket messages
207+
/// themselves).
208+
///
209+
/// The user is responsible for passing it to a websocket implementation, i.e.:
210+
/// ```ignore
211+
/// let (request, tcp_stream) = my_progenitor_client
212+
/// .my_websocket_channel()
213+
/// .answer(42)
214+
/// .send()
215+
/// .await?
216+
/// .into_request_and_tcp_stream();
217+
/// let ws_client = tokio_tungstenite::client_async(request, tcp_stream).await?;
218+
/// ```
219+
///
220+
/// (As no request has been *made*, returning a [ResponseValue] would be inappropriate.)
221+
pub struct WebsocketReactants {
222+
request: HttpBareRequest,
223+
tcp_stream: tokio::net::TcpStream,
224+
}
225+
226+
impl WebsocketReactants {
227+
pub fn new<T>(
228+
rqw: reqwest::Request,
229+
tcp_stream: tokio::net::TcpStream,
230+
) -> Result<Self, Error<T>> {
231+
// rebuild as http::Request, which tungstenite re-exports as its
232+
// "IntoClientRequest" type.
233+
// FIXME: this is obviously a hack, the better thing to do would be to
234+
// implement using http::Request::builder() in the proc macro
235+
let mut rb = http::Request::builder()
236+
.method(rqw.method())
237+
.version(rqw.version())
238+
.uri(
239+
http::Uri::from_str(rqw.url().as_str())
240+
.map_err(|e| Error::InvalidRequest(format!("{:?}", e)))?,
241+
);
242+
for (k, v) in rqw.headers().iter() {
243+
rb = rb.header(k, v);
244+
}
245+
let request = rb
246+
.body(())
247+
.map_err(|e| Error::InvalidRequest(e.to_string()))?;
248+
Ok(Self {
249+
request,
250+
tcp_stream,
251+
})
252+
}
253+
254+
pub fn into_request_and_tcp_stream(
255+
self,
256+
) -> (HttpBareRequest, tokio::net::TcpStream) {
257+
(self.request, self.tcp_stream)
258+
}
259+
}
260+
198261
/// Error produced by generated client methods.
199262
///
200263
/// The type parameter may be a struct if there's a single expected error type
@@ -207,6 +270,9 @@ pub enum Error<E = ()> {
207270
/// A server error either due to the data, or with the connection.
208271
CommunicationError(reqwest::Error),
209272

273+
/// A fundamental input/output error has occurred (e.g. unable to make a socket connection)
274+
IoError(std::io::Error),
275+
210276
/// A documented, expected error response.
211277
ErrorResponse(ResponseValue<E>),
212278

@@ -225,6 +291,7 @@ impl<E> Error<E> {
225291
match self {
226292
Error::InvalidRequest(_) => None,
227293
Error::CommunicationError(e) => e.status(),
294+
Error::IoError(_) => None,
228295
Error::ErrorResponse(rv) => Some(rv.status()),
229296
Error::InvalidResponsePayload(e) => e.status(),
230297
Error::UnexpectedResponse(r) => Some(r.status()),
@@ -239,6 +306,7 @@ impl<E> Error<E> {
239306
match self {
240307
Error::InvalidRequest(s) => Error::InvalidRequest(s),
241308
Error::CommunicationError(e) => Error::CommunicationError(e),
309+
Error::IoError(e) => Error::IoError(e),
242310
Error::ErrorResponse(ResponseValue {
243311
inner: _,
244312
status,
@@ -262,6 +330,12 @@ impl<E> From<reqwest::Error> for Error<E> {
262330
}
263331
}
264332

333+
impl<E> From<std::io::Error> for Error<E> {
334+
fn from(e: std::io::Error) -> Self {
335+
Self::IoError(e)
336+
}
337+
}
338+
265339
impl<E> std::fmt::Display for Error<E>
266340
where
267341
ResponseValue<E>: ErrorFormat,
@@ -274,6 +348,9 @@ where
274348
Error::CommunicationError(e) => {
275349
write!(f, "Communication Error: {}", e)
276350
}
351+
Error::IoError(e) => {
352+
write!(f, "Input/Output Error: {}", e)
353+
}
277354
Error::ErrorResponse(rve) => {
278355
write!(f, "Error Response: ")?;
279356
rve.fmt_info(f)
@@ -377,3 +454,8 @@ impl<E> RequestBuilderExt<E> for RequestBuilder {
377454
})?))
378455
}
379456
}
457+
458+
#[doc(hidden)]
459+
pub fn generate_websocket_key() -> String {
460+
base64::encode(rand::random::<[u8; 16]>())
461+
}

progenitor-impl/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ thiserror = "1.0"
2323
# To publish, use a numbered version
2424
#typify = "0.0.9"
2525
typify = { git = "https://github.com/oxidecomputer/typify" }
26-
unicode-ident = "1.0.3"
26+
unicode-ident = "1.0.2"
2727

2828
[dev-dependencies]
2929
dropshot = { git = "https://github.com/oxidecomputer/dropshot", default-features = false }

progenitor-impl/src/lib.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub enum Error {
2626
UnexpectedFormat(String),
2727
#[error("invalid operation path {0}")]
2828
InvalidPath(String),
29+
#[error("invalid dropshot extension use: {0}")]
30+
InvalidExtension(String),
2931
#[error("internal error {0}")]
3032
InternalError(String),
3133
}
@@ -213,7 +215,13 @@ impl Generator {
213215
let file = quote! {
214216
// Re-export ResponseValue and Error since those are used by the
215217
// public interface of Client.
216-
pub use progenitor_client::{ByteStream, Error, ResponseValue};
218+
pub use progenitor_client::{
219+
ByteStream,
220+
Error,
221+
ResponseValue,
222+
WebsocketReactants,
223+
generate_websocket_key,
224+
};
217225
#[allow(unused_imports)]
218226
use progenitor_client::{encode_path, RequestBuilderExt};
219227

@@ -319,10 +327,12 @@ impl Generator {
319327
#[allow(unused_imports)]
320328
use super::{
321329
encode_path,
330+
generate_websocket_key,
322331
ByteStream,
323332
Error,
324333
RequestBuilderExt,
325334
ResponseValue,
335+
WebsocketReactants,
326336
};
327337
#[allow(unused_imports)]
328338
use std::convert::TryInto;
@@ -358,11 +368,15 @@ impl Generator {
358368
#[allow(unused_imports)]
359369
use super::{
360370
encode_path,
371+
generate_websocket_key,
361372
ByteStream,
362373
Error,
363374
RequestBuilderExt,
364375
ResponseValue,
376+
WebsocketReactants,
365377
};
378+
#[allow(unused_imports)]
379+
use std::convert::TryInto;
366380

367381
#(#builder_struct)*
368382

0 commit comments

Comments
 (0)