wresp 0.1.3

RESP (REdis Serialization Protocol) response writer and serializer
//! RESP 命令层共享错误文案与应答写出辅助(对标 libs/server/Resp/CmdStrings.cs)
//!
//! `write_resp_error`(见 [`crate::ext::RespVecExt`])固定前置 `-ERR `,
//! 而 C# 大量错误常量自带 `ERR`/`WRONGTYPE` 等完整前缀(经 RespWriteUtils.
//! TryWriteError 以 `-<msg>\r\n` 原样写出),故此处提供不加工前缀的原样写出。

use crate::{Resp3, RespWriter, sanitize_error_str};

/// libs/server/Resp/CmdStrings.cs:RESP_OK
pub const RESP_OK: &[u8] = b"+OK\r\n";
/// libs/server/Resp/CmdStrings.cs:RESP_PONG
pub const RESP_PONG: &[u8] = b"+PONG\r\n";
/// libs/server/Resp/CmdStrings.cs:RESP_EMPTYLIST
pub const RESP_EMPTYLIST: &[u8] = b"*0\r\n";
/// libs/server/Resp/CmdStrings.cs:RESP_RETURN_VAL_0
pub const RESP_RETURN_VAL_0: &[u8] = b":0\r\n";
/// libs/server/Resp/CmdStrings.cs:RESP_RETURN_VAL_1
pub const RESP_RETURN_VAL_1: &[u8] = b":1\r\n";
/// libs/server/Resp/CmdStrings.cs:RESP_RETURN_VAL_N1
pub const RESP_RETURN_VAL_N1: &[u8] = b":-1\r\n";
/// libs/server/Resp/CmdStrings.cs:RESP_RETURN_VAL_N2
pub const RESP_RETURN_VAL_N2: &[u8] = b":-2\r\n";

/// wnode 命令层通用兜底错误文案(经 write_resp_error 前置 `-ERR ` 后输出 `-ERR generic error\r\n`)
pub const RESP_ERR_GENERIC: &str = "generic error";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NOAUTH
pub const RESP_ERR_NOAUTH: &str = "NOAUTH Authentication required.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NOPERM
pub const RESP_ERR_NOPERM: &str = "NOPERM this user has no permissions to run the command";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_WRONG_TYPE
pub const RESP_ERR_WRONG_TYPE: &str =
  "WRONGTYPE Operation against a key holding the wrong kind of value.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_UNK_CMD
pub const RESP_ERR_GENERIC_UNK_CMD: &str = "ERR unknown command";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_CLUSTER_DISABLED
pub const RESP_ERR_GENERIC_CLUSTER_DISABLED: &str =
  "ERR This instance has cluster support disabled";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_NOSUCHKEY
pub const RESP_ERR_GENERIC_NOSUCHKEY: &str = "ERR no such key";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_INVALIDEXP_IN_SET
pub const RESP_ERR_GENERIC_INVALIDEXP_IN_SET: &str = "ERR invalid expire time in 'set' command";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_INVALIDEXP_IN_GETEX
pub const RESP_ERR_GENERIC_INVALIDEXP_IN_GETEX: &str = "ERR invalid expire time in 'getex' command";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_OVERFLOWEXP_IN_GETEX
pub const RESP_ERR_OVERFLOWEXP_IN_GETEX: &str = "ERR expire time overflows date in 'getex' command";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_SYNTAX_ERROR
pub const RESP_ERR_GENERIC_SYNTAX_ERROR: &str = "ERR syntax error";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_INVALID_ETAG
pub const RESP_ERR_INVALID_ETAG: &str = "ETAG must be a numerical value greater than or equal to 0";
/// libs/server/Resp/CmdStrings.cs:NOGET
pub const NOGET: &str = "NOGET";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_NAN_INFINITY_INCR
pub const RESP_ERR_GENERIC_NAN_INFINITY_INCR: &str = "ERR increment would produce NaN or Infinity";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_OFFSETOUTOFRANGE
pub const RESP_ERR_GENERIC_OFFSETOUTOFRANGE: &str = "ERR offset is out of range";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER
pub const RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER: &str =
  "ERR value is not an integer or out of range.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_VALUE_IS_OUT_OF_RANGE
pub const RESP_ERR_GENERIC_VALUE_IS_OUT_OF_RANGE: &str =
  "ERR value is out of range, must be positive.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_BIT_IS_NOT_INTEGER
pub const RESP_ERR_GENERIC_BIT_IS_NOT_INTEGER: &str = "ERR bit is not an integer or out of range";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_BITOFFSET_IS_NOT_INTEGER
pub const RESP_ERR_GENERIC_BITOFFSET_IS_NOT_INTEGER: &str =
  "ERR bit offset is not an integer or out of range";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_PROTOCOL_VALUE_IS_NOT_INTEGER
pub const RESP_ERR_PROTOCOL_VALUE_IS_NOT_INTEGER: &str =
  "ERR Protocol version is not an integer or out of range.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_UNSUPPORTED_PROTOCOL_VERSION
pub const RESP_ERR_UNSUPPORTED_PROTOCOL_VERSION: &str = "ERR Unsupported protocol version";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NOT_VALID_FLOAT
pub const RESP_ERR_NOT_VALID_FLOAT: &str = "ERR value is not a valid float";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_TIMEOUT_NOT_VALID_FLOAT
pub const RESP_ERR_TIMEOUT_NOT_VALID_FLOAT: &str = "ERR timeout is not a float or out of range";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NOT_VALID_RADIUS
pub const RESP_ERR_NOT_VALID_RADIUS: &str = "ERR need numeric radius";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_RADIUS_IS_NEGATIVE
pub const RESP_ERR_RADIUS_IS_NEGATIVE: &str = "ERR radius cannot be negative";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NOT_VALID_WIDTH
pub const RESP_ERR_NOT_VALID_WIDTH: &str = "ERR need numeric width";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NOT_VALID_HEIGHT
pub const RESP_ERR_NOT_VALID_HEIGHT: &str = "ERR need numeric height";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_HEIGHT_OR_WIDTH_NEGATIVE
pub const RESP_ERR_HEIGHT_OR_WIDTH_NEGATIVE: &str = "ERR height or width cannot be negative";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NOT_VALID_GEO_DISTANCE_UNIT
pub const RESP_ERR_NOT_VALID_GEO_DISTANCE_UNIT: &str =
  "ERR unsupported unit provided. please use M, KM, FT, MI";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_COUNT_IS_NOT_POSITIVE
pub const RESP_ERR_COUNT_IS_NOT_POSITIVE: &str = "ERR COUNT must be > 0";
/// 慢路径异步扫描/清库 IO 失败的兜底文案(非 C# 文案:存储层错误统一降噪为
/// 此单行,杜绝内部错误细节泄漏给客户端;wnode 执行域与 wedb 集群域共用)
pub const RESP_ERR_SLOW_PATH_STORAGE: &str = "ERR slow path storage error";
/// libs/server/Resp/CmdStrings.cs:RESP_WRONGPASS_INVALID_PASSWORD
pub const RESP_WRONGPASS_INVALID_PASSWORD: &str = "WRONGPASS Invalid password";
/// libs/server/Resp/CmdStrings.cs:RESP_WRONGPASS_INVALID_USERNAME_PASSWORD
pub const RESP_WRONGPASS_INVALID_USERNAME_PASSWORD: &str =
  "WRONGPASS Invalid username/password combination";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_BUSSYKEY
pub const RESP_ERR_BUSSYKEY: &str = "BUSYKEY Target key name already exists.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_INVALID_EXPIRE_TIME
pub const RESP_ERR_INVALID_EXPIRE_TIME: &str = "ERR invalid expire time, must be >= 0";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NOSCRIPT
pub const RESP_ERR_NOSCRIPT: &str = "ERR This Redis command is not allowed from script";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_HCOLLECT_ALREADY_IN_PROGRESS
pub const RESP_ERR_HCOLLECT_ALREADY_IN_PROGRESS: &str = "ERR HCOLLECT scan already in progress";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_ZCOLLECT_ALREADY_IN_PROGRESS
pub const RESP_ERR_ZCOLLECT_ALREADY_IN_PROGRESS: &str = "ERR ZCOLLECT scan already in progress";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_OBJECT_FREQ_UNSUPPORTED
pub const RESP_ERR_OBJECT_FREQ_UNSUPPORTED: &str = "ERR OBJECT FREQ is not supported: Garnet does not track access frequency (no LFU maxmemory policy).";
/// libs/server/Resp/CmdStrings.cs:RESP_INVALID_COMMAND_SPECIFIED
pub const RESP_INVALID_COMMAND_SPECIFIED: &str = "Invalid command specified";
/// libs/server/Resp/CmdStrings.cs:RESP_COMMAND_HAS_NO_KEY_ARGS
pub const RESP_COMMAND_HAS_NO_KEY_ARGS: &str = "The command has no key arguments";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NOT_SUPPORTED_RESP2
pub const RESP_ERR_NOT_SUPPORTED_RESP2: &str = "ERR command not supported in RESP2";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_ASYNC_PROTOCOL_CHANGE
pub const RESP_ERR_ASYNC_PROTOCOL_CHANGE: &str =
  "ERR protocol change is not allowed with pending async operations";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_CANNOT_LIST_CLIENTS
pub const RESP_ERR_CANNOT_LIST_CLIENTS: &str = "ERR Clients cannot be listed.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_UBLOCKING_CLINET
pub const RESP_ERR_UBLOCKING_CLINET: &str = "ERR Unable to unblock client because of error.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NO_SUCH_CLIENT
pub const RESP_ERR_NO_SUCH_CLIENT: &str = "ERR No such client";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_INVALID_CLIENT_ID
pub const RESP_ERR_INVALID_CLIENT_ID: &str = "ERR Invalid client ID";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_INVALID_CLIENT_NAME
pub const RESP_ERR_INVALID_CLIENT_NAME: &str =
  "ERR Client names cannot contain spaces, newlines or special characters.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_INVALID_CLIENT_UNBLOCK_REASON
pub const RESP_ERR_INVALID_CLIENT_UNBLOCK_REASON: &str =
  "ERR CLIENT UNBLOCK reason should be TIMEOUT or ERROR";
/// libs/server/Resp/CmdStrings.cs:RESP_UNBLOCKED_CLIENT_VIA_CLIENT_UNBLOCK
///
/// 无 ERR 前缀(C# 经 TryWriteError 原样写出文案)
pub const RESP_UNBLOCKED_CLIENT_VIA_CLIENT_UNBLOCK: &str =
  "UNBLOCKED client unblocked via CLIENT UNBLOCK";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_EXPDELSCAN_INVALID
pub const RESP_ERR_EXPDELSCAN_INVALID: &str =
  "ERR Cannot execute EXPDELSCAN with background expired key deletion scan enabled";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_CHECKPOINT_ALREADY_IN_PROGRESS
pub const RESP_ERR_CHECKPOINT_ALREADY_IN_PROGRESS: &str = "ERR checkpoint already in progress";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_DB_INDEX_OUT_OF_RANGE
pub const RESP_ERR_DB_INDEX_OUT_OF_RANGE: &str = "ERR DB index is out of range.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_INVALID_FIRST_DB_INDEX
pub const RESP_ERR_INVALID_FIRST_DB_INDEX: &str = "ERR invalid first DB index.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_INVALID_SECOND_DB_INDEX
pub const RESP_ERR_INVALID_SECOND_DB_INDEX: &str = "ERR invalid second DB index.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_SELECT_UNSUCCESSFUL
pub const RESP_ERR_SELECT_UNSUCCESSFUL: &str = "ERR unable to select database.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_SELECT_CLUSTER_MODE
pub const RESP_ERR_GENERIC_SELECT_CLUSTER_MODE: &str = "ERR SELECT is not allowed in cluster mode";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_SWAPDB_CLUSTER_MODE
pub const RESP_ERR_GENERIC_SWAPDB_CLUSTER_MODE: &str = "ERR SWAPDB is not allowed in cluster mode";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_SWAPDB_UNSUPPORTED
pub const RESP_ERR_SWAPDB_UNSUPPORTED: &str =
  "ERR SWAPDB is currently unsupported when multiple clients are connected.";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_DB_ID_CLUSTER_MODE
pub const RESP_ERR_DB_ID_CLUSTER_MODE: &str =
  "ERR specifying non-zero DBID is not allowed in cluster mode";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_FLUSHALL_READONLY_REPLICA
pub const RESP_ERR_FLUSHALL_READONLY_REPLICA: &str =
  "ERR You can't write against a read only replica.";
/// libs/server/Resp/CmdStrings.cs:GenericErrWrongNumArgs
pub const GENERIC_ERR_WRONG_NUM_ARGS: &str = "ERR wrong number of arguments for '{0}' command";

/// 参数数量错误文案编译期展开宏([`GENERIC_ERR_WRONG_NUM_ARGS`] 的单点静态
/// 形态):concat! 零堆分配,供 `&'static str` 场景(如 VectorReply 错误帧)
/// 引用;命令名非字面量的运行时路径走 [`abort_with_wrong_number_of_arguments`]
/// 或模板 replace,三者展开文本逐字节一致
#[macro_export]
macro_rules! wrong_num_args {
  ($cmd:literal) => {
    concat!("ERR wrong number of arguments for '", $cmd, "' command")
  };
}

/// libs/server/Resp/CmdStrings.cs:RESP_ERR_WRONG_NUMBER_OF_ARGUMENTS
pub const RESP_ERR_WRONG_NUMBER_OF_ARGUMENTS: &str = "ERR wrong number of arguments";
/// LMPOP/SMPOP/BZMPOP 等命令的 numkeys 校验文案(跨 list/set/sortedset 三域复用)
pub const RESP_ERR_GENERIC_NUMKEYS: &str = "ERR numkeys should be greater than 0";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_NO_TRANSACTION_PROCEDURE
pub const RESP_ERR_NO_TRANSACTION_PROCEDURE: &str = "ERR Could not get transaction procedure";
/// libs/server/Resp/CmdStrings.cs:RESP_ERR_GENERIC_INVALIDCURSOR
pub const RESP_ERR_GENERIC_INVALIDCURSOR: &str = "ERR invalid cursor";
/// libs/server/Objects/GarnetObjectBase.cs 对象命令不支持的通用文案
/// (跨 hash/set/list/sortedset 四对象域复用)
pub const RESP_ERR_GENERIC_UNSUPPORTED_OPERATION: &str = "ERR unsupported operation";
/// libs/server/Resp/CmdStrings.cs:GenericErrUnknownSubCommand
pub const GENERIC_ERR_UNKNOWN_SUB_COMMAND: &str = "ERR unknown subcommand '{0}'. Try {1} HELP";
/// libs/server/Resp/CmdStrings.cs:GenericErrUnknownSubCommandNoHelp
pub const GENERIC_ERR_UNKNOWN_SUB_COMMAND_NO_HELP: &str = "ERR unknown subcommand '{0}'.";
/// libs/server/Resp/CmdStrings.cs:GenericUnknownClientType
pub const GENERIC_UNKNOWN_CLIENT_TYPE: &str = "ERR Unknown client type '{0}'";
/// libs/server/Resp/CmdStrings.cs:GenericErrDuplicateFilter
pub const GENERIC_ERR_DUPLICATE_FILTER: &str = "ERR Filter '{0}' defined multiple times";
/// libs/server/Resp/CmdStrings.cs:GenericPubSubCommandNotAllowed
pub const GENERIC_PUBSUB_COMMAND_NOT_ALLOWED: &str = "ERR Can't execute '{0}': only (P|S)SUBSCRIBE / (P|S)UNSUBSCRIBE / PING / QUIT are allowed in this context";
/// libs/server/Resp/CmdStrings.cs:GenericErrShouldBeGreaterThanZero
pub const GENERIC_ERR_SHOULD_BE_GREATER_THAN_ZERO: &str = "ERR {0} should be greater than 0";
/// libs/server/Resp/CmdStrings.cs:GenericErrNotAFloat(SortedSet WEIGHTS 固定替换 {0}="weight")
pub const GENERIC_ERR_NOT_A_FLOAT_WEIGHT: &str = "ERR weight value is not a valid float";
/// libs/server/Resp/CmdStrings.cs:GenericParamShouldBeGreaterThanZero
pub const GENERIC_PARAM_SHOULD_BE_GREATER_THAN_ZERO: &str =
  "ERR Parameter `{0}` should be greater than 0";
/// libs/server/Resp/CmdStrings.cs:GenericErrUnknownOptionConfigSet
pub const GENERIC_ERR_UNKNOWN_OPTION_CONFIG_SET: &str =
  "ERR Unknown option or number of arguments for CONFIG SET - '{0}'";
/// libs/server/Resp/CmdStrings.cs:GenericErrCommandDisallowedWithOption (DEBUG 实例化)
pub const RESP_ERR_DEBUG_DISALLOWED: &str = "ERR DEBUG command not allowed. If the enable-debug-command option is set to \"local\", you can run it from a local connection, otherwise you need to set this option in the configuration file, and then restart the server.";

/// 反射参数/命令名截断上限(防恶意超长子命令/选项名攻击)
pub const MAX_PARAM_NAME_LEN: usize = 128;

/// 原样写出错误行 `-<msg>\r\n`(msg 自带 `ERR`/`WRONGTYPE` 等完整前缀,
/// 对标 libs/common/RespWriteUtils.cs:TryWriteError)
#[inline]
pub fn write_error_raw(output: &mut Vec<u8>, msg: &str) {
  RespWriter::new_ref(output).write_error(msg);
}

/// 以 `GenericErrWrongNumArgs` 格式化命令名并写出错误应答(零堆分配直接写入)
#[inline]
pub fn abort_with_wrong_number_of_arguments(output: &mut Vec<u8>, cmd_name: &str) {
  let clean_name = sanitize_error_str(cmd_name, MAX_PARAM_NAME_LEN);
  output.extend_from_slice(b"-ERR wrong number of arguments for '");
  output.extend_from_slice(clean_name.as_bytes());
  output.extend_from_slice(b"' command\r\n");
}

/// 写出不支持选项错误应答:`-ERR Unsupported option <option>\r\n`(零堆分配)
#[inline]
pub fn abort_with_unsupported_option(output: &mut Vec<u8>, option: &str) {
  let clean_opt = sanitize_error_str(option, MAX_PARAM_NAME_LEN);
  output.extend_from_slice(b"-ERR Unsupported option ");
  output.extend_from_slice(clean_opt.as_bytes());
  output.extend_from_slice(b"\r\n");
}

/// 写出未知子命令错误应答:`-ERR unknown subcommand '<sub_command>'. Try <cmd_name> HELP\r\n`(零堆分配)
#[inline]
pub fn abort_with_unknown_subcommand(output: &mut Vec<u8>, sub_command: &str, cmd_name: &str) {
  let clean_sub = sanitize_error_str(sub_command, MAX_PARAM_NAME_LEN);
  let clean_cmd = sanitize_error_str(cmd_name, MAX_PARAM_NAME_LEN);
  output.extend_from_slice(b"-ERR unknown subcommand '");
  output.extend_from_slice(clean_sub.as_bytes());
  output.extend_from_slice(b"'. Try ");
  output.extend_from_slice(clean_cmd.as_bytes());
  output.extend_from_slice(b" HELP\r\n");
}

/// 写出未知子命令或错误参数数量应答(零堆分配)
#[inline]
pub fn abort_with_unknown_subcommand_or_wrong_num_args(
  output: &mut Vec<u8>,
  sub_command: &str,
  cmd_name: &str,
) {
  let clean_sub = sanitize_error_str(sub_command, MAX_PARAM_NAME_LEN);
  let clean_cmd = sanitize_error_str(cmd_name, MAX_PARAM_NAME_LEN);
  output.extend_from_slice(b"-ERR unknown subcommand or wrong number of arguments for '");
  output.extend_from_slice(clean_sub.as_bytes());
  output.extend_from_slice(b"'. Try ");
  output.extend_from_slice(clean_cmd.as_bytes());
  output.extend_from_slice(b" HELP\r\n");
}

/// 原样写出错误应答并终止命令处理
pub fn abort_with_error_message(output: &mut Vec<u8>, error_message: &str) {
  write_error_raw(output, error_message);
}

/// 以 `+PONG\r\n` 等已含类型前缀的原始字节帧写出应答
#[inline]
pub fn write_raw(output: &mut Vec<u8>, frame: &[u8]) {
  output.extend_from_slice(frame);
}

/// RESP2 口径写 map 头(RESP2 分支:map 退化为双倍长度数组)
#[inline]
pub fn write_map_len_resp2(output: &mut Vec<u8>, len: usize) {
  RespWriter::new_ref(output).write_map_length(len);
}

/// 运行时按会话协议版本写 map 头(对标 RespServerSessionOutput.cs:WriteMapLength:
/// RESP3 写 `%<len>\r\n`,RESP2 退化为双倍长度数组 `*<2len>\r\n`)
#[inline]
pub fn write_map_len(output: &mut Vec<u8>, len: usize, resp_protocol_version: u8) {
  if resp_protocol_version >= 3 {
    RespWriter::<&mut Vec<u8>, Resp3>::new_ref_p(output).write_map_length(len);
  } else {
    write_map_len_resp2(output, len);
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn write_error_raw_frames_message() {
    let mut out = Vec::new();
    write_error_raw(&mut out, RESP_ERR_GENERIC_NOSUCHKEY);
    assert_eq!(out, b"-ERR no such key\r\n");
  }

  #[test]
  fn write_error_raw_noperm() {
    let mut out = Vec::new();
    write_error_raw(&mut out, RESP_ERR_NOPERM);
    assert_eq!(
      out,
      b"-NOPERM this user has no permissions to run the command\r\n"
    );
  }

  #[test]
  fn write_error_raw_sanitizes_crlf() {
    let mut out = Vec::new();
    write_error_raw(&mut out, "ERR error with \r\ninjection");
    assert_eq!(out, b"-ERR error with \r\n");
  }

  #[test]
  fn wrong_num_args_formats_command_name() {
    let mut out = Vec::new();
    abort_with_wrong_number_of_arguments(&mut out, "GETEX");
    assert_eq!(
      out,
      b"-ERR wrong number of arguments for 'GETEX' command\r\n"
    );
  }

  #[test]
  fn map_len_resp2_doubles_array_length() {
    let mut out = Vec::new();
    write_map_len_resp2(&mut out, 2);
    assert_eq!(out, b"*4\r\n");
  }

  #[test]
  fn map_len_dispatches_by_protocol_version() {
    // RespServerSessionOutput.cs:WriteMapLength:RESP3 写 %,RESP2 双倍数组
    let mut out = Vec::new();
    write_map_len(&mut out, 3, 3);
    assert_eq!(out, b"%3\r\n");

    let mut out = Vec::new();
    write_map_len(&mut out, 3, 2);
    assert_eq!(out, b"*6\r\n");
  }

  #[test]
  fn raw_frames_passthrough() {
    let mut out = Vec::new();
    write_raw(&mut out, RESP_OK);
    write_raw(&mut out, RESP_RETURN_VAL_N2);
    assert_eq!(out, b"+OK\r\n:-2\r\n");
  }
}