spin_macro/
lib.rs

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