#![cfg(feature = "script")]
use sha1_smol::Sha1;
use crate::cmd::cmd;
use crate::connection::ConnectionLike;
use crate::types::{ErrorKind, FromRedisValue, RedisResult, ToRedisArgs};
use crate::Cmd;
#[derive(Debug, Clone)]
pub struct Script {
code: String,
hash: String,
}
impl Script {
pub fn new(code: &str) -> Script {
let mut hash = Sha1::new();
hash.update(code.as_bytes());
Script {
code: code.to_string(),
hash: hash.digest().to_string(),
}
}
pub fn get_hash(&self) -> &str {
&self.hash
}
#[inline]
pub fn key<T: ToRedisArgs>(&self, key: T) -> ScriptInvocation<'_> {
ScriptInvocation {
script: self,
args: vec![],
keys: key.to_redis_args(),
}
}
#[inline]
pub fn arg<T: ToRedisArgs>(&self, arg: T) -> ScriptInvocation<'_> {
ScriptInvocation {
script: self,
args: arg.to_redis_args(),
keys: vec![],
}
}
#[inline]
pub fn prepare_invoke(&self) -> ScriptInvocation<'_> {
ScriptInvocation {
script: self,
args: vec![],
keys: vec![],
}
}
#[inline]
pub fn invoke<T: FromRedisValue>(&self, con: &mut dyn ConnectionLike) -> RedisResult<T> {
ScriptInvocation {
script: self,
args: vec![],
keys: vec![],
}
.invoke(con)
}
#[inline]
#[cfg(feature = "aio")]
pub async fn invoke_async<C, T>(&self, con: &mut C) -> RedisResult<T>
where
C: crate::aio::ConnectionLike,
T: FromRedisValue,
{
ScriptInvocation {
script: self,
args: vec![],
keys: vec![],
}
.invoke_async(con)
.await
}
}
pub struct ScriptInvocation<'a> {
script: &'a Script,
args: Vec<Vec<u8>>,
keys: Vec<Vec<u8>>,
}
impl<'a> ScriptInvocation<'a> {
#[inline]
pub fn arg<'b, T: ToRedisArgs>(&'b mut self, arg: T) -> &'b mut ScriptInvocation<'a>
where
'a: 'b,
{
arg.write_redis_args(&mut self.args);
self
}
#[inline]
pub fn key<'b, T: ToRedisArgs>(&'b mut self, key: T) -> &'b mut ScriptInvocation<'a>
where
'a: 'b,
{
key.write_redis_args(&mut self.keys);
self
}
#[inline]
pub fn invoke<T: FromRedisValue>(&self, con: &mut dyn ConnectionLike) -> RedisResult<T> {
let eval_cmd = self.eval_cmd();
match eval_cmd.query(con) {
Ok(val) => Ok(val),
Err(err) => {
if err.kind() == ErrorKind::NoScriptError {
self.load_cmd().exec(con)?;
eval_cmd.query(con)
} else {
Err(err)
}
}
}
}
#[inline]
#[cfg(feature = "aio")]
pub async fn invoke_async<T: FromRedisValue>(
&self,
con: &mut impl crate::aio::ConnectionLike,
) -> RedisResult<T> {
let eval_cmd = self.eval_cmd();
match eval_cmd.query_async(con).await {
Ok(val) => {
Ok(val)
}
Err(err) => {
if err.kind() == ErrorKind::NoScriptError {
self.load_cmd().exec_async(con).await?;
eval_cmd.query_async(con).await
} else {
Err(err)
}
}
}
}
#[inline]
pub fn load(&self, con: &mut dyn ConnectionLike) -> RedisResult<String> {
let hash: String = self.load_cmd().query(con)?;
debug_assert_eq!(hash, self.script.hash);
Ok(hash)
}
#[inline]
#[cfg(feature = "aio")]
pub async fn load_async<C>(&self, con: &mut C) -> RedisResult<String>
where
C: crate::aio::ConnectionLike,
{
let hash: String = self.load_cmd().query_async(con).await?;
debug_assert_eq!(hash, self.script.hash);
Ok(hash)
}
fn load_cmd(&self) -> Cmd {
let mut cmd = cmd("SCRIPT");
cmd.arg("LOAD").arg(self.script.code.as_bytes());
cmd
}
fn estimate_buflen(&self) -> usize {
self
.keys
.iter()
.chain(self.args.iter())
.fold(0, |acc, e| acc + e.len())
+ 7
+ self.script.hash.len()
+ 4
}
pub(crate) fn eval_cmd(&self) -> Cmd {
let args_len = 3 + self.keys.len() + self.args.len();
let mut cmd = Cmd::with_capacity(args_len, self.estimate_buflen());
cmd.arg("EVALSHA")
.arg(self.script.hash.as_bytes())
.arg(self.keys.len())
.arg(&*self.keys)
.arg(&*self.args);
cmd
}
}
#[cfg(test)]
mod tests {
use super::Script;
#[test]
fn script_eval_should_work() {
let script = Script::new("return KEYS[1]");
let invocation = script.key("dummy");
let estimated_buflen = invocation.estimate_buflen();
let cmd = invocation.eval_cmd();
assert!(estimated_buflen >= cmd.capacity().1);
let expected = "*4\r\n$7\r\nEVALSHA\r\n$40\r\n4a2267357833227dd98abdedb8cf24b15a986445\r\n$1\r\n1\r\n$5\r\ndummy\r\n";
assert_eq!(
expected,
std::str::from_utf8(cmd.get_packed_command().as_slice()).unwrap()
);
}
}