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
2 changes: 1 addition & 1 deletion .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
${{ runner.os }}-cargo-

- name: Install cargo-nextest
run: cargo install cargo-nextest --locked
run: cargo nextest --version || cargo install cargo-nextest --locked

- name: Run tests
run: cargo nextest run
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ path = "backend/src/main.rs"
name = "integration_test"
path = "backend/tests/integration_test.rs"

[[test]]
name = "application_integration_test"
path = "backend/tests/application_integration_test.rs"

[dependencies]
3 changes: 3 additions & 0 deletions backend/src/application/dto/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod search;

pub use search::*;
142 changes: 142 additions & 0 deletions backend/src/application/dto/search.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use crate::domain::value_objects::{BlockId, PageId, PageReference, Url};

/// Type of search to perform
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SearchType {
/// Keyword-based traditional search
Traditional,
/// Vector/embedding-based semantic search
Semantic,
}

/// Type of results to return
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResultType {
/// Return only pages
PagesOnly,
/// Return only blocks
BlocksOnly,
/// Return only URLs
UrlsOnly,
/// Return all types of results
All,
}

/// Search request parameters
#[derive(Debug, Clone)]
pub struct SearchRequest {
/// The search query text
pub query: String,
/// Type of search (traditional or semantic)
pub search_type: SearchType,
/// Type of results to return
pub result_type: ResultType,
/// Optional filter to limit results to specific pages
pub page_filters: Option<Vec<PageId>>,
}

impl SearchRequest {
pub fn new(query: impl Into<String>) -> Self {
Self {
query: query.into(),
search_type: SearchType::Traditional,
result_type: ResultType::All,
page_filters: None,
}
}

pub fn with_search_type(mut self, search_type: SearchType) -> Self {
self.search_type = search_type;
self
}

pub fn with_result_type(mut self, result_type: ResultType) -> Self {
self.result_type = result_type;
self
}

pub fn with_page_filters(mut self, page_filters: Vec<PageId>) -> Self {
self.page_filters = Some(page_filters);
self
}
}

/// A search result with matched item and context
#[derive(Debug, Clone, PartialEq)]
pub struct SearchResult {
/// The matched item (page, block, or URL)
pub item: SearchItem,
/// Relevance score (higher is more relevant)
pub score: f64,
}

/// The type of item that was matched in a search
#[derive(Debug, Clone, PartialEq)]
pub enum SearchItem {
Page(PageResult),
Block(BlockResult),
Url(UrlResult),
}

/// A page search result
#[derive(Debug, Clone, PartialEq)]
pub struct PageResult {
pub page_id: PageId,
pub title: String,
/// Number of blocks in the page
pub block_count: usize,
/// URLs found in the page
pub urls: Vec<Url>,
/// Page references found in the page
pub page_references: Vec<PageReference>,
}

/// A block search result with hierarchical context
#[derive(Debug, Clone, PartialEq)]
pub struct BlockResult {
pub block_id: BlockId,
pub content: String,
pub page_id: PageId,
pub page_title: String,
/// Hierarchical path from root to this block (block contents)
pub hierarchy_path: Vec<String>,
/// Page references in ancestor and descendant blocks
pub related_pages: Vec<PageReference>,
/// URLs in ancestor and descendant blocks
pub related_urls: Vec<Url>,
}

/// A URL search result with hierarchical context
#[derive(Debug, Clone, PartialEq)]
pub struct UrlResult {
pub url: Url,
pub containing_block_id: BlockId,
pub containing_block_content: String,
pub page_id: PageId,
pub page_title: String,
/// Page references in ancestor blocks
pub ancestor_page_refs: Vec<PageReference>,
/// Page references in descendant blocks
pub descendant_page_refs: Vec<PageReference>,
}

/// Result for URL-to-pages connection query
#[derive(Debug, Clone, PartialEq)]
pub struct PageConnection {
pub page_id: PageId,
pub page_title: String,
/// Blocks that contain the URL
pub blocks_with_url: Vec<BlockId>,
}

/// Result for page-to-links query
#[derive(Debug, Clone, PartialEq)]
pub struct UrlWithContext {
pub url: Url,
pub block_id: BlockId,
pub block_content: String,
/// Hierarchical path from root to the block containing the URL
pub hierarchy_path: Vec<String>,
/// Page references related to this URL (from ancestors and descendants)
pub related_page_refs: Vec<PageReference>,
}
12 changes: 12 additions & 0 deletions backend/src/application/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pub mod dto;
pub mod repositories;
pub mod use_cases;

// Re-export key types to avoid naming conflicts
pub use dto::{
PageConnection, SearchItem, SearchRequest, SearchResult, SearchType, UrlWithContext,
};
pub use repositories::PageRepository;
pub use use_cases::{
BatchIndexPages, GetLinksForPage, GetPagesForUrl, IndexPage, SearchPagesAndBlocks,
};
3 changes: 3 additions & 0 deletions backend/src/application/repositories/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod page_repository;

pub use page_repository::PageRepository;
35 changes: 35 additions & 0 deletions backend/src/application/repositories/page_repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use crate::domain::{aggregates::Page, value_objects::PageId, DomainResult};

/// Repository trait for managing Page aggregates.
///
/// This trait defines the contract for persisting and retrieving Page aggregates
/// from a data store. Implementations can be backed by different storage mechanisms
/// (in-memory, database, etc.).
pub trait PageRepository {
/// Saves a page to the repository.
///
/// If a page with the same ID already exists, it should be updated.
/// Otherwise, a new page should be created.
fn save(&mut self, page: Page) -> DomainResult<()>;

/// Finds a page by its unique identifier.
///
/// Returns `Ok(Some(page))` if found, `Ok(None)` if not found,
/// or an error if the operation fails.
fn find_by_id(&self, id: &PageId) -> DomainResult<Option<Page>>;

/// Finds a page by its title.
///
/// Returns `Ok(Some(page))` if found, `Ok(None)` if not found,
/// or an error if the operation fails.
fn find_by_title(&self, title: &str) -> DomainResult<Option<Page>>;

/// Returns all pages in the repository.
fn find_all(&self) -> DomainResult<Vec<Page>>;

/// Deletes a page by its unique identifier.
///
/// Returns `Ok(true)` if the page was deleted, `Ok(false)` if the page
/// was not found, or an error if the operation fails.
fn delete(&mut self, id: &PageId) -> DomainResult<bool>;
}
167 changes: 167 additions & 0 deletions backend/src/application/use_cases/indexing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use crate::application::repositories::PageRepository;
use crate::domain::{aggregates::Page, DomainResult};

/// Use case for indexing a page
///
/// This use case handles the process of saving a page to the repository,
/// making it available for search and retrieval.
pub struct IndexPage<'a, R: PageRepository> {
repository: &'a mut R,
}

impl<'a, R: PageRepository> IndexPage<'a, R> {
pub fn new(repository: &'a mut R) -> Self {
Self { repository }
}

/// Index a page, making it available for search
///
/// This will save the page to the repository. If a page with the same ID
/// already exists, it will be updated.
pub fn execute(&mut self, page: Page) -> DomainResult<()> {
self.repository.save(page)?;
Ok(())
}
}

/// Use case for batch indexing multiple pages
///
/// This use case handles the process of indexing multiple pages at once,
/// which is useful for initial imports or bulk updates.
pub struct BatchIndexPages<'a, R: PageRepository> {
repository: &'a mut R,
}

impl<'a, R: PageRepository> BatchIndexPages<'a, R> {
pub fn new(repository: &'a mut R) -> Self {
Self { repository }
}

/// Index multiple pages in a batch
///
/// Returns the number of pages successfully indexed.
pub fn execute(&mut self, pages: Vec<Page>) -> DomainResult<usize> {
let mut count = 0;
for page in pages {
self.repository.save(page)?;
count += 1;
}
Ok(count)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{
base::Entity,
entities::Block,
value_objects::{BlockContent, BlockId, PageId},
};
use std::collections::HashMap;

struct InMemoryPageRepository {
pages: HashMap<PageId, Page>,
}

impl InMemoryPageRepository {
fn new() -> Self {
Self {
pages: HashMap::new(),
}
}
}

impl PageRepository for InMemoryPageRepository {
fn save(&mut self, page: Page) -> DomainResult<()> {
self.pages.insert(page.id().clone(), page);
Ok(())
}

fn find_by_id(&self, id: &PageId) -> DomainResult<Option<Page>> {
Ok(self.pages.get(id).cloned())
}

fn find_by_title(&self, title: &str) -> DomainResult<Option<Page>> {
Ok(self.pages.values().find(|p| p.title() == title).cloned())
}

fn find_all(&self) -> DomainResult<Vec<Page>> {
Ok(self.pages.values().cloned().collect())
}

fn delete(&mut self, id: &PageId) -> DomainResult<bool> {
Ok(self.pages.remove(id).is_some())
}
}

#[test]
fn test_index_page() {
let mut repo = InMemoryPageRepository::new();

let page_id = PageId::new("page-1").unwrap();
let page = Page::new(page_id.clone(), "Test Page".to_string());

let mut use_case = IndexPage::new(&mut repo);
use_case.execute(page).unwrap();

// Verify the page was indexed
let retrieved = repo.find_by_id(&page_id).unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().title(), "Test Page");
}

#[test]
fn test_index_page_update_existing() {
let mut repo = InMemoryPageRepository::new();

let page_id = PageId::new("page-1").unwrap();
let page1 = Page::new(page_id.clone(), "Original Title".to_string());

let mut use_case = IndexPage::new(&mut repo);
use_case.execute(page1).unwrap();

// Update with same ID but different content
let mut page2 = Page::new(page_id.clone(), "Updated Title".to_string());
let block = Block::new_root(
BlockId::new("block-1").unwrap(),
BlockContent::new("New content"),
);
page2.add_block(block).unwrap();

let mut use_case2 = IndexPage::new(&mut repo);
use_case2.execute(page2).unwrap();

// Verify the page was updated
let retrieved = repo.find_by_id(&page_id).unwrap().unwrap();
assert_eq!(retrieved.title(), "Updated Title");
assert_eq!(retrieved.all_blocks().count(), 1);
}

#[test]
fn test_batch_index_pages() {
let mut repo = InMemoryPageRepository::new();

let pages = vec![
Page::new(PageId::new("page-1").unwrap(), "Page 1".to_string()),
Page::new(PageId::new("page-2").unwrap(), "Page 2".to_string()),
Page::new(PageId::new("page-3").unwrap(), "Page 3".to_string()),
];

let mut use_case = BatchIndexPages::new(&mut repo);
let count = use_case.execute(pages).unwrap();

assert_eq!(count, 3);
assert_eq!(repo.find_all().unwrap().len(), 3);
}

#[test]
fn test_batch_index_empty() {
let mut repo = InMemoryPageRepository::new();

let mut use_case = BatchIndexPages::new(&mut repo);
let count = use_case.execute(vec![]).unwrap();

assert_eq!(count, 0);
}
}
Loading