use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use serde_json::{json, Value};
use tiny_http::{Method, Request};
use super::api::{error_body, JSON};
use super::assets::http_response;
use super::body::no_store;
use crate::config::FusibleConfig;
const GLB: &str = "model/gltf-binary";
pub(super) fn owns(path: &str) -> bool {
matches!(path, "/api/assembly" | "/api/assembly/model")
}
fn preview_dir(cfg: &FusibleConfig, root: &Path) -> Option<PathBuf> {
let assembly = cfg.assembly.as_ref()?;
let out_dir = assembly.out_dir.clone().unwrap_or_else(|| "dist".into());
Some(root.join(out_dir).join("assembly"))
}
fn revision(path: &Path) -> Option<u128> {
let modified = std::fs::metadata(path).ok()?.modified().ok()?;
Some(modified.duration_since(UNIX_EPOCH).ok()?.as_millis())
}
pub(super) fn report_json(cfg: Option<&FusibleConfig>, root: &Path) -> String {
let Some(dir) = cfg.and_then(|c| preview_dir(c, root)) else {
return json!({ "declared": false, "report": null, "model": null }).to_string();
};
let (report, report_error) = read_report(&dir.join("check.json"));
let model =
revision(&dir.join("preview.glb")).map(|rev| format!("/api/assembly/model?rev={rev}"));
json!({ "declared": true, "report": report, "report_error": report_error, "model": model })
.to_string()
}
fn read_report(path: &Path) -> (Option<Value>, Option<String>) {
match std::fs::read_to_string(path) {
Ok(text) => match serde_json::from_str(&text) {
Ok(report) => (Some(report), None),
Err(err) => (None, Some(format!("check.json does not parse: {err}"))),
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => (None, None),
Err(err) => (None, Some(format!("check.json is unreadable: {err}"))),
}
}
pub(super) fn model_reply(
cfg: Option<&FusibleConfig>,
root: &Path,
) -> (u16, &'static str, Vec<u8>) {
let missing = |message: &str| (404, JSON, error_body(message).into_bytes());
let Some(dir) = cfg.and_then(|c| preview_dir(c, root)) else {
return missing("no assembly declared");
};
match std::fs::read(dir.join("preview.glb")) {
Ok(glb) => (200, GLB, glb),
Err(_) => missing("no assembly preview; run fusible assembly"),
}
}
pub(super) fn serve(request: Request, path: &str) {
let (manifest, cfg) = FusibleConfig::nearest().unzip();
let root = manifest
.as_deref()
.and_then(Path::parent)
.unwrap_or(Path::new(""));
let (status, content_type, body) = reply(request.method(), path, cfg.as_ref(), root);
let _ = request.respond(no_store(http_response(status, content_type, body)));
}
pub(super) fn reply(
method: &Method,
path: &str,
cfg: Option<&FusibleConfig>,
root: &Path,
) -> (u16, &'static str, Vec<u8>) {
if *method != Method::Get {
return (405, JSON, error_body("method not allowed").into_bytes());
}
if path == "/api/assembly" {
return (200, JSON, report_json(cfg, root).into_bytes());
}
model_reply(cfg, root)
}