nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
//! Optional surface overrides from **`NAUTALID_SURFACES_KDL_PATH`**.

use std::path::PathBuf;

use kdl::{KdlDocument, KdlNode};

use super::config::{GrpcSurface, ProxySurface, StaticSurface, SurfaceConfig, WebSocketSurface};

#[derive(Debug, thiserror::Error)]
pub enum KdlSurfaceError {
    #[error("read {path}: {source}")]
    Io {
        path: String,
        source: std::io::Error,
    },
    #[error("KDL parse: {0}")]
    Parse(String),
    #[error("KDL surfaces: {0}")]
    Invalid(String),
}

/// Parsed KDL surface block (optional).
pub struct KdlSurfaceOverrides {
    static_files: Option<StaticSurface>,
    proxy: Option<ProxySurface>,
    websocket: Option<WebSocketSurface>,
    grpc: Option<GrpcSurface>,
}

impl KdlSurfaceOverrides {
    pub fn from_env() -> Result<Self, KdlSurfaceError> {
        let Some(path) = std::env::var("NAUTALID_SURFACES_KDL_PATH")
            .ok()
            .filter(|p| !p.trim().is_empty())
        else {
            return Ok(Self::empty());
        };
        let text = std::fs::read_to_string(&path).map_err(|source| KdlSurfaceError::Io {
            path: path.clone(),
            source,
        })?;
        Self::parse(&text)
    }

    fn empty() -> Self {
        Self {
            static_files: None,
            proxy: None,
            websocket: None,
            grpc: None,
        }
    }

    fn parse(text: &str) -> Result<Self, KdlSurfaceError> {
        let doc: KdlDocument = text
            .parse::<KdlDocument>()
            .map_err(|e: kdl::KdlError| KdlSurfaceError::Parse(e.to_string()))?;
        let surfaces = doc
            .get("surfaces")
            .ok_or_else(|| KdlSurfaceError::Invalid("missing `surfaces` node".into()))?;

        let mut out = Self::empty();
        let Some(children) = surfaces.children() else {
            return Ok(out);
        };

        for child in children.nodes() {
            match child.name().value() {
                "static" => {
                    let root = attr_path(child, "root")?;
                    let prefix = attr_string(child, "prefix").unwrap_or_else(|| "/static".into());
                    out.static_files = Some(StaticSurface { root, prefix });
                }
                "proxy" => {
                    let upstream = attr_string(child, "upstream")
                        .ok_or_else(|| KdlSurfaceError::Invalid("proxy missing upstream".into()))?;
                    let prefix = attr_string(child, "prefix").unwrap_or_else(|| "/proxy".into());
                    out.proxy = Some(ProxySurface { upstream, prefix });
                }
                "websocket" => {
                    let path = attr_string(child, "path").unwrap_or_else(|| "/ws".into());
                    out.websocket = Some(WebSocketSurface { path });
                }
                "grpc" => {
                    let listen = attr_string(child, "listen")
                        .ok_or_else(|| KdlSurfaceError::Invalid("grpc missing listen".into()))?;
                    out.grpc = Some(
                        GrpcSurface::from_listen(&listen)
                            .map_err(KdlSurfaceError::Invalid)?,
                    );
                }
                other => {
                    return Err(KdlSurfaceError::Invalid(format!(
                        "unknown surfaces child `{other}`"
                    )));
                }
            }
        }
        Ok(out)
    }

    pub fn apply(self, cfg: &mut SurfaceConfig) {
        if let Some(s) = self.static_files {
            if crate::surfaces::env_enabled("NAUTALID_STATIC") {
                cfg.static_files = Some(s);
            }
        }
        if let Some(p) = self.proxy {
            if crate::surfaces::env_enabled("NAUTALID_PROXY") {
                cfg.proxy = Some(p);
            }
        }
        if let Some(w) = self.websocket {
            if crate::surfaces::env_enabled("NAUTALID_WEBSOCKET") {
                cfg.websocket = Some(w);
            }
        }
        if let Some(g) = self.grpc {
            if crate::surfaces::env_enabled("NAUTALID_GRPC") {
                cfg.grpc = Some(g);
            }
        }
    }
}

fn attr_string(node: &KdlNode, name: &str) -> Option<String> {
    node.get(name)
        .and_then(|v| v.as_string())
        .map(str::to_owned)
}

fn attr_path(node: &KdlNode, name: &str) -> Result<PathBuf, KdlSurfaceError> {
    let path = attr_string(node, name)
        .ok_or_else(|| KdlSurfaceError::Invalid(format!("missing `{name}`")))?;
    Ok(PathBuf::from(path))
}