Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 226 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
url = { version = "2.5", features = ["serde"] }
blake3 = "1"
dirs = "5"

# SQLite storage backend
rusqlite = { version = "0.32", features = ["bundled"] }

[dev-dependencies]
tempfile = "3"
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//! Do-something: A self-improving recipe scraping agent.

pub mod models;
pub mod storage;

pub use models::*;
pub use storage::*;

// Re-export existing modules
pub mod agent;
Expand Down
153 changes: 153 additions & 0 deletions src/storage/config_dir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Config directory management.
//!
//! Manages the `~/.do-something` directory structure and provides
//! paths to all subdirectories.

use std::env;
use std::fs;
use std::path::PathBuf;

use super::error::{Result, StorageError};

/// Manages the config directory path resolution and initialization.
#[derive(Debug, Clone)]
pub struct ConfigDir {
path: PathBuf,
}

impl ConfigDir {
/// Resolve config directory: `$DO_SOMETHING_CONFIG` or `~/.do-something`.
pub fn resolve() -> Result<Self> {
let path = if let Ok(custom) = env::var("DO_SOMETHING_CONFIG") {
PathBuf::from(custom)
} else {
dirs::home_dir()
.ok_or(StorageError::NotFound)?
.join(".do-something")
};

Ok(Self { path })
}

/// Create a ConfigDir from a specific path (for testing).
pub fn from_path(path: PathBuf) -> Self {
Self { path }
}

/// Ensure directory structure exists.
pub fn init(&self) -> Result<()> {
fs::create_dir_all(&self.path)?;
fs::create_dir_all(self.knowledge_dir())?;
fs::create_dir_all(self.knowledge_dir().join("site_configs"))?;
fs::create_dir_all(self.knowledge_dir().join("user_models"))?;
fs::create_dir_all(self.knowledge_dir().join("patterns"))?;
fs::create_dir_all(self.recipes_dir())?;
fs::create_dir_all(self.signals_dir())?;
fs::create_dir_all(self.state_dir())?;
fs::create_dir_all(self.state_dir().join("sessions"))?;
Ok(())
}

/// Get the root config directory path.
pub fn path(&self) -> &std::path::Path {
&self.path
}

/// Get knowledge directory path.
pub fn knowledge_dir(&self) -> PathBuf {
self.path.join("knowledge")
}

/// Get recipes directory path.
pub fn recipes_dir(&self) -> PathBuf {
self.path.join("recipes")
}

/// Get signals directory path.
pub fn signals_dir(&self) -> PathBuf {
self.path.join("signals")
}

/// Get state directory path.
pub fn state_dir(&self) -> PathBuf {
self.path.join("state")
}

/// Get config file path.
pub fn config_file(&self) -> PathBuf {
self.path.join("config.json")
}

/// Get site configs directory path.
pub fn site_configs_dir(&self) -> PathBuf {
self.knowledge_dir().join("site_configs")
}

/// Get user models directory path.
pub fn user_models_dir(&self) -> PathBuf {
self.knowledge_dir().join("user_models")
}

/// Get patterns directory path.
pub fn patterns_dir(&self) -> PathBuf {
self.knowledge_dir().join("patterns")
}

/// Get sessions directory path.
pub fn sessions_dir(&self) -> PathBuf {
self.state_dir().join("sessions")
}
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;

#[test]
fn resolve_uses_env_variable() {
let dir = tempdir().unwrap();
// SAFETY: Test sets environment variable in single-threaded context
unsafe {
env::set_var("DO_SOMETHING_CONFIG", dir.path());
}

let config = ConfigDir::resolve().unwrap();
assert_eq!(config.path(), dir.path());

// SAFETY: Test removes environment variable in single-threaded context
unsafe {
env::remove_var("DO_SOMETHING_CONFIG");
}
}

#[test]
fn init_creates_directory_structure() {
let dir = tempdir().unwrap();
let config = ConfigDir::from_path(dir.path().to_path_buf());

config.init().unwrap();

assert!(dir.path().exists());
assert!(config.knowledge_dir().exists());
assert!(config.recipes_dir().exists());
assert!(config.signals_dir().exists());
assert!(config.state_dir().exists());
assert!(config.site_configs_dir().exists());
assert!(config.user_models_dir().exists());
assert!(config.patterns_dir().exists());
assert!(config.sessions_dir().exists());
}

#[test]
fn subdirectory_paths_are_correct() {
let dir = tempdir().unwrap();
let config = ConfigDir::from_path(dir.path().to_path_buf());

assert_eq!(config.knowledge_dir(), dir.path().join("knowledge"));
assert_eq!(config.recipes_dir(), dir.path().join("recipes"));
assert_eq!(config.signals_dir(), dir.path().join("signals"));
assert_eq!(config.state_dir(), dir.path().join("state"));
assert_eq!(config.config_file(), dir.path().join("config.json"));
}
}
47 changes: 47 additions & 0 deletions src/storage/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Storage error types.

use std::io;
use thiserror::Error;

/// Storage-related errors.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StorageError {
/// IO error during file operations.
#[error("IO error: {0}")]
Io(#[from] io::Error),

/// JSON serialization/deserialization error.
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),

/// Config directory not found or inaccessible.
#[error("Config directory not found")]
NotFound,

/// Invalid path specified.
#[error("Invalid path: {0}")]
InvalidPath(String),

/// Recipe not found.
#[error("Recipe not found: {0}")]
RecipeNotFound(String),

/// Session not found.
#[error("Session not found: {0}")]
SessionNotFound(String),

/// Lock acquisition failed.
#[error("Lock error: {0}")]
LockError(String),

/// Index corruption.
#[error("Index corruption: {0}")]
IndexCorruption(String),

/// SQLite database error.
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
}

pub type Result<T> = std::result::Result<T, StorageError>;
136 changes: 136 additions & 0 deletions src/storage/file_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//! Combined file-based storage backend.
//!
//! Provides a unified storage interface with file-based implementations
//! for all storage components.

use std::path::PathBuf;
use std::sync::Arc;

use super::config_dir::ConfigDir;
use super::error::Result;
use super::knowledge_store::FileKnowledgeStore;
use super::recipes_db::FileRecipesDb;
use super::session_store::FileSessionStore;
use super::signal_log::FileSignalLog;
use super::traits::Storage;

/// File-based storage backend.
#[derive(Debug)]
pub struct FileStorage {
config: ConfigDir,
recipes: Arc<FileRecipesDb>,
signals: Arc<FileSignalLog>,
knowledge: Arc<FileKnowledgeStore>,
sessions: Arc<FileSessionStore>,
}

impl FileStorage {
/// Open storage using the default config directory.
pub fn open() -> Result<Self> {
let config = ConfigDir::resolve()?;
Self::from_config(config)
}

/// Open storage at a specific path.
pub fn open_at(path: PathBuf) -> Result<Self> {
let config = ConfigDir::from_path(path);
Self::from_config(config)
}

/// Create storage from a ConfigDir.
pub fn from_config(config: ConfigDir) -> Result<Self> {
config.init()?;

let recipes = Arc::new(FileRecipesDb::open(config.recipes_dir())?);
let signals = Arc::new(FileSignalLog::open(config.signals_dir())?);
let knowledge = Arc::new(FileKnowledgeStore::open(config.knowledge_dir())?);
let sessions = Arc::new(FileSessionStore::open(config.sessions_dir())?);

Ok(Self {
config,
recipes,
signals,
knowledge,
sessions,
})
}

/// Get the config directory.
pub fn config_dir(&self) -> &ConfigDir {
&self.config
}
}

impl Storage for FileStorage {
type Recipes = FileRecipesDb;
type Signals = FileSignalLog;
type Knowledge = FileKnowledgeStore;
type Sessions = FileSessionStore;

fn recipes(&self) -> &Self::Recipes {
&self.recipes
}

fn signals(&self) -> &Self::Signals {
&self.signals
}

fn knowledge(&self) -> &Self::Knowledge {
&self.knowledge
}

fn sessions(&self) -> &Self::Sessions {
&self.sessions
}
}

/// Extension trait for file-based storage with direct access to implementations.
impl FileStorage {
/// Get direct access to the file-based recipes storage.
pub fn file_recipes(&self) -> &FileRecipesDb {
&self.recipes
}

/// Get direct access to the file-based signal storage.
pub fn file_signals(&self) -> &FileSignalLog {
&self.signals
}

/// Get direct access to the file-based knowledge storage.
pub fn file_knowledge(&self) -> &FileKnowledgeStore {
&self.knowledge
}

/// Get direct access to the file-based session storage.
pub fn file_sessions(&self) -> &FileSessionStore {
&self.sessions
}
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use crate::storage::traits::{RecipesStorage, SessionStorage};

#[test]
fn open_creates_directory_structure() {
let dir = tempdir().unwrap();
let storage = FileStorage::open_at(dir.path().to_path_buf()).unwrap();

assert!(storage.config.recipes_dir().exists());
assert!(storage.config.signals_dir().exists());
assert!(storage.config.knowledge_dir().exists());
assert!(storage.config.sessions_dir().exists());
}

#[test]
fn storage_trait_methods_work() {
let dir = tempdir().unwrap();
let storage = FileStorage::open_at(dir.path().to_path_buf()).unwrap();

// Can use through trait methods
assert_eq!(storage.recipes().count().unwrap(), 0);
assert_eq!(storage.sessions().count().unwrap(), 0);
}
}
Loading
Loading