use crate::commands::core::AsyncCommands;
use redis::{ErrorKind, FromRedisValue, RedisResult, ToRedisArgs, cmd};
#[derive(Debug, Clone)]
pub struct Script {
code: String,
hash: String,
}
impl Script {
pub fn new(code: &str) -> Script {
let mut sha1 = sha1_smol::Sha1::new();
sha1.update(code.as_bytes());
Script {
code: code.to_string(),
hash: sha1.digest().to_string(),
}
}
pub fn get_hash(&self) -> &str {
&self.hash
}
#[must_use]
pub fn arg<'a, T: ToRedisArgs>(&'a self, arg: T) -> ScriptInvocation<'a> {
let mut invocation = self.prepare_invoke();
invocation.arg(arg);
invocation
}
#[must_use]
pub fn key<'a, T: ToRedisArgs>(&'a self, key: T) -> ScriptInvocation<'a> {
let mut invocation = self.prepare_invoke();
invocation.key(key);
invocation
}
#[must_use]
pub fn prepare_invoke(&self) -> ScriptInvocation<'_> {
ScriptInvocation {
script: self,
args: Vec::new(),
keys: Vec::new(),
}
}
pub async fn invoke_async<C: AsyncCommands, T: FromRedisValue>(
&self,
con: &C,
) -> RedisResult<T> {
self.prepare_invoke().invoke_async(con).await
}
#[cfg(feature = "sync")]
pub fn invoke<C: crate::commands::core::Commands, T: FromRedisValue>(
&self,
con: &C,
) -> RedisResult<T> {
self.prepare_invoke().invoke(con)
}
pub async fn load_async<C: AsyncCommands>(&self, con: &C) -> RedisResult<String> {
let mut load = cmd("SCRIPT");
load.arg("LOAD").arg(self.code.as_bytes());
redis::from_owned_redis_value(con.glide_send_owned(load).await?)
}
#[cfg(feature = "sync")]
pub fn load<C: crate::commands::core::Commands>(&self, con: &C) -> RedisResult<String> {
let mut load = cmd("SCRIPT");
load.arg("LOAD").arg(self.code.as_bytes());
redis::from_owned_redis_value(con.glide_send_owned_sync(load)?)
}
}
#[derive(Debug, Clone)]
pub struct ScriptInvocation<'a> {
script: &'a Script,
args: Vec<Vec<u8>>,
keys: Vec<Vec<u8>>,
}
impl ScriptInvocation<'_> {
pub fn arg<T: ToRedisArgs>(&mut self, arg: T) -> &mut Self {
arg.write_redis_args(&mut self.args);
self
}
pub fn key<T: ToRedisArgs>(&mut self, key: T) -> &mut Self {
key.write_redis_args(&mut self.keys);
self
}
fn evalsha_cmd(&self) -> redis::Cmd {
let mut evalsha = cmd("EVALSHA");
evalsha
.arg(self.script.hash.as_bytes())
.arg(self.keys.len())
.arg(&self.keys)
.arg(&self.args);
evalsha
}
fn eval_cmd(&self) -> redis::Cmd {
let mut eval = cmd("EVAL");
eval.arg(self.script.code.as_bytes())
.arg(self.keys.len())
.arg(&self.keys)
.arg(&self.args);
eval
}
pub async fn invoke_async<C: AsyncCommands, T: FromRedisValue>(
&self,
con: &C,
) -> RedisResult<T> {
match con.glide_send_owned(self.evalsha_cmd()).await {
Err(err) if err.kind() == ErrorKind::NoScriptError => {
redis::from_owned_redis_value(con.glide_send_owned(self.eval_cmd()).await?)
}
other => redis::from_owned_redis_value(other?),
}
}
#[cfg(feature = "sync")]
pub fn invoke<C: crate::commands::core::Commands, T: FromRedisValue>(
&self,
con: &C,
) -> RedisResult<T> {
match con.glide_send_owned_sync(self.evalsha_cmd()) {
Err(err) if err.kind() == ErrorKind::NoScriptError => {
redis::from_owned_redis_value(con.glide_send_owned_sync(self.eval_cmd())?)
}
other => redis::from_owned_redis_value(other?),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sha1_matches_server_semantics() {
let script = Script::new("return 1");
assert_eq!(
script.get_hash(),
"e0e1f9fabfc9d4800c877a703b823ac0578ff8db"
);
}
#[test]
fn invocation_collects_keys_and_args() {
let script = Script::new("return KEYS[1]");
let mut invocation = script.prepare_invoke();
invocation.key("k1").key("k2").arg("a1").arg(2);
assert_eq!(invocation.keys, vec![b"k1".to_vec(), b"k2".to_vec()]);
assert_eq!(invocation.args, vec![b"a1".to_vec(), b"2".to_vec()]);
}
}