#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub use kevy_resp::Reply;
pub mod backup;
pub mod migrate;
pub const DEFAULT_HOST: &str = "127.0.0.1";
pub const DEFAULT_PORT: u16 = 6379;
pub mod bulk;
pub mod shadow;
pub mod backfill_keys;
pub(crate) mod collections;
pub mod doctor;
pub mod lint;
pub fn route_tool(args: &[String]) -> Option<std::process::ExitCode> {
let rest = args.get(1..).unwrap_or(&[]);
match args.first().map(String::as_str)? {
"doctor" => Some(doctor::run_doctor_cli(rest)),
"shadow" => Some(shadow::run_shadow_cli(rest)),
"lint" => Some(lint::run_lint_cli(rest)),
"backfill-keys" => Some(backfill_keys::run_backfill_keys_cli(rest)),
_ => None,
}
}
pub fn format_reply(reply: &Reply, indent: usize) -> String {
match reply {
Reply::Simple(s) => String::from_utf8_lossy(s).into_owned(),
Reply::Error(s) | Reply::BlobError(s) => {
format!("(error) {}", String::from_utf8_lossy(s))
}
Reply::Int(n) => format!("(integer) {n}"),
Reply::Bulk(b) => format!("\"{}\"", String::from_utf8_lossy(b)),
Reply::Nil | Reply::Null => "(nil)".to_string(),
Reply::Array(items) if items.is_empty() => "(empty array)".to_string(),
Reply::Array(items) | Reply::Set(items) | Reply::Push(items) => {
let pad = " ".repeat(indent);
items
.iter()
.enumerate()
.map(|(i, it)| format!("{pad}{}) {}", i + 1, format_reply(it, indent + 1)))
.collect::<Vec<_>>()
.join("\n")
}
Reply::Map(pairs) if pairs.is_empty() => "(empty map)".to_string(),
Reply::Map(pairs) => {
let pad = " ".repeat(indent);
pairs
.iter()
.enumerate()
.map(|(i, (k, v))| {
format!(
"{pad}{}) {} => {}",
i + 1,
format_reply(k, indent + 1),
format_reply(v, indent + 1)
)
})
.collect::<Vec<_>>()
.join("\n")
}
Reply::Double(v) => format!("(double) {v}"),
Reply::Boolean(b) => format!("(boolean) {}", if *b { "t" } else { "f" }),
Reply::Verbatim { fmt, data } => format!(
"(verbatim/{}) \"{}\"",
String::from_utf8_lossy(fmt),
String::from_utf8_lossy(data)
),
Reply::BigNumber(s) => format!("(bignum) {}", String::from_utf8_lossy(s)),
}
}
#[cfg(test)]
mod format_reply_tests {
use super::format_reply;
use kevy_resp::Reply;
fn f(r: &Reply) -> String {
format_reply(r, 0)
}
#[test]
fn every_reply_variant_renders() {
assert_eq!(f(&Reply::Simple(b"OK".to_vec())), "OK");
assert_eq!(f(&Reply::Error(b"ERR nope".to_vec())), "(error) ERR nope");
assert_eq!(f(&Reply::Int(-7)), "(integer) -7");
assert_eq!(f(&Reply::Bulk(b"hi".to_vec())), "\"hi\"");
assert_eq!(f(&Reply::Nil), "(nil)");
assert_eq!(f(&Reply::Array(vec![])), "(empty array)");
assert_eq!(f(&Reply::Double(1.5)), "(double) 1.5");
assert_eq!(f(&Reply::Boolean(true)), "(boolean) t");
assert_eq!(f(&Reply::Boolean(false)), "(boolean) f");
assert_eq!(
f(&Reply::BigNumber(b"123456789012345678901".to_vec())),
"(bignum) 123456789012345678901"
);
assert_eq!(f(&Reply::Null), "(nil)");
assert_eq!(f(&Reply::BlobError(b"ERR nope".to_vec())), "(error) ERR nope");
assert_eq!(
f(&Reply::Verbatim { fmt: *b"txt", data: b"hello".to_vec() }),
"(verbatim/txt) \"hello\""
);
assert_eq!(f(&Reply::Set(vec![Reply::Int(4)])), "1) (integer) 4");
assert_eq!(f(&Reply::Push(vec![Reply::Bulk(b"message".to_vec())])), "1) \"message\"");
}
#[test]
fn arrays_number_from_one_and_nest() {
let flat = Reply::Array(vec![Reply::Int(1), Reply::Bulk(b"x".to_vec())]);
assert_eq!(f(&flat), "1) (integer) 1\n2) \"x\"");
let nested = Reply::Array(vec![Reply::Array(vec![Reply::Int(9)])]);
assert_eq!(f(&nested), "1) 1) (integer) 9");
}
#[test]
fn maps_render_as_pairs() {
assert_eq!(f(&Reply::Map(vec![])), "(empty map)");
let m = Reply::Map(vec![(Reply::Bulk(b"k".to_vec()), Reply::Int(1))]);
assert_eq!(f(&m), "1) \"k\" => (integer) 1");
}
}