From 7ba3b6c410ee72122954635a879bf348d3d3e69b Mon Sep 17 00:00:00 2001 From: Kostya Farber Date: Sat, 30 May 2026 19:27:28 +0100 Subject: [PATCH] feat(document): introduce rust side document crate to unify lower level crates --- Cargo.lock | 10 ++++ Cargo.toml | 1 + crates/shift-document/Cargo.toml | 12 +++++ crates/shift-document/README.md | 44 ++++++++++++++++++ crates/shift-document/src/document.rs | 43 ++++++++++++++++++ crates/shift-document/src/error.rs | 11 +++++ crates/shift-document/src/lib.rs | 7 +++ crates/shift-document/src/new_document.rs | 48 ++++++++++++++++++++ crates/shift-document/tests/document_test.rs | 37 +++++++++++++++ 9 files changed, 213 insertions(+) create mode 100644 crates/shift-document/Cargo.toml create mode 100644 crates/shift-document/README.md create mode 100644 crates/shift-document/src/document.rs create mode 100644 crates/shift-document/src/error.rs create mode 100644 crates/shift-document/src/lib.rs create mode 100644 crates/shift-document/src/new_document.rs create mode 100644 crates/shift-document/tests/document_test.rs diff --git a/Cargo.lock b/Cargo.lock index fefed768..342c034d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1719,6 +1719,16 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "shift-document" +version = "0.1.0" +dependencies = [ + "shift-ir", + "shift-store", + "tempfile", + "thiserror 2.0.18", +] + [[package]] name = "shift-edit" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index d7d13ec4..54c7929b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,5 @@ shift-backends = { path = "crates/shift-backends" } shift-bridge = { path = "crates/shift-bridge" } shift-edit = { path = "crates/shift-edit" } shift-ir = { path = "crates/shift-ir" } +shift-document = { path = "crates/shift-document" } shift-store = { path = "crates/shift-store" } diff --git a/crates/shift-document/Cargo.toml b/crates/shift-document/Cargo.toml new file mode 100644 index 00000000..8da5049e --- /dev/null +++ b/crates/shift-document/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "shift-document" +version = "0.1.0" +edition = "2024" + +[dependencies] +shift-ir = { workspace = true } +shift-store = { workspace = true } +thiserror = "2" + +[dev-dependencies] +tempfile = "3" diff --git a/crates/shift-document/README.md b/crates/shift-document/README.md new file mode 100644 index 00000000..bae74cc4 --- /dev/null +++ b/crates/shift-document/README.md @@ -0,0 +1,44 @@ +# shift-document + +`shift-document` owns the Rust-side workflow for an open Shift document. + +A document coordinates the lower-level crates: + +- `shift-store` for the SQLite working store; +- `shift-ir` for the live in-memory projection; +- `shift-edit` for interactive editing operations. + +Consumers interact with document operations, not raw SQL, raw `Font` mutation, or edit-session internals. + +## Responsibilities + +- create and open durable Shift documents; +- coordinate writes to the SQLite working store; +- maintain the live `shift-ir` projection used by editing, rendering, and export; +- define where interactive edits become durable document state; +- provide Rust-native operations that bridge, CLI, and future app surfaces can wrap. + +## Boundaries + +`shift-document` should not contain raw SQL. SQL belongs in `shift-store`. + +`shift-document` should not contain TypeScript or NAPI types. Those belong in `shift-bridge` and `shift-wire`. + +`shift-document` should not implement low-level vector editing algorithms. Those belong in `shift-edit`. + +`shift-document` should not treat SQLite as a pluggable backend. SQLite is the working store for editable Shift documents. + +## API Language + +Use document-domain language for public APIs: + +- `NewDocument`; +- `ShiftDocument`. + +Avoid transport, storage-abstraction, or version-control language in document APIs: + +- request; +- backend; +- provider; +- store-backed; +- commit session. diff --git a/crates/shift-document/src/document.rs b/crates/shift-document/src/document.rs new file mode 100644 index 00000000..9d0eafe3 --- /dev/null +++ b/crates/shift-document/src/document.rs @@ -0,0 +1,43 @@ +use std::path::Path; + +use crate::{DocumentError, NewDocument}; + +pub struct ShiftDocument { + font: shift_ir::Font, + store: shift_store::ShiftStore, +} + +impl ShiftDocument { + pub fn create_new( + path: impl AsRef, + new_document: NewDocument, + ) -> Result { + let mut store = shift_store::ShiftStore::open(path)?; + store.set_font_info(new_document.font_info())?; + + Ok(Self { + font: shift_ir::Font::new(), + store, + }) + } + + pub fn from_parts(font: shift_ir::Font, store: shift_store::ShiftStore) -> Self { + Self { font, store } + } + + pub fn font(&self) -> &shift_ir::Font { + &self.font + } + + pub fn store(&self) -> &shift_store::ShiftStore { + &self.store + } + + pub fn store_mut(&mut self) -> &mut shift_store::ShiftStore { + &mut self.store + } + + pub fn font_info(&self) -> Result, DocumentError> { + self.store.get_font_info().map_err(DocumentError::from) + } +} diff --git a/crates/shift-document/src/error.rs b/crates/shift-document/src/error.rs new file mode 100644 index 00000000..d6014804 --- /dev/null +++ b/crates/shift-document/src/error.rs @@ -0,0 +1,11 @@ +#[derive(Debug, thiserror::Error)] +pub enum DocumentError { + #[error("store error")] + Store(#[from] shift_store::StoreError), + + #[error("document projection failed: {0}")] + Projection(String), + + #[error("document edit failed: {0}")] + Edit(String), +} diff --git a/crates/shift-document/src/lib.rs b/crates/shift-document/src/lib.rs new file mode 100644 index 00000000..c4459a8b --- /dev/null +++ b/crates/shift-document/src/lib.rs @@ -0,0 +1,7 @@ +mod document; +mod error; +mod new_document; + +pub use document::ShiftDocument; +pub use error::DocumentError; +pub use new_document::NewDocument; diff --git a/crates/shift-document/src/new_document.rs b/crates/shift-document/src/new_document.rs new file mode 100644 index 00000000..66bb6664 --- /dev/null +++ b/crates/shift-document/src/new_document.rs @@ -0,0 +1,48 @@ +const DEFAULT_FAMILY_NAME: &str = "Untitled Font"; + +pub struct NewDocument { + pub family_name: String, + pub units_per_em: i64, +} + +impl Default for NewDocument { + fn default() -> Self { + Self { + family_name: DEFAULT_FAMILY_NAME.to_string(), + units_per_em: shift_ir::FontMetrics::default().units_per_em as i64, + } + } +} + +impl NewDocument { + pub fn new() -> Self { + Self::default() + } + + pub fn with_family_name(family_name: impl Into) -> Self { + Self { + family_name: family_name.into(), + ..Self::default() + } + } + + pub(crate) fn font_info(&self) -> shift_store::FontInfo { + shift_store::FontInfo { + family_name: Some(self.family_name.clone()), + copyright: None, + trademark: None, + description: None, + sample_text: None, + designer: None, + designer_url: None, + manufacturer: None, + manufacturer_url: None, + license_description: None, + license_info_url: None, + vendor_id: None, + version_major: Some(1), + version_minor: Some(0), + units_per_em: self.units_per_em, + } + } +} diff --git a/crates/shift-document/tests/document_test.rs b/crates/shift-document/tests/document_test.rs new file mode 100644 index 00000000..007a331e --- /dev/null +++ b/crates/shift-document/tests/document_test.rs @@ -0,0 +1,37 @@ +use shift_document::{NewDocument, ShiftDocument}; + +#[test] +fn creates_new_document_with_default_font_info() { + let dir = tempfile::tempdir().expect("tempdir should be created"); + let path = dir.path().join("font.shift.sqlite"); + + let document = + ShiftDocument::create_new(&path, NewDocument::new()).expect("document should be created"); + + let loaded = document + .font_info() + .expect("font info query should succeed") + .expect("font info should exist"); + + assert_eq!(loaded.family_name.as_deref(), Some("Untitled Font")); + assert_eq!(loaded.version_major, Some(1)); + assert_eq!(loaded.version_minor, Some(0)); + assert_eq!(loaded.units_per_em, 1000); +} + +#[test] +fn creates_new_document_with_custom_family_name() { + let dir = tempfile::tempdir().expect("tempdir should be created"); + let path = dir.path().join("font.shift.sqlite"); + + let document = ShiftDocument::create_new(&path, NewDocument::with_family_name("Shift Sans")) + .expect("document should be created"); + + let loaded = document + .font_info() + .expect("font info query should succeed") + .expect("font info should exist"); + + assert_eq!(loaded.family_name.as_deref(), Some("Shift Sans")); + assert_eq!(loaded.units_per_em, 1000); +}