use regex::Regex;
use std::path::{Path, PathBuf};
pub fn abspath(p: &str) -> Option<String> {
let regex = Regex::new(":\\d+$").unwrap();
let p_without_suffix = regex.replace(p, "");
let expanded_path = shellexpand::full(&p_without_suffix).ok()?;
let path = Path::new(expanded_path.as_ref());
let abs_path = path.canonicalize().ok()?;
abs_path.to_str().map(|s| s.to_string())
}
pub fn temp_dir() -> PathBuf {
let tempdir = tempfile::tempdir().unwrap();
let options = fs_extra::dir::CopyOptions::new(); fs_extra::copy_items(
&[
"tests/resources/schema_v1_migration_test.db",
"tests/resources/schema_v2_with_embeddings.db",
],
tempdir.path(),
&options,
)
.expect("Failed to copy test project directory");
tempdir.keep()
}
pub fn is_file_path(content: &str) -> bool {
let trimmed = content.trim();
if trimmed.starts_with("http://")
|| trimmed.starts_with("https://")
|| trimmed.starts_with("ftp://")
|| trimmed.starts_with("file://")
{
return false;
}
if trimmed.starts_with('#')
|| trimmed.starts_with('-')
|| trimmed.starts_with('*')
|| trimmed.starts_with('>')
{
return false;
}
if trimmed.starts_with('/')
|| trimmed.starts_with('~')
|| trimmed.starts_with("./")
|| trimmed.starts_with("../")
|| trimmed.starts_with("$HOME")
|| trimmed.starts_with("$")
{
return true;
}
if trimmed.contains('.')
&& !trimmed.contains(' ')
&& !trimmed.contains('\n')
&& Path::new(trimmed).extension().is_some()
{
let extension = Path::new(trimmed)
.extension()
.unwrap()
.to_string_lossy()
.to_lowercase();
if ["md", "markdown", "txt", "text"].contains(&extension.as_str()) {
return true;
}
if trimmed.contains('/') {
return true;
}
}
false
}
pub fn expand_path(path: &str) -> String {
match shellexpand::full(path) {
Ok(expanded) => expanded.to_string(),
Err(_) => path.to_string(),
}
}
pub fn extract_filename(input: &str) -> String {
let parts: Vec<&str> = input.split(':').collect();
let path_str = parts[0];
let path = Path::new(path_str);
path.file_name().map_or(input.to_string(), |filename| {
filename.to_string_lossy().to_string()
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::fs::{self, File};
use std::io::Write;
#[test]
fn given_path_with_suffix_when_abspath_then_removes_suffix_and_canonicalizes() {
let temp_dir = env::temp_dir();
let file_path = temp_dir.join("test_file.txt");
let mut file = File::create(&file_path).expect("Failed to create temporary file");
writeln!(file, "Hello world").expect("Failed to write to temporary file");
let file_str = file_path.to_str().unwrap();
let input = format!("{}:1", file_str);
let abs = abspath(&input);
let canon = fs::canonicalize(&file_path).expect("Failed to canonicalize path");
assert_eq!(abs, Some(canon.to_str().unwrap().to_string()));
fs::remove_file(file_path).expect("Failed to remove temporary file");
}
#[test]
fn given_path_with_suffix_when_extract_filename_then_returns_filename_without_suffix() {
let input = "/home/user/docs/report.pdf:0";
let filename = extract_filename(input);
assert_eq!(filename, "report.pdf");
}
#[test]
fn given_various_strings_when_is_file_path_then_correctly_identifies_paths() {
assert!(is_file_path("/absolute/path/file.md"));
assert!(is_file_path("~/relative/to/home.md"));
assert!(is_file_path("./relative/path.md"));
assert!(is_file_path("../parent/path.md"));
assert!(is_file_path("$HOME/documents/file.md"));
assert!(is_file_path("file.md"));
assert!(!is_file_path("# Markdown heading"));
assert!(!is_file_path("This is a paragraph of text"));
assert!(!is_file_path("https://example.com"));
assert!(!is_file_path(" "));
assert!(!is_file_path(""));
}
#[test]
fn given_path_with_home_var_when_expand_path_then_expands_correctly() {
let home_path = "$HOME/.config/bkmr/bkmr.db";
let expanded = expand_path(home_path);
assert!(!expanded.contains("$HOME"));
if let Ok(home) = env::var("HOME") {
assert!(expanded.starts_with(&home));
assert!(expanded.ends_with("/.config/bkmr/bkmr.db"));
}
}
#[test]
fn given_path_with_tilde_when_expand_path_then_expands_correctly() {
let tilde_path = "~/.config/bkmr/bkmr.db";
let expanded = expand_path(tilde_path);
assert!(!expanded.starts_with("~"));
assert!(expanded.starts_with("/"));
assert!(expanded.ends_with("/.config/bkmr/bkmr.db"));
}
#[test]
fn given_absolute_path_when_expand_path_then_returns_unchanged() {
let absolute_path = "/absolute/path/to/database.db";
let expanded = expand_path(absolute_path);
assert_eq!(expanded, absolute_path);
}
#[test]
fn given_relative_path_when_expand_path_then_returns_unchanged() {
let relative_path = "./relative/database.db";
let expanded = expand_path(relative_path);
assert_eq!(expanded, relative_path);
}
#[test]
fn given_custom_env_var_when_expand_path_then_expands_correctly() {
env::set_var("TEST_DB_PATH", "/tmp/test");
let env_path = "$TEST_DB_PATH/database.db";
let expanded = expand_path(env_path);
assert_eq!(expanded, "/tmp/test/database.db");
assert!(!expanded.contains("$TEST_DB_PATH"));
env::remove_var("TEST_DB_PATH");
}
}