mco_redis_rs/script.rs
1#![cfg(feature = "script")]
2use sha1_smol::Sha1;
3
4use crate::cmd::cmd;
5use crate::connection::ConnectionLike;
6use crate::types::{ErrorKind, FromRedisValue, RedisResult, ToRedisArgs};
7
8/// Represents a lua script.
9#[derive(Debug, Clone)]
10pub struct Script {
11 code: String,
12 hash: String,
13}
14
15/// The script object represents a lua script that can be executed on the
16/// redis server. The object itself takes care of automatic uploading and
17/// execution. The script object itself can be shared and is immutable.
18///
19/// Example:
20///
21/// ```rust,no_run
22/// # let client = redis::Client::open("redis://127.0.0.1/").unwrap();
23/// # let mut con = client.get_connection().unwrap();
24/// let script = redis::Script::new(r"
25/// return tonumber(ARGV[1]) + tonumber(ARGV[2]);
26/// ");
27/// let result = script.arg(1).arg(2).invoke(&mut con);
28/// assert_eq!(result, Ok(3));
29/// ```
30impl Script {
31 /// Creates a new script object.
32 pub fn new(code: &str) -> Script {
33 let mut hash = Sha1::new();
34 hash.update(code.as_bytes());
35 Script {
36 code: code.to_string(),
37 hash: hash.digest().to_string(),
38 }
39 }
40
41 /// Returns the script's SHA1 hash in hexadecimal format.
42 pub fn get_hash(&self) -> &str {
43 &self.hash
44 }
45
46 /// Creates a script invocation object with a key filled in.
47 #[inline]
48 pub fn key<T: ToRedisArgs>(&self, key: T) -> ScriptInvocation<'_> {
49 ScriptInvocation {
50 script: self,
51 args: vec![],
52 keys: key.to_redis_args(),
53 }
54 }
55
56 /// Creates a script invocation object with an argument filled in.
57 #[inline]
58 pub fn arg<T: ToRedisArgs>(&self, arg: T) -> ScriptInvocation<'_> {
59 ScriptInvocation {
60 script: self,
61 args: arg.to_redis_args(),
62 keys: vec![],
63 }
64 }
65
66 /// Returns an empty script invocation object. This is primarily useful
67 /// for programmatically adding arguments and keys because the type will
68 /// not change. Normally you can use `arg` and `key` directly.
69 #[inline]
70 pub fn prepare_invoke(&self) -> ScriptInvocation<'_> {
71 ScriptInvocation {
72 script: self,
73 args: vec![],
74 keys: vec![],
75 }
76 }
77
78 /// Invokes the script directly without arguments.
79 #[inline]
80 pub fn invoke<T: FromRedisValue>(&self, con: &mut dyn ConnectionLike) -> RedisResult<T> {
81 ScriptInvocation {
82 script: self,
83 args: vec![],
84 keys: vec![],
85 }
86 .invoke(con)
87 }
88}
89
90/// Represents a prepared script call.
91pub struct ScriptInvocation<'a> {
92 script: &'a Script,
93 args: Vec<Vec<u8>>,
94 keys: Vec<Vec<u8>>,
95}
96
97/// This type collects keys and other arguments for the script so that it
98/// can be then invoked. While the `Script` type itself holds the script,
99/// the `ScriptInvocation` holds the arguments that should be invoked until
100/// it's sent to the server.
101impl<'a> ScriptInvocation<'a> {
102 /// Adds a regular argument to the invocation. This ends up as `ARGV[i]`
103 /// in the script.
104 #[inline]
105 pub fn arg<'b, T: ToRedisArgs>(&'b mut self, arg: T) -> &'b mut ScriptInvocation<'a>
106 where
107 'a: 'b,
108 {
109 arg.write_redis_args(&mut self.args);
110 self
111 }
112
113 /// Adds a key argument to the invocation. This ends up as `KEYS[i]`
114 /// in the script.
115 #[inline]
116 pub fn key<'b, T: ToRedisArgs>(&'b mut self, key: T) -> &'b mut ScriptInvocation<'a>
117 where
118 'a: 'b,
119 {
120 key.write_redis_args(&mut self.keys);
121 self
122 }
123
124 /// Invokes the script and returns the result.
125 #[inline]
126 pub fn invoke<T: FromRedisValue>(&self, con: &mut dyn ConnectionLike) -> RedisResult<T> {
127 loop {
128 match cmd("EVALSHA")
129 .arg(self.script.hash.as_bytes())
130 .arg(self.keys.len())
131 .arg(&*self.keys)
132 .arg(&*self.args)
133 .query(con)
134 {
135 Ok(val) => {
136 return Ok(val);
137 }
138 Err(err) => {
139 if err.kind() == ErrorKind::NoScriptError {
140 cmd("SCRIPT")
141 .arg("LOAD")
142 .arg(self.script.code.as_bytes())
143 .query(con)?;
144 } else {
145 fail!(err);
146 }
147 }
148 }
149 }
150 }
151
152 /// Asynchronously invokes the script and returns the result.
153 #[inline]
154 #[cfg(feature = "aio")]
155 pub async fn invoke_async<C, T>(&self, con: &mut C) -> RedisResult<T>
156 where
157 C: crate::aio::ConnectionLike,
158 T: FromRedisValue,
159 {
160 let mut eval_cmd = cmd("EVALSHA");
161 eval_cmd
162 .arg(self.script.hash.as_bytes())
163 .arg(self.keys.len())
164 .arg(&*self.keys)
165 .arg(&*self.args);
166
167 let mut load_cmd = cmd("SCRIPT");
168 load_cmd.arg("LOAD").arg(self.script.code.as_bytes());
169 match eval_cmd.query_async(con).await {
170 Ok(val) => {
171 // Return the value from the script evaluation
172 Ok(val)
173 }
174 Err(err) => {
175 // Load the script into Redis if the script hash wasn't there already
176 if err.kind() == ErrorKind::NoScriptError {
177 load_cmd.query_async(con).await?;
178 eval_cmd.query_async(con).await
179 } else {
180 Err(err)
181 }
182 }
183 }
184 }
185}