spin_http/
config.rs

1use serde::{Deserialize, Serialize};
2use spin_http_routes::HttpTriggerRouteConfig;
3
4/// Configuration for the HTTP trigger
5#[derive(Clone, Debug, Deserialize, Serialize)]
6#[serde(deny_unknown_fields)]
7pub struct HttpTriggerConfig {
8    /// Component ID to invoke
9    pub component: Option<String>,
10    /// Static response to send
11    pub static_response: Option<StaticResponse>,
12    /// HTTP route the component will be invoked for
13    pub route: HttpTriggerRouteConfig,
14    /// The HTTP executor the component requires
15    #[serde(default)]
16    pub executor: Option<HttpExecutorType>,
17}
18
19impl HttpTriggerConfig {
20    pub fn lookup_key(&self, trigger_id: &str) -> anyhow::Result<crate::routes::TriggerLookupKey> {
21        match (&self.component, &self.static_response) {
22            (None, None) => Err(anyhow::anyhow!("Triggers must specify either component or static_response - {trigger_id} has neither")),
23            (Some(_), Some(_)) => Err(anyhow::anyhow!("Triggers must specify either component or static_response - {trigger_id} has both")),
24            (Some(c), None) => Ok(crate::routes::TriggerLookupKey::Component(c.to_string())),
25            (None, Some(_)) => Ok(crate::routes::TriggerLookupKey::Trigger(trigger_id.to_string())),
26        }
27    }
28}
29
30/// The executor for the HTTP component.
31/// The component can either implement the Spin HTTP interface,
32/// the `wasi-http` interface, or the Wagi CGI interface.
33///
34/// If an executor is not specified, the inferred default is `HttpExecutor::Spin`.
35#[derive(Clone, Debug, Default, Deserialize, Serialize)]
36#[serde(deny_unknown_fields, rename_all = "kebab-case", tag = "type")]
37pub enum HttpExecutorType {
38    /// The component implements an HTTP based interface.
39    ///
40    /// This can be either `fermyon:spin/inbound-http` or `wasi:http/incoming-handler`
41    #[default]
42    #[serde(alias = "spin")]
43    Http,
44    /// The component implements the Wagi CGI interface.
45    Wagi(WagiTriggerConfig),
46    /// The component implements the WASIp3 HTTP interface (unstable).
47    Wasip3Unstable,
48}
49
50/// Wagi specific configuration for the http executor.
51#[derive(Clone, Debug, Deserialize, Serialize)]
52#[serde(default, deny_unknown_fields)]
53pub struct WagiTriggerConfig {
54    /// The name of the entrypoint. (DEPRECATED)
55    #[serde(skip_serializing)]
56    pub entrypoint: String,
57
58    /// A string representation of the argv array.
59    ///
60    /// This should be a space-separate list of strings. The value
61    /// ${SCRIPT_NAME} will be replaced with the Wagi SCRIPT_NAME,
62    /// and the value ${ARGS} will be replaced with the query parameter
63    /// name/value pairs presented as args. For example,
64    /// `param1=val1&param2=val2` will become `param1=val1 param2=val2`,
65    /// which will then be presented to the program as two arguments
66    /// in argv.
67    pub argv: String,
68}
69
70impl Default for WagiTriggerConfig {
71    fn default() -> Self {
72        /// This is the default Wagi entrypoint.
73        const WAGI_DEFAULT_ENTRYPOINT: &str = "_start";
74        const WAGI_DEFAULT_ARGV: &str = "${SCRIPT_NAME} ${ARGS}";
75
76        Self {
77            entrypoint: WAGI_DEFAULT_ENTRYPOINT.to_owned(),
78            argv: WAGI_DEFAULT_ARGV.to_owned(),
79        }
80    }
81}
82
83/// A static response to be served directly by the host
84/// without instantiating a component.
85#[derive(Clone, Debug, Deserialize, Serialize)]
86pub struct StaticResponse {
87    #[serde(default)]
88    status_code: Option<u16>,
89    #[serde(default)]
90    headers: indexmap::IndexMap<String, String>,
91    #[serde(default)]
92    body: Option<String>,
93}
94
95impl StaticResponse {
96    pub fn status(&self) -> u16 {
97        self.status_code.unwrap_or(200)
98    }
99
100    pub fn headers(&self) -> impl Iterator<Item = (&String, &String)> {
101        self.headers.iter()
102    }
103
104    pub fn body(&self) -> Option<&String> {
105        self.body.as_ref()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn wagi_config_smoke_test() {
115        let HttpExecutorType::Wagi(config) = toml::toml! { type = "wagi" }.try_into().unwrap()
116        else {
117            panic!("wrong type");
118        };
119        assert_eq!(config.entrypoint, "_start");
120        assert_eq!(config.argv, "${SCRIPT_NAME} ${ARGS}");
121    }
122}