use std::fmt;
use std::sync::Arc;
use crate::domain::jsonrpc::WireId;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RequestId {
Number(i64),
Text(Arc<str>),
}
impl RequestId {
#[must_use]
pub const fn number(value: i64) -> Self {
Self::Number(value)
}
#[must_use]
pub fn text(value: impl AsRef<str>) -> Self {
Self::Text(Arc::from(value.as_ref()))
}
#[must_use]
pub fn from_wire(id: Option<&WireId<'_>>) -> Self {
match id {
Some(WireId::Number(value)) => Self::Number(*value),
Some(WireId::Text(text)) => Self::text(text.as_ref()),
None => Self::Number(0),
}
}
#[must_use]
pub fn to_json(&self) -> String {
match self {
Self::Number(value) => value.to_string(),
Self::Text(value) => format!("{value:?}"),
}
}
#[must_use]
pub fn redacted(&self) -> String {
match self {
Self::Number(value) => format!("#{value}"),
Self::Text(value) => {
let head: String = value.chars().take(4).collect();
format!("#{head}…{}", value.chars().count())
}
}
}
}
impl fmt::Display for RequestId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Number(value) => write!(formatter, "{value}"),
Self::Text(value) => formatter.write_str(value),
}
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::panic,
reason = "tests panic on the failing precondition, which is the message worth having"
)]
use super::RequestId;
use std::sync::Arc;
#[test]
fn numbers_and_texts_compare_by_value() {
assert_eq!(RequestId::number(7), RequestId::number(7));
assert_ne!(RequestId::number(7), RequestId::number(8));
assert_eq!(RequestId::text("a"), RequestId::text("a"));
assert_ne!(RequestId::number(7), RequestId::text("7"));
assert_eq!(RequestId::text("abc"), RequestId::text(String::from("abc")));
}
#[test]
fn text_ids_are_refcounted_not_copied() {
let a = RequestId::text("shared");
let b = a.clone();
match (&a, &b) {
(RequestId::Text(left), RequestId::Text(right)) => {
assert!(Arc::ptr_eq(left, right), "clone copied the string");
}
_ => panic!("expected text ids"),
}
}
#[test]
fn to_json_quotes_only_string_ids() {
assert_eq!(RequestId::number(7).to_json(), "7");
assert_eq!(RequestId::number(-1).to_json(), "-1");
assert_eq!(RequestId::text("abc").to_json(), r#""abc""#);
}
#[test]
fn redacting_a_multi_byte_id_does_not_panic() {
for id in ["ab€", "€€€", "工具调用", "café", "🎉ok", "a€b€"] {
let redacted = RequestId::text(id).redacted();
assert!(redacted.starts_with('#'), "{id:?} redacted to {redacted:?}");
assert!(redacted.contains('…'), "{id:?} redacted to {redacted:?}");
}
}
#[test]
fn the_redacted_length_counts_characters() {
assert_eq!(RequestId::text("🎉ok").redacted(), "#🎉ok…3");
assert_eq!(RequestId::text("abcdef").redacted(), "#abcd…6");
}
#[test]
fn redaction_correlates_without_disclosing() {
assert_eq!(RequestId::number(7).redacted(), "#7");
let redacted = RequestId::text("secret-token-value").redacted();
assert!(redacted.starts_with("#sec"), "{redacted}");
assert!(!redacted.contains("secret-token-value"), "{redacted}");
assert!(RequestId::text("ab").redacted().starts_with("#ab"));
}
#[test]
fn display_is_the_bare_value() {
assert_eq!(RequestId::number(7).to_string(), "7");
assert_eq!(RequestId::text("abc").to_string(), "abc");
}
#[test]
fn ids_sort_deterministically() {
let mut ids = vec![
RequestId::text("b"),
RequestId::number(2),
RequestId::text("a"),
RequestId::number(1),
];
ids.sort();
assert_eq!(
ids,
vec![
RequestId::number(1),
RequestId::number(2),
RequestId::text("a"),
RequestId::text("b"),
]
);
}
}