Each task's tests were run against a reference at its end state; the end states were replayed from master in order with the gate at each step (650 to 762 tests); each skeleton compiles against its tests and fails them. The reference is kept off this machine. Lessons T27 (every wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
195 lines
5.5 KiB
Rust
195 lines
5.5 KiB
Rust
//! `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<String, SecretSpec>,
|
|
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<PathBuf>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
#[serde(default)]
|
|
pub env: Option<String>,
|
|
#[serde(default)]
|
|
pub file: Option<PathBuf>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
#[serde(default)]
|
|
pub channels: Vec<String>,
|
|
}
|
|
|
|
#[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: "<path>: <error or why>", 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<Config, toml::de::Error> {
|
|
// `toml::from_str`.
|
|
todo!()
|
|
}
|
|
|
|
pub fn load(path: &Path) -> Result<Config, ConfigError> {
|
|
// 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<String> {
|
|
// 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<ServerUrl, String> {
|
|
// `parse_url` of the url.
|
|
todo!()
|
|
}
|
|
|
|
pub fn token_source(&self) -> Result<SecretSource, String> {
|
|
// The `source()` of [secrets.mattermost_token], or an error if it is missing.
|
|
todo!()
|
|
}
|
|
|
|
pub fn loop_socket(&self) -> PathBuf {
|
|
// [loop] socket, or <home>/run/loop/loop.sock when it is empty.
|
|
todo!()
|
|
}
|
|
|
|
pub fn state_path(&self) -> PathBuf {
|
|
// <home>/gateway/state.json.
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
impl SecretSpec {
|
|
/// Exactly one source, well formed.
|
|
pub fn source(&self) -> Result<SecretSource, String> {
|
|
// 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<ServerUrl, String> {
|
|
// 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!()
|
|
}
|