use std::{env, collections::HashMap, process::Stdio, fs::read_to_string, result::Result, error::Error}; use tokio::process::Command; use tokio::io::AsyncWriteExt; use crate::http::{response::Response, method::Method, header::Header}; pub struct Script { path: String } impl Script { fn new(path: String) -> Self { Self { path } } pub async fn run(&self, body: Option<&String>) -> Result { let mut res = Response::new(); let mut command = Command::new(&self.path); command.stdin(Stdio::piped()); command.stdout(Stdio::piped()); let mut child = match command.spawn() { Ok(child) => child, Err(err) => return Err(format!("{err}")) }; if let Some(body) = body { let Some(mut stdin) = child.stdin.take() else { return Err("failed to read stdin".into()) }; let _ = stdin.write(body.as_bytes()).await; } let output = match child.wait_with_output().await { Ok(output) => output, Err(err) => return Err(format!("{err}")) }; let response = String::from_utf8_lossy(&output.stdout).into_owned(); let mut lines = response.split('\n').into_iter(); res.headers.serialize(&mut lines); let status = match res.headers.get("Status") { Some(header) => { match header.value.parse::() { Ok(status) => status, Err(err) => return Err(format!("{err}")) } }, None => 200 }; let body: String = lines.collect::>().join("\n"); res.headers.put(Header::new("Content-Length", &format!("{}", body.len()))); if body.len() > 0 { res.body = Some(body); } res.status = status; Ok(res) } } pub struct Config { map: HashMap } impl Config { fn key(method: &Method, route: &str) -> String { format!("{}:{}", method.deserialize(), route) } pub fn load() -> Result> { let config_path = env::var("CONFIG_PATH").unwrap_or_else(|_| String::from("config")); let config = read_to_string(&config_path)?; let mut map = HashMap::new(); let lines = config.split("\n").into_iter(); for (i, line) in lines.enumerate() { let mut parts = line.split_whitespace(); let Some(method_str) = parts.next() else { continue }; let method = Method::serialize(&method_str) .ok_or(format!("config line {i} has invalid http method: {method_str}"))?; let route = parts.next() .ok_or(format!("config line {i} missing http route"))?; let script = parts.next() .ok_or(format!{"config line {i} missing script path"})?; let key = Self::key(&method, route); let value = Script::new(script.to_owned()); println!("adding script {script} for {} {route}", method.deserialize()); map.insert(key, value); } if map.len() < 1 { return Err("cannot run server, config is empty".into()) } Ok(Self { map }) } pub fn get(&self, method: &Method, route: &str) -> Option<&Script> { self.map.get(&Self::key(method, route)) } }