use serde_json::Value;
pub(crate) const OFFSET_KEYS: &[&str] = &[
"offset",
"offset_lines",
"offset_bytes",
"o",
"line_start",
"start_line",
];
pub(crate) const LIMIT_KEYS: &[&str] = &[
"limit",
"limit_lines",
"page_size_lines",
"max_lines",
"chunk_lines",
"line_end",
"end_line",
];
pub(crate) const PAGE_KEYS: &[&str] = &["page", "per_page"];
const NORMALIZE_ONLY_KEYS: &[&str] = &["encoding"];
pub(crate) fn bounded_extent_keys() -> impl Iterator<Item = &'static str> {
OFFSET_KEYS.iter().chain(LIMIT_KEYS).chain(PAGE_KEYS).copied()
}
pub(crate) fn normalization_strip_keys() -> impl Iterator<Item = &'static str> {
bounded_extent_keys().chain(NORMALIZE_ONLY_KEYS.iter().copied())
}
pub(crate) fn args_have_bounded_extent(args: &Value) -> bool {
args.as_object()
.is_some_and(|obj| bounded_extent_keys().any(|key| obj.contains_key(key)))
}
fn as_u64_lenient(value: &Value) -> Option<u64> {
value
.as_u64()
.or_else(|| value.as_str().and_then(|s| s.trim().parse::<u64>().ok()))
}
pub(crate) fn extent_offset(args: &Value) -> u64 {
OFFSET_KEYS
.iter()
.find_map(|key| args.get(*key).and_then(as_u64_lenient))
.unwrap_or(0)
}
pub(crate) fn extent_limit(args: &Value) -> Option<u64> {
LIMIT_KEYS.iter().find_map(|key| args.get(*key).and_then(as_u64_lenient))
}
fn raw_flag(args: &Value) -> bool {
args.get("raw").and_then(Value::as_bool).unwrap_or(false)
}
pub(crate) fn extent_covers(cached: &Value, query: &Value) -> bool {
if raw_flag(cached) != raw_flag(query) {
return false;
}
if extent_offset(cached) != extent_offset(query) {
return false;
}
match (extent_limit(cached), extent_limit(query)) {
(Some(c), Some(q)) => c >= q,
(None, None) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn bounded_extent_detects_all_alias_families() {
assert!(args_have_bounded_extent(&json!({"offset": 10})));
assert!(args_have_bounded_extent(&json!({"start_line": 1})));
assert!(args_have_bounded_extent(&json!({"end_line": 40})));
assert!(args_have_bounded_extent(&json!({"limit": 100})));
assert!(args_have_bounded_extent(&json!({"page": 2})));
assert!(args_have_bounded_extent(&json!({"o": 5})));
}
#[test]
fn whole_file_read_is_not_bounded() {
assert!(!args_have_bounded_extent(&json!({"action": "read", "path": "src/lib.rs"})));
assert!(!args_have_bounded_extent(&json!({"action": "read", "path": "src/lib.rs", "encoding": "utf8"})));
}
#[test]
fn normalization_strip_keys_includes_encoding_and_all_extent_keys() {
let keys: Vec<&str> = normalization_strip_keys().collect();
assert!(keys.contains(&"encoding"));
assert!(keys.contains(&"offset"));
assert!(keys.contains(&"limit"));
assert!(keys.contains(&"page"));
assert!(keys.contains(&"start_line"));
}
#[test]
fn extent_offset_reads_aliases_and_defaults_zero() {
assert_eq!(extent_offset(&json!({})), 0);
assert_eq!(extent_offset(&json!({"offset": 42})), 42);
assert_eq!(extent_offset(&json!({"start_line": 7})), 7);
assert_eq!(extent_offset(&json!({"offset": "13"})), 13);
}
#[test]
fn extent_covers_matches_wider_cached_limit() {
let cached = json!({"offset": 0, "limit": 500});
let query = json!({"offset": 0, "limit": 100});
assert!(extent_covers(&cached, &query));
}
#[test]
fn extent_covers_rejects_different_offset() {
let cached = json!({"offset": 0, "limit": 500});
let query = json!({"offset": 100, "limit": 100});
assert!(!extent_covers(&cached, &query));
}
#[test]
fn extent_covers_rejects_different_line_ranges() {
let cached = json!({"start_line": 1, "end_line": 40});
let query = json!({"start_line": 100, "end_line": 140});
assert!(!extent_covers(&cached, &query));
}
#[test]
fn extent_covers_rejects_raw_mode_mismatch() {
let cached = json!({"offset": 0, "limit": 500});
let query = json!({"offset": 0, "limit": 100, "raw": true});
assert!(!extent_covers(&cached, &query));
}
#[test]
fn extent_covers_unbounded_cached_covers_unbounded_query() {
assert!(extent_covers(&json!({"offset": 0}), &json!({"offset": 0})));
assert!(!extent_covers(&json!({"offset": 0}), &json!({"offset": 0, "limit": 100})));
}
}