axum_browser_adapter/
lib.rs

1//! Axum Browser Adapter
2//!
3//! A collection of tools to make integrating Axum with the browser easier
4//!
5//! ## Example
6//!
7//! ```rust
8//! use axum_browser_adapter::{
9//!     wasm_request_to_axum_request,
10//!     axum_response_to_wasm_response,
11//!     wasm_compat,
12//!     WasmRequest,
13//!     WasmResponse
14//! };
15//! use axum::Router;
16//! use axum::routing::get;
17//! use wasm_bindgen::prelude::wasm_bindgen;
18//! use tower_service::Service;
19//!
20//! #[wasm_compat]
21//! pub async fn index() -> &'static str {
22//!     "Hello World"
23//! }
24//!
25//! #[wasm_bindgen]
26//! pub async fn wasm_app(wasm_request: WasmRequest) -> WasmResponse {
27//!    let mut router: Router = Router::new().route("/", get(index));
28//!
29//!    let request = wasm_request_to_axum_request(&wasm_request).unwrap();
30//!
31//!    let axum_response = router.call(request).await.unwrap();
32//!
33//!    let response = axum_response_to_wasm_response(axum_response).await.unwrap();
34//!
35//!    response
36//! }
37//!```
38//! Integrating w/ the browser
39//!
40//! ```html
41//! <!DOCTYPE html>
42//! <html lang="en">
43//! <head>
44//!     <meta charset="UTF-8">
45//!     <title></title>
46//! </head>
47//! <body>
48//! <script type="module">
49//!     import init, {wasm_app, WasmRequest} from './dist/example.js';
50//!
51//!     (async function () {
52//!         await init();
53//!
54//!         const wasmRequest = new WasmRequest("GET", "/", {}, undefined);
55//!         let response = await wasm_app(wasmRequest);
56//!
57//!         document.write(response.body)
58//!     }())
59//! </script>
60//! </body>
61//! </html>
62//! ```
63//!
64//! ## Recipes
65//! You might want to override fetch or use a service worker to intercept HTTP calls in order to the
66//! call the Axum WASM app instead of the a HTTP server.
67//!
68//! **Converting a JavaScript Request to a WasmRequest**
69//! ```javascript
70//!  async function requestToWasmRequest(request) {
71//!     const method = request.method;
72//!     const url = request.url;
73//!     const headers = Object.fromEntries(request.headers.entries());
74//!
75//!     let body = null;
76//!     if (request.body !== null) {
77//!         body = await request.text();
78//!     }
79//!     return new WasmRequest(method, url, headers, body);
80//! }
81//! ```
82//!
83//! **Converting a WasmResponse to a JavaScript Response**
84//!
85//! ```javascript
86//! function wasmResponseToJsResponse(wasmResponse) {
87//!    const body = wasmResponse.body;
88//!    const status = parseInt(wasmResponse.status_code);
89//!    const jsHeaders = new Headers();
90//!    const headers = wasmResponse.headers;
91//!    for (let [key, value] of headers) {
92//!        jsHeaders.append(key, value);
93//!    }
94//!    return new Response(body, {status: status, headers: jsHeaders});
95//! }
96//! ```
97
98use std::collections::HashMap;
99use std::str::FromStr;
100use axum::body::Body;
101use axum::http;
102use axum::response::Response;
103use axum::http::{Method, Request, Uri};
104use serde::{Deserialize, Serialize};
105use serde_wasm_bindgen::{from_value, to_value};
106use wasm_bindgen::prelude::*;
107
108pub use axum_wasm_macros::wasm_compat;
109
110#[wasm_bindgen]
111#[derive(Clone, Serialize, Deserialize, Debug)]
112pub struct WasmRequest {
113    #[wasm_bindgen(skip)]
114    pub method: String,
115    #[wasm_bindgen(skip)]
116    pub url: String,
117    #[wasm_bindgen(skip)]
118    pub headers: HashMap<String, String>,
119    #[wasm_bindgen(skip)]
120    pub body: Option<String>,
121}
122
123#[wasm_bindgen]
124impl WasmRequest {
125    #[wasm_bindgen(constructor)]
126    pub fn new(method: String, url: String, headers_js_value: JsValue, body: Option<String>) -> WasmRequest {
127        let headers: HashMap<String, String> = from_value(headers_js_value).unwrap();
128
129        WasmRequest { method, url, headers, body }
130    }
131
132    pub fn append_header(&mut self, key: String, value: String) {
133        self.headers.insert(key, value);
134    }
135}
136
137pub fn wasm_request_to_axum_request(wasm_request: &WasmRequest) -> Result<Request<Body>, Box<dyn std::error::Error>> {
138    let method = Method::from_str(&wasm_request.method)?;
139
140    let uri = Uri::try_from(&wasm_request.url)?;
141
142    let mut request_builder = Request::builder()
143        .method(method)
144        .uri(uri);
145
146    for (k, v) in &wasm_request.headers {
147        let header_name = http::header::HeaderName::from_bytes(k.as_bytes())?;
148        let header_value = http::header::HeaderValue::from_str(v)?;
149        request_builder = request_builder.header(header_name, header_value);
150    }
151
152    let request = match &wasm_request.body {
153        Some(body_str) => request_builder.body(Body::from(body_str.to_owned()))?,
154        None => request_builder.body(Body::empty())?,
155    };
156
157    Ok(request)
158}
159
160#[wasm_bindgen]
161#[derive(Clone, Serialize, Deserialize, Debug)]
162pub struct WasmResponse {
163    #[wasm_bindgen(skip)]
164    pub status_code: String,
165    #[wasm_bindgen(skip)]
166    pub headers: HashMap<String, String>,
167    #[wasm_bindgen(skip)]
168    pub body: Option<String>,
169}
170
171#[wasm_bindgen]
172impl WasmResponse {
173    #[wasm_bindgen(getter)]
174    pub fn status_code(&self) -> String {
175        self.status_code.to_string()
176    }
177
178    #[wasm_bindgen(getter)]
179    pub fn body(&self) -> Option<String> {
180        self.body.clone()
181    }
182
183    #[wasm_bindgen(getter)]
184    pub fn headers(&self) -> JsValue {
185        to_value(&self.headers).unwrap()
186    }
187}
188
189pub async fn axum_response_to_wasm_response(mut response: Response) -> Result<WasmResponse, Box<dyn std::error::Error>> {
190    let status_code = response.status().to_string();
191
192    let mut headers = HashMap::new();
193    for (name, value) in response.headers() {
194        if let Ok(value_str) = value.to_str() {
195            headers.insert(name.as_str().to_owned(), value_str.to_owned());
196        }
197    }
198
199    let bytes = match http_body::Body::data(response.body_mut()).await {
200        None => vec![],
201        Some(body_bytes) => match body_bytes {
202            Ok(bytes) => bytes.to_vec(),
203            Err(_) => vec![]
204        },
205    };
206    let body_str = String::from_utf8(bytes)?;
207
208    Ok(WasmResponse {
209        status_code,
210        headers,
211        body: Some(body_str),
212    })
213}