rete-cli 0.3.2

CLI to build and inspect Rete graph files.
#![allow(dead_code)]

use std::path::{Path, PathBuf};
use std::{io::Write as _, net::TcpListener};

use assert_cmd::Command;
use tempfile::TempDir;

pub const FIXTURE_NQ: &str = concat!(
    "<http://example.test/alice> <http://example.test/knows> <http://example.test/bob> .\n",
    "<http://example.test/bob> <http://example.test/name> \"Bob\"@en .\n",
    "<http://example.test/alice> <http://example.test/name> \"Alice\"@en <http://example.test/people> .\n",
);

pub struct Fixture {
    _dir: TempDir,
    pub source: PathBuf,
    pub rete: PathBuf,
}

impl Fixture {
    pub fn path(&self, name: &str) -> PathBuf {
        self._dir.path().join(name)
    }

    pub fn write(&self, name: &str, contents: impl AsRef<[u8]>) -> PathBuf {
        let path = self.path(name);
        std::fs::write(&path, contents).unwrap();
        path
    }
}

pub fn rete() -> Command {
    Command::new(env!("CARGO_BIN_EXE_rete"))
}

pub fn fixture() -> Fixture {
    let dir = tempfile::tempdir().unwrap();
    let source = dir.path().join("minimal.nq");
    let output = dir.path().join("minimal.rete");
    std::fs::write(&source, FIXTURE_NQ).unwrap();
    build(&source, &output, &["--no-pyramid"]);
    Fixture {
        _dir: dir,
        source,
        rete: output,
    }
}

pub fn build(source: &Path, output: &Path, extra: &[&str]) {
    let mut command = rete();
    command
        .arg("build")
        .arg(source)
        .arg("-o")
        .arg(output)
        .args(extra)
        .assert()
        .success();
}

pub fn json(command: &mut Command) -> serde_json::Value {
    let output = command.output().unwrap();
    assert!(
        output.status.success(),
        "command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    serde_json::from_slice(&output.stdout).unwrap_or_else(|error| {
        panic!(
            "invalid JSON ({error}): {}",
            String::from_utf8_lossy(&output.stdout)
        )
    })
}

#[derive(Clone, Copy)]
pub enum RangeMode {
    Honor,
    Ignore,
    NotFound,
}

pub fn serve(data: Vec<u8>, mode: RangeMode) -> String {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let url = format!("http://{}/graph.rete", listener.local_addr().unwrap());
    std::thread::spawn(move || {
        for stream in listener.incoming() {
            let mut stream = match stream {
                Ok(stream) => stream,
                Err(_) => continue,
            };
            let mut request = Vec::new();
            let mut byte = [0u8; 1];
            while !request.ends_with(b"\r\n\r\n") {
                use std::io::Read as _;
                if stream.read(&mut byte).unwrap_or(0) == 0 {
                    break;
                }
                request.push(byte[0]);
            }
            if matches!(mode, RangeMode::NotFound) {
                let _ = stream.write_all(
                    b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
                );
                continue;
            }
            let text = String::from_utf8_lossy(&request);
            let total = data.len();
            if text.starts_with("HEAD") {
                let headers = format!(
                    "HTTP/1.1 200 OK\r\nContent-Length: {total}\r\nAccept-Ranges: bytes\r\nConnection: close\r\n\r\n"
                );
                let _ = stream.write_all(headers.as_bytes());
                continue;
            }
            let range = text.lines().find_map(|line| {
                let line = line.to_ascii_lowercase();
                line.strip_prefix("range: bytes=")
                    .map(|value| value.trim().to_string())
            });
            if matches!(mode, RangeMode::Honor) {
                if let Some(range) = range {
                    let (start, end) = range.split_once('-').unwrap();
                    let start: usize = start.parse().unwrap();
                    let end: usize = end.parse().unwrap();
                    let end = end.min(total - 1);
                    let body = &data[start..=end];
                    let headers = format!(
                        "HTTP/1.1 206 Partial Content\r\nContent-Range: bytes {start}-{end}/{total}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
                        body.len()
                    );
                    let _ = stream.write_all(headers.as_bytes());
                    let _ = stream.write_all(body);
                    continue;
                }
            }
            let headers =
                format!("HTTP/1.1 200 OK\r\nContent-Length: {total}\r\nConnection: close\r\n\r\n");
            let _ = stream.write_all(headers.as_bytes());
            let _ = stream.write_all(&data);
        }
    });
    url
}