use shrinkfit::{json, shrink_str, Options, Value};
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
const INDEX: &str = include_str!("../web/index.html");
const EXAMPLES_JS: &str = include_str!("../web/examples.js");
const MAX_BODY: usize = 256 * 1024 * 1024;
pub const USAGE: &str = "\
usage: shrinkfit serve [--port N] [--host ADDR]
Start the local playground web app (default http://127.0.0.1:8765).
Paste JSON or YAML, tweak every option, and see the shrunk output live.";
pub fn run(mut args: impl Iterator<Item = String>) -> Result<(), String> {
let mut port: u16 = 8765;
let mut host = "127.0.0.1".to_string();
while let Some(a) = args.next() {
match a.as_str() {
"-h" | "--help" => {
println!("{USAGE}");
return Ok(());
}
"-p" | "--port" => {
port = args
.next()
.and_then(|v| v.parse().ok())
.ok_or("--port needs a number")?
}
"--host" => host = args.next().ok_or("--host needs an address")?,
other => return Err(format!("unknown option {other}\n\n{USAGE}")),
}
}
let listener =
TcpListener::bind((host.as_str(), port)).map_err(|e| format!("{host}:{port}: {e}"))?;
println!("shrinkfit playground: http://{host}:{port}/ (Ctrl+C to stop)");
for stream in listener.incoming().flatten() {
std::thread::spawn(move || {
let _ = handle(stream);
});
}
Ok(())
}
fn handle(mut s: TcpStream) -> std::io::Result<()> {
let (method, path, body) = match read_request(&mut s)? {
Some(r) => r,
None => return Ok(()),
};
match (method.as_str(), path.split('?').next().unwrap_or("")) {
("GET", "/") | ("GET", "/index.html") => {
respond(&mut s, 200, "text/html; charset=utf-8", INDEX.as_bytes())
}
("GET", "/examples.js") => respond(
&mut s,
200,
"text/javascript; charset=utf-8",
EXAMPLES_JS.as_bytes(),
),
("POST", "/api/shrink") => {
let out = api_shrink(&body);
respond(&mut s, 200, "application/json", out.as_bytes())
}
_ => respond(&mut s, 404, "text/plain", b"not found"),
}
}
fn read_request(s: &mut TcpStream) -> std::io::Result<Option<(String, String, Vec<u8>)>> {
let mut buf = Vec::new();
let mut chunk = [0u8; 8192];
let header_end = loop {
if let Some(p) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
break p;
}
let n = s.read(&mut chunk)?;
if n == 0 {
return Ok(None);
}
buf.extend_from_slice(&chunk[..n]);
if buf.len() > 64 * 1024 && !buf.windows(4).any(|w| w == b"\r\n\r\n") {
return Ok(None);
}
};
let head = String::from_utf8_lossy(&buf[..header_end]).to_string();
let mut lines = head.lines();
let mut first = lines.next().unwrap_or("").split_whitespace();
let method = first.next().unwrap_or("").to_string();
let path = first.next().unwrap_or("").to_string();
let len = lines
.filter_map(|l| l.split_once(':'))
.find(|(k, _)| k.eq_ignore_ascii_case("content-length"))
.and_then(|(_, v)| v.trim().parse::<usize>().ok())
.unwrap_or(0);
if len > MAX_BODY {
return Ok(None);
}
let mut body = buf[header_end + 4..].to_vec();
while body.len() < len {
let n = s.read(&mut chunk)?;
if n == 0 {
break;
}
body.extend_from_slice(&chunk[..n]);
}
body.truncate(len);
Ok(Some((method, path, body)))
}
fn respond(s: &mut TcpStream, code: u16, ctype: &str, body: &[u8]) -> std::io::Result<()> {
let reason = if code == 200 { "OK" } else { "Not Found" };
write!(
s,
"HTTP/1.1 {code} {reason}\r\nContent-Type: {ctype}\r\nContent-Length: {}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n",
body.len()
)?;
s.write_all(body)
}
fn err(msg: String) -> String {
json::to_string(
&Value::Object(vec![
("ok".into(), Value::Bool(false)),
("error".into(), Value::String(msg)),
]),
json::Layout::Compact,
)
}
fn api_shrink(body: &[u8]) -> String {
let text = match std::str::from_utf8(body) {
Ok(t) => t,
Err(_) => return err("request is not valid UTF-8".into()),
};
let req = match json::parse(text) {
Ok(Value::Object(e)) => e,
_ => return err("request must be a JSON object".into()),
};
let get = |k: &str| req.iter().find(|(key, _)| key == k).map(|(_, v)| v);
let input = match get("input") {
Some(Value::String(s)) => s.as_str(),
_ => return err("missing string field \"input\"".into()),
};
let default_opts = Value::Object(Vec::new());
let opts = match Options::from_value(get("options").unwrap_or(&default_opts)) {
Ok(o) => o,
Err(e) => return err(format!("invalid options: {e}")),
};
match shrink_str(input, &opts) {
Ok(out) => json::to_string(
&Value::Object(vec![
("ok".into(), Value::Bool(true)),
("text".into(), Value::String(out.text)),
("report".into(), out.report.to_value()),
]),
json::Layout::Compact,
),
Err(e) => err(e.to_string()),
}
}