use std::str::FromStr;
use gxhash::{GxBuildHasher, HashMap, HashSet};
use sonic_rs::Deserialize;
use wtxn::StoreType;
use super::{
data_provider::IRespCommandData,
simplified::{SimpleRespCommandInfo, populate_simple_command_info},
};
use crate::{
IRespSerializable, RespAclCategories, RespBuffer, RespCommand, RespProtocol, RespWriter,
catalog::LAST_VALID_COMMAND,
command::{FIRST_DATA_COMMAND, LAST_DATA_COMMAND},
key_spec::{
BeginSearchMethod, FindKeysMethod, KeySpecificationFlags, RespCommandKeySpecification,
},
};
const UNKNOWN_COMMAND_NAME: &str = "UNKNOWN";
#[inline]
pub(super) fn static_str(s: String) -> &'static str {
Box::leak(s.into_boxed_str())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RespCommandFlags(pub u32);
impl RespCommandFlags {
pub const ADMIN: Self = Self(1);
pub const ASKING: Self = Self(1 << 1);
pub const BLOCKING: Self = Self(1 << 2);
pub const DENY_OOM: Self = Self(1 << 3);
pub const FAST: Self = Self(1 << 4);
pub const LOADING: Self = Self(1 << 5);
pub const MOVABLE_KEYS: Self = Self(1 << 6);
pub const NO_AUTH: Self = Self(1 << 7);
pub const NO_ASYNC_LOADING: Self = Self(1 << 8);
pub const NO_MANDATORY_KEYS: Self = Self(1 << 9);
pub const NO_MULTI: Self = Self(1 << 10);
pub const NO_SCRIPT: Self = Self(1 << 11);
pub const PUB_SUB: Self = Self(1 << 12);
pub const RANDOM: Self = Self(1 << 13);
pub const READ_ONLY: Self = Self(1 << 14);
pub const SORT_FOR_SCRIPT: Self = Self(1 << 15);
pub const SKIP_MONITOR: Self = Self(1 << 16);
pub const SKIP_SLOW_LOG: Self = Self(1 << 17);
pub const STALE: Self = Self(1 << 18);
pub const WRITE: Self = Self(1 << 19);
pub const ALLOW_BUSY: Self = Self(1 << 20);
pub const MODULE: Self = Self(1 << 21);
#[inline]
pub const fn empty() -> Self {
Self(0)
}
#[inline]
pub fn is_empty(&self) -> bool {
self.0 == 0
}
#[inline]
pub fn intersects(&self, other: Self) -> bool {
self.0 & other.0 != 0
}
const TABLE: [(u32, &'static str, &'static str); 22] = [
(Self::ADMIN.0, "Admin", "admin"),
(Self::ASKING.0, "Asking", "asking"),
(Self::BLOCKING.0, "Blocking", "blocking"),
(Self::DENY_OOM.0, "DenyOom", "denyoom"),
(Self::FAST.0, "Fast", "fast"),
(Self::LOADING.0, "Loading", "loading"),
(Self::MOVABLE_KEYS.0, "MovableKeys", "movablekeys"),
(Self::NO_AUTH.0, "NoAuth", "no_auth"),
(
Self::NO_ASYNC_LOADING.0,
"NoAsyncLoading",
"no_async_loading",
),
(
Self::NO_MANDATORY_KEYS.0,
"NoMandatoryKeys",
"no_mandatory_keys",
),
(Self::NO_MULTI.0, "NoMulti", "no_multi"),
(Self::NO_SCRIPT.0, "NoScript", "noscript"),
(Self::PUB_SUB.0, "PubSub", "pubsub"),
(Self::RANDOM.0, "Random", "random"),
(Self::READ_ONLY.0, "ReadOnly", "readonly"),
(Self::SORT_FOR_SCRIPT.0, "SortForScript", "sort_for_script"),
(Self::SKIP_MONITOR.0, "SkipMonitor", "skip_monitor"),
(Self::SKIP_SLOW_LOG.0, "SkipSlowLog", "skip_slowlog"),
(Self::STALE.0, "Stale", "stale"),
(Self::WRITE.0, "Write", "write"),
(Self::ALLOW_BUSY.0, "AllowBusy", "allow_busy"),
(Self::MODULE.0, "Module", "module"),
];
pub fn descriptions(&self) -> Vec<&'static str> {
Self::TABLE
.iter()
.filter(|(bit, ..)| self.0 & bit != 0)
.map(|(_, _, desc)| *desc)
.collect()
}
pub fn member_names(&self) -> Vec<&'static str> {
Self::TABLE
.iter()
.filter(|(bit, ..)| self.0 & bit != 0)
.map(|(_, name, _)| *name)
.collect()
}
pub fn from_member_names(names: &str) -> Option<Self> {
let mut out = Self::empty();
for name in names.split(',') {
let trimmed = name.trim().to_ascii_uppercase();
let bit = Self::TABLE
.iter()
.find(|(_, member, _)| member.to_ascii_uppercase() == trimmed)
.map(|(bit, ..)| *bit)?;
out.0 |= bit;
}
Some(out)
}
}
pub(crate) fn acl_category_descriptions(cats: RespAclCategories) -> Vec<&'static str> {
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"),
];
ALL
.iter()
.filter(|(bit, _)| cats.bits() & bit != 0)
.map(|(_, desc)| *desc)
.collect()
}
#[derive(Debug, Clone)]
pub struct RespCommandsInfo {
pub command: RespCommand,
pub name: &'static str,
pub is_internal: bool,
pub arity: i32,
pub flags: RespCommandFlags,
pub first_key: i32,
pub last_key: i32,
pub step: i32,
pub acl_categories: RespAclCategories,
pub tips: Vec<&'static str>,
pub key_specifications: Vec<RespCommandKeySpecification>,
pub store_type: StoreType,
pub sub_commands: Vec<RespCommandsInfo>,
pub is_sub_command: bool,
pub parent_is_internal: bool,
}
impl RespCommandsInfo {
pub fn try_get_resp_command_info(name: &str) -> Option<&'static Self> {
try_get_resp_command_info_by_name(name, false, true)
}
}
impl RespCommandsInfo {
pub fn to_resp_format<B: RespBuffer, P: RespProtocol>(&self, writer: &mut RespWriter<B, P>) {
if self.name.trim().is_empty() {
writer.write_null();
return;
}
writer.write_array_length(10);
writer.write_ascii_bulk_string(self.name);
writer.write_int32(self.arity);
let resp_format_flags = self.flags.descriptions();
writer.write_set_length(resp_format_flags.len());
for flag in resp_format_flags {
writer.write_simple_string(flag);
}
writer.write_int32(self.first_key);
writer.write_int32(self.last_key);
writer.write_int32(self.step);
let resp_format_acl_categories = acl_category_descriptions(self.acl_categories);
writer.write_set_length(resp_format_acl_categories.len());
for acl_cat in resp_format_acl_categories {
let out = writer.buf_mut();
out.reserve(2 + acl_cat.len() + 2);
out.extend_from_slice(b"+@");
out.extend_from_slice(acl_cat.as_bytes());
out.extend_from_slice(b"\r\n");
}
writer.write_set_length(self.tips.len());
for tip in &self.tips {
writer.write_ascii_bulk_string(tip);
}
writer.write_set_length(self.key_specifications.len());
for ks in &self.key_specifications {
ks.to_resp_format(writer);
}
writer.write_array_length(self.sub_commands.len());
for sub_command in &self.sub_commands {
sub_command.to_resp_format(writer);
}
}
}
impl IRespSerializable for RespCommandsInfo {
fn to_resp_format<B: RespBuffer, P: RespProtocol>(&self, writer: &mut RespWriter<B, P>) {
self.to_resp_format(writer);
}
}
#[derive(Deserialize, Clone, Default)]
struct KeySpecMethodImport {
#[serde(rename = "TypeDiscriminator")]
discriminator: String,
#[serde(rename = "Index")]
index: Option<i32>,
#[serde(rename = "Keyword")]
keyword: Option<String>,
#[serde(rename = "StartFrom")]
start_from: Option<i32>,
#[serde(rename = "LastKey")]
last_key: Option<i32>,
#[serde(rename = "KeyStep")]
key_step: Option<i32>,
#[serde(rename = "Limit")]
limit: Option<i32>,
#[serde(rename = "KeyNumIdx")]
key_num_idx: Option<i32>,
#[serde(rename = "FirstKey")]
first_key: Option<i32>,
}
#[derive(Deserialize, Clone, Default)]
struct KeySpecificationImport {
#[serde(rename = "BeginSearch")]
begin_search: Option<KeySpecMethodImport>,
#[serde(rename = "FindKeys")]
find_keys: Option<KeySpecMethodImport>,
#[serde(rename = "Notes")]
notes: Option<String>,
#[serde(rename = "Flags")]
flags: Option<String>,
}
impl KeySpecificationImport {
fn convert(self) -> Option<RespCommandKeySpecification> {
let flags = match self.flags {
Some(f) => KeySpecificationFlags::from_wire_names(&f)?,
None => KeySpecificationFlags::NONE,
};
Some(RespCommandKeySpecification {
begin_search: self.begin_search.and_then(|m| m.into_begin_search()),
find_keys: self.find_keys.and_then(|m| m.into_find_keys()),
notes: self.notes,
flags,
})
}
}
impl KeySpecMethodImport {
fn into_begin_search(self) -> Option<BeginSearchMethod> {
if !BeginSearchMethod::can_convert(&self.discriminator) {
return None;
}
Some(match self.discriminator.as_str() {
"BeginSearchIndex" => BeginSearchMethod::Index(self.index.unwrap_or(0)),
"BeginSearchKeyword" => BeginSearchMethod::Keyword {
keyword: self.keyword?,
start_from: self.start_from.unwrap_or(0),
},
_ => BeginSearchMethod::Unknown,
})
}
fn into_find_keys(self) -> Option<FindKeysMethod> {
if !FindKeysMethod::can_convert(&self.discriminator) {
return None;
}
Some(match self.discriminator.as_str() {
"FindKeysRange" => FindKeysMethod::Range {
last_key: self.last_key.unwrap_or(0),
key_step: self.key_step.unwrap_or(0),
limit: self.limit.unwrap_or(0),
},
"FindKeysKeyNum" => FindKeysMethod::KeyNum {
key_num_idx: self.key_num_idx.unwrap_or(0),
first_key: self.first_key.unwrap_or(0),
key_step: self.key_step.unwrap_or(0),
},
_ => FindKeysMethod::Unknown,
})
}
}
#[derive(Deserialize, Clone, Default)]
pub(crate) struct RespCommandsInfoImport {
#[serde(rename = "Command")]
command: String,
#[serde(rename = "Name")]
name: String,
#[serde(rename = "IsInternal", default)]
is_internal: bool,
#[serde(rename = "Arity", default)]
arity: i32,
#[serde(rename = "Flags")]
flags: Option<String>,
#[serde(rename = "FirstKey", default)]
first_key: i32,
#[serde(rename = "LastKey", default)]
last_key: i32,
#[serde(rename = "Step", default)]
step: i32,
#[serde(rename = "AclCategories")]
acl_categories: Option<String>,
#[serde(rename = "Tips")]
tips: Option<Vec<String>>,
#[serde(rename = "KeySpecifications")]
key_specifications: Option<Vec<KeySpecificationImport>>,
#[serde(rename = "StoreType")]
store_type: Option<String>,
#[serde(rename = "SubCommands")]
sub_commands: Option<Vec<RespCommandsInfoImport>>,
}
impl IRespCommandData for RespCommandsInfoImport {
fn name(&self) -> &str {
&self.name
}
}
impl RespCommandsInfoImport {
pub(super) fn convert_all(roots: Vec<Self>) -> Option<Vec<RespCommandsInfo>> {
let mut out = Vec::with_capacity(roots.len());
for entry in roots {
out.push(entry.convert(false, 0)?);
}
Some(out)
}
fn convert(self, parent_is_internal: bool, depth: usize) -> Option<RespCommandsInfo> {
if self.name.is_empty() {
return None;
}
let command = RespCommand::from_str(&self.command).ok()?;
let flags = match &self.flags {
Some(f) => RespCommandFlags::from_member_names(f)?,
None => RespCommandFlags::empty(),
};
let acl_categories = match &self.acl_categories {
Some(a) => RespAclCategories::from_member_names(a)?,
None => RespAclCategories::from_bits_retain(0),
};
let store_type = match &self.store_type {
Some(s) => StoreType::from_member_name(s)?,
None => StoreType::None,
};
let mut key_specifications = Vec::new();
for ks in self.key_specifications.unwrap_or_default() {
key_specifications.push(ks.convert()?);
}
let mut sub_commands = Vec::new();
if depth < 4 {
for sc in self.sub_commands.unwrap_or_default() {
sub_commands.push(sc.convert(self.is_internal, depth + 1)?);
}
}
Some(RespCommandsInfo {
command,
name: static_str(self.name),
is_internal: self.is_internal,
arity: self.arity,
flags,
first_key: self.first_key,
last_key: self.last_key,
step: self.step,
acl_categories,
tips: self
.tips
.unwrap_or_default()
.into_iter()
.map(static_str)
.collect(),
key_specifications,
store_type,
sub_commands,
is_sub_command: depth > 0,
parent_is_internal,
})
}
}
pub struct RespCommandsTables {
pub all: HashMap<&'static str, RespCommandsInfo>,
pub all_sub: HashMap<&'static str, RespCommandsInfo>,
pub external: HashMap<&'static str, RespCommandsInfo>,
pub external_sub: HashMap<&'static str, RespCommandsInfo>,
pub all_names: HashSet<&'static str>,
pub external_names: HashSet<&'static str>,
pub flattened: HashMap<u16, RespCommandsInfo>,
pub simple: Vec<SimpleRespCommandInfo>,
pub acl_command_info: HashMap<u32, Vec<RespCommandsInfo>>,
pub fast_basic: Vec<Option<RespCommandsInfo>>,
}
pub(super) fn build_tables(imported: &[RespCommandsInfo]) -> RespCommandsTables {
let mut all: HashMap<&'static str, RespCommandsInfo> =
HashMap::with_hasher(GxBuildHasher::default());
let mut all_sub: HashMap<&'static str, RespCommandsInfo> =
HashMap::with_hasher(GxBuildHasher::default());
let mut external: HashMap<&'static str, RespCommandsInfo> =
HashMap::with_hasher(GxBuildHasher::default());
let mut external_sub: HashMap<&'static str, RespCommandsInfo> =
HashMap::with_hasher(GxBuildHasher::default());
let mut flattened: HashMap<u16, RespCommandsInfo> =
HashMap::with_hasher(GxBuildHasher::default());
let mut acl_command_info: HashMap<u32, Vec<RespCommandsInfo>> =
HashMap::with_hasher(GxBuildHasher::default());
for entry in imported {
if entry.command == RespCommand::None {
continue;
}
if entry.name == "SLAVEOF" {
continue;
}
flattened.insert(entry.command as u16, entry.clone());
for sc in &entry.sub_commands {
flattened.insert(sc.command as u16, sc.clone());
}
}
for entry in imported {
all.insert(static_str(entry.name.to_lowercase()), entry.clone());
if !entry.is_internal {
external.insert(static_str(entry.name.to_lowercase()), entry.clone());
}
for sc in &entry.sub_commands {
all_sub.insert(static_str(sc.name.to_lowercase()), sc.clone());
if !entry.is_internal && !sc.is_internal {
external_sub.insert(static_str(sc.name.to_lowercase()), sc.clone());
}
}
for cmd in [entry].into_iter().chain(entry.sub_commands.iter()) {
for single in individual_acls(cmd.acl_categories) {
acl_command_info
.entry(single)
.or_default()
.push(cmd.clone());
}
}
}
let mut all_names: HashSet<&'static str> = HashSet::with_hasher(GxBuildHasher::default());
for k in all.keys() {
all_names.insert(k);
}
let mut external_names: HashSet<&'static str> = HashSet::with_hasher(GxBuildHasher::default());
for k in external.keys() {
external_names.insert(k);
}
let table_len = LAST_VALID_COMMAND as usize + 1;
let mut simple = vec![SimpleRespCommandInfo::default(); table_len];
for (cmd_id, slot) in simple
.iter_mut()
.enumerate()
.take(table_len)
.skip(FIRST_DATA_COMMAND as usize)
{
let Some(cmd_info) = flattened.get(&(cmd_id as u16)) else {
continue;
};
populate_simple_command_info(cmd_info, slot);
}
let fast_len = LAST_DATA_COMMAND as usize - FIRST_DATA_COMMAND as usize + 1;
let mut fast_basic: Vec<Option<RespCommandsInfo>> = (0..fast_len).map(|_| None).collect();
for (i, slot) in fast_basic.iter_mut().enumerate() {
if let Some(info) = flattened.get(&((i + FIRST_DATA_COMMAND as usize) as u16)) {
*slot = Some(info.clone());
}
}
RespCommandsTables {
all,
all_sub,
external,
external_sub,
all_names,
external_names,
flattened,
simple,
acl_command_info,
fast_basic,
}
}
pub(crate) fn individual_acls(acl_categories: RespAclCategories) -> Vec<u32> {
let mut out = Vec::new();
let mut remaining = acl_categories.bits();
while remaining != 0 {
let single = remaining.isolate_lowest_one();
remaining &= !single;
out.push(single);
}
out
}
pub fn try_get_commandsfor_acl_category(
acl: RespAclCategories,
) -> Option<Vec<&'static RespCommandsInfo>> {
let tables = super::tables()?;
if acl.bits().count_ones() != 1 {
return None;
}
tables
.acl_command_info
.get(&acl.bits())
.map(|v| v.iter().collect())
}
pub fn try_get_resp_commands_info_count(external_only: bool) -> Option<usize> {
let tables = super::tables()?;
Some(if external_only {
tables.external.len()
} else {
tables.all.len()
})
}
pub fn try_get_resp_commands_info(
external_only: bool,
) -> Option<&'static HashMap<&'static str, RespCommandsInfo>> {
let tables = super::tables()?;
Some(if external_only {
&tables.external
} else {
&tables.all
})
}
pub fn try_get_resp_command_names(external_only: bool) -> Option<&'static HashSet<&'static str>> {
let tables = super::tables()?;
Some(if external_only {
&tables.external_names
} else {
&tables.all_names
})
}
pub fn try_get_resp_command_info_by_name(
cmd_name: &str,
external_only: bool,
include_sub_commands: bool,
) -> Option<&'static RespCommandsInfo> {
let tables = super::tables()?;
let key = cmd_name.to_lowercase();
let primary = if external_only {
&tables.external
} else {
&tables.all
};
primary.get(key.as_str()).or_else(|| {
if include_sub_commands {
let sub = if external_only {
&tables.external_sub
} else {
&tables.all_sub
};
sub.get(key.as_str())
} else {
None
}
})
}
pub fn try_get_resp_command_info_by_cmd(
cmd: RespCommand,
txn_only: bool,
) -> Option<&'static RespCommandsInfo> {
let tables = super::tables()?;
let info = tables.flattened.get(&(cmd as u16))?;
if txn_only && info.flags.intersects(RespCommandFlags::NO_MULTI) {
return None;
}
Some(info)
}
pub fn try_fast_get_resp_command_info(cmd: RespCommand) -> Option<&'static RespCommandsInfo> {
let tables = super::tables()?;
let offset = cmd as usize - FIRST_DATA_COMMAND as usize;
if offset >= tables.fast_basic.len() {
return None;
}
tables.fast_basic[offset].as_ref()
}
pub fn try_get_resp_sub_commands_info(
external_only: bool,
) -> Option<&'static HashMap<&'static str, RespCommandsInfo>> {
let tables = super::tables()?;
Some(if external_only {
&tables.external_sub
} else {
&tables.all_sub
})
}
pub fn try_get_simple_resp_command_info(
cmd: RespCommand,
) -> Option<&'static SimpleRespCommandInfo> {
let tables = super::tables()?;
let cmd_id = cmd as usize;
if cmd_id >= tables.simple.len() {
return None;
}
Some(&tables.simple[cmd_id])
}
pub fn get_resp_command_name(cmd: RespCommand) -> &'static str {
match try_get_resp_command_info_by_cmd(cmd, false) {
Some(info) => info.name,
None => UNKNOWN_COMMAND_NAME,
}
}
#[cfg(test)]
mod tests {
use wtxn::StoreType;
use super::{
RespCommandFlags, acl_category_descriptions, get_resp_command_name, individual_acls,
static_str, try_get_simple_resp_command_info,
};
use crate::{
Resp3, RespAclCategories, RespCommand, RespMemoryWriter,
catalog::{
LAST_VALID_COMMAND, 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_commands_info, try_get_resp_commands_info_count, try_initialize,
},
command::FIRST_DATA_COMMAND,
};
#[test]
fn tables_initialize_and_lookup() {
assert!(try_initialize());
let all = try_get_resp_commands_info(false).unwrap();
let external = try_get_resp_commands_info(true).unwrap();
assert_eq!(all.len(), 260, "根命令数快照");
assert_eq!(external.len(), 256, "外部根命令数快照");
assert_eq!(try_get_resp_commands_info_count(false), Some(all.len()));
assert_eq!(try_get_resp_commands_info_count(true), Some(external.len()));
let get = try_get_resp_command_info_by_name("GET", false, false).unwrap();
assert_eq!(get.arity, 2);
assert_eq!(get.first_key, 1);
assert_eq!(get.last_key, 1);
assert_eq!(get.step, 1);
assert_eq!(get.store_type, StoreType::Main);
assert_eq!(
get.flags,
RespCommandFlags::from_member_names("Fast, ReadOnly").unwrap()
);
let acl_cat = try_get_resp_command_info_by_name("ACL|CAT", false, true).unwrap();
assert_eq!(acl_cat.command, RespCommand::AclCat);
assert!(
try_get_resp_command_info_by_name("ACL|CAT", false, false).is_none(),
"不带子命令检索时不可达"
);
let set = try_get_resp_command_info_by_cmd(RespCommand::Set, false).unwrap();
assert_eq!(set.name, "SET");
assert!(try_get_resp_command_info_by_cmd(RespCommand::Async, true).is_none());
assert!(try_get_resp_command_info_by_cmd(RespCommand::Async, false).is_some());
assert!(try_get_resp_command_info_by_cmd(RespCommand::Secondaryof, false).is_some());
assert!(try_get_resp_command_info_by_cmd(RespCommand::Replicaof, false).is_some());
let fast = try_fast_get_resp_command_info(RespCommand::Append).unwrap();
assert_eq!(fast.name, "APPEND");
assert!(try_fast_get_resp_command_info(RespCommand::Quit).is_none());
assert_eq!(get_resp_command_name(RespCommand::Bitcount), "BITCOUNT");
assert_eq!(get_resp_command_name(RespCommand::Invalid), "UNKNOWN");
let simple = try_get_simple_resp_command_info(RespCommand::Get).unwrap();
assert!(simple.allowed_in_txn);
assert_eq!(simple.arity, 2);
}
#[test]
fn acl_category_index() {
let bitmap = try_get_commandsfor_acl_category(RespAclCategories::BITMAP).unwrap();
let names: Vec<&str> = bitmap.iter().map(|c| c.name).collect();
assert!(names.contains(&"SETBIT"));
assert!(names.contains(&"GETBIT"));
assert!(names.contains(&"BITCOUNT"));
assert!(names.contains(&"BITPOS"));
assert!(names.contains(&"BITFIELD"));
assert!(names.contains(&"BITFIELD_RO"));
assert!(names.contains(&"BITOP"));
assert!(
try_get_commandsfor_acl_category(RespAclCategories::BITMAP | RespAclCategories::STRING)
.is_none()
);
}
#[test]
fn command_info_resp3_snapshot_get() {
let get = try_get_resp_command_info_by_cmd(RespCommand::Get, false).unwrap();
let mut w = RespMemoryWriter::<Resp3>::new_p();
get.to_resp_format(&mut w);
let expected = concat!(
"*10\r\n$3\r\nGET\r\n:2\r\n~2\r\n+fast\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n",
"~3\r\n+@fast\r\n+@read\r\n+@string\r\n~0\r\n~1\r\n%3\r\n$5\r\nflags\r\n~2\r\n+RO\r\n+access\r\n",
"$12\r\nbegin_search\r\n%2\r\n$4\r\ntype\r\n$5\r\nindex\r\n$4\r\nspec\r\n%1\r\n$5\r\nindex\r\n:1\r\n",
"$9\r\nfind_keys\r\n%2\r\n$4\r\ntype\r\n$5\r\nrange\r\n$4\r\nspec\r\n%3\r\n$7\r\nlastkey\r\n:0\r\n$7\r\nkeystep\r\n:1\r\n$5\r\nlimit\r\n:0\r\n*0\r\n",
);
assert_eq!(String::from_utf8(w.into_inner()).unwrap(), expected);
}
#[test]
fn command_info_resp3_snapshot_bitfield() {
let bf = try_get_resp_command_info_by_cmd(RespCommand::Bitfield, false).unwrap();
let mut w = RespMemoryWriter::<Resp3>::new_p();
bf.to_resp_format(&mut w);
let expected = concat!(
"*10\r\n$8\r\nBITFIELD\r\n:-2\r\n~2\r\n+denyoom\r\n+write\r\n:1\r\n:1\r\n:1\r\n",
"~3\r\n+@bitmap\r\n+@slow\r\n+@write\r\n~0\r\n~1\r\n%4\r\n$5\r\nnotes\r\n$59\r\nThis command allows both access and modification of the key\r\n",
"$5\r\nflags\r\n~4\r\n+RW\r\n+access\r\n+update\r\n+variable_flags\r\n",
"$12\r\nbegin_search\r\n%2\r\n$4\r\ntype\r\n$5\r\nindex\r\n$4\r\nspec\r\n%1\r\n$5\r\nindex\r\n:1\r\n",
"$9\r\nfind_keys\r\n%2\r\n$4\r\ntype\r\n$5\r\nrange\r\n$4\r\nspec\r\n%3\r\n$7\r\nlastkey\r\n:0\r\n$7\r\nkeystep\r\n:1\r\n$5\r\nlimit\r\n:0\r\n*0\r\n",
);
assert_eq!(String::from_utf8(w.into_inner()).unwrap(), expected);
}
#[test]
fn command_info_resp2_snapshot_setbit() {
let setbit = try_get_resp_command_info_by_cmd(RespCommand::Setbit, false).unwrap();
let mut w = RespMemoryWriter::new();
setbit.to_resp_format(&mut w);
let expected = concat!(
"*10\r\n$6\r\nSETBIT\r\n:4\r\n*2\r\n+denyoom\r\n+write\r\n:1\r\n:1\r\n:1\r\n",
"*3\r\n+@bitmap\r\n+@slow\r\n+@write\r\n*0\r\n*1\r\n*6\r\n$5\r\nflags\r\n*3\r\n+RW\r\n+access\r\n+update\r\n",
"$12\r\nbegin_search\r\n*4\r\n$4\r\ntype\r\n$5\r\nindex\r\n$4\r\nspec\r\n*2\r\n$5\r\nindex\r\n:1\r\n",
"$9\r\nfind_keys\r\n*4\r\n$4\r\ntype\r\n$5\r\nrange\r\n$4\r\nspec\r\n*6\r\n$7\r\nlastkey\r\n:0\r\n$7\r\nkeystep\r\n:1\r\n$5\r\nlimit\r\n:0\r\n*0\r\n",
);
assert_eq!(String::from_utf8(w.into_inner()).unwrap(), expected);
}
#[test]
fn individual_acls_yields_single_bits() {
let cats = RespAclCategories::from_member_names("Fast, String, Write").unwrap();
let bits = individual_acls(cats);
assert_eq!(
bits,
vec![
RespAclCategories::FAST.bits(),
RespAclCategories::STRING.bits(),
RespAclCategories::WRITE.bits()
]
);
}
#[test]
fn constants_and_helpers() {
assert_eq!(FIRST_DATA_COMMAND, RespCommand::Append);
assert!(try_fast_get_resp_command_info(RespCommand::Evalsha).is_some());
let cats = RespAclCategories::from_member_names("Fast, Read").unwrap();
assert_eq!(acl_category_descriptions(cats), vec!["fast", "read"]);
assert_eq!(static_str("x".to_string()), "x");
let tables_ok = try_get_simple_resp_command_info(LAST_VALID_COMMAND).is_some();
assert!(tables_ok);
}
}