diff --git a/PLAN.md b/PLAN.md index 18165fc..ed47fe4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -166,7 +166,7 @@ Private helpers (e.g., internal `fn` without `pub`) are not flagged, as they don - `ListItem` struct: No doc. - Helper functions (`apply_color`, `apply_attribute`, `show_list_structured`): No docs. -26. **src/filter_plugin/mod.rs** +26. **src/filter_plugin/mod.rs** [DONE] - `FilterOption` struct: Partial. - `FilterPlugin` trait: Partial. - `FilterChain` struct and methods: Partial. @@ -174,7 +174,7 @@ Private helpers (e.g., internal `fn` without `pub`) are not flagged, as they don - `parse_filter_string()` function: Partial. - Helper functions (`create_filter_with_options`, etc.): No docs. -27. **src/services/meta_service.rs** +27. **src/services/meta_service.rs** [DONE] - `MetaService` struct: No doc. - Methods (`new`, `get_plugins`, `initialize_plugins`, etc.): Partial. diff --git a/src/modes/generate_config.rs b/src/modes/generate_config.rs index 13d0ab4..b2b3fd9 100644 --- a/src/modes/generate_config.rs +++ b/src/modes/generate_config.rs @@ -4,7 +4,15 @@ use serde::{Deserialize, Serialize}; use serde_yaml; use crate::meta_plugin::MetaPlugin; +/// Mode for generating a default configuration file. +/// +/// This module creates a commented YAML template with default values for settings, +/// including list format, server config, compression, and meta plugins. + #[derive(Debug, Serialize, Deserialize)] +/// Default configuration structure for the generated template. +/// +/// Includes core settings, list formatting, server options, compression, and meta plugins. struct DefaultConfig { dir: Option, list_format: Vec, @@ -18,6 +26,7 @@ struct DefaultConfig { } #[derive(Debug, Serialize, Deserialize)] +/// Configuration for a column in the list format. struct ColumnConfig { name: String, label: Option, @@ -29,6 +38,7 @@ struct ColumnConfig { #[derive(Debug, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] +/// Alignment options for table columns. enum ColumnAlignment { #[default] Left, @@ -36,6 +46,7 @@ enum ColumnAlignment { } #[derive(Debug, Serialize, Deserialize)] +/// Server configuration options. struct ServerConfig { address: Option, port: Option, @@ -45,11 +56,13 @@ struct ServerConfig { } #[derive(Debug, Serialize, Deserialize)] +/// Configuration for the compression plugin. struct CompressionPluginConfig { name: String, } #[derive(Debug, Serialize, Deserialize)] +/// Configuration for a meta plugin. struct MetaPluginConfig { name: String, #[serde(default)] @@ -58,126 +71,155 @@ struct MetaPluginConfig { outputs: std::collections::HashMap, } -pub fn mode_generate_config(_cmd: &mut Command, _settings: &crate::config::Settings) -> Result<()> { - // Create instances of each meta plugin to get their default options and outputs - let text_plugin = crate::meta_plugin::text::TextMetaPlugin::new(None, None); - let cwd_plugin = crate::meta_plugin::cwd::CwdMetaPlugin::new(None, None); - let digest_plugin = crate::meta_plugin::digest::DigestMetaPlugin::new(None, None); - let hostname_plugin = crate::meta_plugin::hostname::HostnameMetaPlugin::new(None, None); - #[cfg(feature = "magic")] - let magic_file_plugin = crate::meta_plugin::magic_file::MagicFileMetaPlugin::new(None, None); - let env_plugin = crate::meta_plugin::env::EnvMetaPlugin::new(None, None); - - // Create a default configuration - let default_config = DefaultConfig { - dir: Some("~/.local/share/keep".to_string()), - list_format: vec![ - ColumnConfig { - name: "id".to_string(), - label: Some("Item".to_string()), - align: ColumnAlignment::Right, - max_len: None, - }, - ColumnConfig { - name: "time".to_string(), - label: Some("Time".to_string()), - align: ColumnAlignment::Right, - max_len: None, - }, - ColumnConfig { - name: "size".to_string(), - label: Some("Size".to_string()), - align: ColumnAlignment::Right, - max_len: None, - }, - ColumnConfig { - name: "meta:text_line_count".to_string(), - label: Some("Lines".to_string()), - align: ColumnAlignment::Right, - max_len: None, - }, - ColumnConfig { - name: "tags".to_string(), - label: Some("Tags".to_string()), - align: ColumnAlignment::Left, - max_len: Some("40".to_string()), - }, - ColumnConfig { - name: "meta:hostname_full".to_string(), - label: Some("Hostname".to_string()), - align: ColumnAlignment::Left, - max_len: Some("28".to_string()), - }, - ], - human_readable: false, - output_format: Some("table".to_string()), - quiet: false, - force: false, - server: Some(ServerConfig { - address: Some("127.0.0.1".to_string()), - port: Some(8080), - password_file: None, - password: None, - password_hash: None, - }), - compression_plugin: None, - meta_plugins: Some(vec![ - MetaPluginConfig { - name: "text".to_string(), - options: text_plugin.options().clone(), - outputs: convert_outputs_to_string_map(text_plugin.outputs()), - }, - MetaPluginConfig { - name: "cwd".to_string(), - options: cwd_plugin.options().clone(), - outputs: convert_outputs_to_string_map(cwd_plugin.outputs()), - }, - MetaPluginConfig { - name: "digest".to_string(), - options: digest_plugin.options().clone(), - outputs: convert_outputs_to_string_map(digest_plugin.outputs()), - }, - MetaPluginConfig { - name: "hostname".to_string(), - options: hostname_plugin.options().clone(), - outputs: convert_outputs_to_string_map(hostname_plugin.outputs()), - }, - #[cfg(feature = "magic")] - MetaPluginConfig { - name: "magic_file".to_string(), - options: magic_file_plugin.options().clone(), - outputs: convert_outputs_to_string_map(magic_file_plugin.outputs()), - }, - MetaPluginConfig { - name: "env".to_string(), - options: env_plugin.options().clone(), - outputs: convert_outputs_to_string_map(env_plugin.outputs()), - }, - ]), - }; - - // Serialize to YAML and comment out all lines - let yaml = serde_yaml::to_string(&default_config)?; - - // Comment out every line - let commented_yaml = yaml - .lines() - .map(|line| { - if line.trim().is_empty() { - line.to_string() - } else { - format!("# {}", line) - } - }) - .collect::>() - .join("\n"); - - println!("{}", commented_yaml); - - Ok(()) -} + /// Generates and prints a default commented YAML configuration template. + /// + /// Creates instances of available meta plugins to populate default options and outputs, + /// then serializes the config to YAML with all lines commented for easy editing. + /// + /// # Arguments + /// + /// * `_cmd` - Unused Clap command reference. + /// * `_settings` - Unused settings reference. + /// + /// # Returns + /// + /// `Ok(())` on success. + /// + /// # Examples + /// + /// ``` + /// mode_generate_config(&mut cmd, &settings)?; + /// ``` + pub fn mode_generate_config(_cmd: &mut Command, _settings: &crate::config::Settings) -> Result<()> { + // Create instances of each meta plugin to get their default options and outputs + let text_plugin = crate::meta_plugin::text::TextMetaPlugin::new(None, None); + let cwd_plugin = crate::meta_plugin::cwd::CwdMetaPlugin::new(None, None); + let digest_plugin = crate::meta_plugin::digest::DigestMetaPlugin::new(None, None); + let hostname_plugin = crate::meta_plugin::hostname::HostnameMetaPlugin::new(None, None); + #[cfg(feature = "magic")] + let magic_file_plugin = crate::meta_plugin::magic_file::MagicFileMetaPlugin::new(None, None); + let env_plugin = crate::meta_plugin::env::EnvMetaPlugin::new(None, None); + + // Create a default configuration + let default_config = DefaultConfig { + dir: Some("~/.local/share/keep".to_string()), + list_format: vec![ + ColumnConfig { + name: "id".to_string(), + label: Some("Item".to_string()), + align: ColumnAlignment::Right, + max_len: None, + }, + ColumnConfig { + name: "time".to_string(), + label: Some("Time".to_string()), + align: ColumnAlignment::Right, + max_len: None, + }, + ColumnConfig { + name: "size".to_string(), + label: Some("Size".to_string()), + align: ColumnAlignment::Right, + max_len: None, + }, + ColumnConfig { + name: "meta:text_line_count".to_string(), + label: Some("Lines".to_string()), + align: ColumnAlignment::Right, + max_len: None, + }, + ColumnConfig { + name: "tags".to_string(), + label: Some("Tags".to_string()), + align: ColumnAlignment::Left, + max_len: Some("40".to_string()), + }, + ColumnConfig { + name: "meta:hostname_full".to_string(), + label: Some("Hostname".to_string()), + align: ColumnAlignment::Left, + max_len: Some("28".to_string()), + }, + ], + human_readable: false, + output_format: Some("table".to_string()), + quiet: false, + force: false, + server: Some(ServerConfig { + address: Some("127.0.0.1".to_string()), + port: Some(8080), + password_file: None, + password: None, + password_hash: None, + }), + compression_plugin: None, + meta_plugins: Some(vec![ + MetaPluginConfig { + name: "text".to_string(), + options: text_plugin.options().clone(), + outputs: convert_outputs_to_string_map(text_plugin.outputs()), + }, + MetaPluginConfig { + name: "cwd".to_string(), + options: cwd_plugin.options().clone(), + outputs: convert_outputs_to_string_map(cwd_plugin.outputs()), + }, + MetaPluginConfig { + name: "digest".to_string(), + options: digest_plugin.options().clone(), + outputs: convert_outputs_to_string_map(digest_plugin.outputs()), + }, + MetaPluginConfig { + name: "hostname".to_string(), + options: hostname_plugin.options().clone(), + outputs: convert_outputs_to_string_map(hostname_plugin.outputs()), + }, + #[cfg(feature = "magic")] + MetaPluginConfig { + name: "magic_file".to_string(), + options: magic_file_plugin.options().clone(), + outputs: convert_outputs_to_string_map(magic_file_plugin.outputs()), + }, + MetaPluginConfig { + name: "env".to_string(), + options: env_plugin.options().clone(), + outputs: convert_outputs_to_string_map(env_plugin.outputs()), + }, + ]), + }; + + // Serialize to YAML and comment out all lines + let yaml = serde_yaml::to_string(&default_config)?; + + // Comment out every line + let commented_yaml = yaml + .lines() + .map(|line| { + if line.trim().is_empty() { + line.to_string() + } else { + format!("# {}", line) + } + }) + .collect::>() + .join("\n"); + + println!("{}", commented_yaml); + + Ok(()) + } -// Helper function to convert outputs from serde_yaml::Value to String +/// Helper function to convert outputs from serde_yaml::Value to String. +/// +/// Handles null (uses key), strings, and other values by serializing to YAML string. +/// +/// # Arguments +/// +/// * `outputs` - Reference to the outputs HashMap. +/// +/// # Returns +/// +/// A HashMap with string keys and values. fn convert_outputs_to_string_map( outputs: &std::collections::HashMap, ) -> std::collections::HashMap { diff --git a/src/modes/server/mcp/mod.rs b/src/modes/server/mcp/mod.rs index fc0987e..501089a 100644 --- a/src/modes/server/mcp/mod.rs +++ b/src/modes/server/mcp/mod.rs @@ -3,6 +3,10 @@ pub mod tools; pub use server::KeepMcpServer; +/// Module for handling MCP (Model Context Protocol) requests in the server. +/// +/// Provides handlers for JSON-RPC style requests to interact with Keep's storage +/// via the API. use axum::{ extract::State, http::StatusCode, @@ -15,46 +19,69 @@ use serde_json::Value; use crate::modes::server::common::AppState; use crate::modes::server::common::ApiResponse; +/// Request structure for MCP JSON-RPC calls. +/// +/// # Fields +/// +/// * `method` - The MCP method name (e.g., "save_item"). +/// * `params` - Optional JSON parameters for the method. #[derive(Deserialize)] pub struct McpRequest { pub method: String, pub params: Option, } -pub async fn handle_mcp_request( - State(state): State, - Json(request): Json, -) -> impl IntoResponse { - let mcp_server = KeepMcpServer::new(state); - - match mcp_server.handle_request(&request.method, request.params).await { - Ok(result) => { - match serde_json::from_str(&result) { - Ok(parsed_result) => { - let response = ApiResponse { - success: true, - data: Some(parsed_result), - error: None, - }; - (StatusCode::OK, Json(response)) - } - Err(_) => { - let response = ApiResponse { - success: true, - data: Some(serde_json::Value::String(result)), - error: None, - }; - (StatusCode::OK, Json(response)) + /// Handles an MCP request via the Axum framework. + /// + /// Parses the JSON request, delegates to `KeepMcpServer`, and returns an API response. + /// Attempts to parse the result as JSON; falls back to string if invalid. + /// + /// # Arguments + /// + /// * `State(state)` - The application state. + /// * `Json(request)` - The deserialized MCP request. + /// + /// # Returns + /// + /// An `IntoResponse` with status code and JSON API response. + /// + /// # Errors + /// + /// Returns 400 Bad Request on handler errors. + pub async fn handle_mcp_request( + State(state): State, + Json(request): Json, + ) -> impl IntoResponse { + let mcp_server = KeepMcpServer::new(state); + + match mcp_server.handle_request(&request.method, request.params).await { + Ok(result) => { + match serde_json::from_str(&result) { + Ok(parsed_result) => { + let response = ApiResponse { + success: true, + data: Some(parsed_result), + error: None, + }; + (StatusCode::OK, Json(response)) + } + Err(_) => { + let response = ApiResponse { + success: true, + data: Some(serde_json::Value::String(result)), + error: None, + }; + (StatusCode::OK, Json(response)) + } } } - } - Err(e) => { - let response = ApiResponse { - success: false, - data: None, - error: Some(e.to_string()), - }; - (StatusCode::BAD_REQUEST, Json(response)) + Err(e) => { + let response = ApiResponse { + success: false, + data: None, + error: Some(e.to_string()), + }; + (StatusCode::BAD_REQUEST, Json(response)) + } } } -} diff --git a/src/services/async_item_service.rs b/src/services/async_item_service.rs index 21ed360..e78aafe 100644 --- a/src/services/async_item_service.rs +++ b/src/services/async_item_service.rs @@ -1,3 +1,7 @@ +/// Asynchronous service wrapper for `ItemService`. +/// +/// Uses `tokio::task::spawn_blocking` to offload synchronous operations (DB/FS) +/// to a blocking thread pool, allowing non-blocking async usage in servers. use crate::common::PIPESIZE; use crate::config::Settings; use crate::services::error::CoreError; @@ -15,6 +19,7 @@ use tokio::sync::Mutex; /// It uses `tokio::task::spawn_blocking` to run synchronous database and filesystem operations /// on a dedicated thread pool, preventing them from blocking the async runtime. #[allow(dead_code)] +/// Async wrapper for ItemService operations. pub struct AsyncItemService { pub data_dir: PathBuf, db: Arc>, @@ -25,6 +30,19 @@ pub struct AsyncItemService { #[allow(dead_code)] impl AsyncItemService { + /// Creates a new `AsyncItemService`. + /// + /// # Arguments + /// + /// * `data_dir` - Path to data directory. + /// * `db` - Arc-wrapped mutex for DB connection. + /// * `item_service` - Arc-wrapped ItemService. + /// * `cmd` - Arc-wrapped mutex for Clap command. + /// * `settings` - Arc-wrapped settings. + /// + /// # Returns + /// + /// A new `AsyncItemService`. pub fn new( data_dir: PathBuf, db: Arc>, @@ -41,6 +59,22 @@ impl AsyncItemService { } } + /// Internal helper to execute synchronous operations in a blocking task. + /// + /// Spawns a blocking task with the DB connection and ItemService. + /// + /// # Type Parameters + /// + /// * `F` - Closure type. + /// * `T` - Return type. + /// + /// # Arguments + /// + /// * `f` - The synchronous closure to execute. + /// + /// # Returns + /// + /// Result of the closure, or CoreError on task failure. async fn execute_blocking(&self, f: F) -> Result where F: FnOnce(&Connection, &ItemService) -> Result + Send + 'static, diff --git a/src/services/meta_service.rs b/src/services/meta_service.rs index b0fa052..229b4b8 100644 --- a/src/services/meta_service.rs +++ b/src/services/meta_service.rs @@ -142,6 +142,21 @@ impl MetaService { } } + /// Internal helper to process a meta plugin response and store metadata. + /// + /// Iterates over the metadata entries in the response and stores each in the database + /// using `store_meta`. Logs warnings if storage fails. + /// + /// # Arguments + /// + /// * `conn` - Database connection. + /// * `item_id` - Item ID to associate with the metadata. + /// * `_plugin` - Reference to the plugin (unused). + /// * `response` - The plugin response containing metadata. + /// + /// # Errors + /// + /// Logs warnings for individual storage failures but does not return errors. fn process_plugin_response( &self, conn: &Connection, @@ -163,6 +178,21 @@ impl MetaService { } } + /// Collects initial metadata from environment variables and hostname. + /// + /// Gathers metadata from `KEEP_META_*` environment variables and adds hostname + /// if not already present. + /// + /// # Returns + /// + /// A `HashMap` of initial metadata key-value pairs. + /// + /// # Examples + /// + /// ``` + /// let service = MetaService::new(); + /// let initial_meta = service.collect_initial_meta(); + /// ``` pub fn collect_initial_meta(&self) -> HashMap { let mut item_meta: HashMap = crate::modes::common::get_meta_from_env(); @@ -176,6 +206,11 @@ impl MetaService { } impl Default for MetaService { + /// Provides a default `MetaService` instance. + /// + /// # Returns + /// + /// A new `MetaService` via `new()`. fn default() -> Self { Self::new() }