use std::sync::OnceLock;
use gxhash::{GxBuildHasher, HashMap, HashSet};
use sonic_rs::Deserialize;
use super::{
resp_command_argument::{
ArgumentBase, RespCommandArgument, RespCommandArgumentFlags, RespCommandArgumentType,
},
resp_command_data_common::try_import_resp_commands_data,
resp_command_data_provider::IRespCommandData,
resp_commands_info::{try_get_resp_command_info_by_cmd, try_get_resp_command_names},
resp_memory_writer::RespMemoryWriter,
};
use crate::types::RespCommand;
const RESP_COMMANDS_DOCS_JSON: &str = include_str!("RespCommandsDocs.json");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RespCommandGroup {
#[default]
None,
Bitmap,
Cluster,
Connection,
Generic,
Geo,
Hash,
HyperLogLog,
List,
Module,
PubSub,
Scripting,
Sentinel,
Server,
Set,
SortedSet,
Stream,
String,
Transactions,
Vector,
}
impl RespCommandGroup {
pub fn description(&self) -> &'static str {
match self {
Self::None => "None",
Self::Bitmap => "bitmap",
Self::Cluster => "cluster",
Self::Connection => "connection",
Self::Generic => "generic",
Self::Geo => "geo",
Self::Hash => "hash",
Self::HyperLogLog => "hyperloglog",
Self::List => "list",
Self::Module => "module",
Self::PubSub => "pubsub",
Self::Scripting => "scripting",
Self::Sentinel => "sentinel",
Self::Server => "server",
Self::Set => "set",
Self::SortedSet => "sorted-set",
Self::Stream => "stream",
Self::String => "string",
Self::Transactions => "transactions",
Self::Vector => "vector",
}
}
pub fn from_member_name(name: &str) -> Option<Self> {
Some(match name.to_ascii_uppercase().as_str() {
"NONE" => Self::None,
"BITMAP" => Self::Bitmap,
"CLUSTER" => Self::Cluster,
"CONNECTION" => Self::Connection,
"GENERIC" => Self::Generic,
"GEO" => Self::Geo,
"HASH" => Self::Hash,
"HYPERLOGLOG" => Self::HyperLogLog,
"LIST" => Self::List,
"MODULE" => Self::Module,
"PUBSUB" => Self::PubSub,
"SCRIPTING" => Self::Scripting,
"SENTINEL" => Self::Sentinel,
"SERVER" => Self::Server,
"SET" => Self::Set,
"SORTEDSET" => Self::SortedSet,
"STREAM" => Self::Stream,
"STRING" => Self::String,
"TRANSACTIONS" => Self::Transactions,
"VECTOR" => Self::Vector,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RespCommandDocFlags(u8);
impl RespCommandDocFlags {
pub const NONE: Self = Self(0);
pub const DEPRECATED: Self = Self(1);
pub const SYS_CMD: Self = Self(1 << 1);
#[inline]
pub fn is_none(&self) -> bool {
self.0 == 0
}
pub fn descriptions(&self) -> Vec<&'static str> {
[
(Self::DEPRECATED.0, "deprecated"),
(Self::SYS_CMD.0, "syscmd"),
]
.iter()
.filter(|(bit, _)| self.0 & bit != 0)
.map(|(_, desc)| *desc)
.collect()
}
pub fn from_member_names(names: &str) -> Option<Self> {
let mut out = Self::NONE;
for name in names.split(',') {
let bit = match name.trim().to_ascii_uppercase().as_str() {
"NONE" => Self::NONE,
"DEPRECATED" => Self::DEPRECATED,
"SYSCMD" => Self::SYS_CMD,
_ => return None,
};
out.0 |= bit.0;
}
Some(out)
}
}
#[derive(Debug, Clone)]
pub struct RespCommandDocs {
pub command: RespCommand,
pub name: String,
pub summary: Option<String>,
pub group: RespCommandGroup,
pub complexity: Option<String>,
pub doc_flags: RespCommandDocFlags,
pub replaced_by: Option<String>,
pub arguments: Option<Vec<RespCommandArgument>>,
pub sub_commands: Vec<RespCommandDocs>,
pub is_sub_command: bool,
}
impl IRespCommandData for RespCommandDocs {
fn name(&self) -> &str {
&self.name
}
}
impl IRespCommandData for RespCommandDocsImport {
fn name(&self) -> &str {
&self.name
}
}
impl RespCommandDocs {
pub fn to_resp_format(&self, writer: &mut RespMemoryWriter) {
let mut arg_count = 1;
if self.summary.is_some() {
arg_count += 1;
}
if self.complexity.is_some() {
arg_count += 1;
}
if !self.doc_flags.is_none() {
arg_count += 1;
}
if self.replaced_by.is_some() {
arg_count += 1;
}
if self.arguments.is_some() {
arg_count += 1;
}
if !self.sub_commands.is_empty() {
arg_count += 1;
}
writer.write_ascii_bulk_string(&self.name);
writer.write_map_length(arg_count);
if let Some(summary) = &self.summary {
writer.write_bulk_string(b"summary");
writer.write_ascii_bulk_string(summary);
}
writer.write_bulk_string(b"group");
writer.write_ascii_bulk_string(self.group.description());
if let Some(complexity) = &self.complexity {
writer.write_bulk_string(b"complexity");
writer.write_ascii_bulk_string(complexity);
}
if !self.doc_flags.is_none() {
let resp_format_doc_flags = self.doc_flags.descriptions();
writer.write_bulk_string(b"doc_flags");
writer.write_set_length(resp_format_doc_flags.len());
for resp_doc_flag in resp_format_doc_flags {
writer.write_simple_string(resp_doc_flag);
}
}
if let Some(replaced_by) = &self.replaced_by {
writer.write_bulk_string(b"replaced_by");
writer.write_ascii_bulk_string(replaced_by);
}
if let Some(arguments) = &self.arguments {
writer.write_bulk_string(b"arguments");
writer.write_array_length(arguments.len());
for argument in arguments {
argument.to_resp_format(writer);
}
}
if !self.sub_commands.is_empty() {
writer.write_bulk_string(b"subcommands");
writer.write_map_length(self.sub_commands.len());
for sub_command in &self.sub_commands {
sub_command.to_resp_format(writer);
}
}
}
}
#[derive(Deserialize, Clone, Default)]
struct ArgumentImport {
#[serde(rename = "TypeDiscriminator")]
discriminator: String,
#[serde(rename = "Name")]
name: Option<String>,
#[serde(rename = "DisplayText")]
display_text: Option<String>,
#[serde(rename = "Type")]
argument_type: Option<String>,
#[serde(rename = "Token")]
token: Option<String>,
#[serde(rename = "Summary")]
summary: Option<String>,
#[serde(rename = "ArgumentFlags")]
argument_flags: Option<String>,
#[serde(rename = "Value")]
value: Option<String>,
#[serde(rename = "KeySpecIndex")]
key_spec_index: Option<i32>,
#[serde(rename = "Arguments")]
arguments: Option<Vec<ArgumentImport>>,
}
impl ArgumentImport {
fn convert(self) -> Option<RespCommandArgument> {
if !RespCommandArgument::can_convert(&self.discriminator) {
return None;
}
let base = ArgumentBase {
name: self.name.unwrap_or_default(),
display_text: self.display_text,
argument_type: match &self.argument_type {
Some(t) => RespCommandArgumentType::from_member_name(t)?,
None => RespCommandArgumentType::None,
},
token: self.token,
summary: self.summary,
argument_flags: match &self.argument_flags {
Some(f) => RespCommandArgumentFlags::from_member_name(f)?,
None => RespCommandArgumentFlags::NONE,
},
};
Some(match self.discriminator.as_str() {
"RespCommandKeyArgument" => RespCommandArgument::Key {
base,
value: self.value,
key_spec_index: self.key_spec_index.unwrap_or(-1),
},
"RespCommandBasicArgument" => RespCommandArgument::Basic {
base,
value: self.value,
},
_ => {
let arguments = match self.arguments {
Some(args) => {
let mut converted = Vec::with_capacity(args.len());
for arg in args {
converted.push(arg.convert()?);
}
Some(converted)
}
None => None,
};
RespCommandArgument::Container { base, arguments }
}
})
}
}
#[derive(Deserialize, Clone, Default)]
struct RespCommandDocsImport {
#[serde(rename = "Command")]
command: String,
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Summary")]
summary: Option<String>,
#[serde(rename = "Group")]
group: Option<String>,
#[serde(rename = "Complexity")]
complexity: Option<String>,
#[serde(rename = "DocFlags")]
doc_flags: Option<String>,
#[serde(rename = "ReplacedBy")]
replaced_by: Option<String>,
#[serde(rename = "Arguments")]
arguments: Option<Vec<ArgumentImport>>,
#[serde(rename = "SubCommands")]
sub_commands: Option<Vec<RespCommandDocsImport>>,
}
fn convert_import(import: RespCommandDocsImport, parent_is_sub: bool) -> Option<RespCommandDocs> {
let command = if import.command.is_empty() {
RespCommand::None
} else {
super::resp_commands_info_data::resp_command_from_cs_name(&import.command)?
};
let group = match &import.group {
Some(g) => RespCommandGroup::from_member_name(g)?,
None => RespCommandGroup::None,
};
let doc_flags = match &import.doc_flags {
Some(f) => RespCommandDocFlags::from_member_names(f)?,
None => RespCommandDocFlags::NONE,
};
let arguments = match import.arguments {
Some(args) => {
let mut converted = Vec::with_capacity(args.len());
for arg in args {
converted.push(arg.convert()?);
}
Some(converted)
}
None => None,
};
let mut sub_commands = Vec::new();
for sc in import.sub_commands.unwrap_or_default() {
sub_commands.push(convert_import(sc, true)?);
}
Some(RespCommandDocs {
command,
name: import.name,
summary: import.summary,
group,
complexity: import.complexity,
doc_flags,
replaced_by: import.replaced_by,
arguments,
sub_commands,
is_sub_command: parent_is_sub,
})
}
pub struct RespCommandDocsTables {
pub all: HashMap<String, RespCommandDocs>,
pub all_sub: HashMap<String, RespCommandDocs>,
pub external: HashMap<String, RespCommandDocs>,
pub external_sub: HashMap<String, RespCommandDocs>,
}
static TABLES: OnceLock<Option<RespCommandDocsTables>> = OnceLock::new();
fn try_initialize() -> bool {
TABLES
.get_or_init(|| {
if try_initialize_resp_commands_docs() {
Some(build_tables())
} else {
None
}
})
.is_some()
}
static IMPORTED: OnceLock<Vec<RespCommandDocs>> = OnceLock::new();
fn try_initialize_resp_commands_docs() -> bool {
if super::resp_commands_info::try_get_resp_command_names(false).is_none() {
return false;
}
let imported = try_import_resp_commands_data::<RespCommandDocsImport>(RESP_COMMANDS_DOCS_JSON);
let Some(imported) = imported else {
return false;
};
let mut converted = Vec::with_capacity(imported.len());
for entry in imported {
let Some(docs) = convert_import(entry, false) else {
return false;
};
converted.push(docs);
}
IMPORTED.set(converted).is_ok()
}
fn build_tables() -> RespCommandDocsTables {
let imported = IMPORTED.get().expect("导入已完成");
let external_command_names: Option<&'static HashSet<String>> = try_get_resp_command_names(true);
let mut all: HashMap<String, RespCommandDocs> = HashMap::with_hasher(GxBuildHasher::default());
let mut all_sub: HashMap<String, RespCommandDocs> =
HashMap::with_hasher(GxBuildHasher::default());
let mut external: HashMap<String, RespCommandDocs> =
HashMap::with_hasher(GxBuildHasher::default());
let mut external_sub: HashMap<String, RespCommandDocs> =
HashMap::with_hasher(GxBuildHasher::default());
for entry in imported {
all.insert(entry.name.to_lowercase(), entry.clone());
let is_external =
external_command_names.is_some_and(|names| names.contains(&entry.name.to_lowercase()));
if is_external {
external.insert(entry.name.to_lowercase(), entry.clone());
}
for sc in &entry.sub_commands {
all_sub.insert(sc.name.to_lowercase(), sc.clone());
let sub_cmd_info = try_get_resp_command_info_by_cmd(sc.command, false);
let internal = match sub_cmd_info {
Some(info) => info.is_internal || info.parent_is_internal,
None => true,
};
if internal {
continue;
}
external_sub.insert(sc.name.to_lowercase(), sc.clone());
}
}
RespCommandDocsTables {
all,
all_sub,
external,
external_sub,
}
}
fn tables() -> Option<&'static RespCommandDocsTables> {
if !try_initialize() {
return None;
}
TABLES.get().and_then(|t| t.as_ref())
}
pub fn try_get_resp_commands_docs(
external_only: bool,
) -> Option<&'static HashMap<String, RespCommandDocs>> {
let tables = tables()?;
Some(if external_only {
&tables.external
} else {
&tables.all
})
}
pub fn try_get_resp_command_docs(
cmd_name: &str,
external_only: bool,
include_sub_commands: bool,
) -> Option<&'static RespCommandDocs> {
let tables = tables()?;
let key = cmd_name.to_lowercase();
let primary = if external_only {
&tables.external
} else {
&tables.all
};
primary.get(&key).or_else(|| {
if include_sub_commands {
let sub = if external_only {
&tables.external_sub
} else {
&tables.all_sub
};
sub.get(&key)
} else {
None
}
})
}
pub fn try_get_resp_sub_commands_docs(
external_only: bool,
) -> Option<&'static HashMap<String, RespCommandDocs>> {
let tables = tables()?;
Some(if external_only {
&tables.external_sub
} else {
&tables.all_sub
})
}
#[cfg(test)]
mod tests {
use super::{
RespCommandDocFlags, RespCommandGroup, try_get_resp_command_docs, try_get_resp_commands_docs,
try_get_resp_sub_commands_docs,
};
use crate::resp::resp_memory_writer::RespMemoryWriter;
#[test]
fn tables_initialize_and_lookup() {
assert!(super::try_initialize());
let all = try_get_resp_commands_docs(false).unwrap();
let external = try_get_resp_commands_docs(true).unwrap();
assert_eq!(all.len(), 258, "文档根命令数快照");
assert_eq!(external.len(), 258, "外部文档根命令数快照");
let get = try_get_resp_command_docs("get", false, false).unwrap();
assert_eq!(get.group, RespCommandGroup::String);
assert!(
get
.summary
.as_deref()
.unwrap()
.contains("Returns the string value")
);
let config_get = try_get_resp_command_docs("CONFIG|GET", false, true).unwrap();
assert!(config_get.is_sub_command);
assert!(try_get_resp_command_docs("CUSTOMOBJCMD", false, false).is_none());
}
#[test]
fn doc_flags_and_to_resp_format() {
let flags = RespCommandDocFlags::from_member_names("SysCmd").unwrap();
assert_eq!(flags.descriptions(), vec!["syscmd"]);
let get = try_get_resp_command_docs("get", false, false).unwrap();
let mut w = RespMemoryWriter::new(true);
get.to_resp_format(&mut w);
let text = String::from_utf8(w.out).unwrap();
assert!(text.starts_with("$3\r\nGET\r\n"), "{text}");
assert!(text.contains("$7\r\nsummary\r\n"), "{text}");
assert!(text.contains("$5\r\ngroup\r\n$6\r\nstring\r\n"), "{text}");
assert!(text.contains("$9\r\narguments\r\n"), "{text}");
let config = try_get_resp_command_docs("CONFIG", false, false).unwrap();
assert!(!config.sub_commands.is_empty());
let mut w = RespMemoryWriter::new(true);
config.to_resp_format(&mut w);
let text = String::from_utf8(w.out).unwrap();
assert!(text.contains("$11\r\nsubcommands\r\n%"), "{text}");
let subs = try_get_resp_sub_commands_docs(false).unwrap();
assert!(subs.contains_key("config|get"));
}
}