Something went wrong. Try again.
Identities for entities did.bot
agent llm did
Something went wrong. Try again.
19 kB · 552 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553//! The three tables, and how a directory finds its way to a URL.
use std::collections::BTreeMap;use std::fmt;use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::defaults;use crate::validate::Problems;
/// What a service is.////// The set is closed because each variant means something to the validator:/// which upstreams it may name, and which role in a profile it may fill. A/// kind nothing can check would be a string.#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]#[serde(rename_all = "kebab-case")]pub enum ServiceKind { /// The personal data server: accounts, repositories, records. Pds, /// Follows a personal data server and holds what it finds. Index, /// Mirrors an index and answers a browser. Query, /// The canvas. Web,}
impl ServiceKind { /// The name this kind is written as in the configuration file. pub fn as_str(self) -> &'static str { match self { Self::Pds => "pds", Self::Index => "index", Self::Query => "query", Self::Web => "web", } }}
impl fmt::Display for ServiceKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) }}
/// One process: what it is, where it listens, and what it points at.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]pub struct Service { /// What kind of service this is. pub kind: ServiceKind, /// The port it listens on. pub port: u16, /// The interface it listens on. Loopback unless something says otherwise. #[serde(default = "defaults::host")] pub host: String, /// The personal data server this service reads or writes, by service name. /// /// Meaningful to a record host and to an index. Named rather than /// derived, so that a profile's roles can be checked against each other /// instead of assumed to agree. #[serde(default, skip_serializing_if = "Option::is_none")] pub pds: Option<String>, /// The index this service mirrors, by service name. Query services only. #[serde(default, skip_serializing_if = "Option::is_none")] pub index: Option<String>, /// The query service this service asks, by service name. The canvas only. #[serde(default, skip_serializing_if = "Option::is_none")] pub query: Option<String>, /// The DNS zone agent hostnames are minted under. Personal data servers. #[serde(default, skip_serializing_if = "Option::is_none")] pub zone: Option<String>, /// Where the write-ahead log and blobs live, if this one keeps its state. #[serde(default, skip_serializing_if = "Option::is_none")] pub data: Option<PathBuf>, /// The name specification agents are named from. Personal data servers. #[serde(default, skip_serializing_if = "Option::is_none")] pub names: Option<String>,}
impl Service { /// The base URL this service answers on. pub fn url(&self) -> String { format!("http://{}:{}", self.host, self.port) }
/// The `host:port` a listener binds. pub fn listen(&self) -> String { format!("{}:{}", self.host, self.port) }}
/// One service for each role.////// Every field is optional but `pds`, because a profile that names no personal/// data server cannot say where an account would go, and that is the one/// question every consumer of this crate asks.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]pub struct Profile { /// The personal data server accounts are minted on. pub pds: String, /// The index following the personal data server. #[serde(default, skip_serializing_if = "Option::is_none")] pub index: Option<String>, /// The query service the canvas asks. #[serde(default, skip_serializing_if = "Option::is_none")] pub query: Option<String>, /// The canvas. #[serde(default, skip_serializing_if = "Option::is_none")] pub web: Option<String>,}
/// A directory and the profile it uses.#[derive(Debug, Clone, PartialEq, Eq)]pub struct Binding<'a> { /// The directory the binding was written for. pub dir: &'a Path, /// The profile's name. pub profile: &'a str,}
/// Everything this machine has been told about its services.#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]#[serde(deny_unknown_fields)]pub struct Config { /// Every service instance, by name. #[serde(default, rename = "service")] pub services: BTreeMap<String, Service>, /// Every profile, by name. #[serde(default, rename = "profile")] pub profiles: BTreeMap<String, Profile>, /// Directory to profile name. /// /// A `BTreeMap` over strings rather than paths, because a TOML table's /// keys are strings and writing them back out has to round-trip exactly. #[serde(default)] pub bind: BTreeMap<String, String>, /// Whether every component should log what it is doing in detail. /// /// One switch rather than five, and it lives here rather than in each /// process's environment because the question "why did that record not /// appear" is asked of the whole stack at once. `didbot-setup env` /// turns it into a `RUST_LOG` the development scripts export, and the hook /// reads it directly, since nothing exports an environment variable into a /// hook. /// /// It is deliberately not a level. A level invites choosing one per /// component, which is how a developer ends up with the detail turned up /// on the process that was working. #[serde(default)] pub debug: bool,}
/// What can go wrong reading a configuration file.#[derive(Debug, thiserror::Error)]pub enum ConfigError { /// The file could not be read. #[error("cannot read {path}: {source}")] Read { /// The file that could not be read. path: PathBuf, /// Why not. source: std::io::Error, }, /// The file is not the shape this crate expects. #[error("{path} is not usable: {source}")] Parse { /// The file that would not parse. path: PathBuf, /// Why not. source: Box<toml::de::Error>, },}
impl Config { /// Reads a configuration file, or returns [`Config::builtin`] if there is /// none. /// /// A missing file is not an error and never will be: a machine that has /// never run `didbot-setup` still has one of everything on the /// ports the development scripts use, and everything here must answer for /// it. A file that exists and will not parse *is* an error, because the /// alternative is silently ignoring what somebody wrote. pub fn load(path: &Path) -> Result<Self, ConfigError> { let raw = match std::fs::read_to_string(path) { Ok(raw) => raw, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Self::builtin()), Err(source) => { return Err(ConfigError::Read { path: path.to_owned(), source, }) } }; toml::from_str(&raw).map_err(|source| ConfigError::Parse { path: path.to_owned(), source: Box::new(source), }) }
/// Reads the configuration from its usual place. pub fn load_default() -> Result<Self, ConfigError> { Self::load(&defaults::default_config_path()) }
/// One of everything, on the ports the development scripts use. /// /// This is not a placeholder for a real configuration: it is the /// configuration, for every machine that has not needed a second of /// anything. Keeping it in code rather than writing a file at install time /// means a checkout with no setup step behaves identically to one with a /// file that says the same thing. pub fn builtin() -> Self { defaults::builtin() }
/// The profile bound to `dir`, or to the nearest directory above it. /// /// Longest prefix wins, so a binding on a subdirectory overrides one on /// its parent. A directory nobody has bound falls back to the profile /// named [`DEFAULT_PROFILE`](crate::DEFAULT_PROFILE), which is why a /// worktree created five minutes ago talks to the same server as /// everything else without anyone having registered it. pub fn profile_name_for(&self, dir: &Path) -> &str { self.bind .iter() .filter(|(bound, _)| dir.starts_with(Path::new(bound))) .max_by_key(|(bound, _)| bound.len()) .map(|(_, profile)| profile.as_str()) .unwrap_or(defaults::DEFAULT_PROFILE) }
/// The profile bound to `dir`, if it exists. pub fn profile_for(&self, dir: &Path) -> Option<&Profile> { self.profiles.get(self.profile_name_for(dir)) }
/// The personal data server a directory's agents are provisioned on. /// /// The one question the hook asks. `None` when the directory resolves to a /// profile that does not exist, or to one whose personal data server does /// not — both of which [`Config::problems`] reports by name. pub fn pds_url_for(&self, dir: &Path) -> Option<String> { let profile = self.profile_for(dir)?; Some(self.services.get(&profile.pds)?.url()) }
/// Every binding, in the order the file writes them. pub fn bindings(&self) -> impl Iterator<Item = Binding<'_>> { self.bind.iter().map(|(dir, profile)| Binding { dir: Path::new(dir), profile: profile.as_str(), }) }
/// Every service of one kind, by name. pub fn services_of(&self, kind: ServiceKind) -> impl Iterator<Item = (&str, &Service)> { self.services .iter() .filter(move |(_, service)| service.kind == kind) .map(|(name, service)| (name.as_str(), service)) }
/// The first free port at or above whichever of `anchors` sits highest. /// /// The anchors are the upstreams a new service was given, so a record /// host added against a personal data server on 3500 lands on 3501 and a /// second stack reads as a block. With no anchor it starts at the first /// port this project uses, which keeps a service added on its own next to /// the rest rather than a thousand above them. pub fn next_free_port(&self, anchors: &[Option<&str>]) -> u16 { /// The lowest port this project hands out. const FIRST: u16 = 3000;
let anchor = anchors .iter() .flatten() .filter_map(|name| self.services.get(*name)) .map(|service| service.port) .max() .map_or(FIRST, |port| port.saturating_add(1));
let taken: std::collections::BTreeSet<u16> = self.services.values().map(|service| service.port).collect(); (anchor..u16::MAX) .find(|port| !taken.contains(port)) .unwrap_or(anchor) }
/// Everything wrong with this configuration. pub fn problems(&self) -> Problems { crate::validate::check(self) }
/// Renders the configuration back to TOML. pub fn to_toml(&self) -> String { toml::to_string_pretty(self).expect("the configuration types always serialize") }
/// Writes the configuration to `path`, creating its directory. /// /// Written beside the target and renamed over it, so a process reading the /// file while it is being written sees the old one or the new one and /// never half of either. pub fn save(&self, path: &Path) -> std::io::Result<()> { let parent = path.parent().unwrap_or_else(|| Path::new(".")); std::fs::create_dir_all(parent)?; let temp = parent.join(format!(".stack.toml.{}.tmp", std::process::id())); let outcome = std::fs::write(&temp, self.to_toml().as_bytes()) .and_then(|()| std::fs::rename(&temp, path)); if outcome.is_err() { let _ = std::fs::remove_file(&temp); } outcome }
/// The log filter every component runs under, given this configuration. /// /// The quiet form is what the server already defaults to; the loud form /// turns this project's crates up and leaves the HTTP stack's internals /// down, because `hyper` narrates every frame at DEBUG and would bury the /// thing somebody turned this on to see. pub fn log_filter(&self) -> &'static str { if self.debug { crate::DEBUG_LOG_FILTER } else { crate::QUIET_LOG_FILTER } }}
#[cfg(test)]mod tests { use super::*; use crate::defaults::DEFAULT_PROFILE;
fn parse(raw: &str) -> Config { toml::from_str(raw).expect("parses") }
#[test] fn a_machine_with_no_file_still_has_a_stack() { let config = Config::builtin(); assert_eq!( config.pds_url_for(Path::new("/anywhere/at/all")).as_deref(), Some("http://127.0.0.1:3000"), "the ports the development scripts already use" ); assert!(config.problems().is_empty(), "{:?}", config.problems()); }
#[test] fn the_longest_bound_prefix_wins() { let config = parse( r#" [service.pds-main] kind = "pds" port = 3000 [service.pds-beta] kind = "pds" port = 3100 [profile.default] pds = "pds-main" [profile.beta] pds = "pds-beta" [bind] "/home/dev" = "default" "/home/dev/beta" = "beta" "#, ); assert_eq!(config.profile_name_for(Path::new("/home/dev/x")), "default"); assert_eq!( config.profile_name_for(Path::new("/home/dev/beta/deep/inside")), "beta" ); }
#[test] fn an_unbound_directory_falls_back_to_the_default_profile() { // The case that matters: a worktree created a moment ago, that nobody // has registered, and that must still record. let config = parse( r#" [service.pds-main] kind = "pds" port = 3000 [profile.default] pds = "pds-main" [bind] "/home/dev/beta" = "default" "#, ); assert_eq!( config.profile_name_for(Path::new("/somewhere/else")), DEFAULT_PROFILE ); assert_eq!( config.pds_url_for(Path::new("/somewhere/else")).as_deref(), Some("http://127.0.0.1:3000") ); }
#[test] fn a_prefix_that_is_not_a_path_boundary_does_not_match() { // "/home/dev" must not capture "/home/development". let config = parse( r#" [service.pds-main] kind = "pds" port = 3000 [service.pds-beta] kind = "pds" port = 3100 [profile.default] pds = "pds-main" [profile.beta] pds = "pds-beta" [bind] "/home/dev" = "beta" "#, ); assert_eq!( config.profile_name_for(Path::new("/home/development/repo")), DEFAULT_PROFILE ); }
#[test] fn two_profiles_can_share_one_personal_data_server() { // The composition a whole-stack model cannot express, and the reason // roles are named individually. let config = parse( r#" [service.pds-main] kind = "pds" port = 3000 [service.index-a] kind = "index" port = 3001 pds = "pds-main" [service.index-b] kind = "index" port = 3011 pds = "pds-main" [profile.default] pds = "pds-main" index = "index-a" [profile.a] pds = "pds-main" index = "index-a" [profile.b] pds = "pds-main" index = "index-b" [bind] "/repo/a" = "a" "/repo/b" = "b" "#, ); assert!(config.problems().is_empty(), "{:?}", config.problems()); assert_eq!( config.pds_url_for(Path::new("/repo/a")), config.pds_url_for(Path::new("/repo/b")), "one server" ); assert_eq!( config .profile_for(Path::new("/repo/a")) .unwrap() .index .as_deref(), Some("index-a") ); assert_eq!( config .profile_for(Path::new("/repo/b")) .unwrap() .index .as_deref(), Some("index-b") ); }
#[test] fn a_new_service_lands_next_to_its_upstream() { // So that a second stack reads as a block rather than as five numbers // scattered above whatever happened to be highest. let config = parse( r#" [service.pds-beta] kind = "pds" port = 3500 [service.web] kind = "web" port = 8137 [profile.default] pds = "pds-beta" "#, ); assert_eq!(config.next_free_port(&[Some("pds-beta")]), 3501); }
#[test] fn a_service_with_no_upstream_starts_at_the_first_port() { let config = parse( r#" [service.web] kind = "web" port = 8137 [profile.default] pds = "web" "#, ); assert_eq!(config.next_free_port(&[None]), 3000); }
#[test] fn a_port_already_taken_is_skipped() { let config = parse( r#" [service.pds] kind = "pds" port = 3000 [service.index] kind = "index" port = 3001 pds = "pds" [profile.default] pds = "pds" "#, ); assert_eq!(config.next_free_port(&[Some("pds")]), 3002); assert_eq!(config.next_free_port(&[None]), 3002); }
#[test] fn a_written_configuration_reads_back_the_same() { let config = Config::builtin(); let round_tripped: Config = toml::from_str(&config.to_toml()).expect("round trip"); assert_eq!(config, round_tripped); }
#[test] fn an_unknown_key_is_refused_rather_than_ignored() { // A typo in a table name would otherwise be a setting that silently // does nothing, which is the worst outcome for a file nobody reads // twice. let err = toml::from_str::<Config>("[servcie.pds]\nkind = \"pds\"\nport = 3000\n") .expect_err("a misspelled table is refused"); assert!(err.to_string().contains("servcie"), "{err}"); }}