trailgen-core 0.3.0

Typed trail graph, route scoring, loop search, and geospatial route I/O for adequate-trailgen.
Documentation
use crate::{Edge, Result, TrailgenError, TurnBan, Vertex, WalkGraph};
use serde::{Deserialize, Serialize as _};

pub const GRAPH_CACHE: &str = "cache/graph.bin";
const MAGIC: &[u8; 16] = b"TRAILGEN-GRAPH\0\0";
const FORMAT: u16 = 2;

pub fn encode_graph(graph: &WalkGraph) -> Result<Vec<u8>> {
    let mut bytes = Vec::new();
    bytes.extend_from_slice(MAGIC);
    bytes.extend_from_slice(&FORMAT.to_le_bytes());
    let mut encoder = zstd::stream::write::Encoder::new(bytes, 3)
        .map_err(|error| TrailgenError::InvalidData(format!("open graph encoder: {error}")))?;
    graph
        .serialize(&mut rmp_serde::Serializer::new(&mut encoder).with_struct_map())
        .map_err(|error| TrailgenError::InvalidData(format!("encode graph cache: {error}")))?;
    let bytes = encoder
        .finish()
        .map_err(|error| TrailgenError::InvalidData(format!("finish graph cache: {error}")))?;
    Ok(bytes)
}

pub fn decode_graph(bytes: &[u8]) -> Result<WalkGraph> {
    let Some((header, body)) = bytes.split_at_checked(MAGIC.len() + size_of::<u16>()) else {
        return Err(TrailgenError::InvalidData(
            "graph cache header is truncated".to_owned(),
        ));
    };
    if &header[..MAGIC.len()] != MAGIC {
        return Err(TrailgenError::InvalidData(
            "graph cache has the wrong format signature".to_owned(),
        ));
    }
    let format = u16::from_le_bytes(
        header[MAGIC.len()..]
            .try_into()
            .expect("header length was checked"),
    );
    if format != FORMAT {
        return Err(TrailgenError::InvalidData(format!(
            "graph cache format {format} is unsupported"
        )));
    }
    let decoder = zstd::stream::read::Decoder::new(body)
        .map_err(|error| TrailgenError::InvalidData(format!("open graph cache: {error}")))?;
    let stored = rmp_serde::from_read::<_, CachedGraph>(decoder)
        .map_err(|error| TrailgenError::InvalidData(format!("decode graph cache: {error}")))?;
    let mut graph = WalkGraph {
        vertices: stored.vertices,
        edges: stored.edges,
        turn_bans: stored.turn_bans,
        adjacency: Vec::new(),
    };
    graph.validate()?;
    graph.rebuild_adjacency();
    Ok(graph)
}

#[derive(Deserialize)]
struct CachedGraph {
    vertices: Vec<Vertex>,
    edges: Vec<Edge>,
    #[serde(default)]
    turn_bans: Vec<TurnBan>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{GraphBuilder, io::geojson};

    #[test]
    fn graph_cache_is_compact_exact_and_self_identifying() {
        let graph = GraphBuilder::default()
            .build(
                &geojson::network_from_str(include_str!("../tests/fixtures/mini_network.geojson"))
                    .unwrap(),
            )
            .unwrap();
        let encoded = encode_graph(&graph).unwrap();

        assert_eq!(decode_graph(&encoded).unwrap(), graph);
        assert!(encoded.len() < serde_json::to_vec(&graph).unwrap().len());
        let mut corrupt = encoded;
        corrupt[0] ^= 0xff;
        assert!(decode_graph(&corrupt).is_err());
    }
}