webtonic_client/lib.rs
1//! Client crate of the [`WebTonic`](https://github.com/Sawchord/webtonic) project.
2//!
3//! This crate only contains the [`Client`](Client), which requires a browser runtime
4//! to function.
5
6mod websocket;
7
8use bytes::BytesMut;
9use core::{
10 marker::PhantomData,
11 task::{Context, Poll},
12};
13use futures::{future::LocalBoxFuture, FutureExt};
14use http::{request::Request, response::Response};
15use prost::Message;
16use tonic::{body::BoxBody, client::GrpcService};
17use wasm_bindgen::JsValue;
18use web_sys::console;
19use webtonic_proto::{Reply, WebTonicError};
20
21use crate::websocket::WebSocketConnector;
22
23pub(crate) fn console_log(s: &str) {
24 console::log_1(&JsValue::from_str(s));
25}
26
27/// A websocket-tunneled, browser enabled tonic client.
28///
29/// This client can be used in place of tonic's
30/// [`Channel`](https://docs.rs/tonic/0.3.1/tonic/transport/struct.Channel.html).
31/// It tunnels the request through a websocket connection to the server that reconstructs them and send them
32/// to their respective handlers.
33///
34/// # Cryptography
35/// This transport implementation does not directly support encryption.
36/// It is however possible to encrypt the websocket connection itself.
37/// However, client authentication is not possible that way.
38///
39/// # Example
40/// Assuming we have the
41/// [greeter example](https://github.com/hyperium/tonic/blob/master/examples/proto/helloworld/helloworld.proto)
42/// in scope, we can instanciate a connection like so:
43///
44/// ```
45/// let client = Client::connect("ws://localhost:8080").await.unwrap();
46/// let mut client = greeter_client::GreeterClient::new(client);
47///
48/// let request = tonic::Request::new(HelloRequest {
49/// name: "WebTonic".into(),
50/// });
51///
52/// let response = client.say_hello(request).await.unwrap().into_inner();
53/// assert_eq!(response.message, "Hello WebTonic!");
54/// ```
55#[derive(Debug, Clone)]
56pub struct Client<'a> {
57 ws: WebSocketConnector,
58 _a: PhantomData<&'a ()>,
59}
60
61impl Client<'static> {
62 /// Connects the client to the endpoint.
63 ///
64 /// # Arguments
65 /// - `uri`: The uri to connect to.
66 /// **Note**: The sceme is either `ws://` or `wss://`, depending wether encryption is used or not.
67 ///
68 /// # Returns
69 /// - A [`Client`](Client) on success.
70 /// - [`WebTonicError::InvalidUrl`](WebTonicError::InvalidUrl), if the url is malformed.
71 /// - [`WebTonicError::ConnectionError`](WebTonicError::InvalidUrl), if the endpoint can not be reached.
72 ///
73 /// # Example
74 /// ```
75 /// let client = Client::connect("ws://localhost:1337").await.unwrap();
76 /// ```
77 pub async fn connect(uri: &str) -> Result<Self, WebTonicError> {
78 let ws = WebSocketConnector::connect(uri).await?;
79 Ok(Self {
80 ws,
81 _a: PhantomData,
82 })
83 }
84}
85
86impl<'a> GrpcService<BoxBody> for Client<'a> {
87 type ResponseBody = BoxBody;
88 type Error = WebTonicError;
89 type Future = LocalBoxFuture<'a, Result<Response<BoxBody>, WebTonicError>>;
90
91 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
92 // We return an ok, because we are essentially always ready to poll
93 Poll::Ready(Ok(()))
94 }
95
96 fn call(&mut self, request: Request<BoxBody>) -> Self::Future {
97 let ws_clone = self.ws.clone();
98 (async { call(ws_clone, request).await }).boxed_local()
99 }
100}
101
102async fn call(
103 ws: WebSocketConnector,
104 mut request: Request<BoxBody>,
105) -> Result<Response<BoxBody>, WebTonicError> {
106 // Parse request into bytes
107 let request = webtonic_proto::http_request_to_call(&mut request).await;
108 let mut msg = BytesMut::new();
109 request
110 .encode(&mut msg)
111 .map_err(|_| WebTonicError::EncodingError)?;
112
113 // Make the request
114 let msg = ws.send(&msg.into()).await?;
115
116 // Parse response
117 let reply = Reply::decode(msg).map_err(|_| WebTonicError::DecodingError)?;
118 let response =
119 webtonic_proto::reply_to_http_response(reply).ok_or(WebTonicError::DecodingError)?;
120
121 // Return
122 Ok(response)
123}