diff options
Diffstat (limited to 'src/config.rs')
| -rw-r--r-- | src/config.rs | 223 |
1 files changed, 177 insertions, 46 deletions
diff --git a/src/config.rs b/src/config.rs index 9b0d0b8..d095178 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -1,74 +1,205 @@ | |||
| 1 | use anyhow::{Context, Result}; | 1 | use anyhow::Result; |
| 2 | use clap::{Parser, ValueEnum}; | ||
| 2 | use serde::{Deserialize, Serialize}; | 3 | use serde::{Deserialize, Serialize}; |
| 3 | use std::env; | ||
| 4 | 4 | ||
| 5 | /// Database backend type for the relay | 5 | /// Database backend type for the relay |
| 6 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] | 6 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)] |
| 7 | #[serde(rename_all = "lowercase")] | 7 | #[serde(rename_all = "lowercase")] |
| 8 | #[derive(Default)] | ||
| 9 | pub enum DatabaseBackend { | 8 | pub enum DatabaseBackend { |
| 10 | /// In-memory database (default, fastest, no persistence) | 9 | /// LMDB backend (persistent, general purpose) |
| 11 | #[default] | 10 | #[default] |
| 12 | Memory, | 11 | Lmdb, |
| 13 | /// NostrDB backend (persistent, optimized for Nostr) | 12 | /// NostrDB backend (persistent, optimized for Nostr) |
| 14 | NostrDb, | 13 | NostrDb, |
| 15 | /// LMDB backend (persistent, general purpose) | 14 | /// In-memory database (fastest, no persistence - uses temp directory for git data) |
| 16 | Lmdb, | 15 | Memory, |
| 17 | } | 16 | } |
| 18 | 17 | ||
| 19 | impl std::str::FromStr for DatabaseBackend { | 18 | impl std::fmt::Display for DatabaseBackend { |
| 20 | type Err = anyhow::Error; | 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 21 | 20 | match self { | |
| 22 | fn from_str(s: &str) -> Result<Self> { | 21 | Self::Memory => write!(f, "memory"), |
| 23 | match s.to_lowercase().as_str() { | 22 | Self::NostrDb => write!(f, "nostrdb"), |
| 24 | "memory" => Ok(Self::Memory), | 23 | Self::Lmdb => write!(f, "lmdb"), |
| 25 | "nostrdb" => Ok(Self::NostrDb), | ||
| 26 | "lmdb" => Ok(Self::Lmdb), | ||
| 27 | _ => Err(anyhow::anyhow!( | ||
| 28 | "Invalid database backend: {}. Valid options: memory, nostrdb, lmdb", | ||
| 29 | s | ||
| 30 | )), | ||
| 31 | } | 24 | } |
| 32 | } | 25 | } |
| 33 | } | 26 | } |
| 34 | 27 | ||
| 35 | #[derive(Debug, Clone, Serialize, Deserialize)] | 28 | /// ngit-grasp - A GRASP (Git Relays Authorized via Signed-Nostr Proofs) implementation |
| 29 | /// | ||
| 30 | /// Configuration is loaded with the following priority (highest to lowest): | ||
| 31 | /// 1. CLI flags (e.g., --domain example.com) | ||
| 32 | /// 2. Environment variables (e.g., NGIT_DOMAIN=example.com) | ||
| 33 | /// 3. .env file (loaded automatically if present) | ||
| 34 | /// 4. Built-in defaults | ||
| 35 | #[derive(Debug, Clone, Serialize, Deserialize, Parser)] | ||
| 36 | #[command(author, version, about, long_about = None)] | ||
| 37 | #[command(propagate_version = true)] | ||
| 36 | pub struct Config { | 38 | pub struct Config { |
| 39 | /// Domain where this instance is hosted (required, used in GRASP validation) | ||
| 40 | #[arg(long, env = "NGIT_DOMAIN")] | ||
| 37 | pub domain: String, | 41 | pub domain: String, |
| 38 | pub owner_npub: String, | 42 | |
| 39 | pub relay_name: String, | 43 | /// Owner's npub (optional, for relay info in NIP-11) |
| 44 | #[arg(long, env = "NGIT_OWNER_NPUB")] | ||
| 45 | pub owner_npub: Option<String>, | ||
| 46 | |||
| 47 | /// Relay name for NIP-11 information document (defaults to "${domain} grasp relay") | ||
| 48 | #[arg(long = "relay-name", env = "NGIT_RELAY_NAME")] | ||
| 49 | pub relay_name_override: Option<String>, | ||
| 50 | |||
| 51 | /// Relay description for NIP-11 information document | ||
| 52 | #[arg( | ||
| 53 | long, | ||
| 54 | env = "NGIT_RELAY_DESCRIPTION", | ||
| 55 | default_value = "Git Nostr Relay - a grasp implementation" | ||
| 56 | )] | ||
| 40 | pub relay_description: String, | 57 | pub relay_description: String, |
| 58 | |||
| 59 | /// Path to store Git repositories | ||
| 60 | #[arg(long, env = "NGIT_GIT_DATA_PATH", default_value = "./data/git")] | ||
| 41 | pub git_data_path: String, | 61 | pub git_data_path: String, |
| 62 | |||
| 63 | /// Path to store Nostr relay data | ||
| 64 | #[arg(long, env = "NGIT_RELAY_DATA_PATH", default_value = "./data/relay")] | ||
| 42 | pub relay_data_path: String, | 65 | pub relay_data_path: String, |
| 66 | |||
| 67 | /// Server bind address (IP:PORT) | ||
| 68 | #[arg(long, env = "NGIT_BIND_ADDRESS", default_value = "127.0.0.1:8080")] | ||
| 43 | pub bind_address: String, | 69 | pub bind_address: String, |
| 70 | |||
| 71 | /// Database backend type | ||
| 72 | #[arg(long, env = "NGIT_DATABASE_BACKEND", value_enum, default_value_t = DatabaseBackend::Lmdb)] | ||
| 44 | pub database_backend: DatabaseBackend, | 73 | pub database_backend: DatabaseBackend, |
| 45 | } | 74 | } |
| 46 | 75 | ||
| 47 | impl Config { | 76 | impl Config { |
| 48 | pub fn from_env() -> Result<Self> { | 77 | /// Load configuration from CLI args, environment variables, and defaults. |
| 49 | // Load .env file if present | 78 | /// |
| 79 | /// Priority (highest to lowest): | ||
| 80 | /// 1. CLI flags | ||
| 81 | /// 2. Environment variables | ||
| 82 | /// 3. .env file | ||
| 83 | /// 4. Built-in defaults | ||
| 84 | pub fn load() -> Result<Self> { | ||
| 85 | // Load .env file if present (before clap parses, so env vars are available) | ||
| 50 | dotenvy::dotenv().ok(); | 86 | dotenvy::dotenv().ok(); |
| 51 | 87 | ||
| 52 | // Parse database backend from environment | 88 | // Parse CLI args (clap automatically handles env var fallback) |
| 53 | let database_backend = env::var("NGIT_DATABASE_BACKEND") | 89 | let config = Self::parse(); |
| 54 | .ok() | 90 | |
| 55 | .and_then(|s| s.parse().ok()) | 91 | Ok(config) |
| 56 | .unwrap_or_default(); | 92 | } |
| 57 | 93 | ||
| 58 | Ok(Config { | 94 | /// Get relay name (defaults to "${domain} grasp relay" if not set) |
| 59 | domain: env::var("NGIT_DOMAIN").unwrap_or_else(|_| "localhost:8080".to_string()), | 95 | pub fn relay_name(&self) -> String { |
| 60 | owner_npub: env::var("NGIT_OWNER_NPUB").context("NGIT_OWNER_NPUB must be set")?, | 96 | self.relay_name_override |
| 61 | relay_name: env::var("NGIT_RELAY_NAME") | 97 | .clone() |
| 62 | .unwrap_or_else(|_| "ngit-grasp relay".to_string()), | 98 | .unwrap_or_else(|| format!("{} grasp relay", self.domain)) |
| 63 | relay_description: env::var("NGIT_RELAY_DESCRIPTION") | 99 | } |
| 64 | .unwrap_or_else(|_| "A GRASP-compliant Nostr relay for Git".to_string()), | 100 | |
| 65 | git_data_path: env::var("NGIT_GIT_DATA_PATH") | 101 | /// Get effective git data path |
| 66 | .unwrap_or_else(|_| "./data/git".to_string()), | 102 | /// Returns a temp directory when using memory backend, otherwise the configured path |
| 67 | relay_data_path: env::var("NGIT_RELAY_DATA_PATH") | 103 | pub fn effective_git_data_path(&self) -> String { |
| 68 | .unwrap_or_else(|_| "./data/relay".to_string()), | 104 | if self.database_backend == DatabaseBackend::Memory { |
| 69 | bind_address: env::var("NGIT_BIND_ADDRESS") | 105 | std::env::temp_dir() |
| 70 | .unwrap_or_else(|_| "127.0.0.1:8080".to_string()), | 106 | .join("ngit-grasp-git") |
| 71 | database_backend, | 107 | .to_string_lossy() |
| 72 | }) | 108 | .into_owned() |
| 109 | } else { | ||
| 110 | self.git_data_path.clone() | ||
| 111 | } | ||
| 112 | } | ||
| 113 | |||
| 114 | /// Create config for testing | ||
| 115 | #[cfg(test)] | ||
| 116 | pub fn for_testing() -> Self { | ||
| 117 | Self { | ||
| 118 | domain: "localhost:8080".to_string(), | ||
| 119 | owner_npub: Some("npub1test".to_string()), | ||
| 120 | relay_name_override: Some("test relay".to_string()), | ||
| 121 | relay_description: "test description".to_string(), | ||
| 122 | git_data_path: "./test_data/git".to_string(), | ||
| 123 | relay_data_path: "./test_data/relay".to_string(), | ||
| 124 | bind_address: "127.0.0.1:8080".to_string(), | ||
| 125 | database_backend: DatabaseBackend::Memory, | ||
| 126 | } | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | #[cfg(test)] | ||
| 131 | mod tests { | ||
| 132 | use super::*; | ||
| 133 | |||
| 134 | #[test] | ||
| 135 | fn test_default_values() { | ||
| 136 | let config = Config::for_testing(); | ||
| 137 | assert_eq!(config.domain, "localhost:8080"); | ||
| 138 | assert_eq!(config.bind_address, "127.0.0.1:8080"); | ||
| 139 | // for_testing() uses Memory, but the actual default is Lmdb | ||
| 140 | assert_eq!(config.database_backend, DatabaseBackend::Memory); | ||
| 141 | } | ||
| 142 | |||
| 143 | #[test] | ||
| 144 | fn test_lmdb_is_default() { | ||
| 145 | // Verify the actual default via the enum's Default trait | ||
| 146 | assert_eq!(DatabaseBackend::default(), DatabaseBackend::Lmdb); | ||
| 147 | } | ||
| 148 | |||
| 149 | #[test] | ||
| 150 | fn test_memory_backend_uses_temp_dir() { | ||
| 151 | let config = Config { | ||
| 152 | database_backend: DatabaseBackend::Memory, | ||
| 153 | ..Config::for_testing() | ||
| 154 | }; | ||
| 155 | let git_path = config.effective_git_data_path(); | ||
| 156 | assert!(git_path.contains("ngit-grasp-git")); | ||
| 157 | } | ||
| 158 | |||
| 159 | #[test] | ||
| 160 | fn test_lmdb_backend_uses_configured_path() { | ||
| 161 | let config = Config { | ||
| 162 | database_backend: DatabaseBackend::Lmdb, | ||
| 163 | git_data_path: "./my/git/path".to_string(), | ||
| 164 | relay_data_path: "./my/relay/path".to_string(), | ||
| 165 | ..Config::for_testing() | ||
| 166 | }; | ||
| 167 | assert_eq!(config.effective_git_data_path(), "./my/git/path"); | ||
| 168 | } | ||
| 169 | |||
| 170 | #[test] | ||
| 171 | fn test_database_backend_display() { | ||
| 172 | assert_eq!(DatabaseBackend::Memory.to_string(), "memory"); | ||
| 173 | assert_eq!(DatabaseBackend::NostrDb.to_string(), "nostrdb"); | ||
| 174 | assert_eq!(DatabaseBackend::Lmdb.to_string(), "lmdb"); | ||
| 175 | } | ||
| 176 | |||
| 177 | #[test] | ||
| 178 | fn test_relay_name_default() { | ||
| 179 | let config = Config { | ||
| 180 | domain: "example.com".to_string(), | ||
| 181 | relay_name_override: None, | ||
| 182 | ..Config::for_testing() | ||
| 183 | }; | ||
| 184 | assert_eq!(config.relay_name(), "example.com grasp relay"); | ||
| 185 | } | ||
| 186 | |||
| 187 | #[test] | ||
| 188 | fn test_relay_name_override() { | ||
| 189 | let config = Config { | ||
| 190 | domain: "example.com".to_string(), | ||
| 191 | relay_name_override: Some("My Custom Relay".to_string()), | ||
| 192 | ..Config::for_testing() | ||
| 193 | }; | ||
| 194 | assert_eq!(config.relay_name(), "My Custom Relay"); | ||
| 195 | } | ||
| 196 | |||
| 197 | #[test] | ||
| 198 | fn test_owner_npub_optional() { | ||
| 199 | let config = Config { | ||
| 200 | owner_npub: None, | ||
| 201 | ..Config::for_testing() | ||
| 202 | }; | ||
| 203 | assert!(config.owner_npub.is_none()); | ||
| 73 | } | 204 | } |
| 74 | } | 205 | } |