Skip to main content

featherbit/plugins/native/
store_delete.rs

1//! `store-delete` — removes a key from a named store.
2//!
3//! Deleting a key that does not exist is a success. This is the one place the
4//! design deliberately does not mirror `store-get`: a `miss` port here would be
5//! mandatory-wired in every policy that clears state, to signal something
6//! almost no caller acts on.
7
8use async_trait::async_trait;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use crate::context::Context;
13use crate::plugins::resources::PluginResources;
14use crate::plugins::util::store_kv::{self, StoreHandle};
15use crate::plugins::{Plugin, PluginResult};
16use crate::vars::template::Template;
17
18#[cfg(feature = "redis-store")]
19use crate::plugins::PluginOutput;
20
21#[derive(Debug)]
22pub struct StoreDeletePlugin {
23    store: StoreHandle,
24    key: Template,
25}
26
27impl StoreDeletePlugin {
28    /// Accepted keys:
29    /// - `store` (string, required): a declared `stores:` entry.
30    /// - `key` (string, required, templated): the key to delete.
31    pub fn from_config(
32        config: &HashMap<String, serde_json::Value>,
33        resources: &Arc<PluginResources>,
34    ) -> Result<Self, String> {
35        let key = store_kv::required_template(config, "key", "store-delete")?;
36        Ok(Self {
37            store: store_kv::resolve(config, resources, "store-delete")?,
38            key,
39        })
40    }
41}
42
43#[async_trait]
44impl Plugin for StoreDeletePlugin {
45    fn plugin_type(&self) -> &str {
46        "store-delete"
47    }
48
49    fn reads_response_body(&self) -> bool {
50        self.key.references_response_body()
51    }
52
53    #[cfg(feature = "redis-store")]
54    async fn execute(&self, ctx: Context) -> PluginResult {
55        use redis::AsyncCommands;
56
57        let rendered = self.key.render(&ctx).to_string();
58        if rendered.is_empty() {
59            return Err(store_kv::key_invalid(
60                ctx,
61                "store-delete",
62                "DEL",
63                &self.store.name,
64            ));
65        }
66        let key = self.store.key_for(&rendered);
67
68        let mut conn = match self.store.conn().await {
69            Ok(c) => c,
70            Err(e) => {
71                return Err(store_kv::store_error(
72                    ctx,
73                    "store-delete",
74                    "DEL",
75                    &self.store.name,
76                    e,
77                ))
78            }
79        };
80
81        // DEL returns how many keys it removed; 0 is not a failure.
82        match conn.del::<_, u64>(&key).await {
83            Ok(_) => Ok(PluginOutput::success(ctx)),
84            Err(e) => Err(store_kv::store_error(
85                ctx,
86                "store-delete",
87                "DEL",
88                &self.store.name,
89                e.to_string(),
90            )),
91        }
92    }
93
94    #[cfg(not(feature = "redis-store"))]
95    async fn execute(&self, ctx: Context) -> PluginResult {
96        Err(store_kv::store_error(
97            ctx,
98            "store-delete",
99            "DEL",
100            &self.store.name,
101            "built without the redis-store feature".to_string(),
102        ))
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::plugins::resources::PluginResources;
110    use std::collections::HashMap;
111
112    fn cfg(json: serde_json::Value) -> HashMap<String, serde_json::Value> {
113        serde_json::from_value(json).unwrap()
114    }
115
116    #[test]
117    fn test_requires_a_key() {
118        let r = PluginResources::empty();
119        let err = StoreDeletePlugin::from_config(&cfg(serde_json::json!({ "store": "s" })), &r)
120            .unwrap_err();
121        assert!(err.contains("key"), "{err}");
122    }
123}