openzeppelin_relayer/models/
plugin.rs1use std::{collections::HashMap, time::Duration};
2
3use serde::{Deserialize, Deserializer, Serialize};
4use serde_json::Map;
5use utoipa::ToSchema;
6
7fn deserialize_double_option<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
12where
13 D: Deserializer<'de>,
14 T: Deserialize<'de>,
15{
16 Ok(Some(Option::deserialize(deserializer)?))
17}
18
19use crate::constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS;
20
21#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
22pub struct PluginModel {
23 pub id: String,
25 pub path: String,
27 #[schema(value_type = u64)]
29 pub timeout: Duration,
30 #[serde(default)]
32 pub emit_logs: bool,
33 #[serde(default)]
35 pub emit_traces: bool,
36 #[serde(default)]
38 pub raw_response: bool,
39 #[serde(default)]
41 pub allow_get_invocation: bool,
42 #[serde(default)]
44 pub config: Option<Map<String, serde_json::Value>>,
45 #[serde(default)]
47 pub forward_logs: bool,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
51pub struct PluginCallRequest {
52 #[serde(default)]
54 pub params: serde_json::Value,
55 #[serde(default, skip_deserializing)]
57 pub headers: Option<HashMap<String, Vec<String>>>,
58 #[serde(default, skip_deserializing)]
60 pub route: Option<String>,
61 #[serde(default, skip_deserializing)]
63 pub method: Option<String>,
64 #[serde(default, skip_deserializing)]
66 pub query: Option<HashMap<String, Vec<String>>>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema)]
73#[serde(deny_unknown_fields)]
74pub struct UpdatePluginRequest {
75 #[schema(value_type = Option<u64>)]
77 pub timeout: Option<u64>,
78 pub emit_logs: Option<bool>,
80 pub emit_traces: Option<bool>,
82 pub raw_response: Option<bool>,
84 pub allow_get_invocation: Option<bool>,
86 #[serde(
89 default,
90 skip_serializing_if = "Option::is_none",
91 deserialize_with = "deserialize_double_option"
92 )]
93 pub config: Option<Option<Map<String, serde_json::Value>>>,
94 pub forward_logs: Option<bool>,
96}
97
98#[derive(Debug, thiserror::Error)]
100pub enum PluginValidationError {
101 #[error("Invalid timeout: {0}")]
102 InvalidTimeout(String),
103}
104
105impl PluginModel {
106 pub fn apply_update(&self, update: UpdatePluginRequest) -> Result<Self, PluginValidationError> {
109 let mut updated = self.clone();
110
111 if let Some(timeout_secs) = update.timeout {
112 if timeout_secs == 0 {
113 return Err(PluginValidationError::InvalidTimeout(
114 "Timeout must be greater than 0".to_string(),
115 ));
116 }
117 updated.timeout = Duration::from_secs(timeout_secs);
118 }
119
120 if let Some(emit_logs) = update.emit_logs {
121 updated.emit_logs = emit_logs;
122 }
123
124 if let Some(emit_traces) = update.emit_traces {
125 updated.emit_traces = emit_traces;
126 }
127
128 if let Some(raw_response) = update.raw_response {
129 updated.raw_response = raw_response;
130 }
131
132 if let Some(allow_get_invocation) = update.allow_get_invocation {
133 updated.allow_get_invocation = allow_get_invocation;
134 }
135
136 if let Some(config) = update.config {
141 updated.config = config;
142 }
143
144 if let Some(forward_logs) = update.forward_logs {
145 updated.forward_logs = forward_logs;
146 }
147
148 Ok(updated)
149 }
150}
151
152impl Default for PluginModel {
153 fn default() -> Self {
154 Self {
155 id: String::new(),
156 path: String::new(),
157 timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
158 emit_logs: false,
159 emit_traces: false,
160 raw_response: false,
161 allow_get_invocation: false,
162 config: None,
163 forward_logs: false,
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn create_test_plugin() -> PluginModel {
173 PluginModel {
174 id: "test-plugin".to_string(),
175 path: "plugins/test.ts".to_string(),
176 timeout: Duration::from_secs(30),
177 emit_logs: false,
178 emit_traces: false,
179 raw_response: false,
180 allow_get_invocation: false,
181 config: None,
182 forward_logs: false,
183 }
184 }
185
186 #[test]
187 fn test_apply_update_timeout() {
188 let plugin = create_test_plugin();
189 let update = UpdatePluginRequest {
190 timeout: Some(60),
191 ..Default::default()
192 };
193
194 let updated = plugin.apply_update(update).unwrap();
195 assert_eq!(updated.timeout, Duration::from_secs(60));
196 assert!(!updated.emit_logs);
198 }
199
200 #[test]
201 fn test_apply_update_timeout_zero_fails() {
202 let plugin = create_test_plugin();
203 let update = UpdatePluginRequest {
204 timeout: Some(0),
205 ..Default::default()
206 };
207
208 let result = plugin.apply_update(update);
209 assert!(result.is_err());
210 }
211
212 #[test]
213 fn test_apply_update_all_fields() {
214 let plugin = create_test_plugin();
215 let mut config_map = Map::new();
216 config_map.insert("key".to_string(), serde_json::json!("value"));
217
218 let update = UpdatePluginRequest {
219 timeout: Some(120),
220 emit_logs: Some(true),
221 emit_traces: Some(true),
222 raw_response: Some(true),
223 allow_get_invocation: Some(true),
224 config: Some(Some(config_map.clone())),
225 forward_logs: Some(true),
226 };
227
228 let updated = plugin.apply_update(update).unwrap();
229 assert_eq!(updated.timeout, Duration::from_secs(120));
230 assert!(updated.emit_logs);
231 assert!(updated.emit_traces);
232 assert!(updated.raw_response);
233 assert!(updated.allow_get_invocation);
234 assert_eq!(updated.config, Some(config_map));
235 assert!(updated.forward_logs);
236 }
237
238 #[test]
239 fn test_apply_update_clear_config() {
240 let mut plugin = create_test_plugin();
241 let mut config_map = Map::new();
242 config_map.insert("key".to_string(), serde_json::json!("value"));
243 plugin.config = Some(config_map);
244
245 let update = UpdatePluginRequest {
247 config: Some(None),
248 ..Default::default()
249 };
250
251 let updated = plugin.apply_update(update).unwrap();
252 assert!(updated.config.is_none());
253 }
254
255 #[test]
256 fn test_apply_update_no_changes() {
257 let plugin = create_test_plugin();
258 let update = UpdatePluginRequest::default();
259
260 let updated = plugin.apply_update(update).unwrap();
261 assert_eq!(updated.id, plugin.id);
262 assert_eq!(updated.path, plugin.path);
263 assert_eq!(updated.timeout, plugin.timeout);
264 }
265
266 #[test]
269 fn test_deserialize_config_absent() {
270 let req: UpdatePluginRequest = serde_json::from_str("{}").unwrap();
271 assert_eq!(req.config, None, "Absent field should deserialize to None");
272 }
273
274 #[test]
275 fn test_deserialize_config_null() {
276 let req: UpdatePluginRequest = serde_json::from_str(r#"{"config":null}"#).unwrap();
277 assert_eq!(
278 req.config,
279 Some(None),
280 "Null field should deserialize to Some(None)"
281 );
282 }
283
284 #[test]
285 fn test_deserialize_config_object() {
286 let req: UpdatePluginRequest =
287 serde_json::from_str(r#"{"config":{"suite":"integration"}}"#).unwrap();
288 assert!(
289 matches!(req.config, Some(Some(_))),
290 "Object field should deserialize to Some(Some(...))"
291 );
292 let map = req.config.unwrap().unwrap();
293 assert_eq!(map.get("suite"), Some(&serde_json::json!("integration")));
294 }
295}