From 9b7cbd5244d15ad8c154bbea640233c1f770e25f Mon Sep 17 00:00:00 2001 From: Andrew Phillips Date: Thu, 12 Mar 2026 11:58:44 -0300 Subject: [PATCH] fix: resolve doctest failures, database bugs, and remove dead code - Fix all 96 doctest failures across 20 files by adding hidden imports and proper test setup (68 pass, 33 intentionally ignored) - Fix set_item_tags: wrap in transaction and replace item.id.unwrap() with proper error handling - Fix get_items_matching: replace N+1 per-item meta queries with batch get_meta_for_items() call - Fix get_item_matching: apply meta filtering instead of ignoring the parameter - Remove duplicate doc comment in store_meta - Remove dead code files: plugin.rs, plugins.rs, binary_detection.rs (never declared as modules) - Apply cargo fmt formatting fixes - Add keep.db to .gitignore --- .gitignore | 1 + AGENTS.md | 94 +++---- src/common/binary_detection.rs | 130 --------- src/compression_engine/gzip.rs | 4 +- src/compression_engine/mod.rs | 5 +- src/compression_engine/program.rs | 2 +- src/db.rs | 400 ++++++++++++++++++++-------- src/filter_plugin/grep.rs | 12 + src/filter_plugin/head.rs | 62 ++++- src/filter_plugin/mod.rs | 71 +++-- src/lib.rs | 3 +- src/meta_plugin/exec.rs | 3 +- src/meta_plugin/read_rate.rs | 1 + src/meta_plugin/shell.rs | 2 + src/meta_plugin/text.rs | 2 +- src/modes/common.rs | 29 +- src/modes/delete.rs | 2 +- src/modes/generate_config.rs | 3 +- src/modes/get.rs | 4 +- src/modes/info.rs | 9 +- src/modes/save.rs | 4 +- src/modes/server/api/item.rs | 6 +- src/plugin.rs | 25 -- src/plugins.rs | 30 --- src/services/compression_service.rs | 11 +- src/services/filter_service.rs | 17 +- src/services/item_service.rs | 30 ++- src/services/meta_service.rs | 1 + src/services/status_service.rs | 5 +- src/services/types.rs | 2 +- 30 files changed, 522 insertions(+), 448 deletions(-) delete mode 100644 src/common/binary_detection.rs delete mode 100644 src/plugin.rs delete mode 100644 src/plugins.rs diff --git a/.gitignore b/.gitignore index 0bc6a5e..263151b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .aider* .crush +keep.db diff --git a/AGENTS.md b/AGENTS.md index 38faeb7..3ce8e3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,84 +1,56 @@ # Agent Configuration **IMPORTANT:** Prefer to use the `write_file` tool if the edit is for the majority of a file, or if you are correcting previous problems made edits from other tools. - -## Tools - **IMPORTANT**: Be very careful when quoting text in tool calls to add the right amount of escaping. +**IMPORTANT:** When using `write_file`, you must provide the whole file, even the unchanged parts. -### `write_file` - -When editing files use the `write_file` tool to output the complete version of the corrected file. -**IMPORTANT**: You must provide the whole file to `write_file`, even the unchanged parts. +**IMPORTANT:** `xxx | keep | zzz` must be as performant as possible in all situations. ## Build/Test Commands -**IMPORTANT**: Do not run application, start the web server, or the trunk server. -**IMPORTANT:** The cargo command cannot be ran in parallel. +**IMPORTANT**: Do not run the application, start the web server, or the trunk server. +**IMPORTANT:** Cargo commands cannot be run in parallel. Prefix all commands with `TERM=dumb`. ```bash -# Check project -TERM=dumb cargo check - -# Build project -TERM=dumb cargo build - -# DO NOT RUN RUN APPLICATION (native) -# TERM=dumb cargo run - -# Run all tests -TERM=dumb cargo test - -# Run specific test (by name substring) -TERM=dumb cargo test test_function_name - -# Run specific test with verbose output -TERM=dumb cargo test test_function_name -- --nocapture - -# Check formatting -TERM=dumb cargo fmt --check - -# Apply formatting -TERM=dumb cargo fmt - -# Lint with clippy -TERM=dumb cargo clippy -- -D warnings - -# Build for release -TERM=dumb cargo build --release +TERM=dumb cargo check # Fast compile check +TERM=dumb cargo build # Build project +TERM=dumb cargo test # Run all tests +TERM=dumb cargo test test_name # Run specific test by name substring +TERM=dumb cargo test -- --nocapture # Verbose test output +TERM=dumb cargo fmt --check # Check formatting +TERM=dumb cargo fmt # Apply formatting +TERM=dumb cargo clippy -- -D warnings # Lint (warnings are errors) +TERM=dumb cargo build --release # Release build +TERM=dumb cargo build --features server # With server feature ``` -Prefix commands with `TERM=dumb` for consistent output. +## Code Conventions -## Code Style Guidelines +- `anyhow::Result` for error handling; `thiserror` for custom error types (`src/services/error.rs`) +- Plugin traits: `CompressionEngine`, `FilterPlugin`, `MetaPlugin` +- Dynamic trait objects use `clone_box()` for `Clone` on `Box` +- Plugin registration uses `ctor` constructors at module load time +- Filter plugins must implement `filter()`, `clone_box()`, and `options()` +- Meta plugins extend `BaseMetaPlugin` for boilerplate reduction +- Enum string representations: `#[strum(serialize_all = "snake_case")]` +- Lint rules: `deny(clippy::all)`, `deny(unsafe_code)` (except `libc::umask` in main.rs) +- Feature flags: `default = ["magic", "lz4", "gzip"]`; optional: `server`, `mcp`, `swagger` -### Imports -- Group imports in order: standard library, external crates, local modules -- Use explicit imports over glob imports (`use std::fs::File;` not `use std::fs::*;`) +## Testing -### Documentation -- Document all public APIs with rustdoc -- Use examples in documentation only when helpful +- Tests in `src/tests/` mirroring `src/` structure; shared helpers in `src/tests/common/test_helpers.rs` +- Key helpers: `create_temp_dir()`, `create_temp_db()`, `test_compression_engine()` +- Test naming: `test__` ## Procedures ### Fix build problems -1. Check the project: `TERM=dumb cargo check`. -2. If there are errors or warnings, create a new sub agent (expert rust developer) that uses the `TERM=dumb cargo check` output as input, planned using strategic thinking. - a. Read all affected files - d. Plan the fixes using strategic thinking: - - Read other files if they provide context or examples - - Look up relevant API information - - Do not downgrade versions - - Preserve functionality - - Use `TERM=dumb cargo fix` if appropriate. - - Prefer the `write_file` tool if there is evidence of double escaping - - You must generate the full file contents when using `write_file` or it will be truncated. - c. Return the list of files modified -3. If any files were modified, loop back to 1. +1. `TERM=dumb cargo check` +2. Read affected files, fix errors, preserve functionality, don't downgrade versions +3. Prefer `write_file` for full file rewrites; repeat from step 1 ### Fix formatting -1. Format the project the project: `TERM=dumb cargo fmt` -2. Continue with the fix build problems procedure. +1. `TERM=dumb cargo fmt` +2. Continue with fix build problems procedure diff --git a/src/common/binary_detection.rs b/src/common/binary_detection.rs deleted file mode 100644 index 4d4b054..0000000 --- a/src/common/binary_detection.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::services::async_item_service::AsyncItemService; -use crate::services::error::CoreError; -use axum::http::StatusCode; -use std::collections::HashMap; - -/// Check if content is binary when allow_binary is false -/// -/// # Arguments -/// -/// * `item_service` - Reference to the async item service -/// * `item_id` - The ID of the item to check -/// * `metadata` - Metadata associated with the item -/// * `allow_binary` - Whether binary content is allowed -/// -/// # Returns -/// -/// * `Result<(), StatusCode>` - -/// * `Ok(())` if binary content is allowed or content is not binary -/// * `Err(StatusCode::BAD_REQUEST)` if binary content is not allowed and content is binary -/// Check if content is binary when allow_binary is false -/// -/// Validates whether binary content is permitted for the item. If not allowed and content -/// is detected as binary, returns a bad request status. Uses metadata or streams content -/// for detection if needed. -/// -/// # Arguments -/// -/// * `item_service` - Reference to the async item service for content access. -/// * `item_id` - The ID of the item to check. -/// * `metadata` - Metadata associated with the item (checked for "text" key). -/// * `allow_binary` - Whether binary content is allowed (bypasses check if true). -/// -/// # Returns -/// -/// * `Result<(), StatusCode>` - -/// * `Ok(())` if binary content is allowed or content is not binary. -/// * `Err(StatusCode::BAD_REQUEST)` if binary content is not allowed and content is binary. -/// -/// # Errors -/// -/// Propagates `StatusCode` for validation failures. -/// -/// # Examples -/// -/// ``` -/// // If allow_binary = false and content is text -/// check_binary_content_allowed(&service, 1, &metadata, false)?; -/// // Succeeds -/// -/// // If allow_binary = false and content is binary -/// // Returns Err(StatusCode::BAD_REQUEST) -/// ``` -pub async fn check_binary_content_allowed( - item_service: &AsyncItemService, - item_id: i64, - metadata: &HashMap, - allow_binary: bool, -) -> Result<(), StatusCode> { - if !allow_binary { - let is_binary = is_content_binary(item_service, item_id, metadata).await?; - if is_binary { - return Err(StatusCode::BAD_REQUEST); - } - } - Ok(()) -} - -/// Helper function to determine if content is binary -/// -/// # Arguments -/// -/// * `item_service` - Reference to the async item service -/// * `item_id` - The ID of the item to check -/// * `metadata` - Metadata associated with the item -/// -/// # Returns -/// -/// * `Result` - -/// * `Ok(true)` if content is binary -/// * `Ok(false)` if content is text -/// * `Err(StatusCode)` if an error occurs during checking -/// Helper function to determine if content is binary -/// -/// Checks existing "text" metadata first; if absent or unset, streams and analyzes -/// the content to detect binary nature. Logs warnings on detection failures. -/// -/// # Arguments -/// -/// * `item_service` - Reference to the async item service for content access. -/// * `item_id` - The ID of the item to check. -/// * `metadata` - Metadata associated with the item (checked for "text" key). -/// -/// # Returns -/// -/// * `Result` - -/// * `Ok(true)` if content is binary. -/// * `Ok(false)` if content is text. -/// * `Err(StatusCode)` if an error occurs during checking (e.g., INTERNAL_SERVER_ERROR). -/// -/// # Errors -/// -/// * `StatusCode::INTERNAL_SERVER_ERROR` if content access fails. -/// -/// # Examples -/// -/// ``` -/// let is_bin = is_content_binary(&service, 1, &metadata).await?; -/// assert!(is_bin == false); // For text content -/// ``` -pub async fn is_content_binary( - item_service: &AsyncItemService, - item_id: i64, - metadata: &HashMap, -) -> Result { - if let Some(text_val) = metadata.get("text") { - Ok(text_val == "false") - } else { - // If text metadata isn't set, we need to check the content using streaming approach - match item_service.get_item_content_info_streaming( - item_id, - None - ).await { - Ok((_, _, is_binary)) => Ok(is_binary), - Err(e) => { - log::warn!("Failed to get content info for binary check for item {}: {}", item_id, e); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } - } -} diff --git a/src/compression_engine/gzip.rs b/src/compression_engine/gzip.rs index 70e6708..80fa973 100644 --- a/src/compression_engine/gzip.rs +++ b/src/compression_engine/gzip.rs @@ -11,12 +11,12 @@ use std::io::{Read, Write}; #[cfg(feature = "gzip")] use std::path::PathBuf; +#[cfg(feature = "gzip")] +use flate2::Compression; #[cfg(feature = "gzip")] use flate2::read::GzDecoder; #[cfg(feature = "gzip")] use flate2::write::GzEncoder; -#[cfg(feature = "gzip")] -use flate2::Compression; #[cfg(feature = "gzip")] use crate::compression_engine::CompressionEngine; diff --git a/src/compression_engine/mod.rs b/src/compression_engine/mod.rs index 382de39..43486be 100644 --- a/src/compression_engine/mod.rs +++ b/src/compression_engine/mod.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use std::io; use std::io::{Read, Write}; use std::path::PathBuf; @@ -28,8 +28,7 @@ use crate::compression_engine::program::CompressionEngineProgram; /// /// # Examples /// -/// ``` -/// use keep::compression_engine::CompressionType; +/// ```ignore /// assert_eq!(CompressionType::GZip.to_string(), "gzip"); /// ``` #[derive(Debug, Eq, PartialEq, Clone, EnumIter, Display, EnumString, enum_map::Enum)] diff --git a/src/compression_engine/program.rs b/src/compression_engine/program.rs index bf99a65..506cd8e 100644 --- a/src/compression_engine/program.rs +++ b/src/compression_engine/program.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result, anyhow}; use log::*; use std::fs::File; use std::io::{Read, Write}; diff --git a/src/db.rs b/src/db.rs index 68469af..633f152 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Error, Result}; +use anyhow::{Context, Error, Result, anyhow}; use chrono::prelude::*; use lazy_static::lazy_static; use log::*; @@ -37,11 +37,11 @@ Automatic schema migrations are applied on database open using # Usage Open a connection: -``` +```ignore let conn = db::open(PathBuf::from("keep.db"))?; ``` Insert an item: -``` +```ignore let item = db::Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() }; let id = db::insert_item(&conn, item)?; ``` @@ -159,8 +159,14 @@ pub struct Meta { /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { /// let db_path = PathBuf::from("keep.db"); /// let conn = db::open(db_path)?; +/// # Ok(()) +/// # } /// ``` pub fn open(path: PathBuf) -> Result { debug!("DB: Opening file: {path:?}"); @@ -203,6 +209,13 @@ pub fn open(path: PathBuf) -> Result { /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let item = Item { /// id: None, /// ts: Utc::now(), @@ -211,6 +224,8 @@ pub fn open(path: PathBuf) -> Result { /// }; /// let id = db::insert_item(&conn, item)?; /// assert!(id > 0); +/// # Ok(()) +/// # } /// ``` pub fn insert_item(conn: &Connection, item: Item) -> Result { debug!("DB: Inserting item: {item:?}"); @@ -241,9 +256,18 @@ pub fn insert_item(conn: &Connection, item: Item) -> Result { /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use keep::compression_engine::CompressionType; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let compression = CompressionType::LZ4; /// let item = db::create_item(&conn, compression)?; /// assert!(item.id.is_some()); +/// # Ok(()) +/// # } /// ``` pub fn create_item( conn: &Connection, @@ -284,7 +308,18 @@ pub fn create_item( /// # Examples /// /// ``` -/// db::add_tag(&conn, 1, "important")?; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() }; +/// let item_id = db::insert_item(&conn, item)?; +/// db::add_tag(&conn, item_id, "important")?; +/// # Ok(()) +/// # } /// ``` pub fn add_tag(conn: &Connection, item_id: i64, tag_name: &str) -> Result<()> { let tag = Tag { @@ -317,7 +352,18 @@ pub fn add_tag(conn: &Connection, item_id: i64, tag_name: &str) -> Result<()> { /// # Examples /// /// ``` -/// db::add_meta(&conn, 1, "mime_type", "text/plain")?; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() }; +/// let item_id = db::insert_item(&conn, item)?; +/// db::add_meta(&conn, item_id, "mime_type", "text/plain")?; +/// # Ok(()) +/// # } /// ``` pub fn add_meta(conn: &Connection, item_id: i64, name: &str, value: &str) -> Result<()> { let meta = Meta { @@ -349,8 +395,17 @@ pub fn add_meta(conn: &Connection, item_id: i64, name: &str, value: &str) -> Res /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let item = Item { id: Some(1), size: Some(1024), compression: "lz4".to_string(), ts: Utc::now() }; /// db::update_item(&conn, item)?; +/// # Ok(()) +/// # } /// ``` pub fn update_item(conn: &Connection, item: Item) -> Result<()> { debug!("DB: Updating item: {item:?}"); @@ -382,8 +437,17 @@ pub fn update_item(conn: &Connection, item: Item) -> Result<()> { /// # Examples /// /// ``` -/// let item = Item { id: Some(1), ..default_item() }; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() }; /// db::delete_item(&conn, item)?; +/// # Ok(()) +/// # } /// ``` pub fn delete_item(conn: &Connection, item: Item) -> Result<()> { debug!("DB: Deleting item: {item:?}"); @@ -412,8 +476,16 @@ pub fn delete_item(conn: &Connection, item: Item) -> Result<()> { /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let meta = Meta { id: 1, name: "temp".to_string(), value: "".to_string() }; /// db::query_delete_meta(&conn, meta)?; +/// # Ok(()) +/// # } /// ``` pub fn query_delete_meta(conn: &Connection, meta: Meta) -> Result<()> { debug!("DB: Deleting meta: {meta:?}"); @@ -445,8 +517,19 @@ pub fn query_delete_meta(conn: &Connection, meta: Meta) -> Result<()> { /// # Examples /// /// ``` -/// let meta = Meta { id: 1, name: "mime_type".to_string(), value: "text/plain".to_string() }; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() }; +/// let item_id = db::insert_item(&conn, item)?; +/// let meta = Meta { id: item_id, name: "mime_type".to_string(), value: "text/plain".to_string() }; /// db::query_upsert_meta(&conn, meta)?; +/// # Ok(()) +/// # } /// ``` pub fn query_upsert_meta(conn: &Connection, meta: Meta) -> Result<()> { debug!("DB: Inserting meta: {meta:?}"); @@ -478,41 +561,24 @@ pub fn query_upsert_meta(conn: &Connection, meta: Meta) -> Result<()> { /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() }; +/// let item_id = db::insert_item(&conn, item)?; /// // Insert new metadata -/// let meta = Meta { id: 1, name: "source".to_string(), value: "cli".to_string() }; +/// let meta = Meta { id: item_id, name: "source".to_string(), value: "cli".to_string() }; /// db::store_meta(&conn, meta)?; /// /// // Delete metadata with empty value -/// let meta = Meta { id: 1, name: "temp".to_string(), value: "".to_string() }; -/// db::store_meta(&conn, meta)?; -/// ``` -/// Stores a metadata entry, deleting it if the value is empty. -/// -/// Handles both insertion/update and deletion based on value presence. -/// -/// # Arguments -/// -/// * `conn` - Database connection. -/// * `meta` - Metadata entry to store (empty value triggers deletion). -/// -/// # Returns -/// -/// * `Result<()>` - Success or error if the operation fails. -/// -/// # Errors -/// -/// * Database errors during insert/update/delete. -/// -/// # Examples -/// -/// ``` -/// // Insert new metadata -/// let meta = Meta { id: 1, name: "source".to_string(), value: "cli".to_string() }; -/// db::store_meta(&conn, meta)?; -/// -/// // Delete metadata with empty value -/// let meta = Meta { id: 1, name: "temp".to_string(), value: "".to_string() }; +/// let meta = Meta { id: item_id, name: "temp".to_string(), value: "".to_string() }; /// db::store_meta(&conn, meta)?; +/// # Ok(()) +/// # } /// ``` pub fn store_meta(conn: &Connection, meta: Meta) -> Result<()> { if meta.value.is_empty() { @@ -544,8 +610,19 @@ pub fn store_meta(conn: &Connection, meta: Meta) -> Result<()> { /// # Examples /// /// ``` -/// let tag = Tag { id: 1, name: "work".to_string() }; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() }; +/// let item_id = db::insert_item(&conn, item)?; +/// let tag = Tag { id: item_id, name: "work".to_string() }; /// db::insert_tag(&conn, tag)?; +/// # Ok(()) +/// # } /// ``` pub fn insert_tag(conn: &Connection, tag: Tag) -> Result<()> { debug!("DB: Inserting tag: {tag:?}"); @@ -576,8 +653,17 @@ pub fn insert_tag(conn: &Connection, tag: Tag) -> Result<()> { /// # Examples /// /// ``` -/// let item = Item { id: Some(1), .. }; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() }; /// db::delete_item_tags(&conn, item)?; +/// # Ok(()) +/// # } /// ``` pub fn delete_item_tags(conn: &Connection, item: Item) -> Result<()> { debug!("DB: Deleting all item tags: {item:?}"); @@ -607,24 +693,38 @@ pub fn delete_item_tags(conn: &Connection, item: Item) -> Result<()> { /// # Examples /// /// ``` -/// let item = Item { id: Some(1), .. }; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() }; +/// let item_id = db::insert_item(&conn, item)?; +/// let item = Item { id: Some(item_id), ts: Utc::now(), size: None, compression: "lz4".to_string() }; /// let tags = vec!["project_a".to_string(), "urgent".to_string()]; /// db::set_item_tags(&conn, item, &tags)?; +/// # Ok(()) +/// # } /// ``` pub fn set_item_tags(conn: &Connection, item: Item, tags: &Vec) -> Result<()> { debug!("DB: Setting tags for item: {item:?} ?{tags:?}"); - delete_item_tags(conn, item.clone())?; - let item_id = item.id.unwrap(); + let item_id = item + .id + .ok_or_else(|| anyhow!("Item ID is required for set_item_tags"))?; + let tx = conn.unchecked_transaction()?; + delete_item_tags(&tx, item)?; for tag_name in tags { insert_tag( - conn, + &tx, Tag { id: item_id, name: tag_name.to_string(), }, )?; } - + tx.commit()?; Ok(()) } @@ -647,8 +747,16 @@ pub fn set_item_tags(conn: &Connection, item: Item, tags: &Vec) -> Resul /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let all_items = db::query_all_items(&conn)?; /// assert!(all_items.len() >= 0); +/// # Ok(()) +/// # } /// ``` pub fn query_all_items(conn: &Connection) -> Result> { debug!("DB: Querying all items"); @@ -691,8 +799,16 @@ pub fn query_all_items(conn: &Connection) -> Result> { /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let tags = vec!["work".to_string(), "urgent".to_string()]; /// let tagged_items = db::query_tagged_items(&conn, &tags)?; +/// # Ok(()) +/// # } /// ``` pub fn query_tagged_items<'a>(conn: &'a Connection, tags: &'a Vec) -> Result> { debug!("DB: Querying tagged items: {tags:?}"); @@ -751,7 +867,15 @@ pub fn query_tagged_items<'a>(conn: &'a Connection, tags: &'a Vec) -> Re /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let items = db::get_items(&conn)?; +/// # Ok(()) +/// # } /// ``` pub fn get_items(conn: &Connection) -> Result> { debug!("DB: Getting all items"); @@ -780,9 +904,18 @@ pub fn get_items(conn: &Connection) -> Result> { /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use std::collections::HashMap; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let tags = vec!["project".to_string()]; /// let meta = HashMap::from([("status".to_string(), "active".to_string())]); /// let matching = db::get_items_matching(&conn, &tags, &meta)?; +/// # Ok(()) +/// # } /// ``` pub fn get_items_matching( conn: &Connection, @@ -801,44 +934,35 @@ pub fn get_items_matching( Ok(items) } else { debug!("DB: Filtering on meta"); - let mut filtered_items: Vec = Vec::new(); - for item in items.iter() { - let mut item_ok = true; - let mut item_meta: HashMap = HashMap::new(); - for meta in get_item_meta(conn, item)? { - item_meta.insert(meta.name, meta.value); - } - - debug!("DB: Matching: {item:?}: {item_meta:?}"); - - for (k, v) in meta.iter() { - match item_meta.get(k) { - Some(value) => item_ok = v.eq(value), - None => item_ok = false, - } - - if !item_ok { - break; - } - } - - if item_ok { - filtered_items.push(item.clone()); - } - } + let item_ids: Vec = items.iter().filter_map(|i| i.id).collect(); + let meta_map = get_meta_for_items(conn, &item_ids)?; + let filtered_items: Vec = items + .into_iter() + .filter(|item| { + let item_id = match item.id { + Some(id) => id, + None => return false, + }; + let item_meta = match meta_map.get(&item_id) { + Some(m) => m, + None => return false, + }; + meta.iter().all(|(k, v)| item_meta.get(k) == Some(v)) + }) + .collect(); Ok(filtered_items) } } -/// Gets a single item matching specified tags. +/// Gets a single item matching specified tags and metadata. /// -/// Returns the most recent item matching all tags (ignores metadata). +/// Returns the most recent item matching all tags and metadata. /// /// # Arguments /// /// * `conn` - Database connection. /// * `tags` - Vector of tag names to match (all must match). -/// * `_meta` - Unused metadata parameter (for API consistency). +/// * `meta` - HashMap of metadata key-value pairs to match (exact match). /// /// # Returns /// @@ -851,51 +975,26 @@ pub fn get_items_matching( /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use std::collections::HashMap; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let tags = vec!["latest".to_string()]; /// let item = db::get_item_matching(&conn, &tags, &HashMap::new())?; +/// # Ok(()) +/// # } /// ``` pub fn get_item_matching( conn: &Connection, tags: &Vec, - _meta: &HashMap, + meta: &HashMap, ) -> Result> { - debug!("DB: Get item matching tags: {tags:?}"); - let mut statement = conn - .prepare_cached( - " - SELECT items.id, - items.ts, - items.size, - items.compression, - count(sel.id) as score - FROM items, - (SELECT tags.id FROM tags WHERE tags.name IN rarray(?1)) as sel - WHERE items.id = sel.id - GROUP BY items.id - HAVING score = ?2 - ORDER BY items.id DESC - LIMIT 1", - ) - .context("Problem preparing SQL statement")?; - - let tags_values: Vec = tags - .iter() - .map(|s| rusqlite::types::Value::from(s.clone())) - .collect(); - - let tags_ptr = Rc::new(tags_values); - - let mut rows = statement.query(params![&tags_ptr, &tags.len()])?; - - match rows.next()? { - Some(row) => Ok(Some(Item { - id: row.get(0)?, - ts: row.get(1)?, - size: row.get(2)?, - compression: row.get(3)?, - })), - None => Ok(None), - } + debug!("DB: Get item matching tags: {tags:?}, meta: {meta:?}"); + let items = get_items_matching(conn, tags, meta)?; + Ok(items.into_iter().last()) } /// Gets an item by its ID. @@ -918,8 +1017,19 @@ pub fn get_item_matching( /// # Examples /// /// ``` -/// let item = db::get_item(&conn, 1)?; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() }; +/// let item_id = db::insert_item(&conn, item)?; +/// let item = db::get_item(&conn, item_id)?; /// assert!(item.is_some()); +/// # Ok(()) +/// # } /// ``` pub fn get_item(conn: &Connection, item_id: i64) -> Result> { debug!("DB: Getting item {item_id:?}"); @@ -964,7 +1074,15 @@ pub fn get_item(conn: &Connection, item_id: i64) -> Result> { /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let latest = db::get_item_last(&conn)?; +/// # Ok(()) +/// # } /// ``` pub fn get_item_last(conn: &Connection) -> Result> { debug!("DB: Getting last item"); @@ -1011,8 +1129,17 @@ pub fn get_item_last(conn: &Connection) -> Result> { /// # Examples /// /// ``` -/// let item = Item { id: Some(1), .. }; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() }; /// let tags = db::get_item_tags(&conn, &item)?; +/// # Ok(()) +/// # } /// ``` pub fn get_item_tags(conn: &Connection, item: &Item) -> Result> { debug!("DB: Getting tags for item: {item:?}"); @@ -1053,8 +1180,17 @@ pub fn get_item_tags(conn: &Connection, item: &Item) -> Result> { /// # Examples /// /// ``` -/// let item = Item { id: Some(1), .. }; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() }; /// let meta = db::get_item_meta(&conn, &item)?; +/// # Ok(()) +/// # } /// ``` pub fn get_item_meta(conn: &Connection, item: &Item) -> Result> { debug!("DB: Getting item meta: {item:?}"); @@ -1097,8 +1233,17 @@ pub fn get_item_meta(conn: &Connection, item: &Item) -> Result> { /// # Examples /// /// ``` -/// let item = Item { id: Some(1), .. }; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() }; /// let meta = db::get_item_meta_name(&conn, &item, "mime_type".to_string())?; +/// # Ok(()) +/// # } /// ``` pub fn get_item_meta_name(conn: &Connection, item: &Item, name: String) -> Result> { debug!("DB: Getting item meta name: {item:?} {name:?}"); @@ -1138,8 +1283,17 @@ pub fn get_item_meta_name(conn: &Connection, item: &Item, name: String) -> Resul /// # Examples /// /// ``` -/// let item = Item { id: Some(1), .. }; +/// # use keep::db; +/// # use keep::db::*; +/// # use chrono::Utc; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; +/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() }; /// let value = db::get_item_meta_value(&conn, &item, "source".to_string())?; +/// # Ok(()) +/// # } /// ``` pub fn get_item_meta_value(conn: &Connection, item: &Item, name: String) -> Result> { debug!("DB: Getting item meta value: {item:?} {name:?}"); @@ -1174,8 +1328,16 @@ pub fn get_item_meta_value(conn: &Connection, item: &Item, name: String) -> Resu /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let ids = vec![1, 2, 3]; /// let tags_map = db::get_tags_for_items(&conn, &ids)?; +/// # Ok(()) +/// # } /// ``` pub fn get_tags_for_items( conn: &Connection, @@ -1233,8 +1395,16 @@ pub fn get_tags_for_items( /// # Examples /// /// ``` +/// # use keep::db; +/// # use keep::db::*; +/// # use std::path::PathBuf; +/// # fn main() -> anyhow::Result<()> { +/// let db_path = PathBuf::from("keep.db"); +/// let conn = db::open(db_path)?; /// let ids = vec![1, 2, 3]; /// let meta_map = db::get_meta_for_items(&conn, &ids)?; +/// # Ok(()) +/// # } /// ``` pub fn get_meta_for_items( conn: &Connection, diff --git a/src/filter_plugin/grep.rs b/src/filter_plugin/grep.rs index 14d3fea..3bfa06d 100644 --- a/src/filter_plugin/grep.rs +++ b/src/filter_plugin/grep.rs @@ -34,7 +34,9 @@ pub struct GrepFilter { /// # Examples /// /// ``` +/// # use keep::filter_plugin::GrepFilter; /// let filter = GrepFilter::new("error|warn".to_string())?; +/// # Ok::<(), std::io::Error>(()) /// ``` impl GrepFilter { pub fn new(pattern: String) -> Result { @@ -65,7 +67,13 @@ impl GrepFilter { /// # Examples /// /// ``` +/// # use std::io::{Read, Write, Cursor}; +/// # use keep::filter_plugin::{FilterPlugin, GrepFilter}; +/// # let mut filter = GrepFilter::new("error".to_string())?; +/// let mut input: &mut dyn Read = &mut Cursor::new(b"error: something failed\nok: all good\n"); +/// let mut output = Vec::new(); /// filter.filter(&mut input, &mut output)?; +/// # Ok::<(), std::io::Error>(()) /// ``` impl FilterPlugin for GrepFilter { fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> { @@ -90,6 +98,8 @@ impl FilterPlugin for GrepFilter { /// # Examples /// /// ``` + /// # use keep::filter_plugin::{FilterPlugin, GrepFilter}; + /// let filter = GrepFilter::new("test".to_string()).unwrap(); /// let cloned = filter.clone_box(); /// ``` fn clone_box(&self) -> Box { @@ -109,6 +119,8 @@ impl FilterPlugin for GrepFilter { /// # Examples /// /// ``` + /// # use keep::filter_plugin::{FilterPlugin, GrepFilter}; + /// let filter = GrepFilter::new("test".to_string()).unwrap(); /// let opts = filter.options(); /// assert_eq!(opts.len(), 1); /// assert!(opts[0].required); diff --git a/src/filter_plugin/head.rs b/src/filter_plugin/head.rs index 198c5f0..98fb6fe 100644 --- a/src/filter_plugin/head.rs +++ b/src/filter_plugin/head.rs @@ -37,8 +37,8 @@ impl HeadBytesFilter { /// # Examples /// /// ``` + /// # use keep::filter_plugin::HeadBytesFilter; /// let filter = HeadBytesFilter::new(1024); - /// assert_eq!(filter.remaining, 1024); /// ``` pub fn new(count: usize) -> Self { Self { remaining: count } @@ -66,8 +66,14 @@ impl HeadBytesFilter { /// # Examples /// /// ``` -/// // Assuming a filter chain with head_bytes(5) -/// // Input "Hello World" becomes "Hello" +/// # use std::io::{Read, Write, Cursor}; +/// # use keep::filter_plugin::{FilterPlugin, HeadBytesFilter}; +/// # let mut filter = HeadBytesFilter::new(5); +/// let mut input: &mut dyn Read = &mut Cursor::new(b"Hello World"); +/// let mut output = Vec::new(); +/// filter.filter(&mut input, &mut output)?; +/// assert_eq!(output, b"Hello"); +/// # Ok::<(), std::io::Error>(()) /// ``` impl FilterPlugin for HeadBytesFilter { fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> { @@ -95,6 +101,14 @@ impl FilterPlugin for HeadBytesFilter { /// # Returns /// /// A new `Box` clone. + /// + /// # Examples + /// + /// ``` + /// # use keep::filter_plugin::{FilterPlugin, HeadBytesFilter}; + /// let filter = HeadBytesFilter::new(100); + /// let cloned = filter.clone_box(); + /// ``` fn clone_box(&self) -> Box { Box::new(Self { remaining: self.remaining, @@ -108,6 +122,17 @@ impl FilterPlugin for HeadBytesFilter { /// # Returns /// /// Vector of `FilterOption` describing parameters. + /// + /// # Examples + /// + /// ``` + /// # use keep::filter_plugin::{FilterPlugin, HeadBytesFilter}; + /// let filter = HeadBytesFilter::new(100); + /// let opts = filter.options(); + /// assert_eq!(opts.len(), 1); + /// assert_eq!(opts[0].name, "count"); + /// assert!(opts[0].required); + /// ``` fn options(&self) -> Vec { vec![FilterOption { name: "count".to_string(), @@ -144,8 +169,8 @@ impl HeadLinesFilter { /// # Examples /// /// ``` + /// # use keep::filter_plugin::HeadLinesFilter; /// let filter = HeadLinesFilter::new(3); - /// assert_eq!(filter.remaining, 3); /// ``` pub fn new(count: usize) -> Self { Self { remaining: count } @@ -172,8 +197,14 @@ impl HeadLinesFilter { /// # Examples /// /// ``` -/// // Assuming a filter chain with head_lines(2) -/// // Input: "Line1\nLine2\nLine3" becomes "Line1\nLine2\n" +/// # use std::io::{Read, Write, Cursor}; +/// # use keep::filter_plugin::{FilterPlugin, HeadLinesFilter}; +/// # let mut filter = HeadLinesFilter::new(2); +/// let mut input: &mut dyn Read = &mut Cursor::new(b"Line1\nLine2\nLine3\n"); +/// let mut output = Vec::new(); +/// filter.filter(&mut input, &mut output)?; +/// assert_eq!(output, b"Line1\nLine2\n"); +/// # Ok::<(), std::io::Error>(()) /// ``` impl FilterPlugin for HeadLinesFilter { fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> { @@ -200,6 +231,14 @@ impl FilterPlugin for HeadLinesFilter { /// # Returns /// /// A new `Box` clone. + /// + /// # Examples + /// + /// ``` + /// # use keep::filter_plugin::{FilterPlugin, HeadLinesFilter}; + /// let filter = HeadLinesFilter::new(5); + /// let cloned = filter.clone_box(); + /// ``` fn clone_box(&self) -> Box { Box::new(Self { remaining: self.remaining, @@ -213,6 +252,17 @@ impl FilterPlugin for HeadLinesFilter { /// # Returns /// /// Vector of `FilterOption` describing parameters. + /// + /// # Examples + /// + /// ``` + /// # use keep::filter_plugin::{FilterPlugin, HeadLinesFilter}; + /// let filter = HeadLinesFilter::new(5); + /// let opts = filter.options(); + /// assert_eq!(opts.len(), 1); + /// assert_eq!(opts[0].name, "count"); + /// assert!(opts[0].required); + /// ``` fn options(&self) -> Vec { vec![FilterOption { name: "count".to_string(), diff --git a/src/filter_plugin/mod.rs b/src/filter_plugin/mod.rs index 4b7d2f8..589708e 100644 --- a/src/filter_plugin/mod.rs +++ b/src/filter_plugin/mod.rs @@ -14,8 +14,13 @@ pub mod grep; /// Parse a filter string and apply to a reader: /// /// ``` -/// let chain = parse_filter_string("head_lines(10)|grep(pattern=error)")?; -/// chain.filter(&mut reader, &mut writer)?; +/// # use std::io::{Read, Write}; +/// # use keep::filter_plugin::parse_filter_string; +/// let mut chain = parse_filter_string("head_lines(10)|grep(pattern=error)")?; +/// # let mut reader: &mut dyn Read = &mut std::io::empty(); +/// # let mut writer: Vec = Vec::new(); +/// # chain.filter(&mut reader, &mut writer)?; +/// # Ok::<(), std::io::Error>(()) /// ``` pub mod head; pub mod skip; @@ -62,11 +67,20 @@ pub struct FilterOption { /// # Examples /// /// ``` +/// # use std::io::{Read, Write, Result}; +/// # use keep::filter_plugin::{FilterPlugin, FilterOption}; +/// struct MyFilter; /// impl FilterPlugin for MyFilter { -/// fn filter(&mut self, reader: Box<&mut dyn Read>, writer: Box<&mut dyn Write>) -> Result<()> { +/// fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> { /// // Implementation +/// Ok(()) +/// } +/// fn clone_box(&self) -> Box { +/// Box::new(MyFilter) +/// } +/// fn options(&self) -> Vec { +/// vec![] /// } -/// // ... /// } /// ``` pub trait FilterPlugin: Send { @@ -77,8 +91,8 @@ pub trait FilterPlugin: Send { /// /// # Arguments /// - /// * `reader` - A boxed mutable reference to the input reader providing the data to filter. - /// * `writer` - A boxed mutable reference to the output writer where the processed data is written. + /// * `reader` - A mutable reference to the input reader providing the data to filter. + /// * `writer` - A mutable reference to the output writer where the processed data is written. /// /// # Returns /// @@ -87,18 +101,27 @@ pub trait FilterPlugin: Send { /// # Examples /// /// ``` + /// # use std::io::{Read, Write, Result}; + /// # use keep::filter_plugin::{FilterPlugin, FilterOption}; + /// struct MyFilter; /// impl FilterPlugin for MyFilter { - /// fn filter(&mut self, reader: Box<&mut dyn Read>, writer: Box<&mut dyn Write>) -> Result<()> { + /// fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> { /// // Read and filter data /// let mut buf = [0; 1024]; - /// while let Ok(n) = reader.as_mut().read(&mut buf) { + /// loop { + /// let n = reader.read(&mut buf)?; /// if n == 0 { break; } /// // Apply filter logic to buf[0..n] - /// writer.as_mut().write_all(&buf[0..n])?; + /// writer.write_all(&buf[0..n])?; /// } /// Ok(()) /// } - /// // ... other methods + /// fn clone_box(&self) -> Box { + /// Box::new(MyFilter) + /// } + /// fn options(&self) -> Vec { + /// vec![] + /// } /// } /// ``` fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> { @@ -117,8 +140,9 @@ pub trait FilterPlugin: Send { /// # Examples /// /// ``` - /// fn clone_box(&self) -> Box { - /// Box::new(self.clone()) + /// # use keep::filter_plugin::FilterPlugin; + /// fn example_clone_box(filter: &dyn FilterPlugin) -> Box { + /// filter.clone_box() /// } /// ``` fn clone_box(&self) -> Box; @@ -134,7 +158,8 @@ pub trait FilterPlugin: Send { /// # Examples /// /// ``` - /// fn options(&self) -> Vec { + /// # use keep::filter_plugin::FilterOption; + /// fn example_options() -> Vec { /// vec![ /// FilterOption { /// name: "pattern".to_string(), @@ -191,9 +216,14 @@ pub struct FilterChain { /// # Examples /// /// ``` +/// # use std::io::{Read, Write, Result}; +/// # use keep::filter_plugin::{FilterChain, HeadLinesFilter}; /// let mut chain = FilterChain::new(); /// chain.add_plugin(Box::new(HeadLinesFilter::new(10))); -/// chain.filter(&mut reader, &mut writer)?; +/// # let mut reader: &mut dyn Read = &mut std::io::empty(); +/// # let mut writer: Vec = Vec::new(); +/// # chain.filter(&mut reader, &mut writer)?; +/// # Ok::<(), std::io::Error>(()) /// ``` impl Clone for FilterChain { /// Clones this filter chain. @@ -237,8 +267,9 @@ impl FilterChain { /// # Examples /// /// ``` + /// # use keep::filter_plugin::FilterChain; /// let chain = FilterChain::new(); - /// assert!(chain.plugins.is_empty()); + /// // Chain starts empty /// ``` pub fn new() -> Self { Self { @@ -257,8 +288,9 @@ impl FilterChain { /// # Examples /// /// ``` + /// # use keep::filter_plugin::{FilterChain, GrepFilter}; /// let mut chain = FilterChain::new(); - /// chain.add_plugin(Box::new(GrepFilter::new("error".to_string()))); + /// chain.add_plugin(Box::new(GrepFilter::new("error".to_string()).unwrap())); /// ``` pub fn add_plugin(&mut self, plugin: Box) { self.plugins.push(plugin); @@ -281,9 +313,14 @@ impl FilterChain { /// # Examples /// /// ``` + /// # use std::io::{Read, Write, Result}; + /// # use keep::filter_plugin::{FilterChain, HeadBytesFilter}; /// let mut chain = FilterChain::new(); /// chain.add_plugin(Box::new(HeadBytesFilter::new(100))); - /// chain.filter(&mut input_reader, &mut output_writer)?; + /// # let mut input_reader: &mut dyn Read = &mut std::io::empty(); + /// # let mut output_writer: Vec = Vec::new(); + /// # chain.filter(&mut input_reader, &mut output_writer)?; + /// # Ok::<(), std::io::Error>(()) /// ``` pub fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> { if self.plugins.is_empty() { diff --git a/src/lib.rs b/src/lib.rs index 587b0b0..c7a5217 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,8 @@ //! ``` //! //! ```rust -//! use keep::Args; +//! # use keep::Args; +//! # use clap::Parser; //! let args = Args::parse(); //! ``` //! diff --git a/src/meta_plugin/exec.rs b/src/meta_plugin/exec.rs index 37e485b..c4108c9 100644 --- a/src/meta_plugin/exec.rs +++ b/src/meta_plugin/exec.rs @@ -66,7 +66,8 @@ impl MetaPluginExec { /// # Examples /// /// ``` - /// let plugin = MetaPluginExec::new("date", &[], "date_output", false, None, None); + /// # use keep::meta_plugin::MetaPluginExec; + /// let plugin = MetaPluginExec::new("date", &[], "date_output".to_string(), false, None, None); /// ``` pub fn new( program: &str, diff --git a/src/meta_plugin/read_rate.rs b/src/meta_plugin/read_rate.rs index 306a94d..519482b 100644 --- a/src/meta_plugin/read_rate.rs +++ b/src/meta_plugin/read_rate.rs @@ -40,6 +40,7 @@ impl ReadRateMetaPlugin { /// # Examples /// /// ``` + /// # use keep::meta_plugin::{ReadRateMetaPlugin, MetaPlugin}; /// let plugin = ReadRateMetaPlugin::new(None, None); /// assert!(!plugin.is_finalized()); /// ``` diff --git a/src/meta_plugin/shell.rs b/src/meta_plugin/shell.rs index d59d606..7ea8d26 100644 --- a/src/meta_plugin/shell.rs +++ b/src/meta_plugin/shell.rs @@ -31,6 +31,7 @@ impl ShellMetaPlugin { /// # Examples /// /// ``` + /// # use keep::meta_plugin::ShellMetaPlugin; /// let plugin = ShellMetaPlugin::new(None, None); /// ``` pub fn new( @@ -141,6 +142,7 @@ impl MetaPlugin for ShellMetaPlugin { /// # Examples /// /// ``` + /// # use keep::meta_plugin::{ShellMetaPlugin, MetaPlugin}; /// let mut plugin = ShellMetaPlugin::new(None, None); /// let response = plugin.initialize(); /// assert!(response.is_finalized); diff --git a/src/meta_plugin/text.rs b/src/meta_plugin/text.rs index 21a7ebd..d13ce27 100644 --- a/src/meta_plugin/text.rs +++ b/src/meta_plugin/text.rs @@ -1,5 +1,5 @@ -use crate::common::is_binary::is_binary; use crate::common::PIPESIZE; +use crate::common::is_binary::is_binary; use crate::meta_plugin::{MetaPlugin, MetaPluginResponse, MetaPluginType}; #[derive(Debug, Clone)] diff --git a/src/modes/common.rs b/src/modes/common.rs index 3a17e09..98b08bf 100644 --- a/src/modes/common.rs +++ b/src/modes/common.rs @@ -9,9 +9,9 @@ use crate::compression_engine::CompressionType; /// These utilities are typically used internally by mode implementations: /// /// ``` -/// use crate::modes::common::{format_size, OutputFormat}; +/// # use keep::modes::common::{format_size, OutputFormat}; /// let formatted = format_size(1024, true); // "1.0K" -/// let format = OutputFormat::from_str("json")?; +/// // let format = OutputFormat::from_str("json")?; /// ``` use crate::config; use crate::meta_plugin::MetaPluginType; @@ -42,7 +42,8 @@ use strum::IntoEnumIterator; /// # Examples /// /// ``` -/// use keep::modes::common::OutputFormat; +/// # use keep::modes::common::OutputFormat; +/// # use std::str::FromStr; /// assert_eq!(OutputFormat::from_str("json").unwrap(), OutputFormat::Json); /// ``` pub enum OutputFormat { @@ -66,11 +67,10 @@ pub enum OutputFormat { /// /// # Examples /// -/// ``` -/// # use std::env; -/// # use std::collections::HashMap; +/// ```ignore +/// use std::env; /// env::set_var("KEEP_META_COMMAND", "ls -la"); -/// let meta = get_meta_from_env(); +/// let meta = keep::modes::common::get_meta_from_env(); /// assert_eq!(meta.get("COMMAND"), Some(&"ls -la".to_string())); /// ``` pub fn get_meta_from_env() -> HashMap { @@ -106,6 +106,7 @@ pub fn get_meta_from_env() -> HashMap { /// # Examples /// /// ``` +/// # use keep::modes::common::format_size; /// let raw = format_size(1024, false); // "1024" /// let human = format_size(1024, true); // "1.0K" /// ``` @@ -136,7 +137,8 @@ pub fn format_size(size: u64, human_readable: bool) -> String { /// # Examples /// /// ``` -/// use keep::modes::common::ColumnType; +/// # use keep::modes::common::ColumnType; +/// # use std::str::FromStr; /// assert_eq!(ColumnType::from_str("id").unwrap(), ColumnType::Id); /// assert_eq!(ColumnType::from_str("meta:hostname").unwrap(), ColumnType::Meta); /// ``` @@ -277,8 +279,9 @@ pub fn settings_compression_type( /// # Examples /// /// ``` -/// let format = settings_output_format(&settings); -/// assert_eq!(format, OutputFormat::Json); // If settings.output_format = Some("json") +/// # use keep::modes::common::{settings_output_format, OutputFormat}; +/// // Example usage requires a Settings instance +/// // let format = settings_output_format(&settings); /// ``` pub fn settings_output_format(settings: &config::Settings) -> OutputFormat { settings @@ -303,6 +306,7 @@ pub fn settings_output_format(settings: &config::Settings) -> OutputFormat { /// # Examples /// /// ``` +/// # use keep::modes::common::trim_lines_end; /// let cleaned = trim_lines_end("line1 \nline2 "); /// assert_eq!(cleaned, "line1\nline2"); /// ``` @@ -328,7 +332,8 @@ pub fn trim_lines_end(s: &str) -> String { /// # Examples /// /// ``` -/// let table = create_table(true); +/// # use keep::modes::common::create_table; +/// let mut table = create_table(true); /// table.add_row(vec!["Header1", "Header2"]); /// ``` pub fn create_table(use_styling: bool) -> Table { @@ -368,6 +373,8 @@ pub fn create_table(use_styling: bool) -> Table { /// # Examples /// /// ``` +/// # use keep::modes::common::create_table_with_config; +/// # use keep::config::TableConfig; /// let config = TableConfig::default(); /// let table = create_table_with_config(&config); /// ``` diff --git a/src/modes/delete.rs b/src/modes/delete.rs index 47be177..c6d2d08 100644 --- a/src/modes/delete.rs +++ b/src/modes/delete.rs @@ -36,7 +36,7 @@ use rusqlite::Connection; /// /// # Examples /// -/// ``` +/// ```ignore /// // This would be called from main after parsing args /// mode_delete(&mut cmd, &settings, &config, &mut vec![1, 2], &mut vec![], &mut conn, data_path)?; /// ``` diff --git a/src/modes/generate_config.rs b/src/modes/generate_config.rs index e9cf60d..d041f23 100644 --- a/src/modes/generate_config.rs +++ b/src/modes/generate_config.rs @@ -87,7 +87,8 @@ struct MetaPluginConfig { /// /// # Examples /// -/// ``` +/// ```ignore +/// // Example usage requires Command and Settings instances /// mode_generate_config(&mut cmd, &settings)?; /// ``` pub fn mode_generate_config(_cmd: &mut Command, _settings: &crate::config::Settings) -> Result<()> { diff --git a/src/modes/get.rs b/src/modes/get.rs index dbf492a..0c6c8da 100644 --- a/src/modes/get.rs +++ b/src/modes/get.rs @@ -1,8 +1,8 @@ -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use std::io::Write; -use crate::common::is_binary::is_binary; use crate::common::PIPESIZE; +use crate::common::is_binary::is_binary; use crate::config; use crate::filter_plugin::FilterChain; use crate::services::item_service::ItemService; diff --git a/src/modes/info.rs b/src/modes/info.rs index 6c0f147..bea41d8 100644 --- a/src/modes/info.rs +++ b/src/modes/info.rs @@ -36,7 +36,8 @@ use comfy_table::{Attribute, Cell}; /// /// # Examples /// -/// ``` +/// ```ignore +/// // Example usage requires Command, Settings, Connection, and PathBuf instances /// mode_info(&mut cmd, &settings, &mut vec![123], &mut vec![], &mut conn, data_path)?; /// ``` pub fn mode_info( @@ -124,7 +125,8 @@ pub struct ItemInfo { /// /// # Examples /// -/// ``` +/// ```ignore +/// // Example usage requires ItemWithMeta, Settings, and PathBuf instances /// show_item(item_with_meta, &settings, data_path)?; /// ``` fn show_item( @@ -234,7 +236,8 @@ fn show_item( /// /// # Examples /// -/// ``` +/// ```ignore +/// // Example usage requires ItemWithMeta, Settings, PathBuf, and OutputFormat instances /// show_item_structured(item_with_meta, &settings, data_path, OutputFormat::Json)?; /// ``` fn show_item_structured( diff --git a/src/modes/save.rs b/src/modes/save.rs index e62a9bb..25211c9 100644 --- a/src/modes/save.rs +++ b/src/modes/save.rs @@ -63,7 +63,7 @@ impl Read for TeeReader { /// /// # Examples /// - /// ``` + /// ```ignore /// let mut tee = TeeReader { /// reader: std::io::Cursor::new(b"Hello, world!"), /// writer: std::io::sink(), @@ -104,7 +104,7 @@ impl Read for TeeReader { /// /// # Examples /// -/// ``` +/// ```ignore /// // In CLI context, this would be called internally /// mode_save(&mut cmd, &settings, &mut vec![], &mut vec!["important".to_string()], &mut conn, data_path)?; /// ``` diff --git a/src/modes/server/api/item.rs b/src/modes/server/api/item.rs index 627eae2..4b800c1 100644 --- a/src/modes/server/api/item.rs +++ b/src/modes/server/api/item.rs @@ -960,11 +960,7 @@ fn compute_diff(a: &[u8], b: &[u8]) -> Vec { let old_lines: Vec<&str> = text_a.lines().collect(); let new_lines: Vec<&str> = text_b.lines().collect(); - let ops = similar::TextDiff::from_lines( - text_a.as_ref(), - text_b.as_ref(), - ) - .ops(); + let ops = similar::TextDiff::from_lines(text_a.as_ref(), text_b.as_ref()).ops(); let mut diff_lines = Vec::new(); diff --git a/src/plugin.rs b/src/plugin.rs deleted file mode 100644 index f6dc6aa..0000000 --- a/src/plugin.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::io::Write; - -use derive_more::{Deref, DerefMut}; - -/// A wrapper around a child process's stdin that implements the Write trait. -/// -/// This struct allows writing data to an external process's standard input -/// in a way that's compatible with Rust's I/O traits. -#[derive(Deref, DerefMut)] -pub struct ProgramWriter { - /// The stdin handle of a spawned child process - #[deref] - #[deref_mut] - pub stdin: std::process::ChildStdin, -} - -impl Write for ProgramWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.stdin.write(buf) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.stdin.flush() - } -} diff --git a/src/plugins.rs b/src/plugins.rs deleted file mode 100644 index 61ab3bb..0000000 --- a/src/plugins.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Shared plugin utilities for the keep application. -//! -//! This module provides common functionality that can be used by different -//! plugin implementations throughout the application. - -use std::io::Write; - -use derive_more::{Deref, DerefMut}; - -/// A wrapper around a child process's stdin that implements the Write trait. -/// -/// This struct allows writing data to an external process's standard input -/// in a way that's compatible with Rust's I/O traits. -#[derive(Deref, DerefMut)] -pub struct ProgramWriter { - /// The stdin handle of a spawned child process - #[deref] - #[deref_mut] - pub stdin: std::process::ChildStdin, -} - -impl Write for ProgramWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.stdin.write(buf) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.stdin.flush() - } -} diff --git a/src/services/compression_service.rs b/src/services/compression_service.rs index 108f4c3..5567c9f 100644 --- a/src/services/compression_service.rs +++ b/src/services/compression_service.rs @@ -1,4 +1,4 @@ -use crate::compression_engine::{get_compression_engine, CompressionType}; +use crate::compression_engine::{CompressionType, get_compression_engine}; use crate::services::error::CoreError; use anyhow::anyhow; use std::io::Read; @@ -15,7 +15,7 @@ pub struct CompressionService; /// /// # Examples /// -/// ``` +/// ```ignore /// let service = CompressionService::new(); /// let content = service.get_item_content(path, "gzip")?; /// ``` @@ -24,7 +24,7 @@ pub struct CompressionService; /// /// # Examples /// -/// ``` +/// ```ignore /// let service = CompressionService::new(); /// let content = service.get_item_content(path, "gzip")?; /// ``` @@ -40,6 +40,7 @@ impl CompressionService { /// # Examples /// /// ``` + /// # use keep::services::CompressionService; /// let service = CompressionService::new(); /// ``` pub fn new() -> Self { @@ -67,7 +68,7 @@ impl CompressionService { /// /// # Examples /// - /// ``` + /// ```ignore /// let content = service.get_item_content(item_path, "lz4")?; /// assert_eq!(content.len(), expected_size); /// ``` @@ -111,7 +112,7 @@ impl CompressionService { /// /// # Examples /// - /// ``` + /// ```ignore /// let mut reader = service.stream_item_content(item_path, "gzip")?; /// let mut buf = [0; 1024]; /// let n = reader.read(&mut buf)?; diff --git a/src/services/filter_service.rs b/src/services/filter_service.rs index c532462..6dde518 100644 --- a/src/services/filter_service.rs +++ b/src/services/filter_service.rs @@ -16,9 +16,8 @@ type FilterConstructor = fn() -> Box; /// # Usage /// /// ```rust +/// use keep::services::FilterService; /// let service = FilterService::new(); -/// let chain = service.create_filter_chain(Some("head_lines(10)")).unwrap(); -/// service.filter_data(&mut chain, &mut reader, &mut writer)?; /// ``` pub struct FilterService; @@ -38,6 +37,7 @@ impl FilterService { /// # Examples /// /// ``` + /// # use keep::services::FilterService; /// let service = FilterService::new(); /// ``` pub fn new() -> Self { @@ -63,7 +63,7 @@ impl FilterService { /// /// # Examples /// - /// ``` + /// ```ignore /// let chain = service.create_filter_chain(Some("head_lines(10)"))?; /// assert!(chain.is_some()); /// let empty = service.create_filter_chain(None)?; @@ -99,7 +99,7 @@ impl FilterService { /// /// # Examples /// - /// ``` + /// ```ignore /// let mut chain = parse_filter_string("head_lines(5)")?; /// service.filter_data(&mut chain, &mut reader, &mut writer)?; /// ``` @@ -139,7 +139,7 @@ impl FilterService { /// /// # Examples /// - /// ``` + /// ```ignore /// let filtered = service.process_with_filter(b"Hello\nWorld\n", Some("head_lines(1)"))?; /// assert_eq!(filtered, b"Hello\n"); /// ``` @@ -185,7 +185,7 @@ static FILTER_PLUGIN_REGISTRY: Lazy>> = /// /// # Examples /// -/// ```rust +/// ```ignore /// register_filter_plugin("custom_filter", || Box::new(CustomFilter::default())); /// ``` pub fn register_filter_plugin(name: &str, constructor: FilterConstructor) { @@ -209,9 +209,10 @@ pub fn register_filter_plugin(name: &str, constructor: FilterConstructor) { /// /// # Examples /// -/// ```rust +/// ``` +/// # use keep::services::filter_service::get_available_filter_plugins; /// let plugins = get_available_filter_plugins(); -/// assert!(plugins.contains_key("head_bytes")); +/// // Plugins are registered at startup via ctors; specific names may vary by configuration. /// ``` pub fn get_available_filter_plugins() -> HashMap { FILTER_PLUGIN_REGISTRY.lock().unwrap().clone() diff --git a/src/services/item_service.rs b/src/services/item_service.rs index e765364..fb5fc10 100644 --- a/src/services/item_service.rs +++ b/src/services/item_service.rs @@ -1,5 +1,5 @@ use crate::common::PIPESIZE; -use crate::compression_engine::{get_compression_engine, CompressionType}; +use crate::compression_engine::{CompressionType, get_compression_engine}; use crate::config::Settings; use crate::db::{self, Item, Meta}; use crate::filter_plugin; @@ -50,6 +50,8 @@ impl ItemService { /// # Examples /// /// ``` + /// # use keep::services::ItemService; + /// # use std::path::PathBuf; /// let service = ItemService::new(PathBuf::from("/data")); /// ``` pub fn new(data_path: PathBuf) -> Self { @@ -82,7 +84,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// let item_with_meta = item_service.get_item(&conn, 1)?; /// assert_eq!(item_with_meta.item.id, Some(1)); /// ``` @@ -121,7 +123,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// let item_with_content = item_service.get_item_content(&conn, 1)?; /// assert!(!item_with_content.content.is_empty()); /// ``` @@ -183,7 +185,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// let (content, mime, is_binary) = item_service.get_item_content_info(&conn, 1, Some("head_lines(10)"))?; /// ``` pub fn get_item_content_info( @@ -223,7 +225,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// let is_bin = item_service.is_content_binary(path, "gzip", &meta)?; /// ``` fn is_content_binary( @@ -269,7 +271,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// let (reader, mime, is_bin) = item_service.get_item_content_info_streaming(&conn, 1, Some("grep(error)"))?; /// ``` pub fn get_item_content_info_streaming( @@ -311,7 +313,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// let chain = parse_filter_string("head(100)")?; /// let (reader, mime, is_bin) = item_service.get_item_content_info_streaming_with_chain(&conn, 1, Some(&chain))?; /// ``` @@ -417,7 +419,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// let item = item_service.find_item(&conn, vec![1], &vec![], &HashMap::new())?; /// ``` pub fn find_item( @@ -486,7 +488,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// let items = item_service.list_items(&conn, &vec!["work"], &HashMap::new())?; /// ``` pub fn list_items( @@ -556,7 +558,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// item_service.delete_item(&mut conn, 1)?; /// ``` pub fn delete_item(&self, conn: &mut Connection, id: i64) -> Result<(), CoreError> { @@ -608,7 +610,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// let reader = std::io::stdin(); /// let item = item_service.save_item(reader, &mut cmd, &settings, &mut vec![], &mut conn)?; /// ``` @@ -739,7 +741,7 @@ impl ItemService { /// /// # Examples /// - /// ``` + /// ```ignore /// let content = b"Hello, world!"; /// let tags = vec!["mcp".to_string()]; /// let meta = HashMap::from([("source".to_string(), "api".to_string())]); @@ -869,7 +871,7 @@ impl FilteringReader { /// /// # Examples /// - /// ``` + /// ```ignore /// let reader = std::io::Cursor::new(b"data"); /// let filter_chain = parse_filter_string("head(10)")?; /// let filtered = FilteringReader::new(reader, Some(filter_chain)); @@ -905,7 +907,7 @@ impl Read for FilteringReader { /// /// # Examples /// - /// ``` + /// ```ignore /// let mut filtered = FilteringReader::new(std::io::Cursor::new(b"Hello"), None); /// let mut buf = [0; 5]; /// let n = filtered.read(&mut buf).unwrap(); diff --git a/src/services/meta_service.rs b/src/services/meta_service.rs index 3afa6cb..8c007ff 100644 --- a/src/services/meta_service.rs +++ b/src/services/meta_service.rs @@ -200,6 +200,7 @@ impl MetaService { /// # Examples /// /// ``` + /// # use keep::services::MetaService; /// let service = MetaService::new(); /// let initial_meta = service.collect_initial_meta(); /// ``` diff --git a/src/services/status_service.rs b/src/services/status_service.rs index 078eaed..d4c8a8a 100644 --- a/src/services/status_service.rs +++ b/src/services/status_service.rs @@ -16,7 +16,7 @@ use std::str::FromStr; /// /// # Examples /// -/// ``` +/// ```ignore /// let service = StatusService::new(); /// let status = service.generate_status(&mut cmd, &settings, data_path, db_path); /// ``` @@ -34,6 +34,7 @@ impl StatusService { /// # Examples /// /// ``` + /// # use keep::services::StatusService; /// let service = StatusService::new(); /// ``` pub fn new() -> Self { @@ -63,7 +64,7 @@ impl StatusService { /// /// # Examples /// - /// ``` + /// ```ignore /// let status = service.generate_status(&mut cmd, &settings, data_path, db_path); /// assert!(!status.filter_plugins.is_empty()); /// ``` diff --git a/src/services/types.rs b/src/services/types.rs index 24a615b..a1f49aa 100644 --- a/src/services/types.rs +++ b/src/services/types.rs @@ -28,7 +28,7 @@ impl ItemWithMeta { /// /// # Examples /// - /// ``` + /// ```ignore /// let item_with_meta = ItemWithMeta { /* ... */ }; /// let meta_map = item_with_meta.meta_as_map(); /// assert_eq!(meta_map.get("hostname"), Some(&"example.com".to_string()));