diff --git a/Cargo.toml b/Cargo.toml index 845ccbb..485df16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,5 +14,5 @@ colored = "3.1.1" dirs = "6.0.0" reqwest = { version = "0.13.2", features = ["blocking", "query"] } rustyline = "18.0.0" -serde = "1.0.228" +serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" diff --git a/src/main.rs b/src/main.rs index 2aeddce..53b1420 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,6 +60,9 @@ fn run_repl(ctx: Rc>) { match handle(cmd, &mut state, rl.history()) { Ok(ControlFlow::Continue) => {} Ok(ControlFlow::Exit) => { + if let Err(e) = state.save() { + eprintln!("Failed to save state: {}", e); + } break; } Err(e) => { @@ -83,6 +86,9 @@ fn run_repl(ctx: Rc>) { Parsed::Exit => { println!("{}", "Bye!".dimmed()); + if let Err(e) = ctx.borrow().save() { + eprintln!("Failed to save state: {}", e); + } break; } }, @@ -94,16 +100,24 @@ fn run_repl(ctx: Rc>) { } Err(ReadlineError::Interrupted) => { + if let Err(e) = ctx.borrow().save() { + eprintln!("Failed to save state: {}", e); + } println!("^C"); continue; } Err(ReadlineError::Eof) => { - println!(); + if let Err(e) = ctx.borrow().save() { + eprintln!("Failed to save state: {}", e); + } break; } Err(err) => { + if let Err(e) = ctx.borrow().save() { + eprintln!("Failed to save state: {}", e); + } eprintln!("Error: {:?}", err); break; } @@ -155,7 +169,7 @@ fn main() { match args.as_slice() { [] => { - let ctx = Rc::new(RefCell::new(ShellState::new())); + let ctx = Rc::new(RefCell::new(ShellState::load())); run_repl(ctx); } diff --git a/src/request.rs b/src/request.rs index 3cbebcc..76051ca 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,6 +1,7 @@ +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Request { pub method: Method, pub path: String, @@ -33,7 +34,7 @@ impl Request { } } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub enum Method { GET, POST, diff --git a/src/state.rs b/src/state.rs index 7a909b8..051b7b9 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,7 +1,10 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fs, path::PathBuf}; + +use serde::{Deserialize, Serialize}; use crate::request::Request; +#[derive(Serialize, Deserialize)] pub struct ShellState { base_url: Option, headers: HashMap, @@ -43,6 +46,36 @@ impl ShellState { Ok(()) } + fn state_file_path() -> PathBuf { + let home = dirs::home_dir().expect("could not determine home directory"); + home.join(".reqsh_state.json") + } + + pub fn save(&self) -> Result<(), String> { + let path = Self::state_file_path(); + let json = serde_json::to_string_pretty(self) + .map_err(|e| format!("Failed to serialize state: {}", e))?; + fs::write(&path, json).map_err(|e| format!("Failed to write state file: {}", e))?; + Ok(()) + } + + pub fn load() -> Self { + let path = Self::state_file_path(); + if path.exists() { + let contents = fs::read_to_string(&path).unwrap_or_default(); + match serde_json::from_str(&contents) { + Ok(state) => return state, + Err(e) => { + eprintln!( + "Warning: Failed to parse state file: {}. Starting fresh.", + e + ); + } + } + } + Self::new() + } + pub fn get_request(&self, name: &str) -> Option<&Request> { self.saved_requests.get(name) } @@ -183,4 +216,57 @@ mod test { assert!(saved.is_some()); assert_eq!(saved.unwrap().path, "/test"); } + + #[test] + fn save_and_load_state_roundtrip() { + use crate::request::Method; + use std::fs; + + let temp_dir = std::env::temp_dir(); + let temp_file = temp_dir.join("reqsh_test_state.json"); + + let _ = fs::remove_file(&temp_file); + + let mut state = ShellState::new(); + state.set_base_url("https://api.test.com"); + state.set_header("Auth".to_string(), "Token123".to_string()); + state.set_variable("user".to_string(), "admin".to_string()); + state.set_timeout(60); + + let req = crate::request::Request::new(Method::POST, "/login".to_string()); + state.set_last_request(req); + state.save_request("login_req".to_string()).unwrap(); + + let json = serde_json::to_string_pretty(&state).unwrap(); + fs::write(&temp_file, &json).unwrap(); + + let loaded_json = fs::read_to_string(&temp_file).unwrap(); + let loaded_state: ShellState = serde_json::from_str(&loaded_json).unwrap(); + + assert_eq!(loaded_state.get_base_url(), Some("https://api.test.com")); + assert_eq!( + loaded_state.get_headers().get("Auth"), + Some(&"Token123".to_string()) + ); + assert_eq!( + loaded_state.get_variable("user"), + Some(&"admin".to_string()) + ); + assert_eq!(loaded_state.get_timeout(), Some(60)); + + assert!(loaded_state.get_request("login_req").is_some()); + + let _ = fs::remove_file(&temp_file); + } + + #[test] + fn load_missing_file_returns_default_state() { + let temp_dir = std::env::temp_dir(); + let temp_file = temp_dir.join("reqsh_nonexistent_state.json"); + let _ = std::fs::remove_file(&temp_file); + let contents = ""; + let state: Result = serde_json::from_str(contents); + + assert!(state.is_err()); + } }