Skip to main content

flams_router_base/
lib.rs

1#![allow(clippy::must_use_candidate)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4#[cfg(any(
5    all(feature = "ssr", feature = "hydrate", not(feature = "docs-only")),
6    not(any(feature = "ssr", feature = "hydrate"))
7))]
8compile_error!("exactly one of the features \"ssr\" or \"hydrate\" must be enabled");
9
10mod login_state;
11use ftml_uris::FtmlUri;
12pub use login_state::*;
13#[cfg(feature = "ssr")]
14pub mod uris;
15pub mod ws;
16
17#[macro_export]
18macro_rules! maybe_lazy {
19    ($t:path) => {{
20        #[cfg(any(debug_assertions,feature="docs-only"))]
21        {$t}
22        #[cfg(not(any(debug_assertions,feature="docs-only")))]
23        {leptos_router::Lazy::<$t>::new()}
24    }};
25    ($name:ident = $e:expr) => {
26        #[cfg(any(debug_assertions,feature="docs-only"))]
27        #[component]
28        pub fn $name() -> AnyView { $e }
29        #[cfg(not(any(debug_assertions,feature="docs-only")))]
30        #[derive(Debug,Clone,serde::Deserialize)]
31        pub struct $name;
32        #[cfg(not(any(debug_assertions,feature="docs-only")))]
33        #[leptos_router::lazy_route]
34        impl leptos_router::LazyRoute for $name {
35            fn data() -> Self {
36                Self
37            }
38            fn view($name:Self) -> leptos::prelude::AnyView {
39                $e//ftml_dom::global_setup(move || flams_router_content::Views::top(move || $e))
40            }
41        }
42    }
43}
44
45use leptos::{either::EitherOf3, prelude::*};
46
47pub fn vscode_link(archive: &ftml_uris::ArchiveId, rel_path: &str) -> impl IntoView + use<> {
48    let href = format!("vscode://kwarc.flams/open?a={archive}&rp={rel_path}");
49    view! {
50        <a href=href><ftml_component_utils::icons::VSCodeIcon/></a>
51    }
52}
53
54#[component]
55pub fn RequireLogin(children: Children) -> impl IntoView {
56    require_login(children)
57}
58
59pub fn require_login(children: Children) -> AnyView {
60    use flams_web_utils::components::display_error;
61    use ftml_component_utils::Spinner;
62
63    let children = std::sync::Arc::new(flams_utils::parking_lot::Mutex::new(Some(children)));
64    (move || match LoginState::get() {
65        LoginState::Loading => view!(<Spinner/>).into_any(),
66        LoginState::Admin | LoginState::NoAccounts | LoginState::User { is_admin: true, .. } => {
67            (children.clone().lock().take()).map(|f| f()).into_any()
68        }
69        _ => view!(<div>{display_error("Not logged in".into())}</div>).into_any(),
70    })
71    .into_any()
72}
73
74#[cfg(feature = "ssr")]
75/// #### Errors
76pub fn get_oauth() -> Result<(flams_git::gl::auth::GitLabOAuth, String), String> {
77    use flams_git::gl::auth::GitLabOAuth;
78    use leptos::prelude::*;
79    let Some(session) = use_context::<axum_login::AuthSession<flams_database::DBBackend>>() else {
80        return Err("Internal Error".to_string());
81    };
82    let Some(user) = session.user else {
83        return Err("Not logged in".to_string());
84    };
85    let Some(oauth): Option<GitLabOAuth> = expect_context() else {
86        return Err("Not Gitlab integration set up".to_string());
87    };
88    Ok((oauth, user.secret))
89}
90
91pub trait ServerFnExt {
92    type Output;
93    type Error;
94    #[cfg(feature = "hydrate")]
95    #[allow(async_fn_in_trait)]
96    async fn call_remote(self, url: String) -> Result<Self::Output, Self::Error>;
97}
98
99#[cfg(feature = "hydrate")]
100mod hydrate {
101    use super::ServerFnExt;
102    use bytes::Bytes;
103    use futures::{Stream, StreamExt};
104    use leptos::server_fn::codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
105    use leptos::server_fn::request::BrowserMockReq;
106    use leptos::server_fn::request::browser::{BrowserRequest as OrigBrowserRequest, Request};
107    use leptos::server_fn::response::BrowserMockRes;
108    use leptos::server_fn::response::browser::BrowserResponse as OrigBrowserResponse;
109    use leptos::{
110        prelude::*,
111        server_fn::{request::ClientReq, response::ClientRes},
112        wasm_bindgen::JsCast,
113    };
114    use send_wrapper::SendWrapper;
115    use wasm_streams::ReadableStream;
116
117    // -------------------------------------
118    //
119    struct Paired<
120        In,
121        F: leptos::server_fn::ServerFn<
122                Client = leptos::server_fn::client::browser::BrowserClient,
123                Server = leptos::server_fn::mock::BrowserMockServer,
124                Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>, //OutputEncoding = leptos::server_fn::codec::Json,
125            >,
126    > {
127        sfn: F,
128        url: String,
129    }
130
131    struct BrowserRequest(SendWrapper<Request>);
132    struct BrowserFormData(SendWrapper<leptos::web_sys::FormData>);
133    struct EncodingWrap<E: Encoding>(E);
134
135    impl<E: Encoding> server_fn::ContentType for EncodingWrap<E> {
136        const CONTENT_TYPE: &'static str = E::CONTENT_TYPE;
137    }
138    impl<E: Encoding> Encoding for EncodingWrap<E> {
139        const METHOD: http::Method = E::METHOD;
140    }
141
142    impl<
143        In: Encoding,
144        Err: Send + Sync + FromServerFnError,
145        F: leptos::server_fn::ServerFn<
146                Client = leptos::server_fn::client::browser::BrowserClient,
147                Server = leptos::server_fn::mock::BrowserMockServer,
148                Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
149                Error = Err,
150                InputStreamError = Err,
151                OutputStreamError = Err,
152            > + FromReq<In, BrowserMockReq, Err>,
153    > FromReq<EncodingWrap<In>, BrowserMockReq, Err> for Paired<In, F>
154    {
155        async fn from_req(req: BrowserMockReq) -> Result<Self, Err> {
156            Ok(Self {
157                sfn: <F as FromReq<In, BrowserMockReq, Err>>::from_req(req).await?,
158                url: String::new(),
159            })
160        }
161    }
162
163    impl<
164        In: Encoding,
165        Err: Send + Sync + FromServerFnError,
166        F: leptos::server_fn::ServerFn<
167                Client = leptos::server_fn::client::browser::BrowserClient,
168                Server = leptos::server_fn::mock::BrowserMockServer,
169                Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
170                Error = Err,
171                InputStreamError = Err,
172                OutputStreamError = Err,
173            > + IntoReq<In, OrigBrowserRequest, Err>,
174    > IntoReq<EncodingWrap<In>, BrowserRequest, Err> for Paired<In, F>
175    {
176        fn into_req(self, _path: &str, accepts: &str) -> Result<BrowserRequest, Err> {
177            let Paired { sfn, url } = self;
178            let path = format!("{}{}", url, F::PATH);
179            let req = <F as IntoReq<In, OrigBrowserRequest, _>>::into_req(sfn, &path, accepts)?;
180            let req: Request = req.into();
181            Ok(BrowserRequest(SendWrapper::new(req)))
182        }
183    }
184
185    impl<
186        In: Encoding,
187        Err: Send + Sync + FromServerFnError,
188        F: leptos::server_fn::ServerFn<
189                Client = leptos::server_fn::client::browser::BrowserClient,
190                Server = leptos::server_fn::mock::BrowserMockServer,
191                Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
192                Error = Err,
193                InputStreamError = Err,
194                OutputStreamError = Err,
195            >,
196    > leptos::server_fn::ServerFn for Paired<In, F>
197    where
198        F: FromReq<In, BrowserMockReq, Err>
199            + IntoReq<In, OrigBrowserRequest, Err>
200            + server_fn::serde::Serialize
201            + server_fn::serde::de::DeserializeOwned
202            + std::fmt::Debug
203            + Clone
204            + Send
205            + Sync
206            + for<'de> server_fn::serde::Deserialize<'de>,
207        F::Output: IntoRes<server_fn::codec::Json, BrowserMockRes, Err>
208            + FromRes<server_fn::codec::Json, OrigBrowserResponse, Err>
209            + Send
210            + for<'de> server_fn::serde::Deserialize<'de>,
211    {
212        const PATH: &'static str = F::PATH;
213        type Client = ClientWrap;
214        type Server = F::Server;
215        type Protocol = leptos::server_fn::Http<EncodingWrap<In>, server_fn::codec::Json>;
216        type Output = F::Output;
217        type Error = Err;
218        type InputStreamError = Err;
219        type OutputStreamError = Err;
220        fn middlewares() -> Vec<
221            std::sync::Arc<
222                dyn server_fn::middleware::Layer<
223                        <Self::Server as server_fn::server::Server<Self::Error>>::Request,
224                        <Self::Server as server_fn::server::Server<Self::Error>>::Response,
225                    >,
226            >,
227        > {
228            F::middlewares()
229        }
230        async fn run_body(self) -> Result<Self::Output, Self::Error> {
231            unreachable!()
232        }
233    }
234
235    impl<E: FromServerFnError> ClientReq<E> for BrowserRequest {
236        type FormData = BrowserFormData;
237
238        fn try_new_req_query(
239            path: &str,
240            content_type: &str,
241            accepts: &str,
242            query: &str,
243            method: http::Method,
244        ) -> Result<Self, E> {
245            let mut url = String::with_capacity(path.len() + 1 + query.len());
246            url.push_str(path);
247            url.push('?');
248            url.push_str(query);
249            let inner = match method {
250                http::Method::GET => Request::get(&url),
251                http::Method::DELETE => Request::delete(&url),
252                http::Method::POST => Request::post(&url),
253                http::Method::PUT => Request::put(&url),
254                http::Method::PATCH => Request::patch(&url),
255                m => {
256                    return Err(E::from_server_fn_error(
257                        ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
258                    ));
259                }
260            };
261            Ok(Self(SendWrapper::new(
262                inner
263                    .header("Content-Type", content_type)
264                    .header("Accept", accepts)
265                    .build()
266                    .map_err(|e| {
267                        E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
268                    })?,
269            )))
270        }
271
272        fn try_new_req_text(
273            path: &str,
274            content_type: &str,
275            accepts: &str,
276            body: String,
277            method: http::Method,
278        ) -> Result<Self, E> {
279            let url = path;
280            let inner = match method {
281                http::Method::POST => Request::post(&url),
282                http::Method::PATCH => Request::patch(&url),
283                http::Method::PUT => Request::put(&url),
284                m => {
285                    return Err(E::from_server_fn_error(
286                        ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
287                    ));
288                }
289            };
290            Ok(Self(SendWrapper::new(
291                inner
292                    .header("Content-Type", content_type)
293                    .header("Accept", accepts)
294                    .body(body)
295                    .map_err(|e| {
296                        E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
297                    })?,
298            )))
299        }
300
301        fn try_new_req_bytes(
302            path: &str,
303            content_type: &str,
304            accepts: &str,
305            body: Bytes,
306            method: http::Method,
307        ) -> Result<Self, E> {
308            let url = path;
309            let body: &[u8] = &body;
310            let body = leptos::web_sys::js_sys::Uint8Array::from(body).buffer();
311            let inner = match method {
312                http::Method::POST => Request::post(&url),
313                http::Method::PATCH => Request::patch(&url),
314                http::Method::PUT => Request::put(&url),
315                m => {
316                    return Err(E::from_server_fn_error(
317                        ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
318                    ));
319                }
320            };
321            Ok(Self(SendWrapper::new(
322                inner
323                    .header("Content-Type", content_type)
324                    .header("Accept", accepts)
325                    .body(body)
326                    .map_err(|e| {
327                        E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
328                    })?,
329            )))
330        }
331
332        fn try_new_req_multipart(
333            path: &str,
334            accepts: &str,
335            body: Self::FormData,
336            method: http::Method,
337        ) -> Result<Self, E> {
338            let url = path;
339            let inner = match method {
340                http::Method::POST => Request::post(&url),
341                http::Method::PATCH => Request::patch(&url),
342                http::Method::PUT => Request::put(&url),
343                m => {
344                    return Err(E::from_server_fn_error(
345                        ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
346                    ));
347                }
348            };
349            Ok(Self(SendWrapper::new(
350                inner
351                    .header("Accept", accepts)
352                    .body(body.0.take())
353                    .map_err(|e| {
354                        E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
355                    })?,
356            )))
357        }
358
359        fn try_new_req_form_data(
360            path: &str,
361            accepts: &str,
362            content_type: &str,
363            body: Self::FormData,
364            method: http::Method,
365        ) -> Result<Self, E> {
366            let form_data = body.0.take();
367            let url_params =
368                leptos::web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data)
369                    .map_err(|e| {
370                        E::from_server_fn_error(ServerFnErrorErr::Serialization(
371                            e.as_string().unwrap_or_else(|| {
372                                "Could not serialize FormData to URLSearchParams".to_string()
373                            }),
374                        ))
375                    })?;
376            let inner = match method {
377                http::Method::POST => Request::post(path),
378                http::Method::PUT => Request::put(path),
379                http::Method::PATCH => Request::patch(path),
380                m => {
381                    return Err(E::from_server_fn_error(
382                        ServerFnErrorErr::UnsupportedRequestMethod(m.to_string()),
383                    ));
384                }
385            };
386            Ok(Self(SendWrapper::new(
387                inner
388                    .header("Content-Type", content_type)
389                    .header("Accept", accepts)
390                    .body(url_params)
391                    .map_err(|e| {
392                        E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
393                    })?,
394            )))
395        }
396
397        fn try_new_req_streaming(
398            path: &str,
399            accepts: &str,
400            content_type: &str,
401            body: impl Stream<Item = Bytes> + Send + 'static,
402            method: http::Method,
403        ) -> Result<Self, E> {
404            fn streaming_request(
405                path: &str,
406                accepts: &str,
407                content_type: &str,
408                method: http::Method,
409                body: impl Stream<Item = Bytes> + 'static,
410            ) -> Result<Request, leptos::wasm_bindgen::JsValue> {
411                use leptos::wasm_bindgen::JsValue;
412                let stream = ReadableStream::from_stream(body.map(|bytes| {
413                    let data = leptos::web_sys::js_sys::Uint8Array::from(bytes.as_ref());
414                    let data = JsValue::from(data);
415                    Ok(data) as Result<JsValue, JsValue>
416                }))
417                .into_raw();
418
419                let headers = leptos::web_sys::Headers::new()?;
420                headers.append("Content-Type", content_type)?;
421                headers.append("Accept", accepts)?;
422
423                let init = leptos::web_sys::RequestInit::new();
424                init.set_headers(&headers);
425                init.set_method(method.as_str());
426                init.set_body(&stream);
427
428                // Chrome requires setting `duplex: "half"` on streaming requests
429                leptos::web_sys::js_sys::Reflect::set(
430                    &init,
431                    &JsValue::from_str("duplex"),
432                    &JsValue::from_str("half"),
433                )?;
434                let req = leptos::web_sys::Request::new_with_str_and_init(path, &init)?;
435                Ok(Request::from(req))
436            }
437
438            // TODO abort signal
439            let request =
440                streaming_request(path, accepts, content_type, method, body).map_err(|e| {
441                    E::from_server_fn_error(ServerFnErrorErr::Request(format!("{e:?}")))
442                })?;
443            Ok(Self(SendWrapper::new(request)))
444        }
445    }
446
447    struct ClientWrap;
448    impl<
449        Error: FromServerFnError + Send,
450        InputStreamError: FromServerFnError,
451        OutputStreamError: FromServerFnError,
452    > leptos::server_fn::client::Client<Error, InputStreamError, OutputStreamError> for ClientWrap
453    {
454        type Request = BrowserRequest;
455        type Response = BrowserResponse;
456
457        fn send(req: BrowserRequest) -> impl Future<Output = Result<Self::Response, Error>> + Send {
458            SendWrapper::new(async move {
459                let request = req.0.take();
460                let res = request
461                    .send()
462                    .await
463                    .map(|res| BrowserResponse(SendWrapper::new(res)))
464                    .map_err(|e| {
465                        Error::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
466                    });
467                res
468            })
469        }
470        async fn open_websocket(
471            _: &str,
472        ) -> Result<
473            (
474                impl Stream<Item = Result<Bytes, Bytes>> + Send + 'static,
475                impl futures::Sink<Bytes> + Send + 'static,
476            ),
477            Error,
478        > {
479            Err::<(futures::stream::BoxStream<Result<Bytes, Bytes>>, Vec<Bytes>), _>(
480                Error::from_server_fn_error(ServerFnErrorErr::ServerError(
481                    "Not implemented".to_string(),
482                )),
483            )
484        }
485
486        fn spawn(future: impl Future<Output = ()> + Send + 'static) {
487            wasm_bindgen_futures::spawn_local(future);
488        }
489    }
490
491    struct BrowserResponse(SendWrapper<leptos::server_fn::response::browser::Response>);
492
493    impl<E: FromServerFnError> ClientRes<E> for BrowserResponse {
494        fn try_into_string(self) -> impl Future<Output = Result<String, E>> + Send {
495            SendWrapper::new(async move {
496                self.0.text().await.map_err(|e| {
497                    E::from_server_fn_error(ServerFnErrorErr::Deserialization(e.to_string()))
498                })
499            })
500        }
501
502        fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, E>> + Send {
503            SendWrapper::new(async move {
504                self.0.binary().await.map(Bytes::from).map_err(|e| {
505                    E::from_server_fn_error(ServerFnErrorErr::Deserialization(e.to_string()))
506                })
507            })
508        }
509
510        fn try_into_stream(
511            self,
512        ) -> Result<impl Stream<Item = Result<Bytes, Bytes>> + Send + Sync + 'static, E> {
513            let stream = ReadableStream::from_raw(self.0.body().unwrap())
514                .into_stream()
515                .map(|data| match data {
516                    Err(e) => {
517                        leptos::web_sys::console::error_1(&e);
518                        Err(format!("{e:?}").into())
519                    }
520                    Ok(data) => {
521                        let data = data.unchecked_into::<leptos::web_sys::js_sys::Uint8Array>();
522                        let mut buf = Vec::new();
523                        let length = data.length();
524                        buf.resize(length as usize, 0);
525                        data.copy_to(&mut buf);
526                        Ok(Bytes::from(buf))
527                    }
528                });
529            Ok(SendWrapper::new(stream))
530        }
531
532        fn status(&self) -> u16 {
533            self.0.status()
534        }
535
536        fn status_text(&self) -> String {
537            self.0.status_text()
538        }
539
540        fn location(&self) -> String {
541            self.0
542                .headers()
543                .get("Location")
544                .unwrap_or_else(|| self.0.url())
545        }
546
547        fn has_redirect(&self) -> bool {
548            self.0
549                .headers()
550                .get(leptos::server_fn::redirect::REDIRECT_HEADER)
551                .is_some()
552        }
553    }
554
555    // -------------------------------------
556
557    impl<
558        In: Encoding,
559        Err: Send + Sync + FromServerFnError,
560        F: leptos::server_fn::ServerFn<
561                Client = leptos::server_fn::client::browser::BrowserClient,
562                Server = leptos::server_fn::mock::BrowserMockServer,
563                Protocol = leptos::server_fn::Http<In, server_fn::codec::Json>,
564                Error = Err,
565                InputStreamError = Err,
566                OutputStreamError = Err,
567            >,
568    > ServerFnExt for F
569    where
570        F: FromReq<In, BrowserMockReq, Err>
571            + IntoReq<In, OrigBrowserRequest, Err>
572            + server_fn::serde::Serialize
573            + server_fn::serde::de::DeserializeOwned
574            + std::fmt::Debug
575            + Clone
576            + Send
577            + Sync
578            + for<'de> server_fn::serde::Deserialize<'de>,
579        F::Output: IntoRes<server_fn::codec::Json, BrowserMockRes, Err>
580            + FromRes<server_fn::codec::Json, OrigBrowserResponse, Err>
581            + Send
582            + for<'de> server_fn::serde::Deserialize<'de>,
583    {
584        type Output = <Self as leptos::server_fn::ServerFn>::Output;
585        type Error = <Self as leptos::server_fn::ServerFn>::Error;
586        #[cfg(feature = "hydrate")]
587        async fn call_remote(self, url: String) -> Result<Self::Output, Self::Error> {
588            use leptos::server_fn::ServerFn;
589            Paired { sfn: self, url }.run_on_client().await
590        }
591    }
592}