use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::config::ArgumentPrintMode;
use crate::identifiers::{CallId, TaskId};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SerializedArguments(pub BTreeMap<String, String>);
impl SerializedArguments {
pub fn new() -> Self {
Self(BTreeMap::new())
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.0.insert(key.into(), value.into());
}
pub fn compute_args_id(&self) -> String {
if self.0.is_empty() {
return "no_args".to_string();
}
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
for (k, v) in &self.0 {
let ek = serde_json::to_string(k).unwrap_or_else(|_| k.clone());
let ev = serde_json::to_string(v).unwrap_or_else(|_| v.clone());
hasher.update(ek.as_bytes());
hasher.update(b"=");
hasher.update(ev.as_bytes());
hasher.update(b";");
}
format!("{:x}", hasher.finalize())
}
}
impl Default for SerializedArguments {
fn default() -> Self {
Self::new()
}
}
impl SerializedArguments {
pub fn cc_arg_pairs(&self) -> Vec<(String, String)> {
if self.0.is_empty() {
vec![(String::new(), String::new())]
} else {
self.0.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
}
}
}
impl SerializedArguments {
pub fn cc_key(args: Option<&Self>) -> String {
match args {
None => String::new(),
Some(a) if a.0.is_empty() => String::new(),
Some(a) => a.compute_args_id(),
}
}
pub fn display(&self, mode: ArgumentPrintMode, truncate_length: usize) -> String {
if self.0.is_empty() {
return "<no_args>".to_string();
}
match mode {
ArgumentPrintMode::Hidden => "<arguments hidden>".to_string(),
ArgumentPrintMode::Keys => {
let keys: Vec<&str> = self.0.keys().map(std::string::String::as_str).collect();
format!("{{{}}}", keys.join(", "))
}
ArgumentPrintMode::Full => {
let pairs: Vec<String> = self.0.iter().map(|(k, v)| format!("{k}: {v}")).collect();
format!("{{{}}}", pairs.join(", "))
}
ArgumentPrintMode::Truncated => {
let pairs: Vec<String> = self
.0
.iter()
.map(|(k, v)| {
if v.len() > truncate_length {
let end = v
.char_indices()
.nth(truncate_length)
.map_or(v.len(), |(i, _)| i);
format!("{k}: {}...", &v[..end])
} else {
format!("{k}: {v}")
}
})
.collect();
format!("{{{}}}", pairs.join(", "))
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallDTO {
pub call_id: CallId,
pub task_id: TaskId,
pub serialized_arguments: SerializedArguments,
}
impl CallDTO {
pub fn new(task_id: TaskId, args: SerializedArguments) -> Self {
let args_id = args.compute_args_id();
let call_id = CallId::new(task_id.clone(), args_id);
Self {
call_id,
task_id,
serialized_arguments: args,
}
}
}
impl std::fmt::Display for CallDTO {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let keys: Vec<&str> = self
.serialized_arguments
.0
.keys()
.map(std::string::String::as_str)
.collect();
write!(
f,
"Call(task={}, arguments=[{}])",
self.task_id,
keys.join(", ")
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn args_id_is_deterministic() {
let mut args1 = SerializedArguments::new();
args1.insert("x", "42");
args1.insert("y", "hello");
let mut args2 = SerializedArguments::new();
args2.insert("y", "hello");
args2.insert("x", "42");
assert_eq!(args1.compute_args_id(), args2.compute_args_id());
}
#[test]
fn different_args_different_id() {
let mut args1 = SerializedArguments::new();
args1.insert("x", "42");
let mut args2 = SerializedArguments::new();
args2.insert("x", "43");
assert_ne!(args1.compute_args_id(), args2.compute_args_id());
}
#[test]
fn empty_args_id() {
let args = SerializedArguments::new();
let id = args.compute_args_id();
assert!(!id.is_empty());
let args2 = SerializedArguments::default();
assert_eq!(id, args2.compute_args_id());
}
#[test]
fn call_dto_new() {
let task_id = TaskId::new("mod", "func");
let mut args = SerializedArguments::new();
args.insert("a", "1");
let call = CallDTO::new(task_id.clone(), args.clone());
assert_eq!(call.task_id, task_id);
assert_eq!(call.call_id.task_id, task_id);
assert_eq!(&*call.call_id.args_id, args.compute_args_id());
assert_eq!(call.serialized_arguments, args);
}
#[test]
fn serde_round_trip_call_dto() {
let task_id = TaskId::new("mod", "func");
let mut args = SerializedArguments::new();
args.insert("key", "val");
let call = CallDTO::new(task_id, args);
let json = serde_json::to_string(&call).unwrap();
let back: CallDTO = serde_json::from_str(&json).unwrap();
assert_eq!(back.call_id, call.call_id);
assert_eq!(back.task_id, call.task_id);
assert_eq!(back.serialized_arguments, call.serialized_arguments);
}
#[test]
fn args_id_no_delimiter_collision() {
let mut args1 = SerializedArguments::new();
args1.insert("a", "b;c=d");
let mut args2 = SerializedArguments::new();
args2.insert("a", "b");
args2.insert("c", "d");
assert_ne!(args1.compute_args_id(), args2.compute_args_id());
}
#[test]
fn truncated_display_safe_on_multibyte_utf8() {
let mut args = SerializedArguments::new();
args.insert("x", "日本語テスト");
let result = args.display(ArgumentPrintMode::Truncated, 2);
assert!(result.contains("日本..."));
}
}