Skip to main content

rustolio_rpc/
extractor.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11use rustolio_utils::http;
12
13use crate::ServerFnError;
14
15pub trait Extract<B, C>: Sized {
16    fn extract(
17        req: &http::Request<B>,
18        ctx: C,
19    ) -> impl std::future::Future<Output = Result<Self, ServerFnError>>;
20}
21
22#[cfg(test)]
23mod tests {
24    use rustolio_utils::http::StatusCode;
25    use rustolio_utils::prelude::*;
26
27    use super::*;
28
29    #[derive(Debug)]
30    struct MyCustomError;
31
32    struct User {
33        username: String,
34    }
35
36    #[derive(Debug)]
37    struct Context {}
38
39    impl<B, C> Extract<B, C> for User {
40        async fn extract(req: &http::Request<B>, _ctx: C) -> Result<Self, ServerFnError> {
41            let username = req
42                .header("x-user-name")
43                .ok_or(ServerFnError::http_error(
44                    StatusCode::FORBIDDEN,
45                    "Not logged in",
46                ))?
47                .to_str()
48                .map_err(|_| {
49                    ServerFnError::http_error(StatusCode::BAD_REQUEST, "Could not read HeaderValue")
50                })?
51                .to_string();
52            Ok(Self { username })
53        }
54    }
55
56    async fn rpc_fn<B>(
57        req: http::Request<B>,
58        ctx: &'static Context,
59    ) -> Result<String, ServerFnError<MyCustomError>> {
60        // Extractor call (is generated in #[rpc] macro)
61        let user: User = match User::extract(&req, ctx).await {
62            Ok(v) => v,
63            Err(e) => return Err(e.into()),
64        };
65
66        // rpc fn code
67        Ok(user.username)
68    }
69
70    #[tokio::test]
71    async fn test_rpc_extractor() {
72        let ctx = Context {};
73        service!(ctx as Context);
74
75        let req = http::Request::get("/").build().unwrap();
76        let e = rpc_fn(req, ctx)
77            .await
78            .expect_err("Extractor should fail as there is no header set");
79        assert!(matches!(
80            e,
81            ServerFnError::HttpError(StatusCode::FORBIDDEN, _)
82        ));
83
84        let req = http::Request::get("/")
85            .header("x-user-name", "SomeUser")
86            .build()
87            .unwrap();
88        let username = rpc_fn(req, ctx)
89            .await
90            .expect("User should be extracter as there is a username in the header");
91        assert_eq!(username, "SomeUser");
92    }
93}