Files
mnote/rust/crates/mnote-web/src/page_aggregate/builder.rs
T
lix-2026 1882db7681 收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
2026-06-01 09:29:12 +08:00

401 lines
11 KiB
Rust

//! PageAggregate 构建器(Builder 模式)。
//!
//! 使用方法:
//! ```rust,ignore
//! let aggregate = PageAggregate::builder()
//! .document_id("doc_1")
//! .workspace_id("ws_demo")
//! .title("我的页面")
//! .build();
//! ```
use crate::page_aggregate::{
PageAggregate, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout, PageOptions,
PagePermissions, PageStats, PageTree,
};
use bridge_runtime::project_legacy_content_to_block_document;
use serde_json::{to_value, Value};
/// PageAggregate 构建器。
///
/// 所有字段都有与当前 `build_page_aggregate_snapshot` 一致的默认值。
/// 使用 `build()` 方法消费 builder 并产出 `PageAggregate`。
#[derive(Debug)]
pub struct PageAggregateBuilder {
document_id: String,
workspace_id: String,
source: PageAggregateSource,
parent_id: Option<String>,
path: Vec<String>,
sidebar_tree_membership: Vec<String>,
body_ref: Option<String>,
title: String,
updated_at: Value,
read_only: bool,
disable_download: bool,
disable_copy: bool,
wide_layout: bool,
small_text: bool,
layout_density: String,
show_heading_numbers: bool,
show_toc: bool,
show_structure: bool,
protect_editing: bool,
show_word_count: bool,
collapse_backlinks: bool,
page_font: String,
hide_child_pages: bool,
show_block_ref_count: bool,
hide_title_header: bool,
embed_default_block_id: Value,
content: Value,
revision: Value,
conflict_detection_key: Value,
page_subtree: Value,
word_count: u64,
character_count: u64,
block_count: u64,
todo_total: u64,
todo_done: u64,
}
impl PageAggregateBuilder {
/// 创建新的构建器,所有字段使用与当前 `build_page_aggregate_snapshot` 一致的默认值。
pub fn new() -> Self {
Self {
document_id: String::new(),
workspace_id: "default".to_string(),
source: PageAggregateSource::KernelProjection,
parent_id: None,
path: Vec::new(),
sidebar_tree_membership: vec!["page-tree".into(), "file-tree".into()],
body_ref: None,
title: "无标题".to_string(),
updated_at: Value::Null,
read_only: false,
disable_download: false,
disable_copy: false,
wide_layout: false,
small_text: false,
layout_density: "normal".to_string(),
show_heading_numbers: false,
show_toc: false,
show_structure: false,
protect_editing: false,
show_word_count: true,
collapse_backlinks: false,
page_font: "default".to_string(),
hide_child_pages: false,
show_block_ref_count: false,
hide_title_header: false,
embed_default_block_id: Value::Null,
content: Value::Null,
revision: Value::Null,
conflict_detection_key: Value::Null,
page_subtree: Value::Null,
word_count: 0,
character_count: 0,
block_count: 0,
todo_total: 0,
todo_done: 0,
}
}
// ── Identity ──
pub fn document_id(mut self, value: impl Into<String>) -> Self {
self.document_id = value.into();
self
}
pub fn workspace_id(mut self, value: impl Into<String>) -> Self {
self.workspace_id = value.into();
self
}
pub fn source(mut self, value: PageAggregateSource) -> Self {
self.source = value;
self
}
pub fn parent_id(mut self, value: Option<String>) -> Self {
self.parent_id = value;
self
}
pub fn path(mut self, value: Vec<String>) -> Self {
self.path = value;
self
}
pub fn sidebar_tree_membership(mut self, value: Vec<String>) -> Self {
self.sidebar_tree_membership = value;
self
}
pub fn body_ref(mut self, value: Option<String>) -> Self {
self.body_ref = value;
self
}
// ── Head ──
pub fn title(mut self, value: impl Into<String>) -> Self {
self.title = value.into();
self
}
pub fn updated_at(mut self, value: Value) -> Self {
self.updated_at = value;
self
}
pub fn read_only(mut self, value: bool) -> Self {
self.read_only = value;
self
}
pub fn disable_download(mut self, value: bool) -> Self {
self.disable_download = value;
self
}
pub fn disable_copy(mut self, value: bool) -> Self {
self.disable_copy = value;
self
}
// ── Layout / PageOptions ──
pub fn wide_layout(mut self, value: bool) -> Self {
self.wide_layout = value;
self
}
pub fn small_text(mut self, value: bool) -> Self {
self.small_text = value;
self
}
pub fn layout_density(mut self, value: impl Into<String>) -> Self {
self.layout_density = value.into();
self
}
pub fn show_heading_numbers(mut self, value: bool) -> Self {
self.show_heading_numbers = value;
self
}
pub fn show_toc(mut self, value: bool) -> Self {
self.show_toc = value;
self
}
pub fn show_structure(mut self, value: bool) -> Self {
self.show_structure = value;
self
}
pub fn protect_editing(mut self, value: bool) -> Self {
self.protect_editing = value;
self
}
pub fn show_word_count(mut self, value: bool) -> Self {
self.show_word_count = value;
self
}
pub fn collapse_backlinks(mut self, value: bool) -> Self {
self.collapse_backlinks = value;
self
}
pub fn page_font(mut self, value: impl Into<String>) -> Self {
self.page_font = value.into();
self
}
pub fn hide_child_pages(mut self, value: bool) -> Self {
self.hide_child_pages = value;
self
}
pub fn show_block_ref_count(mut self, value: bool) -> Self {
self.show_block_ref_count = value;
self
}
pub fn embed_default_block_id(mut self, value: Value) -> Self {
self.embed_default_block_id = value;
self
}
// ── Body ──
pub fn content(mut self, value: Value) -> Self {
self.content = value;
self
}
pub fn revision(mut self, value: Value) -> Self {
self.revision = value;
self
}
pub fn conflict_detection_key(mut self, value: Value) -> Self {
self.conflict_detection_key = value;
self
}
// ── Tree ──
pub fn page_subtree(mut self, value: Value) -> Self {
self.page_subtree = value;
self
}
// ── Stats ──
pub fn word_count(mut self, value: u64) -> Self {
self.word_count = value;
self
}
pub fn character_count(mut self, value: u64) -> Self {
self.character_count = value;
self
}
pub fn block_count(mut self, value: u64) -> Self {
self.block_count = value;
self
}
pub fn todo_total(mut self, value: u64) -> Self {
self.todo_total = value;
self
}
pub fn todo_done(mut self, value: u64) -> Self {
self.todo_done = value;
self
}
// ── Build ──
/// 消费 builder 并产出 `PageAggregate`。
pub fn build(self) -> PageAggregate {
let block_document = project_legacy_content_to_block_document(
&self.document_id,
&self.content,
&self.revision,
)
.ok();
let block_projection_version = if block_document.is_some() { 1 } else { 0 };
let page_options = PageOptions {
wide_layout: self.wide_layout,
small_text: self.small_text,
layout_density: self.layout_density,
show_heading_numbers: self.show_heading_numbers,
show_toc: self.show_toc,
show_structure: self.show_structure,
protect_editing: self.protect_editing,
show_word_count: self.show_word_count,
collapse_backlinks: self.collapse_backlinks,
page_font: self.page_font,
hide_child_pages: self.hide_child_pages,
show_block_ref_count: self.show_block_ref_count,
hide_title_header: self.hide_title_header,
embed_default_block_id: self.embed_default_block_id,
};
let layout_options = to_value(&page_options).expect("PageOptions 序列化不应失败");
let updated_at_text = self.updated_at.as_str().map(ToOwned::to_owned);
let page_id = self.document_id.clone();
PageAggregate {
schema: "mnote.page_aggregate.v1".to_string(),
projection_version: PageAggregate::VERSION,
source: self.source,
page_id,
parent_id: self.parent_id,
title: self.title.clone(),
path: if self.path.is_empty() {
vec![self.document_id.clone()]
} else {
self.path
},
sidebar_tree_membership: self.sidebar_tree_membership,
body_ref: self.body_ref,
layout_options,
updated_at: updated_at_text,
identity: PageIdentity {
document_id: self.document_id,
workspace_id: self.workspace_id,
},
head: PageHead {
title: self.title,
updated_at: self.updated_at,
permissions: PagePermissions {
read_only: self.read_only,
disable_download: self.disable_download,
disable_copy: self.disable_copy,
},
},
layout: PageLayout { page_options },
body: PageBody {
content: self.content,
revision: self.revision,
conflict_detection_key: self.conflict_detection_key,
file_version: Value::Null,
block_document: block_document.unwrap_or(Value::Null),
block_projection_version,
projection_source: "builder.content".into(),
attachment_refs: Value::Array(Vec::new()),
},
tree: PageTree {
page_subtree: self.page_subtree,
},
stats: PageStats {
word_count: self.word_count,
character_count: self.character_count,
block_count: self.block_count,
todo_total: self.todo_total,
todo_done: self.todo_done,
},
}
}
}
impl Default for PageAggregateBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_defaults_to_kernel_projection() {
let aggregate = PageAggregateBuilder::new()
.document_id("doc_1")
.workspace_id("ws_demo")
.build();
assert_eq!(aggregate.source, PageAggregateSource::KernelProjection);
}
#[test]
fn explicit_compat_source_is_still_supported() {
let aggregate = PageAggregateBuilder::new()
.document_id("doc_1")
.workspace_id("ws_demo")
.source(PageAggregateSource::CompatMetaContentJoin)
.build();
assert_eq!(aggregate.source, PageAggregateSource::CompatMetaContentJoin);
}
}