Skip to main content

psibase/
serve_http.rs

1use crate::{
2    allow_cors_with_origin, check, generate_action_templates, HttpReply, HttpRequest,
3    ProcessActionStruct, ToServiceSchema, WithActionStruct,
4};
5use async_graphql::{
6    http::{receive_body, GraphiQLSource},
7    EmptyMutation, EmptySubscription,
8};
9use futures::executor::block_on;
10
11const SIMPLE_UI: &[u8] = br#"<html><div id="root" class="ui container"></div><script src="/common/SimpleUI.mjs" type="module"></script></html>"#;
12
13/// Serve a developer UI
14///
15/// This function serves a simple developer UI to help get you started. The UI it
16/// generates is not suitable for end users.
17///
18/// This serves the following:
19/// - `GET /` or `GET /index.html`: provided by [serve_simple_index]
20/// - `GET /action_templates`: provided by [serve_action_templates]
21/// - `POST /pack_action/x`: provided by [serve_pack_action]
22///
23/// `Wrapper` should be generated by [`#[psibase::service]`](macro@crate::service).
24pub fn serve_simple_ui<Wrapper: WithActionStruct + ToServiceSchema>(
25    request: &HttpRequest,
26) -> Option<HttpReply> {
27    None.or_else(|| serve_simple_index(request))
28        .or_else(|| serve_action_templates::<Wrapper>(request))
29        .or_else(|| serve_pack_action::<Wrapper>(request))
30}
31
32/// Serve index.html for a developer UI
33///
34/// This function serves the following HTML for `GET /` or `GET /index.html`:
35///
36/// ```html
37/// <html>
38///     <div id="root" class="ui container"></div>
39///     <script src="/common/SimpleUI.mjs" type="module"></script>
40/// </html>
41/// ```
42pub fn serve_simple_index(request: &HttpRequest) -> Option<HttpReply> {
43    if request.method == "GET" && (request.target == "/" || request.target == "/index.html") {
44        Some(HttpReply {
45            status: 200,
46            contentType: "text/html".into(),
47            body: SIMPLE_UI.to_vec().into(),
48            headers: allow_cors_with_origin("*"),
49        })
50    } else {
51        None
52    }
53}
54
55/// Handle `/action_templates` request
56///
57/// If `request` is a `GET /action_templates`, then this returns a
58/// JSON object containing a field for each action in `Service`. The
59/// field names match the action names. The field values are objects
60/// with the action arguments, each containing sample data.
61///
62/// If `request` doesn't match the above, then this returns `None`.
63///
64/// `Wrapper` should be generated by [`#[psibase::service]`](macro@crate::service).
65pub fn serve_action_templates<Wrapper: ToServiceSchema>(
66    request: &HttpRequest,
67) -> Option<HttpReply> {
68    if request.method == "GET" && request.target == "/action_templates" {
69        Some(HttpReply {
70            status: 200,
71            contentType: "text/html".into(),
72            body: generate_action_templates::<Wrapper>().into(),
73            headers: allow_cors_with_origin("*"),
74        })
75    } else {
76        None
77    }
78}
79
80/// Handle `/pack_action/` request
81///
82/// If `request` is a `POST /pack_action/x`, where `x` is an action
83/// on `Wrapper`, then this parses a JSON object containing the arguments
84/// to `x`, packs them using fracpack, and returns the result as an
85/// `application/octet-stream`.
86///
87/// If `request` doesn't match the above, or the action name is not found,
88/// then this returns `None`.
89///
90/// `Wrapper` should be generated by [`#[psibase::service]`](macro@crate::service).
91pub fn serve_pack_action<Wrapper: WithActionStruct>(request: &HttpRequest) -> Option<HttpReply> {
92    struct PackAction<'a>(&'a [u8]);
93
94    impl<'a> ProcessActionStruct for PackAction<'a> {
95        type Output = HttpReply;
96
97        fn process<
98            Return: serde::Serialize + serde::de::DeserializeOwned,
99            ArgStruct: fracpack::Pack + serde::Serialize + serde::de::DeserializeOwned,
100        >(
101            self,
102        ) -> Self::Output {
103            let arg_struct_result = serde_json::from_slice::<ArgStruct>(self.0);
104            if let Err(err) = &arg_struct_result {
105                check(false, &format!("err parsing action args json {}", err));
106            }
107            HttpReply {
108                status: 200,
109                contentType: "application/octet-stream".into(),
110                body: arg_struct_result.unwrap().packed().into(),
111                headers: allow_cors_with_origin("*"),
112            }
113        }
114    }
115
116    if request.method == "POST" && request.target.starts_with("/pack_action/") {
117        Wrapper::with_action_struct(&request.target[13..], PackAction(&request.body))
118    } else {
119        None
120    }
121}
122
123/// Handle `/graphql` request
124///
125/// This handles graphql requests, including fetching the schema.
126///
127/// * `GET /graphql`: Returns the schema.
128/// * `GET /graphql?query=...`: Run query in URL and return JSON result.
129/// * `POST /graphql?query=...`: Run query in URL and return JSON result.
130/// * `POST /graphql` with `Content-Type = application/graphql`: Run query that's in body and return JSON result.
131/// * `POST /graphql` with `Content-Type = application/json`: Body contains a JSON object of the form `{"query"="..."}`. Run query and return JSON result.
132pub fn serve_graphql<Query: async_graphql::ObjectType + 'static>(
133    request: &HttpRequest,
134    query: Query,
135) -> Option<HttpReply> {
136    let (base, args) = if let Some((b, q)) = request.target.split_once('?') {
137        (b, q)
138    } else {
139        (request.target.as_ref(), "")
140    };
141    if base != "/graphql" || request.method != "GET" && request.method != "POST" {
142        return None;
143    }
144    block_on(async move {
145        let schema = async_graphql::Schema::new(query, EmptyMutation, EmptySubscription);
146        if let Some(request) = args.strip_prefix("?query=") {
147            let res = schema.execute(request).await;
148            Some(HttpReply {
149                status: 200,
150                contentType: "application/json".into(),
151                body: serde_json::to_vec(&res).unwrap().into(),
152                headers: allow_cors_with_origin("*"),
153            })
154        } else if request.method == "GET" {
155            Some(HttpReply {
156                status: 200,
157                contentType: "text".into(), // TODO
158                body: schema.sdl().into_bytes().into(),
159                headers: allow_cors_with_origin("*"),
160            })
161        } else if request.contentType == "application/graphql" {
162            let res = schema
163                .execute(std::str::from_utf8(&request.body.0).unwrap())
164                .await;
165            Some(HttpReply {
166                status: 200,
167                contentType: "application/json".into(),
168                body: serde_json::to_vec(&res).unwrap().into(),
169                headers: allow_cors_with_origin("*"),
170            })
171        } else {
172            let request_result = receive_body(
173                Some(&request.contentType),
174                request.body.as_ref(),
175                Default::default(),
176            )
177            .await;
178
179            if let Err(err) = &request_result {
180                check(false, &format!("err parsing graphql query {}", err));
181            }
182
183            let res = schema.execute(request_result.unwrap()).await;
184            Some(HttpReply {
185                status: 200,
186                contentType: "application/json".into(),
187                body: serde_json::to_vec(&res).unwrap().into(),
188                headers: allow_cors_with_origin("*"),
189            })
190        }
191    })
192}
193
194/// Serve GraphiQL UI
195///
196/// This function serves the GraphiQL UI for `GET /graphiql.html` and `GET /graphiql`.
197/// Use with [serve_graphql].
198///
199/// This wraps [graphiql_source].
200pub fn serve_graphiql(request: &HttpRequest) -> Option<HttpReply> {
201    if request.method == "GET"
202        && (request.target == "/graphiql.html" || request.target == "/graphiql")
203    {
204        Some(HttpReply {
205            status: 200,
206            contentType: "text/html".into(),
207            body: GraphiQLSource::build().endpoint("/graphql").finish().into(),
208            headers: allow_cors_with_origin("*"),
209        })
210    } else {
211        None
212    }
213}