use std::sync::OnceLock;
use bitflags::bitflags;
use wresources::RESP_COMMANDS_INFO_JSON;
use crate::command::RespCommand;
pub mod commands_info;
pub mod data_provider;
pub mod simplified;
pub use commands_info::{
RespCommandFlags, RespCommandsInfo, RespCommandsTables, get_resp_command_name,
try_fast_get_resp_command_info, try_get_commandsfor_acl_category,
try_get_resp_command_info_by_cmd, try_get_resp_command_info_by_name, try_get_resp_command_names,
try_get_resp_commands_info, try_get_resp_commands_info_count, try_get_resp_sub_commands_info,
try_get_simple_resp_command_info,
};
use commands_info::{RespCommandsInfoImport, build_tables};
pub use data_provider::{
DefaultRespCommandsDataProvider, IRespCommandData, get_resp_commands_data_provider,
try_import_resp_commands_data,
};
pub use simplified::{
SimpleRespCommandInfo, SimpleRespKeySpec, SimpleRespKeySpecBeginSearch,
SimpleRespKeySpecFindKeys, populate_simple_command_info, try_get_simple_key_spec,
};
pub struct CmdEntry {
pub cs: &'static str,
pub name: &'static str,
pub cmd: RespCommand,
pub cats: RespAclCategories,
pub parent: Option<RespCommand>,
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RespAclCategories: u32 {
const ADMIN = 1;
const BITMAP = 1 << 1;
const BLOCKING = 1 << 2;
const CONNECTION = 1 << 3;
const DANGEROUS = 1 << 4;
const GEO = 1 << 5;
const HASH = 1 << 6;
const HYPERLOGLOG = 1 << 7;
const FAST = 1 << 8;
const KEYSPACE = 1 << 9;
const LIST = 1 << 10;
const PUBSUB = 1 << 11;
const READ = 1 << 12;
const SCRIPTING = 1 << 13;
const SET = 1 << 14;
const SORTEDSET = 1 << 15;
const SLOW = 1 << 16;
const STREAM = 1 << 17;
const STRING = 1 << 18;
const TRANSACTION = 1 << 19;
const WRITE = 1 << 20;
const GARNET = 1 << 21;
const CUSTOM = 1 << 22;
const VECTOR = 1 << 23;
const ALL = (1 << 24) - 1;
}
}
impl RespAclCategories {
pub fn from_member_names(names: &str) -> Option<Self> {
const ALL: [(u32, &str); 24] = [
(RespAclCategories::ADMIN.bits(), "ADMIN"),
(RespAclCategories::BITMAP.bits(), "BITMAP"),
(RespAclCategories::BLOCKING.bits(), "BLOCKING"),
(RespAclCategories::CONNECTION.bits(), "CONNECTION"),
(RespAclCategories::DANGEROUS.bits(), "DANGEROUS"),
(RespAclCategories::GEO.bits(), "GEO"),
(RespAclCategories::HASH.bits(), "HASH"),
(RespAclCategories::HYPERLOGLOG.bits(), "HYPERLOGLOG"),
(RespAclCategories::FAST.bits(), "FAST"),
(RespAclCategories::KEYSPACE.bits(), "KEYSPACE"),
(RespAclCategories::LIST.bits(), "LIST"),
(RespAclCategories::PUBSUB.bits(), "PUBSUB"),
(RespAclCategories::READ.bits(), "READ"),
(RespAclCategories::SCRIPTING.bits(), "SCRIPTING"),
(RespAclCategories::SET.bits(), "SET"),
(RespAclCategories::SORTEDSET.bits(), "SORTEDSET"),
(RespAclCategories::SLOW.bits(), "SLOW"),
(RespAclCategories::STREAM.bits(), "STREAM"),
(RespAclCategories::STRING.bits(), "STRING"),
(RespAclCategories::TRANSACTION.bits(), "TRANSACTION"),
(RespAclCategories::WRITE.bits(), "WRITE"),
(RespAclCategories::GARNET.bits(), "GARNET"),
(RespAclCategories::CUSTOM.bits(), "CUSTOM"),
(RespAclCategories::VECTOR.bits(), "VECTOR"),
];
let mut bits = 0u32;
for name in names.split(',') {
let trimmed = name.trim().to_ascii_uppercase();
let bit = ALL
.iter()
.find(|(_, member)| *member == trimmed)
.map(|(bit, _)| *bit)?;
bits |= bit;
}
Some(Self::from_bits_retain(bits))
}
}
struct Catalog {
tables: RespCommandsTables,
entries: Vec<CmdEntry>,
}
static CATALOG: OnceLock<Option<Catalog>> = OnceLock::new();
fn try_initialize_resp_commands_info() -> Option<Vec<RespCommandsInfo>> {
let imported = try_import_resp_commands_data::<RespCommandsInfoImport>(RESP_COMMANDS_INFO_JSON)?;
RespCommandsInfoImport::convert_all(imported)
}
fn init_catalog() -> Option<Catalog> {
let roots = try_initialize_resp_commands_info()?;
Some(Catalog {
tables: build_tables(&roots),
entries: project_entries(&roots),
})
}
pub(crate) fn try_initialize() -> bool {
CATALOG.get_or_init(init_catalog).is_some()
}
pub(crate) fn tables() -> Option<&'static RespCommandsTables> {
try_initialize();
CATALOG.get().and_then(|c| c.as_ref()).map(|c| &c.tables)
}
fn entries() -> &'static [CmdEntry] {
try_initialize();
CATALOG
.get()
.and_then(|c| c.as_ref())
.map(|c| c.entries.as_slice())
.unwrap_or(&[])
}
fn project_entries(roots: &[RespCommandsInfo]) -> Vec<CmdEntry> {
fn push(out: &mut Vec<CmdEntry>, entry: &RespCommandsInfo, parent: Option<RespCommand>) {
let cs: &'static str = entry.command.into();
out.push(CmdEntry {
cs,
name: commands_info::static_str(entry.name.to_lowercase()),
cmd: entry.command,
cats: entry.acl_categories,
parent,
});
for sub in &entry.sub_commands {
push(out, sub, Some(entry.command));
}
}
let mut out = Vec::new();
for root in roots {
push(&mut out, root, None);
}
out
}
const EXPANDED_SET: [RespCommand; 4] = [
RespCommand::Setexnx,
RespCommand::Setexxx,
RespCommand::Setkeepttl,
RespCommand::Setkeepttlxx,
];
const EXPANDED_BITOP: [RespCommand; 5] = [
RespCommand::BitopAnd,
RespCommand::BitopNot,
RespCommand::BitopOr,
RespCommand::BitopXor,
RespCommand::BitopDiff,
];
#[inline]
pub const fn normalize_for_acls(cmd: RespCommand) -> RespCommand {
match cmd {
RespCommand::Setexnx
| RespCommand::Setexxx
| RespCommand::Setkeepttl
| RespCommand::Setkeepttlxx => RespCommand::Set,
RespCommand::BitopAnd
| RespCommand::BitopNot
| RespCommand::BitopOr
| RespCommand::BitopXor
| RespCommand::BitopDiff => RespCommand::Bitop,
_ => cmd,
}
}
#[inline]
pub fn expand_for_acls(cmd: RespCommand) -> &'static [RespCommand] {
match cmd {
RespCommand::Set => &EXPANDED_SET,
RespCommand::Bitop => &EXPANDED_BITOP,
_ => &[],
}
}
#[inline]
pub const fn is_no_auth(cmd: RespCommand) -> bool {
let v = (cmd as u16).wrapping_sub(RespCommand::Auth as u16);
v <= (RespCommand::Quit as u16).wrapping_sub(RespCommand::Auth as u16)
}
pub use crate::command::LAST_VALID_COMMAND;
#[inline]
pub fn try_get_resp_command_info(cmd: RespCommand) -> Option<&'static CmdEntry> {
entries().iter().find(|e| e.cmd == cmd)
}
#[inline]
pub fn try_get_by_cs_name(name: &str) -> Option<&'static CmdEntry> {
entries().iter().find(|e| e.cs.eq_ignore_ascii_case(name))
}
#[inline]
pub fn children_of(cmd: RespCommand) -> impl Iterator<Item = &'static CmdEntry> {
entries().iter().filter(move |e| e.parent == Some(cmd))
}
#[inline]
pub fn commands_for_category(acl: RespAclCategories) -> impl Iterator<Item = &'static CmdEntry> {
entries().iter().filter(move |e| e.cats.intersects(acl))
}
#[cfg(test)]
mod tests {
use super::{
CmdEntry, RespAclCategories, children_of, commands_for_category, entries, try_get_by_cs_name,
try_get_resp_command_info, try_initialize,
};
use crate::RespCommand;
#[test]
fn catalog_size_and_aliases() {
assert!(try_initialize());
assert_eq!(entries().len(), 353);
assert_eq!(entries().iter().filter(|e| e.parent.is_none()).count(), 260);
assert_eq!(entries().iter().filter(|e| e.parent.is_some()).count(), 93);
let aliases: Vec<&CmdEntry> = entries()
.iter()
.filter(|e| e.cmd == RespCommand::Secondaryof)
.collect();
assert_eq!(aliases.len(), 2);
assert!(aliases.iter().any(|e| e.name == "secondaryof"));
assert!(aliases.iter().any(|e| e.name == "slaveof"));
let head: Vec<&str> = entries().iter().take(3).map(|e| e.name).collect();
assert_eq!(head, ["acl", "acl|cat", "acl|deluser"]);
}
#[test]
fn acl_lookup() {
let acl = try_get_resp_command_info(RespCommand::Acl).unwrap();
assert_eq!(acl.cs, "ACL");
assert_eq!(acl.name, "acl");
assert_eq!(acl.cats, RespAclCategories::SLOW);
assert_eq!(acl.parent, None);
let cat = try_get_by_cs_name("acl_cat").unwrap();
assert_eq!(cat.cmd, RespCommand::AclCat);
assert_eq!(cat.parent, Some(RespCommand::Acl));
assert_eq!(cat.cats, RespAclCategories::SLOW);
let deluser = try_get_by_cs_name("ACL_DELUSER").unwrap();
assert_eq!(
deluser.cats,
RespAclCategories::ADMIN | RespAclCategories::DANGEROUS | RespAclCategories::SLOW
);
assert_eq!(children_of(RespCommand::Acl).count(), 10);
assert!(try_get_by_cs_name("NO_SUCH_COMMAND").is_none());
}
#[test]
fn category_members() {
let bitmap: Vec<&CmdEntry> = commands_for_category(RespAclCategories::BITMAP).collect();
assert!(bitmap.iter().any(|e| e.name == "setbit"));
assert_eq!(
commands_for_category(RespAclCategories::ALL).count(),
entries().len()
);
}
#[test]
fn member_names_parse() {
let cats = RespAclCategories::from_member_names("Fast, String, Write").unwrap();
assert_eq!(
cats,
RespAclCategories::FAST | RespAclCategories::STRING | RespAclCategories::WRITE
);
assert!(RespAclCategories::from_member_names("Nope").is_none());
}
}