upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/config.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-03 11:17:39 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-03 11:19:20 +0000
commit57bc8cd9c021feaf08e139e8fb62800bc476068e (patch)
treec62abdffb4c91999cae2f570597b9ac154c2e51d /src/config.rs
parent2f8ecd482077d82f2d1a937c7f979eaaa87a27b2 (diff)
improved settings cli flags > env vars > defaults
Diffstat (limited to 'src/config.rs')
-rw-r--r--src/config.rs223
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 @@
1use anyhow::{Context, Result}; 1use anyhow::Result;
2use clap::{Parser, ValueEnum};
2use serde::{Deserialize, Serialize}; 3use serde::{Deserialize, Serialize};
3use 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)]
9pub enum DatabaseBackend { 8pub 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
19impl std::str::FromStr for DatabaseBackend { 18impl 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)]
36pub struct Config { 38pub 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
47impl Config { 76impl 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)]
131mod 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}