pub(crate) mod claude;
pub(crate) mod codex;
pub(crate) mod cost;
pub(crate) mod gemini;
pub(crate) mod host;
pub(crate) mod host_loop;
pub(crate) mod opencode;
use serde_json::Value;
pub(crate) fn string_key(value: &Value, keys: &[&str]) -> Option<String> {
match value {
Value::Object(map) => {
for key in keys {
if let Some(Value::String(found)) = map.get(*key) {
return Some(found.clone());
}
}
for child in map.values() {
if let Some(found) = string_key(child, keys) {
return Some(found);
}
}
None
}
Value::Array(items) => items.iter().find_map(|item| string_key(item, keys)),
_ => None,
}
}
pub(crate) fn number_key(value: &Value, keys: &[&str]) -> Option<u64> {
match value {
Value::Object(map) => {
for key in keys {
if let Some(found) = map.get(*key) {
if let Some(number) = as_u64(found) {
return Some(number);
}
}
}
for child in map.values() {
if let Some(found) = number_key(child, keys) {
return Some(found);
}
}
None
}
Value::Array(items) => items.iter().find_map(|item| number_key(item, keys)),
_ => None,
}
}
pub(crate) fn as_u64(value: &Value) -> Option<u64> {
match value {
Value::Number(number) => number.as_u64(),
Value::String(text) => text.parse().ok(),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn as_u64_from_number() {
assert_eq!(as_u64(&json!(42)), Some(42));
}
#[test]
fn as_u64_from_zero() {
assert_eq!(as_u64(&json!(0)), Some(0));
}
#[test]
fn as_u64_from_string() {
assert_eq!(as_u64(&json!("1024")), Some(1024));
}
#[test]
fn as_u64_rejects_negative() {
assert_eq!(as_u64(&json!(-1)), None);
}
#[test]
fn as_u64_rejects_float() {
assert_eq!(as_u64(&json!(1.5)), None);
}
#[test]
fn as_u64_rejects_non_numeric_string() {
assert_eq!(as_u64(&json!("hello")), None);
}
#[test]
fn as_u64_rejects_bool() {
assert_eq!(as_u64(&json!(true)), None);
}
#[test]
fn as_u64_rejects_null() {
assert_eq!(as_u64(&json!(null)), None);
}
#[test]
fn string_key_flat_object() {
let v = json!({"name": "alice", "role": "admin"});
assert_eq!(string_key(&v, &["name"]), Some("alice".into()));
}
#[test]
fn string_key_first_candidate_wins() {
let v = json!({"a": "first", "b": "second"});
assert_eq!(string_key(&v, &["a", "b"]), Some("first".into()));
}
#[test]
fn string_key_fallback_candidate() {
let v = json!({"b": "fallback"});
assert_eq!(string_key(&v, &["a", "b"]), Some("fallback".into()));
}
#[test]
fn string_key_nested_object() {
let v = json!({"outer": {"inner": {"target": "found"}}});
assert_eq!(string_key(&v, &["target"]), Some("found".into()));
}
#[test]
fn string_key_inside_array() {
let v = json!([{"x": 1}, {"name": "bob"}]);
assert_eq!(string_key(&v, &["name"]), Some("bob".into()));
}
#[test]
fn string_key_skips_non_string_match() {
let v = json!({"count": 42, "nested": {"count": "forty-two"}});
assert_eq!(string_key(&v, &["count"]), Some("forty-two".into()));
}
#[test]
fn string_key_missing_returns_none() {
let v = json!({"a": "b"});
assert_eq!(string_key(&v, &["missing"]), None);
}
#[test]
fn string_key_empty_candidates() {
let v = json!({"a": "b"});
assert_eq!(string_key(&v, &[]), None);
}
#[test]
fn string_key_scalar_value() {
assert_eq!(string_key(&json!("plain"), &["x"]), None);
assert_eq!(string_key(&json!(42), &["x"]), None);
}
#[test]
fn number_key_flat_object() {
let v = json!({"tokens": 500});
assert_eq!(number_key(&v, &["tokens"]), Some(500));
}
#[test]
fn number_key_string_coercion() {
let v = json!({"tokens": "1024"});
assert_eq!(number_key(&v, &["tokens"]), Some(1024));
}
#[test]
fn number_key_nested() {
let v = json!({"usage": {"prompt_tokens": 200}});
assert_eq!(number_key(&v, &["prompt_tokens"]), Some(200));
}
#[test]
fn number_key_inside_array() {
let v = json!([{"other": "x"}, {"count": 7}]);
assert_eq!(number_key(&v, &["count"]), Some(7));
}
#[test]
fn number_key_first_candidate_wins() {
let v = json!({"a": 1, "b": 2});
assert_eq!(number_key(&v, &["a", "b"]), Some(1));
}
#[test]
fn number_key_missing_returns_none() {
let v = json!({"a": 1});
assert_eq!(number_key(&v, &["missing"]), None);
}
#[test]
fn number_key_skips_non_numeric_string() {
let v = json!({"val": "hello"});
assert_eq!(number_key(&v, &["val"]), None);
}
#[test]
fn number_key_zero() {
let v = json!({"n": 0});
assert_eq!(number_key(&v, &["n"]), Some(0));
}
}