use std::path::{Path, PathBuf};
use std::sync::mpsc::{Receiver, channel};
use crate::error::JiqError;
#[derive(Debug, Clone, PartialEq)]
pub enum LoadingState {
Loading,
Complete(String),
Error(JiqError),
}
pub struct FileLoader {
pub state: LoadingState,
pub rx: Option<Receiver<Result<String, JiqError>>>,
}
impl FileLoader {
pub fn spawn_load(path: PathBuf) -> Self {
let (tx, rx) = channel();
std::thread::spawn(move || {
let result = load_file_sync(&path);
let _ = tx.send(result);
});
Self {
state: LoadingState::Loading,
rx: Some(rx),
}
}
pub fn spawn_load_stdin() -> Self {
let (tx, rx) = channel();
std::thread::spawn(move || {
let result = load_stdin_sync();
let _ = tx.send(result);
});
Self {
state: LoadingState::Loading,
rx: Some(rx),
}
}
pub fn poll(&mut self) -> Option<Result<String, JiqError>> {
if let Some(rx) = &self.rx {
match rx.try_recv() {
Ok(result) => {
self.rx = None;
self.state = match &result {
Ok(json) => LoadingState::Complete(json.clone()),
Err(e) => LoadingState::Error(e.clone()),
};
Some(result)
}
Err(std::sync::mpsc::TryRecvError::Empty) => None,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
self.rx = None;
let err = JiqError::Io("File loader thread disconnected".to_string());
self.state = LoadingState::Error(err.clone());
Some(Err(err))
}
}
} else {
None
}
}
pub fn state(&self) -> &LoadingState {
&self.state
}
pub fn is_loading(&self) -> bool {
matches!(self.state, LoadingState::Loading)
}
}
fn validate_json_or_jsonl(content: &str) -> Result<(), JiqError> {
let deserializer = serde_json::Deserializer::from_str(content).into_iter::<serde_json::Value>();
let mut count = 0;
for result in deserializer {
result.map_err(|e| JiqError::InvalidJson(e.to_string()))?;
count += 1;
}
if count == 0 {
return Err(JiqError::InvalidJson("Empty input".to_string()));
}
Ok(())
}
fn load_file_sync(path: &Path) -> Result<String, JiqError> {
use std::fs::File;
use std::io::Read;
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
validate_json_or_jsonl(&contents)?;
Ok(contents)
}
fn load_stdin_sync() -> Result<String, JiqError> {
use std::io::{self, IsTerminal, Read};
if io::stdin().is_terminal() {
return Err(JiqError::Io(
"No input provided. Usage: jiq <file> or echo '{}' | jiq".to_string(),
));
}
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
validate_json_or_jsonl(&buffer)?;
Ok(buffer)
}
#[cfg(test)]
#[path = "loader_tests.rs"]
mod loader_tests;