use anyhow::Result;
use haystack_core::HaystackProvider;
use terraphim_types::{Document, SearchQuery};
mod client;
mod models;
pub use client::{GrepAppClient, SearchParams};
pub use models::{Hit, SearchResponse};
pub struct GrepAppHaystack {
client: GrepAppClient,
pub default_language: Option<String>,
pub default_repo: Option<String>,
pub default_path: Option<String>,
}
impl GrepAppHaystack {
pub fn new() -> Result<Self> {
Ok(Self {
client: GrepAppClient::new()?,
default_language: None,
default_repo: None,
default_path: None,
})
}
pub fn with_filters(
language: Option<String>,
repo: Option<String>,
path: Option<String>,
) -> Result<Self> {
Ok(Self {
client: GrepAppClient::new()?,
default_language: language,
default_repo: repo,
default_path: path,
})
}
fn hit_to_document(&self, hit: &Hit) -> Document {
let repo = &hit.source.repo.raw;
let path = &hit.source.path.raw;
let branch = &hit.source.branch.raw;
let url = format!("https://github.com/{}/blob/{}/{}", repo, branch, path);
let snippet = hit
.source
.content
.snippet
.replace("<mark>", "")
.replace("</mark>", "");
let file_name = path.rsplit('/').next().unwrap_or(path);
let title = format!("{} - {}", repo, file_name);
let id = format!("{}:{}:{}", repo, branch, path);
Document {
id,
url,
title,
body: snippet.clone(),
description: Some(format!("Code from {} in {}", path, repo)),
tags: Some(vec![repo.to_string(), file_name.to_string()]),
..Default::default()
}
}
}
impl Default for GrepAppHaystack {
fn default() -> Self {
Self::new().expect("Failed to create default GrepAppHaystack")
}
}
impl HaystackProvider for GrepAppHaystack {
type Error = anyhow::Error;
async fn search(&self, query: &SearchQuery) -> Result<Vec<Document>, Self::Error> {
let search_term = query.search_term.to_string();
let params = SearchParams {
query: search_term,
language: self.default_language.clone(),
repo: self.default_repo.clone(),
path: self.default_path.clone(),
};
tracing::info!(
"Searching grep.app with query: '{}', language: {:?}, repo: {:?}, path: {:?}",
params.query,
params.language,
params.repo,
params.path
);
let hits = self.client.search(¶ms).await?;
let documents: Vec<Document> = hits.iter().map(|hit| self.hit_to_document(hit)).collect();
tracing::info!("Found {} documents from grep.app", documents.len());
Ok(documents)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_haystack_creation() {
let haystack = GrepAppHaystack::new();
assert!(haystack.is_ok());
let haystack = GrepAppHaystack::with_filters(
Some("Rust".to_string()),
Some("terraphim/terraphim-ai".to_string()),
None,
);
assert!(haystack.is_ok());
assert_eq!(haystack.unwrap().default_language, Some("Rust".to_string()));
}
#[test]
fn test_hit_to_document() {
let haystack = GrepAppHaystack::new().unwrap();
let hit = Hit {
source: models::HitSource {
repo: models::RepoField {
raw: "terraphim/terraphim-ai".to_string(),
},
path: models::PathField {
raw: "src/main.rs".to_string(),
},
branch: models::BranchField {
raw: "main".to_string(),
},
content: models::ContentField {
snippet: "async fn <mark>search</mark>() { }".to_string(),
},
},
};
let doc = haystack.hit_to_document(&hit);
assert_eq!(
doc.url,
"https://github.com/terraphim/terraphim-ai/blob/main/src/main.rs"
);
assert_eq!(doc.title, "terraphim/terraphim-ai - main.rs");
assert_eq!(doc.body, "async fn search() { }");
assert!(doc.tags.is_some());
assert_eq!(doc.tags.unwrap(), vec!["terraphim/terraphim-ai", "main.rs"]);
}
}