1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
use bytes::Bytes;
use futures::{Stream, StreamExt};
use leptos::server_fn::error::{
    ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER,
};
use leptos::server_fn::response::Res;
use spin_sdk::http::Headers;
use std::pin::Pin;
use std::{
    fmt::{Debug, Display},
    str::FromStr,
};
use typed_builder::TypedBuilder;
/// This is here because the orphan rule does not allow us to implement it on IncomingRequest with
/// the generic error. So we have to wrap it to make it happy
pub struct SpinResponse(pub SpinResponseParts);

#[derive(TypedBuilder)]
pub struct SpinResponseParts {
    pub status_code: u16,
    pub headers: Headers,
    pub body: SpinBody,
}

/// We either can return a fairly simple Box type for normal bodies or a Stream for Streaming
/// server functions
pub enum SpinBody {
    Plain(Vec<u8>),
    Streaming(Pin<Box<dyn Stream<Item = Result<Bytes, Box<dyn std::error::Error>>> + Send>>),
}
impl<CustErr> Res<CustErr> for SpinResponse
where
    CustErr: Send + Sync + Debug + FromStr + Display + 'static,
{
    fn try_from_string(content_type: &str, data: String) -> Result<Self, ServerFnError<CustErr>> {
        let headers = Headers::new(&[("Content-Type".to_string(), content_type.into())]);
        let parts = SpinResponseParts::builder()
            .status_code(200)
            .headers(headers)
            .body(SpinBody::Plain(data.into()))
            .build();
        Ok(SpinResponse(parts))
    }

    fn try_from_bytes(content_type: &str, data: Bytes) -> Result<Self, ServerFnError<CustErr>> {
        let headers = Headers::new(&[("Content-Type".to_string(), content_type.into())]);
        let parts = SpinResponseParts::builder()
            .status_code(200)
            .headers(headers)
            .body(SpinBody::Plain(data.into()))
            .build();
        Ok(SpinResponse(parts))
    }

    fn try_from_stream(
        content_type: &str,
        data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send + 'static,
    ) -> Result<Self, ServerFnError<CustErr>> {
        let body = data.map(|n| {
            n.map_err(ServerFnErrorErr::from)
                .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
        });

        let headers = Headers::new(&[("Content-Type".to_string(), content_type.into())]);
        let parts = SpinResponseParts::builder()
            .status_code(200)
            .headers(headers)
            .body(SpinBody::Streaming(Box::pin(body)))
            .build();
        Ok(SpinResponse(parts))
    }

    fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self {
        let headers = Headers::new(&[(SERVER_FN_ERROR_HEADER.to_string(), path.into())]);
        let parts = SpinResponseParts::builder()
            .status_code(500)
            .headers(headers)
            .body(SpinBody::Plain(
                err.ser().unwrap_or_else(|_| err.to_string()).into(),
            ))
            .build();
        SpinResponse(parts)
    }

    fn redirect(&mut self, _path: &str) {
        //TODO: Enabling these seems to override location header
        // not sure what's causing that
        //let res_options = expect_context::<ResponseOptions>();
        //res_options.insert_header("Location", path);
        //res_options.set_status(302);
    }
}