//! In-memory storage layer for Supervisor //! //! Provides CRUD operations for: //! - API Keys //! - Runners //! - Jobs use crate::auth::{ApiKey, ApiKeyScope}; use crate::error::{SupervisorError, SupervisorResult}; use hero_job::Job; use std::collections::{HashMap, HashSet}; /// In-memory storage for all supervisor data pub struct Store { /// API keys (key_value -> ApiKey) api_keys: HashMap, /// Registered runner IDs runners: HashSet, /// In-memory job storage (job_id -> Job) jobs: HashMap, } impl Store { /// Create a new store pub fn new() -> Self { Self { api_keys: HashMap::new(), runners: HashSet::new(), jobs: HashMap::new(), } } // ==================== API Key Operations ==================== /// Create an API key with a specific value pub fn key_create(&mut self, key: ApiKey) -> ApiKey { self.api_keys.insert(key.name.clone(), key.clone()); key } /// Create a new API key with generated UUID pub fn key_create_new(&mut self, name: String, scope: ApiKeyScope) -> ApiKey { let key = ApiKey::new(name, scope); self.api_keys.insert(key.name.clone(), key.clone()); key } /// Get an API key by its value pub fn key_get(&self, key_name: &str) -> Option<&ApiKey> { self.api_keys.get(key_name) } /// Delete an API key pub fn key_delete(&mut self, key_name: &str) -> Option { self.api_keys.remove(key_name) } /// List all API keys pub fn key_list(&self) -> Vec { self.api_keys.values().cloned().collect() } /// List API keys by scope pub fn key_list_by_scope(&self, scope: ApiKeyScope) -> Vec { self.api_keys .values() .filter(|k| k.scope == scope) .cloned() .collect() } // ==================== Runner Operations ==================== /// Add a runner pub fn runner_add(&mut self, runner_id: String) -> SupervisorResult<()> { self.runners.insert(runner_id); Ok(()) } /// Remove a runner pub fn runner_remove(&mut self, runner_id: &str) -> SupervisorResult<()> { self.runners.remove(runner_id); Ok(()) } /// Check if a runner exists pub fn runner_exists(&self, runner_id: &str) -> bool { self.runners.contains(runner_id) } /// List all runner IDs pub fn runner_list_all(&self) -> Vec { self.runners.iter().cloned().collect() } // ==================== Job Operations ==================== /// Store a job in memory pub fn job_store(&mut self, job: Job) -> SupervisorResult<()> { self.jobs.insert(job.id.clone(), job); Ok(()) } /// Get a job from memory pub fn job_get(&self, job_id: &str) -> SupervisorResult { self.jobs .get(job_id) .cloned() .ok_or_else(|| SupervisorError::JobNotFound { job_id: job_id.to_string(), }) } /// Delete a job from memory pub fn job_delete(&mut self, job_id: &str) -> SupervisorResult<()> { self.jobs .remove(job_id) .ok_or_else(|| SupervisorError::JobNotFound { job_id: job_id.to_string(), })?; Ok(()) } /// List all job IDs pub fn job_list(&self) -> Vec { self.jobs.keys().cloned().collect() } /// Check if a job exists pub fn job_exists(&self, job_id: &str) -> bool { self.jobs.contains_key(job_id) } } impl Clone for Store { fn clone(&self) -> Self { Self { api_keys: self.api_keys.clone(), runners: self.runners.clone(), jobs: self.jobs.clone(), } } } #[cfg(test)] mod tests { use super::*; use hero_job::JobBuilder; fn create_test_store() -> Store { Store::new() } fn create_test_job(id: &str, runner: &str) -> Job { let job = JobBuilder::new() .caller_id("test_caller") .context_id("test_context") .runner(runner) .payload("test payload") .build() .unwrap(); job.id = id.to_string(); // Set ID manually job } #[test] fn test_api_key_operations() { let mut store = create_test_store(); // Create key let key = store.key_create_new("test_key".to_string(), ApiKeyScope::Admin); assert_eq!(key.name, "test_key"); assert_eq!(key.scope, ApiKeyScope::Admin); // Get key let retrieved = store.key_get(&key.key); assert!(retrieved.is_some()); assert_eq!(retrieved.unwrap().name, "test_key"); // List keys let keys = store.key_list(); assert_eq!(keys.len(), 1); // List by scope let admin_keys = store.key_list_by_scope(ApiKeyScope::Admin); assert_eq!(admin_keys.len(), 1); // Delete key let removed = store.key_delete(&key.key); assert!(removed.is_some()); assert!(store.key_get(&key.key).is_none()); } #[test] fn test_runner_operations() { let mut store = create_test_store(); // Add runner assert!(store.runner_add("runner1".to_string()).is_ok()); assert!(store.runner_exists("runner1")); // List runners let runners = store.runner_list_all(); assert_eq!(runners.len(), 1); assert!(runners.contains(&"runner1".to_string())); // List all runners let all_runners = store.runner_list_all(); assert_eq!(all_runners.len(), 1); // Remove runner assert!(store.runner_remove("runner1").is_ok()); assert!(!store.runner_exists("runner1")); } #[test] fn test_job_operations() { let mut store = create_test_store(); let job = create_test_job("job1", "runner1"); // Store job assert!(store.job_store(job.clone()).is_ok()); assert!(store.job_exists("job1")); // Get job let retrieved = store.job_get("job1"); assert!(retrieved.is_ok()); assert_eq!(retrieved.unwrap().id, "job1"); // List jobs let jobs = store.job_list(); assert_eq!(jobs.len(), 1); assert!(jobs.contains(&"job1".to_string())); // Delete job assert!(store.job_delete("job1").is_ok()); assert!(!store.job_exists("job1")); assert!(store.job_get("job1").is_err()); } #[test] fn test_job_not_found() { let store = create_test_store(); let result = store.job_get("nonexistent"); assert!(result.is_err()); } #[test] fn test_multiple_jobs() { let mut store = create_test_store(); // Add multiple jobs for i in 1..=3 { let job = create_test_job(&format!("job{}", i), "runner1"); assert!(store.job_store(job).is_ok()); } // Verify all exist assert_eq!(store.job_list().len(), 3); assert!(store.job_exists("job1")); assert!(store.job_exists("job2")); assert!(store.job_exists("job3")); // Delete one assert!(store.job_delete("job2").is_ok()); assert_eq!(store.job_list().len(), 2); assert!(!store.job_exists("job2")); } #[test] fn test_store_clone() { let mut store = create_test_store(); store.runner_add("runner1".to_string()).unwrap(); let job = create_test_job("job1", "runner1"); store.job_store(job).unwrap(); // Clone the store let cloned = store.clone(); // Verify cloned data assert!(cloned.runner_exists("runner1")); assert!(cloned.job_exists("job1")); } }