automaat_processor_redis_command/
lib.rs1#![deny(
44 clippy::all,
45 clippy::cargo,
46 clippy::nursery,
47 clippy::pedantic,
48 deprecated_in_future,
49 future_incompatible,
50 missing_docs,
51 nonstandard_style,
52 rust_2018_idioms,
53 rustdoc,
54 warnings,
55 unused_results,
56 unused_qualifications,
57 unused_lifetimes,
58 unused_import_braces,
59 unsafe_code,
60 unreachable_pub,
61 trivial_casts,
62 trivial_numeric_casts,
63 missing_debug_implementations,
64 missing_copy_implementations
65)]
66#![warn(variant_size_differences)]
67#![allow(clippy::multiple_crate_versions, missing_doc_code_examples)]
68#![doc(html_root_url = "https://docs.rs/automaat-processor-redis-command/0.1.0")]
69
70use automaat_core::{Context, Processor};
71use redis::RedisError;
72use serde::{Deserialize, Serialize};
73use std::{error, fmt, str::from_utf8};
74use url::Url;
75
76#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
78#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
79pub struct RedisCommand {
80 pub command: String,
86
87 pub arguments: Option<Vec<String>>,
92
93 #[serde(with = "url_serde")]
100 pub url: Url,
101}
102
103#[cfg(feature = "juniper")]
112#[graphql(name = "RedisCommandInput")]
113#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, juniper::GraphQLInputObject)]
114pub struct Input {
115 command: String,
116 arguments: Option<Vec<String>>,
117 #[serde(with = "url_serde")]
118 url: Url,
119}
120
121#[cfg(feature = "juniper")]
122impl From<Input> for RedisCommand {
123 fn from(input: Input) -> Self {
124 Self {
125 command: input.command,
126 arguments: input.arguments,
127 url: input.url,
128 }
129 }
130}
131
132impl<'a> Processor<'a> for RedisCommand {
133 const NAME: &'static str = "Redis Command";
134
135 type Error = Error;
136 type Output = String;
137
138 fn run(&self, _context: &Context) -> Result<Option<Self::Output>, Self::Error> {
156 use redis::Value;
157
158 let client = redis::Client::open(self.url.as_str())?;
159 let conn = client.get_connection()?;
160 let args = self.arguments.clone().unwrap_or_else(Default::default);
161
162 redis::cmd(self.command.as_str())
163 .arg(args)
164 .query(&conn)
165 .map_err(Into::into)
166 .map(|v| match v {
167 Value::Nil => None,
168 Value::Status(string) => Some(string),
169 Value::Data(ref val) => match from_utf8(val) {
170 Ok(string) => Some(string.to_owned()),
171 Err(_) => Some(format!("{:?}", val)),
172 },
173 other => Some(format!("{:?}", other)),
174 })
175 }
176}
177
178#[derive(Debug)]
183pub enum Error {
184 Response(RedisError),
186
187 AuthenticationFailed(RedisError),
189
190 Type(RedisError),
192
193 ExecAbort(RedisError),
195
196 BusyLoading(RedisError),
198
199 NoScript(RedisError),
201
202 InvalidClientConfig(RedisError),
204
205 Io(RedisError),
208
209 Extension(RedisError),
212
213 #[doc(hidden)]
214 __Unknown, }
216
217impl fmt::Display for Error {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 match *self {
220 Error::Response(ref err)
221 | Error::AuthenticationFailed(ref err)
222 | Error::Type(ref err)
223 | Error::ExecAbort(ref err)
224 | Error::BusyLoading(ref err)
225 | Error::NoScript(ref err)
226 | Error::InvalidClientConfig(ref err)
227 | Error::Io(ref err)
228 | Error::Extension(ref err) => write!(f, "Redis error: {}", err),
229 Error::__Unknown => unreachable!(),
230 }
231 }
232}
233
234impl error::Error for Error {
235 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
236 match *self {
237 Error::Response(ref err)
238 | Error::AuthenticationFailed(ref err)
239 | Error::Type(ref err)
240 | Error::ExecAbort(ref err)
241 | Error::BusyLoading(ref err)
242 | Error::NoScript(ref err)
243 | Error::InvalidClientConfig(ref err)
244 | Error::Io(ref err)
245 | Error::Extension(ref err) => Some(err),
246 Error::__Unknown => unreachable!(),
247 }
248 }
249}
250
251impl From<RedisError> for Error {
252 fn from(err: RedisError) -> Self {
253 use redis::ErrorKind;
254
255 match err.kind() {
256 ErrorKind::ResponseError => Error::Response(err),
257 ErrorKind::AuthenticationFailed => Error::AuthenticationFailed(err),
258 ErrorKind::TypeError => Error::Type(err),
259 ErrorKind::ExecAbortError => Error::ExecAbort(err),
260 ErrorKind::BusyLoadingError => Error::BusyLoading(err),
261 ErrorKind::NoScriptError => Error::NoScript(err),
262 ErrorKind::InvalidClientConfig => Error::InvalidClientConfig(err),
263 ErrorKind::IoError => Error::Io(err),
264 ErrorKind::ExtensionError => Error::Extension(err),
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 fn processor_stub() -> RedisCommand {
274 RedisCommand {
275 command: "PING".to_owned(),
276 arguments: None,
277 url: Url::parse("redis://127.0.0.1").unwrap(),
278 }
279 }
280
281 mod run {
282 use super::*;
283
284 #[test]
285 fn test_command() {
286 let mut processor = processor_stub();
287 processor.command = "PING".to_owned();
288
289 let context = Context::new().unwrap();
290 let output = processor.run(&context).unwrap();
291
292 assert_eq!(output, Some("PONG".to_owned()))
293 }
294
295 #[test]
296 fn test_command_and_arguments() {
297 let mut processor = processor_stub();
298 processor.command = "PING".to_owned();
299 processor.arguments = Some(vec!["hello world".to_owned()]);
300
301 let context = Context::new().unwrap();
302 let output = processor.run(&context).unwrap();
303
304 assert_eq!(output, Some("hello world".to_owned()))
305 }
306
307 #[test]
308 fn test_unknown_command() {
309 let mut processor = processor_stub();
310 processor.command = "UNKNOWN".to_owned();
311
312 let context = Context::new().unwrap();
313 let error = processor.run(&context).unwrap_err();
314
315 assert!(error.to_string().contains("unknown command `UNKNOWN`"));
316 }
317 }
318
319 #[test]
320 fn test_readme_deps() {
321 version_sync::assert_markdown_deps_updated!("README.md");
322 }
323
324 #[test]
325 fn test_html_root_url() {
326 version_sync::assert_html_root_url_updated!("src/lib.rs");
327 }
328}