vacuna 0.4.3

Simple web server for static files
Documentation
use knus::{Decode, DecodeScalar, parse};
use std::{fs::read_to_string, ops::Bound};

#[derive(Decode)]
pub struct Config {
    #[knus(children(name = "server"))]
    pub servers: Vec<Server>,
}

#[derive(Decode, Clone)]
pub struct Settings {
    #[knus(child, unwrap(argument))]
    pub payload_limit: Option<u64>,
}

#[derive(Decode)]
pub struct Server {
    #[knus(children(name = "listen"))]
    pub listen: Vec<Bind>,
    #[knus(children(name = "host"))]
    pub host: Vec<ConfigHost>,
    #[knus(child)]
    pub auto: Option<Auto>,
    #[knus(child)]
    pub settings: Option<Settings>,
}
#[derive(Decode)]
pub struct ConfigHost {
    #[knus(children(name = "name"))]
    pub name: Vec<HostName>,
    #[knus(children(name = "path"))]
    pub path: Vec<Path>,
    #[knus(children(name = "proxy"))]
    pub proxy: Vec<Proxy>,
    #[knus(children(name = "log"))]
    pub log: Vec<Log>,
}

#[derive(Decode)]
pub struct Auto {
    #[knus(argument)]
    base_path: String,
    #[knus(child, unwrap(argument), default)]
    pub list: bool,
    #[knus(children(name = "header"))]
    pub headers: Vec<Header>,
    #[knus(child, unwrap(argument))]
    pub index: Option<String>,
}
impl Auto {
    pub fn base_path(&self) -> &str {
        &self.base_path
    }
}

#[derive(Decode)]
pub struct Bind {
    #[knus(argument)]
    sock_addr: String,
    #[knus(child)]
    ssl: Option<SSL>,
}
impl Bind {
    pub fn sock_addr(&self) -> String {
        self.sock_addr.clone()
    }
    pub fn ssl(&self) -> &Option<SSL> {
        &self.ssl
    }
}

#[derive(Decode)]
pub struct SSL {
    #[knus(children(name = "cert"))]
    pub certs: Vec<Certs>,
    #[knus(child, unwrap(argument))]
    pub letsencrypt: Option<String>,
    #[knus(child, unwrap(argument))]
    pub acme_redirect: Option<String>,
    #[knus(child)]
    pub acme: Option<SSLAcme>,
}

#[derive(Decode)]
pub struct SSLAcme {
    #[knus(child, unwrap(arguments))]
    contacts: Vec<String>,
    #[knus(child, unwrap(argument))]
    store: String,
    #[knus(child, unwrap(argument))]
    provider: Option<String>,
    #[knus(child, unwrap(arguments))]
    extra_domains: Option<Vec<String>>,
}

impl SSLAcme {
    pub fn contacts(&self) -> &Vec<String> {
        &self.contacts
    }

    pub fn store(&self) -> &String {
        &self.store
    }

    pub fn provider(&self) -> &Option<String> {
        &self.provider
    }

    pub fn extra_domains(&self) -> &Option<Vec<String>> {
        &self.extra_domains
    }
}

#[derive(Decode, Clone)]
pub struct Certs {
    #[knus(child, unwrap(argument))]
    pub cert: String,
    #[knus(child, unwrap(argument))]
    pub key: String,
}
impl Certs {
    pub fn new(cert: &str, key: &str) -> Self {
        Self {
            cert: cert.to_owned(),
            key: key.to_owned(),
        }
    }
}

#[derive(Decode, Clone)]
pub struct HostName(#[knus(argument)] pub String);
impl HostName {
    pub fn value(&self) -> String {
        self.0.clone()
    }
}

#[derive(Decode, Clone, Debug)]
pub struct Path {
    #[knus(argument)]
    pub path: String,
    #[knus(child, unwrap(argument))]
    pub location: String,
    #[knus(child, unwrap(argument), default)]
    pub list: bool,
    #[knus(children(name = "header"))]
    pub headers: Vec<Header>,
    #[knus(child, unwrap(argument))]
    pub index: Option<String>,
    #[knus(children(name = "pre_compression"))]
    pub compressions: Vec<PreCompressed>,
}
impl Path {
    #[cfg(test)]
    pub(crate) fn new(path: &str, location: &str) -> Self {
        Self {
            path: path.to_owned(),
            location: location.to_owned(),
            list: false,
            headers: Default::default(),
            index: None,
            compressions: Default::default(),
        }
    }
    pub(crate) fn strip_prefix(&mut self, prefix: &str) {
        if let Some(new_value) = self.path.strip_prefix(&prefix) {
            self.path = new_value.to_owned();
        }
    }
}

#[derive(DecodeScalar, Clone, Debug)]
pub enum PreCompType {
    Gzip,
    Brotli,
    Zstd,
    None,
}

#[derive(Decode, Clone, Debug)]
pub struct PreCompressed {
    #[knus(argument)]
    pub(crate) compression_type: PreCompType,
    #[knus(argument)]
    pub(crate) extension: String,
}
impl PreCompressed {
    pub fn defaults() -> Vec<Self> {
        let mut defs = Vec::new();
        defs.push(Self::new(PreCompType::Gzip, ".gz"));
        defs.push(Self::new(PreCompType::Brotli, ".br"));
        defs.push(Self::new(PreCompType::Zstd, ".zst"));
        defs.push(Self::new(PreCompType::None, ""));
        defs
    }
    fn new(t: PreCompType, extension: &str) -> Self {
        PreCompressed {
            compression_type: t,
            extension: extension.to_owned(),
        }
    }
}

#[derive(Decode, Clone, Debug)]
pub struct Header {
    #[knus(argument)]
    name: String,
    #[knus(argument)]
    value: String,
}
impl Header {
    pub fn name(&self) -> &str {
        &self.name
    }
    pub fn value(&self) -> &str {
        &self.value
    }
}

#[derive(Decode, Clone)]
pub struct Proxy {
    #[knus(argument)]
    pub path: String,
    #[knus(child, unwrap(argument))]
    pub url: String,
}

#[derive(DecodeScalar, Clone, Default)]
pub enum LogLevel {
    Trace,
    Debug,
    #[default]
    Info,
    Warn,
    Error,
}

#[derive(Decode, Clone)]
pub struct Log {
    #[knus(argument, default = 100)]
    status_from: u16,
    #[knus(argument, default = 600)]
    status_to: u16,
    #[knus(property)]
    level: LogLevel,
    #[knus(child, unwrap(argument),default="%a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T".into())]
    format: String,
}

impl Log {
    pub fn nothing() -> Self {
        Self {
            status_from: 600,
            status_to: 600,
            level: LogLevel::Trace,
            format: "".to_owned(),
        }
    }
    pub fn status_range(&self) -> (Bound<u16>, Bound<u16>) {
        (Bound::Included(self.status_from), Bound::Excluded(self.status_to))
    }
    pub fn format(&self) -> &str {
        &self.format
    }
    pub fn level(&self) -> &LogLevel {
        &self.level
    }
}

impl Config {
    pub fn read<P: AsRef<std::path::Path>>(path: P) -> std::result::Result<Self, Box<dyn std::error::Error>> {
        let pn = path.as_ref().to_string_lossy();
        let text = read_to_string(&path)?;
        Ok(parse(&pn, &text)?)
    }

    pub fn from_str(text: &str) -> std::result::Result<Self, Box<dyn std::error::Error>> {
        Ok(parse("", text)?)
    }
}

#[cfg(test)]
mod tests {
    use super::Config;
    #[test]
    fn test_simple_reading() {
        let read = Config::read("./fixtures/simple.kdl").unwrap();
        assert_eq!(read.servers[0].host[0].name[0].0, "tglman.com");
        assert_eq!(read.servers[0].host[0].name[1].0, "www.tglman.com");
        assert_eq!(
            read.servers[0].listen[3]
                .ssl
                .as_ref()
                .unwrap()
                .acme
                .as_ref()
                .unwrap()
                .extra_domains
                .as_ref()
                .unwrap()[0],
            "may.example.com"
        );
    }
}