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(&self, ctx: Context) -> PluginResult {
56        let ctx = self.runner.run(ctx)?;
57        Ok(PluginOutput::success(ctx))
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use crate::context::{GatewayRequest, GatewayResponse, Protocol};
65
66    fn test_context() -> Context {
67        Context {
68            request: GatewayRequest {
69                method: "GET".to_string(),
70                path: "/test".to_string(),
71                host: "localhost".to_string(),
72                scheme: "http".to_string(),
73                headers: HashMap::new(),
74                query_params: HashMap::new(),
75                body: bytes::Bytes::new(),
76                remote_addr: "127.0.0.1:1234".to_string(),
77                protocol: Protocol::Http1,
78            },
79            response: GatewayResponse {
80                status_code: 200,
81                headers: HashMap::new(),
82                body: bytes::Bytes::new(),
83                stream: None,
84            },
85            message: HashMap::new(),
86            errors: Vec::new(),
87        }
88    }
89
90    fn cfg(functions: serde_json::Value) -> HashMap<String, serde_json::Value> {
91        let mut c = HashMap::new();
92        c.insert("functions".to_string(), functions);
93        c
94    }
95
96    #[tokio::test]
97    async fn test_serverless_post_function_mutates_response() {
98        let p = ServerlessPostFunctionPlugin::from_config(&cfg(serde_json::json!([
99            "function execute(ctx)\n  ctx.response.headers[\"x-served-by\"] = {\"featherbit\"}\n  return ctx\nend"
100        ])))
101        .unwrap();
102        let out = p.execute(test_context()).await.unwrap();
103        assert_eq!(
104            out.context.response.headers.get("x-served-by"),
105            Some(&vec!["featherbit".to_string()])
106        );
107        assert_eq!(p.plugin_type(), "serverless-post-function");
108    }
109
110    #[test]
111    fn test_serverless_post_function_empty_rejected() {
112        assert!(ServerlessPostFunctionPlugin::from_config(&HashMap::new()).is_err());
113    }
114}