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"];
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()
}
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)
}
fn extent_values_are_valid(args: &Value) -> bool {
let Some(object) = args.as_object() else {
return false;
};
if object.get("raw").is_some_and(|value| !value.is_boolean()) {
return false;
}
let values_are_numeric =
bounded_extent_keys().all(|key| object.get(key).is_none_or(|value| as_u64_lenient(value).is_some()));
let has_at_most_one_alias = |keys: &[&str]| keys.iter().filter(|key| object.contains_key(**key)).count() <= 1;
values_are_numeric && has_at_most_one_alias(OFFSET_KEYS) && has_at_most_one_alias(LIMIT_KEYS)
}
#[derive(Debug, PartialEq, Eq)]
struct PaginationExtent {
page: Option<u64>,
per_page: Option<u64>,
}
fn pagination_extent(args: &Value) -> Result<Option<PaginationExtent>, ()> {
let Some(object) = args.as_object() else {
return Ok(None);
};
if !object.contains_key("page") && !object.contains_key("per_page") {
return Ok(None);
}
let page = match object.get("page") {
Some(value) => Some(as_u64_lenient(value).ok_or(())?),
None => None,
};
let per_page = match object.get("per_page") {
Some(value) => Some(as_u64_lenient(value).ok_or(())?),
None => None,
};
Ok(Some(PaginationExtent { page, per_page }))
}
fn pagination_matches(cached: &Value, query: &Value) -> bool {
match (pagination_extent(cached), pagination_extent(query)) {
(Ok(None), Ok(None)) => true,
(Ok(Some(cached)), Ok(Some(query))) => cached == query,
_ => false,
}
}
pub(crate) fn extent_covers(cached: &Value, query: &Value) -> bool {
if !extent_values_are_valid(cached) || !extent_values_are_valid(query) {
return false;
}
if raw_flag(cached) != raw_flag(query) {
return false;
}
if !pagination_matches(cached, 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_all_extent_keys() {
let keys: Vec<&str> = normalization_strip_keys().collect();
assert!(keys.contains(&"offset"));
assert!(keys.contains(&"limit"));
assert!(keys.contains(&"page"));
assert!(keys.contains(&"start_line"));
assert!(!keys.contains(&"encoding"));
}
#[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_requires_matching_pagination() {
let cached = json!({"page": 1, "per_page": 50});
assert!(extent_covers(&cached, &json!({"page": 1, "per_page": 50})));
assert!(!extent_covers(&cached, &json!({"page": 2, "per_page": 50})));
assert!(!extent_covers(&cached, &json!({"page": 1, "per_page": 100})));
assert!(!extent_covers(&cached, &json!({"offset": 0, "limit": 50})));
}
#[test]
fn extent_covers_rejects_invalid_pagination() {
assert!(!extent_covers(&json!({"page": "unknown"}), &json!({"page": "unknown"})));
assert!(!extent_covers(&json!({"page": 1}), &json!({"page": null})));
}
#[test]
fn extent_covers_rejects_invalid_extent_values() {
assert!(!extent_covers(&json!({"offset": "unknown"}), &json!({"offset": 0})));
assert!(!extent_covers(&json!({"limit": "unknown"}), &json!({"limit": 10})));
assert!(!extent_covers(&json!({"raw": "yes"}), &json!({"raw": false})));
}
#[test]
fn extent_covers_rejects_conflicting_extent_aliases() {
let cached = json!({"offset": 0, "offset_lines": 20, "limit": 100});
let query = json!({"offset": 0, "offset_lines": 40, "limit": 100});
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})));
}
}