featherbit/plugins/native/
store_set.rs1use async_trait::async_trait;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use crate::context::Context;
8use crate::plugins::resources::PluginResources;
9use crate::plugins::util::store_kv::{self, StoreHandle};
10use crate::plugins::{Plugin, PluginResult};
11use crate::vars::template::Template;
12
13#[cfg(feature = "redis-store")]
14use crate::plugins::PluginOutput;
15
16pub struct StoreSetPlugin {
17 store: StoreHandle,
18 key: Template,
19 value: Template,
20 ttl_seconds: Option<u64>,
21}
22
23impl std::fmt::Debug for StoreSetPlugin {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct("StoreSetPlugin")
34 .field("store", &self.store)
35 .field("key", &self.key)
36 .field("value", &self.value)
37 .field("ttl_seconds", &self.ttl_seconds)
38 .finish()
39 }
40}
41
42impl StoreSetPlugin {
43 pub fn from_config(
50 config: &HashMap<String, serde_json::Value>,
51 resources: &Arc<PluginResources>,
52 ) -> Result<Self, String> {
53 let key = store_kv::required_template(config, "key", "store-set")?;
54 let value = store_kv::required_template(config, "value", "store-set")?;
55 let ttl_seconds = store_kv::optional_ttl(config, "store-set")?;
56 Ok(Self {
57 store: store_kv::resolve(config, resources, "store-set")?,
58 key,
59 value,
60 ttl_seconds,
61 })
62 }
63}
64
65#[async_trait]
66impl Plugin for StoreSetPlugin {
67 fn plugin_type(&self) -> &str {
68 "store-set"
69 }
70
71 fn reads_response_body(&self) -> bool {
72 self.key.references_response_body() || self.value.references_response_body()
73 }
74
75 #[cfg(feature = "redis-store")]
76 async fn execute(&self, ctx: Context) -> PluginResult {
77 use redis::AsyncCommands;
78
79 let rendered = self.key.render(&ctx).to_string();
80 if rendered.is_empty() {
81 return Err(store_kv::key_invalid(
82 ctx,
83 "store-set",
84 "SET",
85 &self.store.name,
86 ));
87 }
88 let key = self.store.key_for(&rendered);
89 let value = self.value.render(&ctx).to_string();
90
91 let mut conn = match self.store.conn().await {
92 Ok(c) => c,
93 Err(e) => {
94 return Err(store_kv::store_error(
95 ctx,
96 "store-set",
97 "SET",
98 &self.store.name,
99 e,
100 ))
101 }
102 };
103
104 let result = match self.ttl_seconds {
105 Some(ttl) => conn.set_ex::<_, _, ()>(&key, value, ttl).await,
106 None => conn.set::<_, _, ()>(&key, value).await,
107 };
108
109 match result {
110 Ok(()) => Ok(PluginOutput::success(ctx)),
111 Err(e) => Err(store_kv::store_error(
112 ctx,
113 "store-set",
114 "SET",
115 &self.store.name,
116 e.to_string(),
117 )),
118 }
119 }
120
121 #[cfg(not(feature = "redis-store"))]
122 async fn execute(&self, ctx: Context) -> PluginResult {
123 Err(store_kv::store_error(
124 ctx,
125 "store-set",
126 "SET",
127 &self.store.name,
128 "built without the redis-store feature".to_string(),
129 ))
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::plugins::resources::PluginResources;
137 use std::collections::HashMap;
138
139 fn cfg(json: serde_json::Value) -> HashMap<String, serde_json::Value> {
140 serde_json::from_value(json).unwrap()
141 }
142
143 #[test]
144 fn test_requires_a_value() {
145 let r = PluginResources::empty();
146 let err =
147 StoreSetPlugin::from_config(&cfg(serde_json::json!({ "store": "s", "key": "k" })), &r)
148 .unwrap_err();
149 assert!(err.contains("value"), "{err}");
150 }
151
152 #[test]
155 fn test_rejects_zero_ttl() {
156 let r = PluginResources::empty();
157 let err = StoreSetPlugin::from_config(
158 &cfg(serde_json::json!({ "store": "s", "key": "k", "value": "1", "ttl_seconds": 0 })),
159 &r,
160 )
161 .unwrap_err();
162 assert!(err.contains("ttl_seconds"), "{err}");
163 }
164}