spin_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3
4const WIT_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/wit");
5
6/// Generates the entrypoint to a Spin Redis component written in Rust.
7#[proc_macro_attribute]
8pub fn redis_component(_attr: TokenStream, item: TokenStream) -> TokenStream {
9    let func = syn::parse_macro_input!(item as syn::ItemFn);
10    let func_name = &func.sig.ident;
11    let await_postfix = func.sig.asyncness.map(|_| quote!(.await));
12    let preamble = preamble(Export::Redis);
13
14    quote!(
15        #func
16        mod __spin_redis {
17            mod preamble {
18                #preamble
19            }
20            impl self::preamble::exports::fermyon::spin::inbound_redis::Guest for preamble::Spin {
21                fn handle_message(msg: self::preamble::exports::fermyon::spin::inbound_redis::Payload) -> Result<(), self::preamble::fermyon::spin::redis_types::Error> {
22                    ::spin_sdk::http::run(async move {
23                        match super::#func_name(msg.try_into().expect("cannot convert from Spin Redis payload"))#await_postfix {
24                            Ok(()) => Ok(()),
25                            Err(e) => {
26                                eprintln!("{}", e);
27                                Err(self::preamble::fermyon::spin::redis_types::Error::Error)
28                            },
29                        }
30                    })
31                }
32            }
33        }
34    )
35        .into()
36}
37
38/// The entrypoint to a WASI HTTP component written in Rust.
39///
40/// Functions annotated with this attribute can be of two forms:
41/// * Request/Response
42/// * Input/Output Params
43///
44/// When in doubt prefer the Request/Response variant unless streaming response bodies is something you need.
45///
46/// ### Request/Response
47///
48/// This form takes the form of a function with one `request` param and one `response` return value.
49///
50/// Requests are anything that implements `spin_sdk::http::conversions::TryFromIncomingRequest` which includes
51/// `spin_sdk::http::Request`, `spin_sdk::http::IncomingRequest`, and even hyperium's popular `http` crate's `Request`
52/// type.
53///
54/// Responses are anything that implements `spin_sdk::http::IntoResponse`. This includes `Result<impl IntoResponse, impl IntoResponse`,
55/// `spin_sdk::http::Response`, and even the `http` crate's `Response` type.
56///
57/// For example:
58/// ```ignore
59/// use spin_sdk::http_component;
60/// use spin_sdk::http::{Request, IntoResponse};
61///
62/// #[http_component]
63/// async fn my_handler(request: Request) -> anyhow::Result<impl IntoResponse> {
64///   // Your logic goes here
65/// }
66/// ```
67///
68/// ### Input/Output Params
69///
70/// Input/Output functions allow for streaming HTTP bodies. This form is by its very nature harder to use than
71/// the request/response form above so it should only be favored when stream response bodies is desired.
72///
73/// The `request` param can be anything that implements `spin_sdk::http::TryFromIncomingRequest`. And
74/// the `response_out` param must be a `spin_sdk::http::ResponseOutparam`. See the docs of `ResponseOutparam`
75/// for how to use this type.
76///
77/// For example:
78///
79/// ```ignore
80/// use spin_sdk::http_component;
81/// use spin_sdk::http::{IncomingRequest, ResponseOutparam};
82///
83/// #[http_component]
84/// async fn my_handler(request: IncomingRequest, response_out: ResponseOutparam) {
85///   // Your logic goes here
86/// }
87/// ```
88#[proc_macro_attribute]
89pub fn http_component(_attr: TokenStream, item: TokenStream) -> TokenStream {
90    let func = syn::parse_macro_input!(item as syn::ItemFn);
91    let func_name = &func.sig.ident;
92    let preamble = preamble(Export::WasiHttp);
93    let is_native_wasi_http_handler = func.sig.inputs.len() == 2;
94    let await_postfix = func.sig.asyncness.map(|_| quote!(.await));
95    let handler = if is_native_wasi_http_handler {
96        quote! { super::#func_name(req, response_out)#await_postfix }
97    } else {
98        quote! { handle_response(response_out, super::#func_name(req)#await_postfix).await }
99    };
100
101    quote!(
102        #func
103        mod __spin_wasi_http {
104            mod preamble {
105              #preamble
106            }
107            impl self::preamble::exports::wasi::http::incoming_handler::Guest for self::preamble::Spin {
108                fn handle(request: self::preamble::wasi::http::types::IncomingRequest, response_out: self::preamble::wasi::http::types::ResponseOutparam) {
109                    let request: ::spin_sdk::http::IncomingRequest = ::std::convert::Into::into(request);
110                    let response_out: ::spin_sdk::http::ResponseOutparam = ::std::convert::Into::into(response_out);
111                    ::spin_sdk::http::run(async move {
112                        match ::spin_sdk::http::conversions::TryFromIncomingRequest::try_from_incoming_request(request).await {
113                            ::std::result::Result::Ok(req) => #handler,
114                            ::std::result::Result::Err(e) => handle_response(response_out, e).await,
115                        }
116                    });
117                }
118            }
119
120            async fn handle_response<R: ::spin_sdk::http::IntoResponse>(response_out: ::spin_sdk::http::ResponseOutparam, resp: R) {
121                let mut response = ::spin_sdk::http::IntoResponse::into_response(resp);
122                let body = ::std::mem::take(response.body_mut());
123                match ::std::convert::TryInto::try_into(response) {
124                    ::std::result::Result::Ok(response) => {
125                        if let Err(e) = ::spin_sdk::http::ResponseOutparam::set_with_body(response_out, response, body).await {
126                            ::std::eprintln!("Could not set `ResponseOutparam`: {e}");
127                        }
128                    }
129                    ::std::result::Result::Err(e) => {
130                        ::std::eprintln!("Could not convert response: {e}");
131                    }
132                }
133            }
134
135            impl From<self::preamble::wasi::http::types::IncomingRequest> for ::spin_sdk::http::IncomingRequest {
136                fn from(req: self::preamble::wasi::http::types::IncomingRequest) -> Self {
137                    unsafe { Self::from_handle(req.into_handle()) }
138                }
139            }
140
141            impl From<::spin_sdk::http::OutgoingResponse> for self::preamble::wasi::http::types::OutgoingResponse {
142                fn from(resp: ::spin_sdk::http::OutgoingResponse) -> Self {
143                    unsafe { Self::from_handle(resp.into_handle()) }
144                }
145            }
146
147            impl From<self::preamble::wasi::http::types::ResponseOutparam> for ::spin_sdk::http::ResponseOutparam {
148                fn from(resp: self::preamble::wasi::http::types::ResponseOutparam) -> Self {
149                    unsafe { Self::from_handle(resp.into_handle()) }
150                }
151            }
152        }
153
154    )
155    .into()
156}
157
158#[derive(Copy, Clone)]
159enum Export {
160    WasiHttp,
161    Redis,
162}
163
164fn preamble(export: Export) -> proc_macro2::TokenStream {
165    let export_decl = match export {
166        Export::WasiHttp => quote!("wasi:http/incoming-handler": Spin),
167        Export::Redis => quote!("fermyon:spin/inbound-redis": Spin),
168    };
169    let world = match export {
170        Export::WasiHttp => quote!("wasi-http-trigger"),
171        Export::Redis => quote!("redis-trigger"),
172    };
173    quote! {
174        #![allow(missing_docs)]
175        ::spin_sdk::wit_bindgen::generate!({
176            world: #world,
177            path: #WIT_PATH,
178            runtime_path: "::spin_sdk::wit_bindgen::rt",
179            exports: {
180                #export_decl
181            }
182        });
183        pub struct Spin;
184    }
185}