From 57bc8cd9c021feaf08e139e8fb62800bc476068e Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 3 Dec 2025 11:17:39 +0000 Subject: improved settings cli flags > env vars > defaults --- .env.example | 101 ++++++++--- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 74 +++++++- docs/GETTING_STARTED.md | 437 ------------------------------------------------ src/config.rs | 223 +++++++++++++++++++----- src/http/landing.rs | 8 +- src/http/mod.rs | 4 +- src/http/nip11.rs | 12 +- src/main.rs | 18 +- src/nostr/builder.rs | 15 +- 11 files changed, 361 insertions(+), 533 deletions(-) delete mode 100644 docs/GETTING_STARTED.md diff --git a/.env.example b/.env.example index 796415e..0a93b1f 100644 --- a/.env.example +++ b/.env.example @@ -1,34 +1,91 @@ # ngit-grasp Configuration +# +# Configuration Priority (highest to lowest): +# 1. CLI flags (e.g., --domain example.com) +# 2. Environment variables (e.g., NGIT_DOMAIN=example.com) +# 3. This .env file +# 4. Built-in defaults +# +# Run `ngit-grasp --help` for all CLI options -# Domain where this instance is hosted (used in GRASP validation) -NGIT_DOMAIN=gitnostr.com +# ============================================================================ +# REQUIRED +# ============================================================================ -# Owner's npub (for relay info) -NGIT_OWNER_NPUB=npub1... +# Domain where this instance is hosted (required, used in GRASP validation) +# CLI: --domain +# No default - must be set +# NGIT_DOMAIN= -# Relay information (NIP-11) -NGIT_RELAY_NAME=My GRASP Relay -NGIT_RELAY_DESCRIPTION=A GRASP-compliant Git relay with Nostr authorization +# ============================================================================ +# SERVER CONFIGURATION +# ============================================================================ -# Storage paths -NGIT_GIT_DATA_PATH=./data/git -NGIT_RELAY_DATA_PATH=./data/relay +# Server bind address (IP:PORT) +# CLI: --bind-address
+# Default: 127.0.0.1:8080 +# NGIT_BIND_ADDRESS=127.0.0.1:8080 -# Database backend (memory, nostrdb, lmdb) -# - memory: In-memory database (default, fastest, no persistence) -# - nostrdb: NostrDB backend (persistent, optimized for Nostr) [Not yet implemented] -# - lmdb: LMDB backend (persistent, general purpose) -NGIT_DATABASE_BACKEND=memory +# ============================================================================ +# RELAY INFORMATION (NIP-11) +# ============================================================================ -# Server configuration -NGIT_BIND_ADDRESS=127.0.0.1:8080 +# Owner's npub (optional, for relay info in NIP-11) +# CLI: --owner-npub +# Default: (none) +# NGIT_OWNER_NPUB=npub1... -# Logging -RUST_LOG=info +# Relay name shown in NIP-11 information document +# CLI: --relay-name +# Default: ${domain} grasp relay (e.g., "gitnostr.com grasp relay") +# NGIT_RELAY_NAME=My GRASP Relay -# Optional: Proactive sync settings (GRASP-02) +# Relay description shown in NIP-11 information document +# CLI: --relay-description +# Default: Git Nostr Relay - a grasp implementation +# NGIT_RELAY_DESCRIPTION="A GRASP-compliant Git relay with Nostr authorization" + +# ============================================================================ +# STORAGE +# ============================================================================ + +# Path to store Git repositories +# CLI: --git-data-path +# Default: ./data/git +# NGIT_GIT_DATA_PATH=./data/git + +# Path to store Nostr relay data +# CLI: --relay-data-path +# Default: ./data/relay +# NGIT_RELAY_DATA_PATH=./data/relay + +# Database backend for Nostr events +# CLI: --database-backend +# Options: lmdb, memory, nostrdb +# Default: lmdb +# - lmdb: LMDB backend (persistent, general purpose) - RECOMMENDED +# - memory: In-memory database (fastest, no persistence, uses temp dirs) +# - nostrdb: NostrDB backend (persistent, Nostr-optimized) [Not yet implemented] +# +# Note: When using 'memory' backend, git_data_path and relay_data_path +# are automatically set to temporary directories for ephemeral testing. +# NGIT_DATABASE_BACKEND=lmdb + +# ============================================================================ +# LOGGING +# ============================================================================ + +# Rust log level (not a ngit-grasp config, but useful for debugging) +# Options: error, warn, info, debug, trace +# RUST_LOG=info + +# ============================================================================ +# FUTURE/PLANNED OPTIONS (not yet implemented) +# ============================================================================ + +# Proactive sync settings (GRASP-02) # NGIT_PROACTIVE_SYNC_ENABLED=true # NGIT_PROACTIVE_SYNC_INTERVAL_SECS=3600 -# Optional: Archive mode (GRASP-05) -# NGIT_ARCHIVE_MODE=false +# Archive mode (GRASP-05) +# NGIT_ARCHIVE_MODE=false \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 6fcb65f..2935855 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1352,6 +1352,7 @@ version = "0.1.0" dependencies = [ "anyhow", "base64 0.22.1", + "clap", "dotenvy", "flate2", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 02c66ac..a42125d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Configuration dotenvy = "0.15" +clap = { version = "4.5", features = ["derive", "env"] } # Error handling anyhow = "1.0" diff --git a/README.md b/README.md index d2b2629..7dede9b 100644 --- a/README.md +++ b/README.md @@ -100,15 +100,71 @@ nix develop -c cargo test --lib ## Configuration -Environment variables (see `.env.example`): - -- `NGIT_DOMAIN`: Your domain (e.g., `gitnostr.com`) -- `NGIT_OWNER_NPUB`: Relay owner's npub -- `NGIT_RELAY_NAME`: Relay name for NIP-11 -- `NGIT_RELAY_DESCRIPTION`: Relay description -- `NGIT_GIT_DATA_PATH`: Path to store Git repositories -- `NGIT_RELAY_DATA_PATH`: Path to store Nostr events -- `NGIT_BIND_ADDRESS`: Server bind address (default: `127.0.0.1:8080`) +Configuration is loaded with the following priority (highest to lowest): + +1. **CLI flags** (e.g., `--domain example.com`) +2. **Environment variables** (e.g., `NGIT_DOMAIN=example.com`) +3. **.env file** (loaded automatically if present) +4. **Built-in defaults** + +This means CLI flags always take precedence over environment variables, which take precedence over `.env` file values. + +### CLI Usage + +```bash +# View all options with defaults +ngit-grasp --help + +# Run with CLI flags (override everything else) +ngit-grasp --domain relay.example.com --owner-npub npub1... --bind-address 0.0.0.0:8080 + +# Mix CLI flags with environment variables +NGIT_OWNER_NPUB=npub1... ngit-grasp --domain relay.example.com +``` + +### Configuration Options + +| Option | CLI Flag | Environment Variable | Default | +| ----------------- | --------------------- | ------------------------ | -------------------------------------------- | +| Domain | `--domain` | `NGIT_DOMAIN` | (required) | +| Owner npub | `--owner-npub` | `NGIT_OWNER_NPUB` | (optional) | +| Relay name | `--relay-name` | `NGIT_RELAY_NAME` | `${domain} grasp relay` | +| Relay description | `--relay-description` | `NGIT_RELAY_DESCRIPTION` | `Git Nostr Relay - a grasp implementation` | +| Git data path | `--git-data-path` | `NGIT_GIT_DATA_PATH` | `./data/git` (temp dir for memory backend) | +| Relay data path | `--relay-data-path` | `NGIT_RELAY_DATA_PATH` | `./data/relay` (temp dir for memory backend) | +| Bind address | `--bind-address` | `NGIT_BIND_ADDRESS` | `127.0.0.1:8080` | +| Database backend | `--database-backend` | `NGIT_DATABASE_BACKEND` | `lmdb` | + +### Database Backends + +- `lmdb`: LMDB backend (default, persistent, general purpose) +- `memory`: In-memory database (fastest, no persistence - uses temp directories) +- `nostrdb`: NostrDB backend (persistent, optimized for Nostr) [Not yet implemented] + +> **Note:** When using the `memory` backend, git data are automatically stored in temporary directories for ephemeral testing. This is useful for development and CI/CD pipelines. + +### Example: Production Deployment + +```bash +# Using environment variables (recommended for production) +export NGIT_DOMAIN=gitnostr.com +export NGIT_OWNER_NPUB=npub1... +export NGIT_BIND_ADDRESS=0.0.0.0:8080 +export NGIT_DATABASE_BACKEND=lmdb +ngit-grasp +``` + +### Example: Development + +```bash +# Using .env file +cp .env.example .env +# Edit .env with your settings +ngit-grasp + +# Or override specific values with CLI flags +ngit-grasp --domain localhost:3000 --bind-address 127.0.0.1:3000 +``` ## Documentation diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md deleted file mode 100644 index 7fea590..0000000 --- a/docs/GETTING_STARTED.md +++ /dev/null @@ -1,437 +0,0 @@ -# Getting Started with Implementation - -This guide helps you start implementing ngit-grasp based on the architecture design. - -## Prerequisites - -- Rust 1.75 or later -- Git 2.x -- Basic understanding of async Rust (tokio) -- Familiarity with actix-web (helpful) -- Understanding of Nostr basics (helpful) - -## Step 1: Initialize Cargo Project - -```bash -# Create new binary project -cargo init --name ngit-grasp - -# Or if already created: -cargo build -``` - -## Step 2: Add Dependencies - -Edit `Cargo.toml`: - -```toml -[package] -name = "ngit-grasp" -version = "0.1.0" -edition = "2021" -rust-version = "1.75" - -[dependencies] -# HTTP Server -actix-web = "4" -actix-cors = "0.7" - -# Async Runtime -tokio = { version = "1", features = ["full"] } - -# Git Protocol -git-http-backend = "0.1.3" - -# Nostr -nostr-sdk = { version = "0.43", features = ["all-nips"] } -nostr-relay-builder = "0.43" - -# Serialization -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -# Error Handling -anyhow = "1" -thiserror = "1" - -# Logging -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -# Environment -dotenv = "0.15" - -# Utilities -async-trait = "0.1" -futures = "0.3" -bytes = "1" - -[dev-dependencies] -tokio-test = "0.4" -``` - -## Step 3: Project Structure - -Create the directory structure: - -```bash -mkdir -p src/{git,nostr,storage} -mkdir -p tests/{integration,fixtures} -mkdir -p data/{git,relay} -``` - -## Step 4: Configuration Module - -Create `src/config.rs`: - -```rust -use anyhow::Result; -use std::env; -use std::net::SocketAddr; -use std::path::PathBuf; - -#[derive(Debug, Clone)] -pub struct Config { - pub domain: String, - pub owner_npub: String, - pub relay_name: String, - pub relay_description: String, - pub git_data_path: PathBuf, - pub relay_data_path: PathBuf, - pub bind_address: SocketAddr, -} - -impl Config { - pub fn from_env() -> Result { - dotenv::dotenv().ok(); - - Ok(Config { - domain: env::var("NGIT_DOMAIN")?, - owner_npub: env::var("NGIT_OWNER_NPUB")?, - relay_name: env::var("NGIT_RELAY_NAME")?, - relay_description: env::var("NGIT_RELAY_DESCRIPTION")?, - git_data_path: PathBuf::from( - env::var("NGIT_GIT_DATA_PATH") - .unwrap_or_else(|_| "./data/git".to_string()) - ), - relay_data_path: PathBuf::from( - env::var("NGIT_RELAY_DATA_PATH") - .unwrap_or_else(|_| "./data/relay".to_string()) - ), - bind_address: env::var("NGIT_BIND_ADDRESS") - .unwrap_or_else(|_| "127.0.0.1:8080".to_string()) - .parse()?, - }) - } -} -``` - -## Step 5: Core Types - -Create `src/git/types.rs`: - -```rust -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RefUpdate { - pub old_oid: String, - pub new_oid: String, - pub ref_name: String, -} - -impl RefUpdate { - pub fn is_create(&self) -> bool { - self.old_oid == "0000000000000000000000000000000000000000" - } - - pub fn is_delete(&self) -> bool { - self.new_oid == "0000000000000000000000000000000000000000" - } - - pub fn is_update(&self) -> bool { - !self.is_create() && !self.is_delete() - } -} - -#[derive(Debug, thiserror::Error)] -pub enum GitError { - #[error("Invalid pkt-line format")] - InvalidPktLine, - - #[error("Invalid ref update format")] - InvalidRefUpdate, - - #[error("Repository not found: {0}")] - RepositoryNotFound(String), - - #[error("Invalid repository path")] - InvalidPath, -} -``` - -## Step 6: Main Application State - -Create `src/main.rs`: - -```rust -use actix_web::{web, App, HttpServer}; -use anyhow::Result; -use std::sync::Arc; -use tracing::info; - -mod config; -mod git; -mod nostr; -mod storage; - -use config::Config; - -#[derive(Clone)] -pub struct AppState { - pub config: Arc, - // TODO: Add NostrClient, RepositoryManager, etc. -} - -#[actix_web::main] -async fn main() -> Result<()> { - // Initialize logging - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::from_default_env() - ) - .init(); - - // Load configuration - let config = Config::from_env()?; - info!("Starting ngit-grasp on {}", config.bind_address); - - // Create application state - let state = AppState { - config: Arc::new(config.clone()), - }; - - // Start HTTP server - HttpServer::new(move || { - App::new() - .app_data(web::Data::new(state.clone())) - .configure(git::routes::configure) - .configure(nostr::routes::configure) - }) - .bind(config.bind_address)? - .run() - .await?; - - Ok(()) -} -``` - -## Step 7: Git Module Skeleton - -Create `src/git/mod.rs`: - -```rust -pub mod routes; -pub mod handler; -pub mod parser; -pub mod authorization; -pub mod types; - -pub use types::{RefUpdate, GitError}; -``` - -Create `src/git/routes.rs`: - -```rust -use actix_web::web; - -pub fn configure(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/{npub}/{identifier}.git") - .route("/info/refs", web::get().to(super::handler::info_refs)) - .route("/git-upload-pack", web::post().to(super::handler::git_upload_pack)) - .route("/git-receive-pack", web::post().to(super::handler::git_receive_pack)) - ); -} -``` - -## Step 8: First Test - -Create `tests/integration/basic_test.rs`: - -```rust -use actix_web::{test, App}; - -#[actix_web::test] -async fn test_server_starts() { - // TODO: Initialize test app - // TODO: Make test request - assert!(true); -} -``` - -Run tests: - -```bash -cargo test -``` - -## Step 9: Implementation Order - -Follow this order for implementation: - -### Phase 1: Basic Infrastructure (Week 1) -1. ✅ Config module -2. ✅ Main server setup -3. ✅ Core types -4. ⏭️ Git pkt-line parser -5. ⏭️ Ref update parser -6. ⏭️ Parser tests - -### Phase 2: Git Protocol (Week 2) -1. ⏭️ Git upload-pack handler (read-only) -2. ⏭️ Repository manager -3. ⏭️ Path validation and security -4. ⏭️ Integration tests for cloning - -### Phase 3: Nostr Relay (Week 2-3) -1. ⏭️ Nostr relay setup with nostr-relay-builder -2. ⏭️ Repository announcement policy -3. ⏭️ Event hooks for repo creation -4. ⏭️ NIP-11 configuration - -### Phase 4: Authorization (Week 3-4) -1. ⏭️ Maintainer resolution logic -2. ⏭️ State validation logic -3. ⏭️ Git receive-pack with inline validation -4. ⏭️ Integration tests for pushing - -### Phase 5: Polish (Week 4-6) -1. ⏭️ Error handling improvements -2. ⏭️ Logging and observability -3. ⏭️ Performance optimization -4. ⏭️ GRASP-01 compliance testing -5. ⏭️ Documentation updates - -## Development Workflow - -### Running Locally - -```bash -# Copy environment template -cp .env.example .env - -# Edit configuration -vim .env - -# Run in development mode -cargo run - -# With debug logging -RUST_LOG=debug cargo run -``` - -### Testing - -```bash -# Run all tests -cargo test - -# Run with output -cargo test -- --nocapture - -# Run specific test -cargo test test_parse_ref_updates - -# Run integration tests only -cargo test --test '*' -``` - -### Code Quality - -```bash -# Format code -cargo fmt - -# Check formatting -cargo fmt --check - -# Lint -cargo clippy - -# Lint with all features -cargo clippy --all-features -- -D warnings -``` - -## Debugging Tips - -### Enable Detailed Logging - -```bash -RUST_LOG=trace cargo run -``` - -### Test with Real Git Client - -```bash -# In another terminal, after server is running -mkdir test-repo && cd test-repo -git init -echo "test" > README.md -git add . && git commit -m "test" - -# Try to push (will fail without Nostr setup) -git remote add origin http://localhost:8080/npub.../test.git -git push origin main -``` - -### Use curl for HTTP Testing - -```bash -# Test info/refs endpoint -curl -v http://localhost:8080/npub.../test.git/info/refs?service=git-upload-pack -``` - -## Common Issues - -### "Repository not found" -- Check that repository announcement was sent to Nostr relay -- Verify repository was created in git_data_path -- Check logs for repo creation - -### "Push rejected" -- Verify state event exists on relay -- Check state event matches push refs -- Verify maintainer list includes pusher - -### "Cannot connect to relay" -- Check relay is running -- Verify WebSocket endpoint -- Check firewall/network settings - -## Next Steps - -After basic setup: - -1. Implement pkt-line parser (see [GIT_PROTOCOL.md](GIT_PROTOCOL.md)) -2. Add comprehensive tests -3. Implement Nostr relay policies -4. Add authorization logic -5. Test with ngit CLI - -## Resources - -- [ARCHITECTURE.md](ARCHITECTURE.md) - Detailed design -- [GIT_PROTOCOL.md](GIT_PROTOCOL.md) - Git protocol reference -- [actix-web docs](https://actix.rs/docs/) -- [nostr-sdk docs](https://docs.rs/nostr-sdk/) -- [tokio docs](https://docs.rs/tokio/) - -## Getting Help - -- Check existing documentation in `docs/` -- Review reference implementation at `../ngit-relay` -- Open an issue for questions -- Read GRASP protocol spec - -Good luck! 🚀 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 @@ -use anyhow::{Context, Result}; +use anyhow::Result; +use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; -use std::env; /// Database backend type for the relay -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)] #[serde(rename_all = "lowercase")] -#[derive(Default)] pub enum DatabaseBackend { - /// In-memory database (default, fastest, no persistence) + /// LMDB backend (persistent, general purpose) #[default] - Memory, + Lmdb, /// NostrDB backend (persistent, optimized for Nostr) NostrDb, - /// LMDB backend (persistent, general purpose) - Lmdb, + /// In-memory database (fastest, no persistence - uses temp directory for git data) + Memory, } -impl std::str::FromStr for DatabaseBackend { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "memory" => Ok(Self::Memory), - "nostrdb" => Ok(Self::NostrDb), - "lmdb" => Ok(Self::Lmdb), - _ => Err(anyhow::anyhow!( - "Invalid database backend: {}. Valid options: memory, nostrdb, lmdb", - s - )), +impl std::fmt::Display for DatabaseBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Memory => write!(f, "memory"), + Self::NostrDb => write!(f, "nostrdb"), + Self::Lmdb => write!(f, "lmdb"), } } } -#[derive(Debug, Clone, Serialize, Deserialize)] +/// ngit-grasp - A GRASP (Git Relays Authorized via Signed-Nostr Proofs) implementation +/// +/// Configuration is loaded with the following priority (highest to lowest): +/// 1. CLI flags (e.g., --domain example.com) +/// 2. Environment variables (e.g., NGIT_DOMAIN=example.com) +/// 3. .env file (loaded automatically if present) +/// 4. Built-in defaults +#[derive(Debug, Clone, Serialize, Deserialize, Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] pub struct Config { + /// Domain where this instance is hosted (required, used in GRASP validation) + #[arg(long, env = "NGIT_DOMAIN")] pub domain: String, - pub owner_npub: String, - pub relay_name: String, + + /// Owner's npub (optional, for relay info in NIP-11) + #[arg(long, env = "NGIT_OWNER_NPUB")] + pub owner_npub: Option, + + /// Relay name for NIP-11 information document (defaults to "${domain} grasp relay") + #[arg(long = "relay-name", env = "NGIT_RELAY_NAME")] + pub relay_name_override: Option, + + /// Relay description for NIP-11 information document + #[arg( + long, + env = "NGIT_RELAY_DESCRIPTION", + default_value = "Git Nostr Relay - a grasp implementation" + )] pub relay_description: String, + + /// Path to store Git repositories + #[arg(long, env = "NGIT_GIT_DATA_PATH", default_value = "./data/git")] pub git_data_path: String, + + /// Path to store Nostr relay data + #[arg(long, env = "NGIT_RELAY_DATA_PATH", default_value = "./data/relay")] pub relay_data_path: String, + + /// Server bind address (IP:PORT) + #[arg(long, env = "NGIT_BIND_ADDRESS", default_value = "127.0.0.1:8080")] pub bind_address: String, + + /// Database backend type + #[arg(long, env = "NGIT_DATABASE_BACKEND", value_enum, default_value_t = DatabaseBackend::Lmdb)] pub database_backend: DatabaseBackend, } impl Config { - pub fn from_env() -> Result { - // Load .env file if present + /// Load configuration from CLI args, environment variables, and defaults. + /// + /// Priority (highest to lowest): + /// 1. CLI flags + /// 2. Environment variables + /// 3. .env file + /// 4. Built-in defaults + pub fn load() -> Result { + // Load .env file if present (before clap parses, so env vars are available) dotenvy::dotenv().ok(); - // Parse database backend from environment - let database_backend = env::var("NGIT_DATABASE_BACKEND") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or_default(); - - Ok(Config { - domain: env::var("NGIT_DOMAIN").unwrap_or_else(|_| "localhost:8080".to_string()), - owner_npub: env::var("NGIT_OWNER_NPUB").context("NGIT_OWNER_NPUB must be set")?, - relay_name: env::var("NGIT_RELAY_NAME") - .unwrap_or_else(|_| "ngit-grasp relay".to_string()), - relay_description: env::var("NGIT_RELAY_DESCRIPTION") - .unwrap_or_else(|_| "A GRASP-compliant Nostr relay for Git".to_string()), - git_data_path: env::var("NGIT_GIT_DATA_PATH") - .unwrap_or_else(|_| "./data/git".to_string()), - relay_data_path: env::var("NGIT_RELAY_DATA_PATH") - .unwrap_or_else(|_| "./data/relay".to_string()), - bind_address: env::var("NGIT_BIND_ADDRESS") - .unwrap_or_else(|_| "127.0.0.1:8080".to_string()), - database_backend, - }) + // Parse CLI args (clap automatically handles env var fallback) + let config = Self::parse(); + + Ok(config) + } + + /// Get relay name (defaults to "${domain} grasp relay" if not set) + pub fn relay_name(&self) -> String { + self.relay_name_override + .clone() + .unwrap_or_else(|| format!("{} grasp relay", self.domain)) + } + + /// Get effective git data path + /// Returns a temp directory when using memory backend, otherwise the configured path + pub fn effective_git_data_path(&self) -> String { + if self.database_backend == DatabaseBackend::Memory { + std::env::temp_dir() + .join("ngit-grasp-git") + .to_string_lossy() + .into_owned() + } else { + self.git_data_path.clone() + } + } + + /// Create config for testing + #[cfg(test)] + pub fn for_testing() -> Self { + Self { + domain: "localhost:8080".to_string(), + owner_npub: Some("npub1test".to_string()), + relay_name_override: Some("test relay".to_string()), + relay_description: "test description".to_string(), + git_data_path: "./test_data/git".to_string(), + relay_data_path: "./test_data/relay".to_string(), + bind_address: "127.0.0.1:8080".to_string(), + database_backend: DatabaseBackend::Memory, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_values() { + let config = Config::for_testing(); + assert_eq!(config.domain, "localhost:8080"); + assert_eq!(config.bind_address, "127.0.0.1:8080"); + // for_testing() uses Memory, but the actual default is Lmdb + assert_eq!(config.database_backend, DatabaseBackend::Memory); + } + + #[test] + fn test_lmdb_is_default() { + // Verify the actual default via the enum's Default trait + assert_eq!(DatabaseBackend::default(), DatabaseBackend::Lmdb); + } + + #[test] + fn test_memory_backend_uses_temp_dir() { + let config = Config { + database_backend: DatabaseBackend::Memory, + ..Config::for_testing() + }; + let git_path = config.effective_git_data_path(); + assert!(git_path.contains("ngit-grasp-git")); + } + + #[test] + fn test_lmdb_backend_uses_configured_path() { + let config = Config { + database_backend: DatabaseBackend::Lmdb, + git_data_path: "./my/git/path".to_string(), + relay_data_path: "./my/relay/path".to_string(), + ..Config::for_testing() + }; + assert_eq!(config.effective_git_data_path(), "./my/git/path"); + } + + #[test] + fn test_database_backend_display() { + assert_eq!(DatabaseBackend::Memory.to_string(), "memory"); + assert_eq!(DatabaseBackend::NostrDb.to_string(), "nostrdb"); + assert_eq!(DatabaseBackend::Lmdb.to_string(), "lmdb"); + } + + #[test] + fn test_relay_name_default() { + let config = Config { + domain: "example.com".to_string(), + relay_name_override: None, + ..Config::for_testing() + }; + assert_eq!(config.relay_name(), "example.com grasp relay"); + } + + #[test] + fn test_relay_name_override() { + let config = Config { + domain: "example.com".to_string(), + relay_name_override: Some("My Custom Relay".to_string()), + ..Config::for_testing() + }; + assert_eq!(config.relay_name(), "My Custom Relay"); + } + + #[test] + fn test_owner_npub_optional() { + let config = Config { + owner_npub: None, + ..Config::for_testing() + }; + assert!(config.owner_npub.is_none()); } } diff --git a/src/http/landing.rs b/src/http/landing.rs index b978851..f9fca5b 100644 --- a/src/http/landing.rs +++ b/src/http/landing.rs @@ -282,7 +282,7 @@ pub fn get_html(config: &Config) -> String { format!( include_str!("../../templates/landing.html"), base_css = get_base_css(), - relay_name = config.relay_name, + relay_name = config.relay_name(), relay_description = config.relay_description, version = get_version(), curation = curation, @@ -357,7 +357,7 @@ pub fn get_generic_404_html(config: &Config, path: &str) -> String { "##, base_css = get_base_css(), - relay_name = config.relay_name, + relay_name = config.relay_name(), path = path, version = get_version(), footer_script = get_footer_script(), @@ -456,7 +456,7 @@ pub fn get_404_html(config: &Config, npub: &str, identifier: &str) -> String { "##, base_css = get_base_css(), - relay_name = config.relay_name, + relay_name = config.relay_name(), npub = npub, identifier = identifier, version = get_version(), @@ -598,7 +598,7 @@ pub fn get_repo_html(config: &Config, npub: &str, identifier: &str) -> String { "##, base_css = get_base_css(), - relay_name = config.relay_name, + relay_name = config.relay_name(), npub = npub, identifier = identifier, version = get_version(), diff --git a/src/http/mod.rs b/src/http/mod.rs index 4665281..8b1f687 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -118,7 +118,7 @@ impl Service> for HttpService { let path = req.uri().path().to_string(); let query = req.uri().query().map(|s| s.to_string()); let method = req.method().clone(); - let git_data_path = self.config.git_data_path.clone(); + let git_data_path = self.config.effective_git_data_path(); let database = self.database.clone(); // Handle OPTIONS preflight requests (CORS) @@ -427,7 +427,7 @@ pub async fn run_server( let bind_addr: SocketAddr = config.bind_address.parse()?; tracing::info!("Starting HTTP server on {}", bind_addr); - tracing::info!("Relay name: {}", config.relay_name); + tracing::info!("Relay name: {}", config.relay_name()); tracing::info!("Domain: {}", config.domain); let listener = TcpListener::bind(&bind_addr).await?; diff --git a/src/http/nip11.rs b/src/http/nip11.rs index ecb9769..e6a1e46 100644 --- a/src/http/nip11.rs +++ b/src/http/nip11.rs @@ -57,9 +57,9 @@ impl RelayInformationDocument { /// Create NIP-11 relay information document from configuration pub fn from_config(config: &Config) -> Self { Self { - name: config.relay_name.clone(), + name: config.relay_name(), description: config.relay_description.clone(), - pubkey: Some(config.owner_npub.clone()), + pubkey: config.owner_npub.clone(), contact: None, // Could be added to config if needed supported_nips: vec![ 1, // NIP-01: Basic protocol flow @@ -98,8 +98,8 @@ mod tests { fn test_relay_information_document_structure() { let config = Config { domain: "relay.example.com".to_string(), - owner_npub: "npub1test".to_string(), - relay_name: "Test Relay".to_string(), + owner_npub: Some("npub1test".to_string()), + relay_name_override: Some("Test Relay".to_string()), relay_description: "A test relay".to_string(), git_data_path: "./data/git".to_string(), relay_data_path: "./data/relay".to_string(), @@ -128,8 +128,8 @@ mod tests { fn test_relay_information_document_json() { let config = Config { domain: "relay.example.com".to_string(), - owner_npub: "npub1test".to_string(), - relay_name: "Test Relay".to_string(), + owner_npub: Some("npub1test".to_string()), + relay_name_override: Some("Test Relay".to_string()), relay_description: "A test relay".to_string(), git_data_path: "./data/git".to_string(), relay_data_path: "./data/relay".to_string(), diff --git a/src/main.rs b/src/main.rs index 1f18ab2..f80e920 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,10 @@ use anyhow::Result; use tracing::{info, Level}; use tracing_subscriber::FmtSubscriber; -use ngit_grasp::{config::Config, http, nostr}; +use ngit_grasp::{ + config::{Config, DatabaseBackend}, + http, nostr, +}; #[tokio::main] async fn main() -> Result<()> { @@ -14,10 +17,17 @@ async fn main() -> Result<()> { info!("Starting ngit-grasp with nostr-relay-builder..."); - // Load configuration - let config = Config::from_env()?; + // Load configuration (priority: CLI flags > env vars > .env file > defaults) + let config = Config::load()?; + info!("Configuration loaded: {}", config.bind_address); - info!("Git data directory: {}", config.git_data_path); + info!("Domain: {}", config.domain); + info!("Relay name: {}", config.relay_name()); + info!("Git data directory: {}", config.effective_git_data_path()); + if config.database_backend != DatabaseBackend::Memory { + info!("Relay data directory: {}", config.relay_data_path); + } + info!("Database backend: {}", config.database_backend); // Create Nostr relay with NIP-34 validation // Returns both the relay and database for direct queries in handlers diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index eabb38f..904cba4 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs @@ -1203,22 +1203,31 @@ pub fn create_relay(config: &Config) -> Result { tracing::info!("Using LMDB backend at: {}", db_path.display()); // Ensure the database directory exists std::fs::create_dir_all(db_path).map_err(|e| { - anyhow::anyhow!("Failed to create LMDB directory {}: {}", db_path.display(), e) + anyhow::anyhow!( + "Failed to create LMDB directory {}: {}", + db_path.display(), + e + ) })?; Arc::new(NostrLMDB::open(db_path).map_err(|e| { - anyhow::anyhow!("Failed to open LMDB database at {}: {}", db_path.display(), e) + anyhow::anyhow!( + "Failed to open LMDB database at {}: {}", + db_path.display(), + e + ) })?) } }; // Build relay with GRASP-01 validation // Clone Arc for the write policy so both relay and policy can access the database + let git_data_path = config.effective_git_data_path(); let builder = RelayBuilder::default() .database(database.clone()) .write_policy(Nip34WritePolicy::new( &config.domain, database.clone(), - &config.git_data_path, + &git_data_path, )); tracing::info!( -- cgit v1.2.3