automaat_processor_redis_command/
lib.rs

1//! An [Automaat] processor to execute Redis commands.
2//!
3//! Execute Redis commands in an Automaat-based workflow. The return value of
4//! the Redis command is returned as the output of the processor.
5//!
6//! [Automaat]: automaat_core
7//!
8//! # Examples
9//!
10//! Execute the Redis `PING` command, with the "hello world" argument, and
11//! receive the response back as the output of the run.
12//!
13//! See the [official documentation on `PING`][ping].
14//!
15//! [ping]: https://redis.io/commands/ping
16//!
17//! ```rust
18//! # fn main() -> Result<(), Box<std::error::Error>> {
19//! use automaat_core::{Context, Processor};
20//! use automaat_processor_redis_command::RedisCommand;
21//! use url::Url;
22//!
23//! let context = Context::new()?;
24//! let redis_url = Url::parse("redis://127.0.0.1")?;
25//!
26//! let processor = RedisCommand {
27//!     command: "PING".to_owned(),
28//!     arguments: Some(vec!["hello world".to_owned()]),
29//!     url: redis_url
30//! };
31//!
32//! let output = processor.run(&context)?;
33//!
34//! assert_eq!(output, Some("hello world".to_owned()));
35//! #     Ok(())
36//! # }
37//! ```
38//!
39//! # Package Features
40//!
41//! * `juniper` – creates a set of objects to be used in GraphQL-based
42//!   requests/responses.
43#![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/// The processor configuration.
77#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
78#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
79pub struct RedisCommand {
80    /// The main Redis command to execute.
81    ///
82    /// See the [main Redis documentation] for a list of available commands.
83    ///
84    /// [main Redis documentation]: https://redis.io/commands
85    pub command: String,
86
87    /// The arguments belonging to the main `command`.
88    ///
89    /// The acceptable value of these arguments depends on the command being
90    /// executed.
91    pub arguments: Option<Vec<String>>,
92
93    /// The URL of the Redis server.
94    ///
95    /// See the [redis-rs] "connection parameters" documentation for more
96    /// details.
97    ///
98    /// [redis-rs]: https://docs.rs/redis/latest/redis#connection-parameters
99    #[serde(with = "url_serde")]
100    pub url: Url,
101}
102
103/// The GraphQL [Input Object][io] used to initialize the processor via an API.
104///
105/// [`RedisCommand`] implements `From<Input>`, so you can directly initialize
106/// the processor using this type.
107///
108/// _requires the `juniper` package feature to be enabled_
109///
110/// [io]: https://graphql.github.io/graphql-spec/June2018/#sec-Input-Objects
111#[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    /// Run the configured Redis command, and return its results.
139    ///
140    /// # Output
141    ///
142    /// The value returned by the Redis server is fairly untyped, and not always
143    /// easily represented in the final output. In general, the most common
144    /// values are correctly mapped, such as `Nil` becoming `None`, and all
145    /// valid UTF-8 data is returned as `Some`, containing the data as a string.
146    ///
147    /// Any value that cannot be coerced into a valid UTF-8 string, is
148    /// represented in the best possible way as a valid UTF-8 string, but won't
149    /// completely match the original output of Redis.
150    ///
151    /// # Errors
152    ///
153    /// See the [`Error`] enum for all possible error values that can be
154    /// returned. These values wrap the [`redis::ErrorKind`] values.
155    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/// Represents all the ways that [`RedisCommand`] can fail.
179///
180/// This type is not intended to be exhaustively matched, and new variants may
181/// be added in the future without a major version bump.
182#[derive(Debug)]
183pub enum Error {
184    /// The server generated an invalid response.
185    Response(RedisError),
186
187    /// The authentication with the server failed.
188    AuthenticationFailed(RedisError),
189
190    /// Operation failed because of a type mismatch.
191    Type(RedisError),
192
193    /// A script execution was aborted.
194    ExecAbort(RedisError),
195
196    /// The server cannot response because it's loading a dump.
197    BusyLoading(RedisError),
198
199    /// A script that was requested does not actually exist.
200    NoScript(RedisError),
201
202    /// An error that was caused because the parameter to the client were wrong.
203    InvalidClientConfig(RedisError),
204
205    /// This kind is returned if the redis error is one that is not native to
206    /// the system. This is usually the case if the cause is another error.
207    Io(RedisError),
208
209    /// An extension error. This is an error created by the server that is not
210    /// directly understood by the library.
211    Extension(RedisError),
212
213    #[doc(hidden)]
214    __Unknown, // Match against _ instead, more variants may be added in the future.
215}
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}