diff --git a/benches/change.rs b/benches/change.rs index e1bd945..731f55c 100644 --- a/benches/change.rs +++ b/benches/change.rs @@ -3,23 +3,28 @@ use std::borrow::Cow; use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main}; -use hyperflash::{change_resolver::resolve_changes, change_router::{Transforms, determine_changes}, note::{FieldContent, Identified, Note, NoteField, NoteModel}, note_id_generator::GitNoteIdGenerator}; +use hyperflash::{ + deck::model::Model, + deck::note::ids::GitNoteIdGenerator, + deck::note::{Field, Identified, Note}, + history::{resolve_changes, Transforms, determine_changes}, +}; use uuid::Uuid; fn leak(s: String) -> &'static str { Box::leak(s.into_boxed_str()) } fn make_note(i: usize) -> Note<'static> { - let mut model = NoteModel::default(); + let mut model = Model::default(); model.id = Uuid::nil(); Note { fields: vec![ - NoteField { + Field { name: "Front".into(), - content: FieldContent::parse(leak(format!("Question {i}"))), + content: leak(format!("Question {i}")), }, - NoteField { + Field { name: "Back".into(), - content: FieldContent::parse(leak(format!("Answer {i}"))), + content: leak(format!("Answer {i}")), }, ], model: Cow::Owned(model), diff --git a/benches/parse.rs b/benches/parse.rs index 5ff0b5a..19d6ea2 100644 --- a/benches/parse.rs +++ b/benches/parse.rs @@ -3,14 +3,14 @@ use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main}; use evalexpr::build_operator_tree; use hyperflash::{ - note::{Field, NoteModel}, + deck::model::{Field, Model}, parse::{construct, lex, parse_tree}, }; use semver::Version; use uuid::Uuid; -fn bench_model() -> NoteModel { - NoteModel { +fn bench_model() -> Model { + Model { name: "Basic".to_string(), id: Uuid::nil(), templates: vec![], diff --git a/src/deck/cards.rs b/src/deck/cards.rs index a69a405..0c6c721 100644 --- a/src/deck/cards.rs +++ b/src/deck/cards.rs @@ -6,7 +6,18 @@ use gix::{Commit, Repository, object::tree::Entry}; use tracing::debug; use uuid::Uuid; -use crate::{change_resolver::resolve_changes, change_router::determine_changes, deck::Deck, error::DeckError, note::{Identified, Note, NoteModel}, note_id_generator::{GitNoteIdGenerator, NoteIdGenerator}, parse::{Diagnostic, FlashSyntaxNode, ImportExpander, Severity, construct, parse_tree, syntax::{FlashTreeBuilder, SyntaxKind, child, children, node_text}}}; +use crate::{ + deck::{model::Model, note::{Identified, Note}, Deck}, + error::DeckError, + history::{resolve_changes, determine_changes}, + parse::{ + construct, parse_tree, + syntax::{FlashTreeBuilder, SyntaxKind, child, children, node_text}, + Diagnostic, FlashSyntaxNode, ImportExpander, Severity, + }, +}; + +use super::note::ids::{GitNoteIdGenerator, NoteIdGenerator}; impl Deck { /// The deck's cards, queried from the backing [`tree`](Deck::tree). @@ -87,7 +98,7 @@ pub(crate) fn build_tree(content: &str, source_name: &str) -> Result( tree: &'t FlashSyntaxNode, - models: &'t [NoteModel], + models: &'t [Model], source_name: &str, content: &str, ) -> Result>, DeckError> { @@ -144,7 +155,7 @@ pub(crate) fn content(backing_vcs: &Repository, entry: &Entry) -> Result NoteModel { - NoteModel { + fn basic_model() -> Model { + Model { name: "Basic".to_string(), id: Uuid::new_v4(), templates: vec![], @@ -218,8 +233,8 @@ mod tests { defaults: None, css: String::new(), fields: vec![ - Field { name: "Front".into(), sticky: None, associated_media: None }, - Field { name: "Back".into(), sticky: None, associated_media: None }, + SchemaField { name: "Front".into(), sticky: None, associated_media: None }, + SchemaField { name: "Back".into(), sticky: None, associated_media: None }, ], latex_pre: None, latex_post: None, @@ -229,18 +244,18 @@ mod tests { } } - fn make_note<'m>(model: &'m NoteModel, front: &'static str, back: &'static str) -> Note<'m> { + fn make_note<'m>(model: &'m Model, front: &'static str, back: &'static str) -> Note<'m> { Note { model: Cow::Borrowed(model), tags: vec![], fields: vec![ - NoteField { name: "Front".into(), content: FieldContent::parse(front) }, - NoteField { name: "Back".into(), content: FieldContent::parse(back) }, + Field { name: "Front".into(), content: front }, + Field { name: "Back".into(), content: back }, ], } } - fn empty_deck(models: Vec) -> Deck { + fn empty_deck(models: Vec) -> Deck { let repo = gix::open(env!("CARGO_MANIFEST_DIR")).expect("project must be a git repo"); let (tree, _) = parse_tree(""); Deck { models, backing_vcs: repo, tree, note_ids: vec![], configuration: DeckConfig::default() } diff --git a/src/deck/definition.rs b/src/deck/definition.rs deleted file mode 100644 index 88a0783..0000000 --- a/src/deck/definition.rs +++ /dev/null @@ -1,21 +0,0 @@ -use gix::Repository; -use uuid::Uuid; - -use crate::{config::DeckConfig, note::NoteModel, parse::FlashSyntaxNode}; - -/// A deck, backed by its concrete syntax [`tree`](Deck::tree). -/// -/// The tree is the single source of truth for the deck's text: it holds every -/// byte of `index.flash` losslessly. Cards are not stored — they are *queried* -/// from the tree on demand via [`Deck::cards`], each borrowing its field -/// text straight out of the tree. -#[derive(Clone)] -pub struct Deck { - pub models: Vec, - pub backing_vcs: Repository, - /// The lossless concrete syntax tree of the deck's `index.flash`. - pub tree: FlashSyntaxNode, - /// Stable note ids, aligned with the order of [`Deck::cards`]. - pub note_ids: Vec, - pub configuration: DeckConfig, -} diff --git a/src/deck/init.rs b/src/deck/init.rs deleted file mode 100644 index 5d2f9f4..0000000 --- a/src/deck/init.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::{fs, path::Path}; - -use gix::Repository; -use tracing::{debug, info, instrument, warn}; -use uuid::Uuid; - -use crate::{config::DeckConfig, deck::{Deck, blob_entry::BEntry, cards, history}, deck_locator::scan_deck_contents, error::DeckError, model_catalog::{FilesystemModelCatalog, ModelCatalog}, note::NoteModel, note_id_generator::{GitNoteIdGenerator, NoteIdGenerator}}; - -fn load_or_create_config(deck_path: &Path, deck_identifer: &Uuid) -> Result { - let config_path = deck_path.join("config.toml"); - - let mut configuration: DeckConfig = if config_path.exists() { - let config_content = fs::read_to_string(&config_path) - .map_err(|_| DeckError::DeckConfigNotFound(config_path.clone()))?; - toml::from_str(&config_content)? - } else { - DeckConfig { - flash_uuid: deck_identifer.to_string(), - name: deck_path.file_prefix().expect("should always work bro").to_str().unwrap().to_string(), - ..Default::default() - } - }; - - if configuration.flash_uuid.is_empty() { - configuration.flash_uuid = deck_identifer.to_string() - } - - Ok(configuration) -} - -impl Deck { - #[instrument(skip(deck_path))] - pub fn from>(deck_path: P) -> Result { - let model_catalog = FilesystemModelCatalog; - let note_id_generator = GitNoteIdGenerator; - Self::from_with(deck_path, &model_catalog, ¬e_id_generator) - } - - #[instrument(skip(deck_path, model_catalog, note_id_generator))] - pub fn from_with>( - deck_path: P, - model_catalog: &impl ModelCatalog, - note_id_generator: &impl NoteIdGenerator, - ) -> Result { - let deck_path = deck_path.as_ref(); - info!("Initializing deck from: {:?}", deck_path); - - let card_paths = - scan_deck_contents(deck_path).map_err(|e| DeckError::ScanContentsFailed(Box::new(e)))?; - - if card_paths.is_empty() { - warn!("No card files found in deck directory"); - } - - let models = - model_catalog.load_models(deck_path).map_err(|e| DeckError::LoadModelsFailed(Box::new(e)))?; - - info!("Loaded {} models", models.len()); - - let repo_path = deck_path.join(".git"); - debug!("Opening repository at: {:?}", repo_path); - let backing_vcs = gix::open(repo_path).map_err(DeckError::OpenGitFailed)?; - - let vcs = backing_vcs.clone(); - - let deck_identifer = history::derive_core_id(&vcs)?; - - let configuration = load_or_create_config(deck_path, &deck_identifer)?; - - let hist = history::get(&vcs, "index.flash")?; - - let content: Vec = hist - .iter() - .map(|(entry, _)| cards::content(&backing_vcs, entry)) - .collect::, DeckError>>()?; - - let (note_ids, tree) = - cards::process_history(&models, &content, &backing_vcs, &hist, note_id_generator)?; - - info!("Deck initialized successfully"); - Ok(Self { models, backing_vcs, tree, note_ids, configuration }) - } - - #[instrument(skip(backing_vcs))] - pub fn read_file_content(backing_vcs: &Repository, entry: &BEntry) -> Result { - let binary_blob = backing_vcs.find_blob(entry.entry().id())?; - let content = String::from_utf8(binary_blob.data.clone()).map_err(|_| { - DeckError::InvalidUtf8(backing_vcs.workdir().expect("Worktree should be checked out").into()) - })?; - Ok(content) - } - - #[instrument(skip(models, backing_vcs))] - pub fn generate_note_uuids( - models: &[NoteModel], - backing_vcs: &Repository, - target: (gix::object::tree::Entry, gix::Commit), - ) -> Result, DeckError> { - GitNoteIdGenerator.generate_note_ids_for_revision(models, backing_vcs, target) - } -} diff --git a/src/deck_locator.rs b/src/deck/locator.rs similarity index 100% rename from src/deck_locator.rs rename to src/deck/locator.rs diff --git a/src/deck/mod.rs b/src/deck/mod.rs index 5402f2c..3b8e256 100644 --- a/src/deck/mod.rs +++ b/src/deck/mod.rs @@ -1,8 +1,132 @@ pub mod blob_entry; pub mod cards; -pub mod definition; -pub mod history; -pub mod init; -pub mod note_ids; +pub mod locator; +pub mod model; +pub mod note; +pub mod uuid_generator; -pub use self::definition::*; +use std::{fs, path::Path}; + +use gix::Repository; +use tracing::{debug, info, instrument, warn}; +use uuid::Uuid; + +use crate::{ + config::DeckConfig, + deck::{blob_entry::BEntry, model::Model, note::ids::NoteIdGenerator}, + error::DeckError, + history::{self}, + parse::FlashSyntaxNode, +}; + +/// A deck, backed by its concrete syntax [`tree`](Deck::tree). +/// +/// The tree is the single source of truth for the deck's text: it holds every +/// byte of `index.flash` losslessly. Cards are not stored — they are *queried* +/// from the tree on demand via [`Deck::cards`], each borrowing its field +/// text straight out of the tree. +#[derive(Clone)] +pub struct Deck { + pub models: Vec, + pub backing_vcs: Repository, + /// The lossless concrete syntax tree of the deck's `index.flash`. + pub tree: FlashSyntaxNode, + /// Stable note ids, aligned with the order of [`Deck::cards`]. + pub note_ids: Vec, + pub configuration: DeckConfig, +} + +fn load_or_create_config(deck_path: &Path, deck_identifer: &Uuid) -> Result { + let config_path = deck_path.join("config.toml"); + + let mut configuration: DeckConfig = if config_path.exists() { + let config_content = fs::read_to_string(&config_path) + .map_err(|_| DeckError::DeckConfigNotFound(config_path.clone()))?; + toml::from_str(&config_content)? + } else { + DeckConfig { + flash_uuid: deck_identifer.to_string(), + name: deck_path.file_prefix().expect("should always work bro").to_str().unwrap().to_string(), + ..Default::default() + } + }; + + if configuration.flash_uuid.is_empty() { + configuration.flash_uuid = deck_identifer.to_string() + } + + Ok(configuration) +} + +impl Deck { + #[instrument(skip(deck_path))] + pub fn from>(deck_path: P) -> Result { + let model_catalog = model::catalog::FilesystemModelCatalog; + let note_id_generator = note::ids::GitNoteIdGenerator; + Self::from_with(deck_path, &model_catalog, ¬e_id_generator) + } + + #[instrument(skip(deck_path, model_catalog, note_id_generator))] + pub fn from_with>( + deck_path: P, + model_catalog: &impl model::catalog::ModelCatalog, + note_id_generator: &impl NoteIdGenerator, + ) -> Result { + let deck_path = deck_path.as_ref(); + info!("Initializing deck from: {:?}", deck_path); + + let card_paths = + locator::scan_deck_contents(deck_path) + .map_err(|e| DeckError::ScanContentsFailed(Box::new(e)))?; + + if card_paths.is_empty() { + warn!("No card files found in deck directory"); + } + + let models = + model_catalog.load_models(deck_path).map_err(|e| DeckError::LoadModelsFailed(Box::new(e)))?; + + info!("Loaded {} models", models.len()); + + let repo_path = deck_path.join(".git"); + debug!("Opening repository at: {:?}", repo_path); + let backing_vcs = gix::open(repo_path).map_err(DeckError::OpenGitFailed)?; + + let vcs = backing_vcs.clone(); + + let deck_identifer = history::derive_core_id(&vcs)?; + + let configuration = load_or_create_config(deck_path, &deck_identifer)?; + + let hist = history::get(&vcs, "index.flash")?; + + let content: Vec = hist + .iter() + .map(|(entry, _)| cards::content(&backing_vcs, entry)) + .collect::, DeckError>>()?; + + let (note_ids, tree) = + cards::process_history(&models, &content, &backing_vcs, &hist, note_id_generator)?; + + info!("Deck initialized successfully"); + Ok(Self { models, backing_vcs, tree, note_ids, configuration }) + } + + #[instrument(skip(backing_vcs))] + pub fn read_file_content(backing_vcs: &Repository, entry: &BEntry) -> Result { + let binary_blob = backing_vcs.find_blob(entry.entry().id())?; + let content = String::from_utf8(binary_blob.data.clone()).map_err(|_| { + DeckError::InvalidUtf8(backing_vcs.workdir().expect("Worktree should be checked out").into()) + })?; + Ok(content) + } + + #[instrument(skip(models, backing_vcs))] + pub fn generate_note_uuids( + models: &[Model], + backing_vcs: &Repository, + target: (gix::object::tree::Entry, gix::Commit), + ) -> Result, DeckError> { + note::ids::GitNoteIdGenerator.generate_note_ids_for_revision(models, backing_vcs, target) + } +} diff --git a/src/model_catalog.rs b/src/deck/model/catalog.rs similarity index 58% rename from src/model_catalog.rs rename to src/deck/model/catalog.rs index 5016f73..514b7d5 100644 --- a/src/model_catalog.rs +++ b/src/deck/model/catalog.rs @@ -2,10 +2,10 @@ use std::{fs::ReadDir, path::Path}; use dir_spec::data_home; -use crate::{error::DeckError, model_loader, note::NoteModel}; +use crate::{deck::model::Model, error::DeckError}; pub trait ModelCatalog { - fn load_models(&self, deck_path: &Path) -> Result, DeckError>; + fn load_models(&self, deck_path: &Path) -> Result, DeckError>; } #[derive(Clone, Copy, Debug, Default)] @@ -20,7 +20,7 @@ impl FilesystemModelCatalog { } impl ModelCatalog for FilesystemModelCatalog { - fn load_models(&self, deck_path: &Path) -> Result, DeckError> { - model_loader::load_models(self.find_models()?, deck_path) + fn load_models(&self, deck_path: &Path) -> Result, DeckError> { + super::loader::load_models(self.find_models()?, deck_path) } } diff --git a/src/note/model/completion.rs b/src/deck/model/completion.rs similarity index 90% rename from src/note/model/completion.rs rename to src/deck/model/completion.rs index 40b1cf4..3044caf 100644 --- a/src/note/model/completion.rs +++ b/src/deck/model/completion.rs @@ -2,10 +2,10 @@ use std::{fs, path::Path}; use tracing::{debug, info}; -use crate::{error::DeckError, note::{Complete, NoteModel, Partial}}; +use crate::{deck::model::{Complete, Model, Partial}, error::DeckError}; -impl NoteModel { - pub fn complete(self, dir: &Path) -> Result, DeckError> { +impl Model { + pub fn complete(self, dir: &Path) -> Result, DeckError> { let mut css = String::new(); let css_path = dir.join("style.css"); if css_path.exists() { @@ -54,12 +54,12 @@ impl NoteModel { // Find or create template let tmpl = templates .iter_mut() - .find(|t: &&mut crate::note::model::Template| t.name == template_name); + .find(|t: &&mut crate::deck::model::Template| t.name == template_name); let tmpl = if let Some(t) = tmpl { t } else { - templates.push(crate::note::model::Template { + templates.push(crate::deck::model::Template { name: template_name.clone(), order: templates.len() as i32, question_format: String::new(), @@ -90,7 +90,7 @@ impl NoteModel { } } - Ok(NoteModel { + Ok(Model { name, id: self.id, templates, @@ -111,7 +111,7 @@ impl NoteModel { mod tests { use std::fs; - use crate::note::NoteModel; + use crate::deck::model::Model; #[test] fn complete_without_optional_files() { @@ -122,7 +122,7 @@ mod tests { fs::write(model_path.join("style.css"), "body { color: red; }").unwrap(); fs::write(model_path.join("Front+front.hbs"), "{{Front}}").unwrap(); - let partial = NoteModel { name: Some("Test".into()), ..NoteModel::default() }; + let partial = Model { name: Some("Test".into()), ..Model::default() }; let complete = partial.complete(&model_path).unwrap(); assert_eq!(complete.name, "Test"); assert_eq!(complete.css, "body { color: red; }"); @@ -136,7 +136,7 @@ mod tests { fs::write(model_path.join("style.css"), "").unwrap(); fs::write(model_path.join("Q+front.hbs"), "{{Q}}").unwrap(); - let partial = NoteModel::default(); + let partial = Model::default(); let complete = partial.complete(&model_path).unwrap(); assert_eq!(complete.name, "MyModel"); } @@ -149,7 +149,7 @@ mod tests { fs::write(model_path.join("style.css"), ".card { font-size: 14px; }").unwrap(); fs::write(model_path.join("F+front.hbs"), "{{F}}").unwrap(); - let partial = NoteModel::default(); + let partial = Model::default(); let complete = partial.complete(&model_path).unwrap(); assert_eq!(complete.css, ".card { font-size: 14px; }"); } @@ -164,7 +164,7 @@ mod tests { fs::write(model_path.join("pre.tex"), "\\prelude").unwrap(); fs::write(model_path.join("post.tex"), "\\postlude").unwrap(); - let partial = NoteModel::default(); + let partial = Model::default(); let complete = partial.complete(&model_path).unwrap(); assert_eq!(complete.latex_pre, Some("\\prelude".into())); assert_eq!(complete.latex_post, Some("\\postlude".into())); @@ -177,7 +177,7 @@ mod tests { fs::create_dir(&model_path).unwrap(); fs::write(model_path.join("F+front.hbs"), "{{F}}").unwrap(); - let partial = NoteModel::default(); + let partial = Model::default(); let complete = partial.complete(&model_path).unwrap(); assert_eq!(complete.css, ""); } @@ -191,7 +191,7 @@ mod tests { fs::write(model_path.join("Card1+front.hbs"), "{{Front}}").unwrap(); fs::write(model_path.join("Card1+back.hbs"), "{{Back}}").unwrap(); - let partial = NoteModel::default(); + let partial = Model::default(); let complete = partial.complete(&model_path).unwrap(); assert_eq!(complete.templates.len(), 1); assert_eq!(complete.templates[0].name, "Card1"); @@ -210,7 +210,7 @@ mod tests { fs::write(model_path.join("C+back.hbs"), "A").unwrap(); fs::write(model_path.join("C+back.browser.hbs"), "A_browser").unwrap(); - let partial = NoteModel::default(); + let partial = Model::default(); let complete = partial.complete(&model_path).unwrap(); let tmpl = &complete.templates[0]; assert_eq!(tmpl.question_format, "Q"); @@ -227,7 +227,7 @@ mod tests { fs::write(model_path.join("style.css"), "").unwrap(); fs::write(model_path.join("invalid.hbs"), "{{F}}").unwrap(); - let partial = NoteModel::default(); + let partial = Model::default(); let result = partial.complete(&model_path); assert!(result.is_err()); } @@ -243,7 +243,7 @@ mod tests { let partial_id; let partial_schema; let complete = { - let partial = NoteModel { name: Some("CustomName".into()), ..NoteModel::default() }; + let partial = Model { name: Some("CustomName".into()), ..Model::default() }; partial_id = partial.id; partial_schema = partial.schema_version.clone(); partial.complete(&model_path).unwrap() diff --git a/src/deck/model/field.rs b/src/deck/model/field.rs new file mode 100644 index 0000000..23c3893 --- /dev/null +++ b/src/deck/model/field.rs @@ -0,0 +1,10 @@ +use std::path::PathBuf; + +use serde::Deserialize; + +#[derive(Deserialize, Ord, PartialOrd, Eq, Hash, Clone, PartialEq, Debug)] +pub struct Field { + pub name: String, + pub sticky: Option, + pub associated_media: Option>, +} diff --git a/src/model_loader.rs b/src/deck/model/loader.rs similarity index 82% rename from src/model_loader.rs rename to src/deck/model/loader.rs index bd22a56..83c5128 100644 --- a/src/model_loader.rs +++ b/src/deck/model/loader.rs @@ -2,10 +2,10 @@ use std::{fs::{self, ReadDir}, path::Path}; use tracing::{debug, info, instrument}; -use crate::{error::DeckError, note::{NoteModel, Partial}}; +use crate::{deck::model::{Model, Partial}, error::DeckError}; #[instrument] -pub fn load_models(model_paths: ReadDir, deck_path: &Path) -> Result, DeckError> { +pub fn load_models(model_paths: ReadDir, deck_path: &Path) -> Result, DeckError> { info!("Loading models"); let mut all_models = Vec::new(); @@ -20,7 +20,7 @@ pub fn load_models(model_paths: ReadDir, deck_path: &Path) -> Result = toml::from_str(&config_content).unwrap(); + let model: Model = toml::from_str(&config_content).unwrap(); // TODO: This path should be more dynamic let model = model.complete(model_path.as_path())?; diff --git a/src/deck/model/mod.rs b/src/deck/model/mod.rs new file mode 100644 index 0000000..188e681 --- /dev/null +++ b/src/deck/model/mod.rs @@ -0,0 +1,113 @@ +pub mod catalog; +pub mod completion; +pub mod field; +pub mod loader; +pub mod template; + +pub use self::field::Field; +pub use self::template::{Defaults, Template}; + +use std::fmt::Debug; +use std::hash::Hash; + +use evalexpr::Node; +use semver::Version; +use serde::Deserialize; +use uuid::Uuid; + +pub trait ModelStage: Default + Clone + std::fmt::Debug + PartialEq + Eq + PartialOrd + Ord + Hash { + type Name: Clone + Debug + PartialEq + Eq + PartialOrd + Ord + Hash + Default; + type Css: Clone + Debug + PartialEq + Eq + PartialOrd + Ord + Hash + Default; + type Templates: Clone + Debug + PartialEq + Eq + PartialOrd + Ord + Hash + Default; + type Latex: Clone + Debug + PartialEq + Eq + PartialOrd + Ord + Hash + Default; +} + +#[derive(Default, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Partial; + +#[derive(Default, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Complete; + +impl ModelStage for Partial { + type Css = String; + type Latex = Option; + type Name = Option; + type Templates = Vec