//! `gatewayd.toml` into a typed `Config`. Our own format: unknown keys are errors in every table. use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { pub mattermost: MattermostConfig, pub secrets: BTreeMap, pub allow: AllowConfig, #[serde(default, rename = "loop")] pub loop_: LoopConfig, #[serde(default)] pub paths: Paths, #[serde(default)] pub limits: Limits, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields)] pub struct MattermostConfig { pub url: String, #[serde(default)] pub ca_file: Option, } /// Where one secret comes from: exactly one of the three is set (checked by `Config::load`). #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)] #[serde(deny_unknown_fields)] pub struct SecretSpec { #[serde(default)] pub credential: Option, #[serde(default)] pub env: Option, #[serde(default)] pub file: Option, } /// A checked `SecretSpec`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum SecretSource { Credential(String), Env(String), File(PathBuf), } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields)] pub struct AllowConfig { pub users: Vec, #[serde(default)] pub channels: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)] #[serde(deny_unknown_fields, default)] pub struct LoopConfig { pub socket: PathBuf, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields, default)] pub struct Paths { pub home: PathBuf, } impl Default for Paths { fn default() -> Self { Paths { home: std::env::var_os("BOXMAKER_HOME") .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from("/var/lib/boxmaker")), } } } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields, default)] pub struct Limits { pub queue: u32, pub typing_every_ms: u64, pub ping_every_ms: u64, pub dead_after_ms: u64, } impl Default for Limits { fn default() -> Self { Limits { queue: 20, typing_every_ms: 3_000, ping_every_ms: 30_000, dead_after_ms: 60_000, } } } /// `url` taken apart. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ServerUrl { pub tls: bool, pub host: String, pub port: u16, } #[derive(Debug)] pub enum ConfigError { Read(PathBuf, std::io::Error), Parse(PathBuf, toml::de::Error), Invalid(PathBuf, String), } impl std::fmt::Display for ConfigError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Each variant: ": ", with `path.display()`. todo!() } } impl std::error::Error for ConfigError {} /// The one secret M4a needs. pub const MATTERMOST_TOKEN: &str = "mattermost_token"; impl Config { /// Parse without the checks `load` makes. pub fn parse(text: &str) -> Result { // `toml::from_str`. todo!() } pub fn load(path: &Path) -> Result { // Read the file (else Read), parse (else Parse), then `problem()` (Some(why) is Invalid). todo!() } /// The first thing wrong with the values, or `None`. pub fn problem(&self) -> Option { // The first of these, in this order, with the exact messages in the task: the url // (`parse_url`); ca_file not absolute; [secrets.mattermost_token] missing; each secret // whose `source()` fails; allow.users empty; any id in allow.users or allow.channels not // `valid_id`; any limit that is 0. todo!() } /// The server's address, from `url`. Call only on a checked `Config`. pub fn server(&self) -> Result { // `parse_url` of the url. todo!() } pub fn token_source(&self) -> Result { // The `source()` of [secrets.mattermost_token], or an error if it is missing. todo!() } pub fn loop_socket(&self) -> PathBuf { // [loop] socket, or /run/loop/loop.sock when it is empty. todo!() } pub fn state_path(&self) -> PathBuf { // /gateway/state.json. todo!() } } impl SecretSpec { /// Exactly one source, well formed. pub fn source(&self) -> Result { // Exactly one of the three set, else "needs exactly one of credential, env and file". // credential: not empty, only ASCII letters, digits, _ . -. env: not empty, only A-Z, 0-9, // _. file: an absolute path. The messages are in the task. todo!() } } /// A Mattermost id: 26 characters of `a-z0-9`. pub fn valid_id(id: &str) -> bool { // 26 bytes, each a-z or 0-9. todo!() } /// `http://host[:port]` or `https://host[:port]`, nothing else. pub fn parse_url(url: &str) -> Result { // http:// or https://, then a host, then optionally ":" and a port. The port: 1..=65535 written // exactly as `port.to_string()` (so no "+1", no "080"). Default 443 for https, 80 for http. The // host: 1..=253 bytes of a-z, 0-9, "." and "-", not starting or ending with "." or "-". // Anything else, including a path, a user or an upper-case letter, is the one error message in // the task. todo!() }