use serde_json::Value;
use std::{
fs,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
use crate::error::{AppError, io_err, json_err};
pub fn home_dir() -> Result<PathBuf, AppError> {
std::env::home_dir().ok_or_else(|| AppError::Io {
operation: "resolve home directory",
path: "$HOME".to_string(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "home directory not found"),
})
}
pub fn now_epoch() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
pub fn discover_files(base: &Path, extension: &str, max_depth: usize) -> Vec<PathBuf> {
let mut files = Vec::new();
collect_dir(base, extension, 0, max_depth, &mut files);
files.sort();
files
}
fn collect_dir(
dir: &Path,
extension: &str,
depth: usize,
max_depth: usize,
files: &mut Vec<PathBuf>,
) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
let path = entry.path();
if file_type.is_symlink() {
continue;
}
if file_type.is_dir() {
if depth < max_depth {
collect_dir(&path, extension, depth + 1, max_depth, files);
}
} else if path
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case(extension))
{
files.push(path);
}
}
}
pub fn read_jsonl(path: &Path) -> Result<Vec<Value>, AppError> {
let content = fs::read(path).map_err(|e| io_err("read", path, e))?;
let mut out = Vec::new();
for line in content.split(|b| *b == b'\n') {
if line.is_empty() {
continue;
}
if let Ok(value) = serde_json::from_slice::<Value>(line) {
out.push(value);
}
}
Ok(out)
}
pub fn read_json(path: &Path) -> Result<Value, AppError> {
let content = fs::read(path).map_err(|e| io_err("read", path, e))?;
serde_json::from_slice(&content).map_err(|e| json_err(path, e))
}
pub fn u64_get(value: &Value, keys: &[&str]) -> u64 {
match get_nested(value, keys) {
Some(v) => v
.as_u64()
.unwrap_or_else(|| v.as_f64().map(|f| f.max(0.0) as u64).unwrap_or(0)),
None => 0,
}
}
pub fn str_get<'a>(value: &'a Value, keys: &[&str]) -> Option<&'a str> {
get_nested(value, keys).and_then(Value::as_str)
}
pub fn cost_get(value: &Value, keys: &[&str]) -> Option<f64> {
let raw = get_nested(value, keys)?;
let cost = raw.as_f64()?;
(cost > 0.0 && cost.is_finite()).then_some(cost)
}
pub fn bool_get(value: &Value, keys: &[&str]) -> bool {
get_nested(value, keys)
.and_then(Value::as_bool)
.unwrap_or(false)
}
pub fn timestamp_to_epoch(value: &Value) -> Option<i64> {
if let Some(n) = value.as_i64() {
return Some(if n > 100_000_000_000 { n / 1000 } else { n });
}
let s = value.as_str()?;
if let Ok(n) = s.parse::<i64>() {
return Some(if n > 100_000_000_000 { n / 1000 } else { n });
}
chrono::DateTime::parse_from_rfc3339(s)
.ok()
.map(|dt| dt.timestamp())
}
fn get_nested<'a>(value: &'a Value, keys: &[&str]) -> Option<&'a Value> {
let mut current = value;
for key in keys {
current = current.get(key)?;
}
Some(current)
}
#[cfg(test)]
#[path = "../tests/io_load_test.rs"]
mod tests;