huskies: merge 617_story_split_gateway_into_service_and_transport
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
//! Gateway configuration types — pure parsing and validation.
|
||||
//!
|
||||
//! Contains `ProjectEntry`, `GatewayConfig`, and validation logic.
|
||||
//! All filesystem I/O (loading from disk) lives in `io.rs`.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A single project entry in `projects.toml`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ProjectEntry {
|
||||
/// Base URL of the project's huskies container (e.g. `http://localhost:3001`).
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// Top-level `projects.toml` config.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct GatewayConfig {
|
||||
/// Map of project name → container URL.
|
||||
#[serde(default)]
|
||||
pub projects: BTreeMap<String, ProjectEntry>,
|
||||
}
|
||||
|
||||
/// Validate that a gateway config has at least one project.
|
||||
///
|
||||
/// Returns the name of the first project (alphabetically) on success,
|
||||
/// or an error message if the config is empty.
|
||||
pub fn validate_config(config: &GatewayConfig) -> Result<String, String> {
|
||||
if config.projects.is_empty() {
|
||||
return Err("projects.toml must define at least one project".to_string());
|
||||
}
|
||||
Ok(config.projects.keys().next().unwrap().clone())
|
||||
}
|
||||
|
||||
/// Validate that a project name exists in the given project map.
|
||||
///
|
||||
/// Returns the project's URL on success.
|
||||
pub fn validate_project_exists(
|
||||
projects: &BTreeMap<String, ProjectEntry>,
|
||||
name: &str,
|
||||
) -> Result<String, String> {
|
||||
projects.get(name).map(|p| p.url.clone()).ok_or_else(|| {
|
||||
let available: Vec<&str> = projects.keys().map(|s| s.as_str()).collect();
|
||||
format!(
|
||||
"unknown project '{name}'. Available: {}",
|
||||
available.join(", ")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Escape a string as a TOML quoted string.
|
||||
pub fn toml_string(s: &str) -> String {
|
||||
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
|
||||
}
|
||||
|
||||
/// Serialize a `bot.toml` content string from the given fields.
|
||||
pub fn serialize_bot_config(
|
||||
transport: &str,
|
||||
homeserver: Option<&str>,
|
||||
username: Option<&str>,
|
||||
password: Option<&str>,
|
||||
slack_bot_token: Option<&str>,
|
||||
slack_signing_secret: Option<&str>,
|
||||
) -> String {
|
||||
match transport {
|
||||
"slack" => {
|
||||
format!(
|
||||
"enabled = true\ntransport = \"slack\"\n\nslack_bot_token = {}\nslack_signing_secret = {}\nslack_channel_ids = []\n",
|
||||
toml_string(slack_bot_token.unwrap_or("")),
|
||||
toml_string(slack_signing_secret.unwrap_or("")),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
format!(
|
||||
"enabled = true\ntransport = \"matrix\"\n\nhomeserver = {}\nusername = {}\npassword = {}\nroom_ids = []\nallowed_users = []\n",
|
||||
toml_string(homeserver.unwrap_or("")),
|
||||
toml_string(username.unwrap_or("")),
|
||||
toml_string(password.unwrap_or("")),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_valid_projects_toml() {
|
||||
let toml_str = r#"
|
||||
[projects.huskies]
|
||||
url = "http://localhost:3001"
|
||||
|
||||
[projects.robot-studio]
|
||||
url = "http://localhost:3002"
|
||||
"#;
|
||||
let config: GatewayConfig = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(config.projects.len(), 2);
|
||||
assert_eq!(config.projects["huskies"].url, "http://localhost:3001");
|
||||
assert_eq!(config.projects["robot-studio"].url, "http://localhost:3002");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_projects_toml() {
|
||||
let toml_str = "[projects]\n";
|
||||
let config: GatewayConfig = toml::from_str(toml_str).unwrap();
|
||||
assert!(config.projects.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_config_rejects_empty() {
|
||||
let config = GatewayConfig {
|
||||
projects: BTreeMap::new(),
|
||||
};
|
||||
assert!(validate_config(&config).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_config_returns_first_project_name() {
|
||||
let mut projects = BTreeMap::new();
|
||||
projects.insert(
|
||||
"beta".into(),
|
||||
ProjectEntry {
|
||||
url: "http://b".into(),
|
||||
},
|
||||
);
|
||||
projects.insert(
|
||||
"alpha".into(),
|
||||
ProjectEntry {
|
||||
url: "http://a".into(),
|
||||
},
|
||||
);
|
||||
let config = GatewayConfig { projects };
|
||||
assert_eq!(validate_config(&config).unwrap(), "alpha");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_project_exists_succeeds() {
|
||||
let mut projects = BTreeMap::new();
|
||||
projects.insert(
|
||||
"p1".into(),
|
||||
ProjectEntry {
|
||||
url: "http://p1".into(),
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
validate_project_exists(&projects, "p1").unwrap(),
|
||||
"http://p1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_project_exists_fails() {
|
||||
let projects = BTreeMap::new();
|
||||
assert!(validate_project_exists(&projects, "missing").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_string_escapes_quotes() {
|
||||
assert_eq!(toml_string(r#"a"b"#), r#""a\"b""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_string_escapes_backslashes() {
|
||||
assert_eq!(toml_string(r"a\b"), r#""a\\b""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_bot_config_matrix() {
|
||||
let content = serialize_bot_config(
|
||||
"matrix",
|
||||
Some("https://mx.io"),
|
||||
Some("@bot:mx.io"),
|
||||
Some("pass"),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(content.contains("transport = \"matrix\""));
|
||||
assert!(content.contains("homeserver = \"https://mx.io\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_bot_config_slack() {
|
||||
let content =
|
||||
serialize_bot_config("slack", None, None, None, Some("xoxb-123"), Some("secret"));
|
||||
assert!(content.contains("transport = \"slack\""));
|
||||
assert!(content.contains("slack_bot_token = \"xoxb-123\""));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user