#![cfg(feature = "script")]
use sha1_smol::Sha1;
use crate::{
Cmd, ErrorKind, RedisWrite, ToSingleRedisArg,
connection::ConnectionLike,
types::{FromRedisValue, RedisResult, ToRedisArgs},
};
#[derive(Debug, Clone)]
pub struct Script {
code: String,
hash: String,
}
impl Script {
pub fn new(code: &str) -> Self {
let mut hash = Sha1::new();
hash.update(code.as_bytes());
Self {
code: code.to_string(),
hash: hash.digest().to_string(),
}
}
pub fn get_hash(&self) -> &str {
&self.hash
}
#[inline]
pub fn load(&self, con: &mut dyn ConnectionLike) -> RedisResult<String> {
let hash: String = Cmd::load_script(self).query(con)?;
debug_assert_eq!(hash, self.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 = Cmd::load_script(self).query_async(con).await?;
debug_assert_eq!(hash, self.hash);
Ok(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
}
}
impl ToRedisArgs for Script {
fn write_redis_args<W>(&self, out: &mut W)
where
W: ?Sized + RedisWrite,
{
out.write_arg(self.code.as_bytes());
}
#[inline]
fn args_size(&self) -> usize {
self.code.len()
}
}
impl ToSingleRedisArg for Script {}
pub struct ScriptInvocation<'a> {
script: &'a Script,
args: Vec<Vec<u8>>,
keys: Vec<Vec<u8>>,
}
impl std::fmt::Debug for ScriptInvocation<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self { script, args, keys } = self;
f.debug_struct("ScriptInvocation")
.field("script", script)
.field("args", &args.len())
.field("keys", &keys.len())
.finish()
}
}
impl<'a> ScriptInvocation<'a> {
#[inline]
pub fn arg<'b, T: ToRedisArgs>(&'b mut self, arg: T) -> &'b mut Self
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 Self
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 = Cmd::invoke_script(self);
match eval_cmd.query(con) {
Ok(val) => Ok(val),
Err(err) => {
if err.kind() == ErrorKind::Server(crate::ServerErrorKind::NoScript) {
self.load(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 = Cmd::invoke_script(self);
match eval_cmd.query_async(con).await {
Ok(val) => {
Ok(val)
}
Err(err) => {
if err.kind() == ErrorKind::Server(crate::ServerErrorKind::NoScript) {
self.load_async(con).await?;
eval_cmd.query_async(con).await
} else {
Err(err)
}
}
}
}
#[inline]
pub fn load(&self, con: &mut dyn ConnectionLike) -> RedisResult<String> {
self.script.load(con)
}
#[inline]
#[cfg(feature = "aio")]
pub async fn load_async<C>(&self, con: &mut C) -> RedisResult<String>
where
C: crate::aio::ConnectionLike,
{
self.script.load_async(con).await
}
}
impl ToRedisArgs for ScriptInvocation<'_> {
fn write_redis_args<W>(&self, out: &mut W)
where
W: ?Sized + RedisWrite,
{
out.write_arg(self.script.hash.as_bytes());
self.keys.len().write_redis_args(out);
self.keys.write_redis_args(out);
self.args.write_redis_args(out);
}
#[inline]
fn num_of_args(&self) -> usize {
2 + self.keys.len() + self.args.len()
}
#[inline]
fn args_size(&self) -> usize {
self.keys
.iter()
.chain(self.args.iter())
.fold(0, |acc, e| acc + e.len())
+ self.script.hash.len()
+ 4
}
}
#[cfg(test)]
mod tests {
use super::Script;
use crate::Cmd;
use crate::types::ToRedisArgs;
#[test]
fn script_eval_should_work() {
let script = Script::new("return KEYS[1]");
let invocation = script.key("dummy");
assert_eq!(invocation.args_size(), 49);
let cmd = Cmd::invoke_script(&invocation);
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()
);
}
}