use serde_json::Value;
pub struct ReprParamsConfig {
pub max_chars: usize,
pub batches: usize,
pub is_multi: bool,
}
impl Default for ReprParamsConfig {
fn default() -> Self {
Self {
max_chars: 300,
batches: 10,
is_multi: false,
}
}
}
pub fn repr_params(params: &Value, config: &ReprParamsConfig) -> String {
match params {
Value::Array(arr) if config.is_multi && !arr.is_empty() => repr_multi_params(arr, config),
Value::Array(arr) => repr_array(arr, config),
Value::Object(obj) => repr_object(obj, config),
_ => format!("{:?}", params),
}
}
fn repr_multi_params(params: &[Value], config: &ReprParamsConfig) -> String {
let total = params.len();
if total <= config.batches {
return format!("{:?}", params);
}
let show_count = config.batches / 2;
let first_items: Vec<String> = params
.iter()
.take(show_count)
.map(|v| format!("{:?}", v))
.collect();
let last_items: Vec<String> = params
.iter()
.rev()
.take(show_count)
.map(|v| format!("{:?}", v))
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
format!(
"[{} ... displaying {} of {} total bound parameter sets ... {}]",
first_items.join(", "),
config.batches,
total,
last_items.join(", ")
)
}
fn byte_index_at_char(s: &str, n: usize) -> usize {
s.char_indices().nth(n).map(|(i, _)| i).unwrap_or(s.len())
}
fn repr_array(arr: &[Value], config: &ReprParamsConfig) -> String {
let repr = format!("{:?}", arr);
let char_count = repr.chars().count();
if char_count <= config.max_chars {
return repr;
}
let half_chars = config.max_chars / 2;
let truncated_chars = char_count - config.max_chars;
let start_end = byte_index_at_char(&repr, half_chars);
let tail_start = byte_index_at_char(&repr, char_count - half_chars);
format!(
"{} ... ({} characters truncated) ... {}",
&repr[..start_end],
truncated_chars,
&repr[tail_start..]
)
}
fn repr_object(obj: &serde_json::Map<String, Value>, config: &ReprParamsConfig) -> String {
let repr = format!("{:?}", obj);
let char_count = repr.chars().count();
if char_count <= config.max_chars {
return repr;
}
if obj.len() > 100 {
let half_chars = config.max_chars / 2;
let truncated_params = obj.len() - 10;
let start_end = byte_index_at_char(&repr, half_chars);
let tail_start = byte_index_at_char(&repr, char_count - half_chars);
format!(
"{} ... {} parameters truncated ... {}",
&repr[..start_end],
truncated_params,
&repr[tail_start..]
)
} else {
let half_chars = config.max_chars / 2;
let truncated_chars = char_count - config.max_chars;
let start_end = byte_index_at_char(&repr, half_chars);
let tail_start = byte_index_at_char(&repr, char_count - half_chars);
format!(
"{} ... ({} characters truncated) ... {}",
&repr[..start_end],
truncated_chars,
&repr[tail_start..]
)
}
}
pub fn truncate_param(value: &str, max_chars: usize) -> String {
let char_count = value.chars().count();
if char_count <= max_chars {
return value.to_string();
}
let half_chars = max_chars / 2;
let truncated_chars = char_count - max_chars;
let start_end = byte_index_at_char(value, half_chars);
let tail_start = byte_index_at_char(value, char_count - half_chars);
format!(
"{} ... ({} characters truncated) ... {}",
&value[..start_end],
truncated_chars,
&value[tail_start..]
)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use serde_json::json;
#[rstest]
fn test_repr_params_large_list_of_dict() {
let params: Vec<Value> = (0..100).map(|i| json!({"data": i.to_string()})).collect();
let config = ReprParamsConfig {
max_chars: 300,
batches: 10,
is_multi: true,
};
let result = repr_params(&Value::Array(params), &config);
eprintln!("LARGE_LIST OUTPUT: {}", result);
assert!(
result.contains("displaying 10 of 100 total bound parameter sets"),
"Expected truncation message not found in: {}",
result
);
assert!(
result.contains(r#""data""#) && result.contains(r#""0""#),
"Expected first element with data: 0 not found in: {}",
result
);
assert!(
result.contains(r#""99""#),
"Expected last element with data: 99 not found in: {}",
result
);
assert!(result.starts_with('[') && result.ends_with(']'));
}
#[rstest]
fn test_repr_params_positional_array() {
let params = json!([[1, 2, 3], 5]);
let config = ReprParamsConfig {
max_chars: 300,
batches: 10,
is_multi: false,
};
let result = repr_params(¶ms, &config);
assert_eq!(
result,
"[Array [Number(1), Number(2), Number(3)], Number(5)]"
);
}
#[rstest]
fn test_repr_params_unknown_list() {
let large_array: Vec<i32> = (0..300).collect();
let params = json!([large_array, 5]);
let config = ReprParamsConfig {
max_chars: 80,
batches: 10,
is_multi: false,
};
let result = repr_params(¶ms, &config);
assert!(
result.contains("characters truncated"),
"Expected truncation message not found in: {}",
result
);
assert!(
result.matches("...").count() >= 2,
"Expected ellipsis pattern not found in: {}",
result
);
assert!(
result.starts_with('['),
"Expected array start not found in: {}",
result
);
}
#[rstest]
fn test_repr_params_named_dict() {
let mut params = serde_json::Map::new();
for i in 0..10 {
params.insert(format!("key_{}", i), json!(i));
}
let config = ReprParamsConfig {
max_chars: 300,
batches: 10,
is_multi: false,
};
let result = repr_params(&Value::Object(params.clone()), &config);
assert!(
!result.contains("truncated"),
"Unexpected truncation in: {}",
result
);
for i in 0..10 {
assert!(
result.contains(&format!(r#""key_{}""#, i)),
"Expected key_{} not found in: {}",
i,
result
);
}
assert!(
result.starts_with('{'),
"Expected object start not found in: {}",
result
);
}
#[rstest]
fn test_repr_params_huge_named_dict() {
let mut params = serde_json::Map::new();
for i in 0..800 {
params.insert(format!("key_{}", i), json!(i));
}
let config = ReprParamsConfig {
max_chars: 1400,
batches: 10,
is_multi: false,
};
let result = repr_params(&Value::Object(params), &config);
assert!(
result.contains("parameters truncated"),
"Expected parameters truncation message not found in: {}",
result
);
assert!(
result.matches("...").count() >= 2,
"Expected ellipsis pattern not found in: {}",
result
);
assert!(
result.starts_with('{'),
"Expected object start not found in: {}",
result
);
}
#[rstest]
fn test_repr_params_ismulti_named_dict() {
let param: serde_json::Map<String, Value> =
(0..10).map(|i| (format!("key_{}", i), json!(i))).collect();
let params: Vec<Value> = (0..50).map(|_| Value::Object(param.clone())).collect();
let config = ReprParamsConfig {
max_chars: 80,
batches: 5,
is_multi: true,
};
let result = repr_params(&Value::Array(params), &config);
assert!(
result.contains("displaying 5 of 50 total bound parameter sets"),
"Expected multi-batch truncation message not found in: {}",
result
);
assert!(
result.starts_with('[') && result.ends_with(']'),
"Expected array format not found in: {}",
result
);
}
#[rstest]
fn test_truncate_param() {
let large_param = "a".repeat(5000);
let result = truncate_param(&large_param, 298);
assert!(
result.len() < 5000,
"Expected truncated length, got: {}",
result.len()
);
assert!(
result.len() > 298,
"Result should be longer than max_chars due to truncation message"
);
assert!(
result.contains("characters truncated"),
"Expected truncation message not found in: {}",
result
);
assert!(
result.starts_with("aaaa"),
"Expected start pattern not found in: {}",
result
);
assert!(
result.ends_with("aaaa"),
"Expected end pattern not found in: {}",
result
);
assert!(
result.matches("...").count() == 2,
"Expected exactly 2 ellipsis markers, found: {}",
result.matches("...").count()
);
}
#[rstest]
fn test_truncate_param_small() {
let small_param = "small";
let result = truncate_param(small_param, 100);
assert_eq!(result, "small");
}
#[rstest]
fn test_repr_array_with_multibyte_utf8_does_not_panic() {
let params = json!(["こんにちは世界", "テスト文字列", "日本語データ"]);
let arr = params.as_array().unwrap();
let config = ReprParamsConfig {
max_chars: 20,
batches: 10,
is_multi: false,
};
let result = repr_array(arr, &config);
assert!(result.contains("characters truncated"));
}
#[rstest]
fn test_repr_object_with_multibyte_utf8_does_not_panic() {
let mut obj = serde_json::Map::new();
for i in 0..10 {
obj.insert(format!("キー_{}", i), json!(format!("値_{}", i)));
}
let config = ReprParamsConfig {
max_chars: 20,
batches: 10,
is_multi: false,
};
let result = repr_object(&obj, &config);
assert!(result.contains("characters truncated"));
}
#[rstest]
fn test_repr_object_huge_with_multibyte_utf8_does_not_panic() {
let mut obj = serde_json::Map::new();
for i in 0..200 {
obj.insert(format!("キー_{}", i), json!(format!("値_{}", i)));
}
let config = ReprParamsConfig {
max_chars: 50,
batches: 10,
is_multi: false,
};
let result = repr_object(&obj, &config);
assert!(result.contains("parameters truncated"));
}
#[rstest]
fn test_truncate_param_with_multibyte_utf8_does_not_panic() {
let multibyte_param = "あ".repeat(500);
let result = truncate_param(&multibyte_param, 50);
assert!(result.contains("characters truncated"));
assert!(result.starts_with("あ"));
assert!(result.ends_with("あ"));
}
#[rstest]
fn test_truncate_param_with_mixed_ascii_and_multibyte() {
let mixed = format!("{}abc{}", "日本語".repeat(50), "中文字".repeat(50));
let result = truncate_param(&mixed, 30);
assert!(result.contains("characters truncated"));
}
#[rstest]
fn test_byte_index_at_char_with_ascii() {
let s = "hello";
assert_eq!(byte_index_at_char(s, 0), 0);
assert_eq!(byte_index_at_char(s, 3), 3);
assert_eq!(byte_index_at_char(s, 5), 5);
assert_eq!(byte_index_at_char(s, 10), 5); }
#[rstest]
fn test_byte_index_at_char_with_multibyte() {
let s = "あいう";
assert_eq!(byte_index_at_char(s, 0), 0);
assert_eq!(byte_index_at_char(s, 1), 3);
assert_eq!(byte_index_at_char(s, 2), 6);
assert_eq!(byte_index_at_char(s, 3), 9);
assert_eq!(byte_index_at_char(s, 10), 9); }
#[rstest]
#[case("あいうえお".repeat(30), 20, "あ", "お")] #[case("日本語テスト".repeat(30), 20, "日", "ト")] #[case("中文测试".repeat(30), 20, "中", "试")] #[case("αβγδεζηθ".repeat(30), 20, "α", "θ")] fn test_truncate_param_multibyte_produces_valid_utf8_regression(
#[case] input: String,
#[case] max_chars: usize,
#[case] expected_start: &str,
#[case] expected_end: &str,
) {
let result = truncate_param(&input, max_chars);
assert!(
result.contains("characters truncated"),
"Regression #762: truncation message must be present, got: {}",
result
);
assert!(
result.starts_with(expected_start),
"Regression #762: result must start with a complete character '{}', got: {}",
expected_start,
result
);
assert!(
result.ends_with(expected_end),
"Regression #762: result must end with a complete character '{}', got: {}",
expected_end,
result
);
let prefix: &str = result.split(" ... ").next().unwrap_or("");
let prefix_char_count = prefix.chars().count();
assert_eq!(
prefix_char_count,
max_chars / 2,
"Regression #762: prefix must contain exactly half of max_chars characters, got {}",
prefix_char_count
);
}
#[rstest]
fn test_truncate_param_ascii_then_multibyte_split_regression() {
let input = format!("{}{}", "abcdefghij", "あ".repeat(200));
let result = truncate_param(&input, 30);
assert!(
result.contains("characters truncated"),
"Regression #762: truncation message expected, got: {}",
result
);
let _ = result.chars().count(); }
}