Skip to main content

featherbit/plugins/native/
serverless_post_function.rs

1//! The `serverless-post-function` node — runs one or more inline Lua functions
2//! against the `Context`, threading it through each in sequence.
3//!
4//! Port of APISIX's `serverless-post-function` plugin. It shares **all** logic
5//! with [`serverless-pre-function`](super::serverless_pre_function) via the
6//! shared [`ServerlessRunner`]; the only difference is the registered
7//! node-type name (and, by convention, its placement in the policy graph:
8//! after the `upstream` node rather than before it).
9//!
10//! See [`super::serverless_pre_function`] for the full documentation of the
11//! function contract and the deviations from APISIX (the `execute(ctx)`
12//! contract and phase-by-graph-position).
13
14use async_trait::async_trait;
15use std::collections::HashMap;
16
17use crate::context::Context;
18use crate::plugins::native::serverless_pre_function::ServerlessRunner;
19use crate::plugins::{Plugin, PluginOutput, PluginResult};
20
21/// The `serverless-post-function` node. Runs its Lua functions after the
22/// upstream call (by convention of its graph placement).
23pub struct ServerlessPostFunctionPlugin {
24    runner: ServerlessRunner,
25}
26
27impl ServerlessPostFunctionPlugin {
28    /// Builds the plugin from node config. Same config shape as
29    /// [`serverless-pre-function`](super::serverless_pre_function::ServerlessPreFunctionPlugin::from_config).
30    ///
31    /// ```yaml
32    /// type: serverless-post-function
33    /// config:
34    ///   phase: body_filter     # accepted for compatibility; inert
35    ///   functions:
36    ///     - |
37    ///       function execute(ctx)
38    ///         ctx.response.headers["x-served-by"] = {"featherbit"}
39    ///         return ctx
40    ///       end
41    /// ```
42    pub fn from_config(config: &HashMap<String, serde_json::Value>) -> Result<Self, String> {
43        Ok(Self {
44            runner: ServerlessRunner::from_config(config, "serverless-post-function")?,
45        })
46    }
47}
48
49#[async_trait]
50impl Plugin for ServerlessPostFunctionPlugin {
51    fn plugin_type(&self) -> &str {
52        "serverless-post-function"
53    }
54
55    async fn execute(
56        &self,
57        ctx: Context,
58        _named_inputs: &HashMap<String, serde_json::Value>,
59    ) -> PluginResult {
60        let ctx = self.runner.run(ctx)?;
61        Ok(PluginOutput {
62            context: ctx,
63            named_outputs: HashMap::new(),
64        })
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::context::{GatewayRequest, GatewayResponse, Protocol};
72
73    fn test_context() -> Context {
74        Context {
75            request: GatewayRequest {
76                method: "GET".to_string(),
77                path: "/test".to_string(),
78                host: "localhost".to_string(),
79                scheme: "http".to_string(),
80                headers: HashMap::new(),
81                query_params: HashMap::new(),
82                body: bytes::Bytes::new(),
83                remote_addr: "127.0.0.1:1234".to_string(),
84                protocol: Protocol::Http1,
85            },
86            response: GatewayResponse {
87                status_code: 200,
88                headers: HashMap::new(),
89                body: bytes::Bytes::new(),
90            },
91            message: HashMap::new(),
92            errors: Vec::new(),
93        }
94    }
95
96    fn cfg(functions: serde_json::Value) -> HashMap<String, serde_json::Value> {
97        let mut c = HashMap::new();
98        c.insert("functions".to_string(), functions);
99        c
100    }
101
102    #[tokio::test]
103    async fn test_serverless_post_function_mutates_response() {
104        let p = ServerlessPostFunctionPlugin::from_config(&cfg(serde_json::json!([
105            "function execute(ctx)\n  ctx.response.headers[\"x-served-by\"] = {\"featherbit\"}\n  return ctx\nend"
106        ])))
107        .unwrap();
108        let out = p.execute(test_context(), &HashMap::new()).await.unwrap();
109        assert_eq!(
110            out.context.response.headers.get("x-served-by"),
111            Some(&vec!["featherbit".to_string()])
112        );
113        assert_eq!(p.plugin_type(), "serverless-post-function");
114    }
115
116    #[test]
117    fn test_serverless_post_function_empty_rejected() {
118        assert!(ServerlessPostFunctionPlugin::from_config(&HashMap::new()).is_err());
119    }
120}