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),
}
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))
}