mdbook_journal/
journal.rs

1use crate::prelude::*;
2
3mod entry;
4mod loader;
5mod persistence;
6mod topic;
7
8pub use entry::*;
9pub use loader::*;
10pub use persistence::*;
11pub use topic::*;
12
13pub struct Journal<LOADER>
14where
15    LOADER: JournalLoaderTrait,
16{
17    /// directory realative to the mdbook `SUMMARY.md`
18    source_root: PathBuf,
19    /// All of the topics tracked by journal
20    topics: TopicMap,
21    /// Responsible for saving and loading entries
22    persistence: LOADER::DataDriver,
23}
24
25impl<LOADER> Journal<LOADER>
26where
27    LOADER: JournalLoaderTrait,
28{
29    pub fn install(config: LOADER::ConfigSource) -> Result<()> {
30        LOADER::install(config)
31    }
32
33    pub fn load(config: LOADER::ConfigSource) -> Result<Self> {
34        let (persistence, topics, source_root) = LOADER::load(config)?;
35
36        Ok(Self {
37            source_root,
38            persistence,
39            topics,
40        })
41    }
42
43    pub fn with_topic<T>(&self, topic: &T) -> Result<&Topic>
44    where
45        T: AsRef<str>,
46    {
47        self.topics
48            .find(topic)
49            .with_context(|| format!("Topic Not Found [{}]", topic.as_ref()))
50    }
51
52    pub fn each_topic(&self) -> impl Iterator<Item = &Topic> {
53        self.topics.iter()
54    }
55
56    pub fn persist_entry(&self, entry: &Entry) -> Result<PathBuf> {
57        let topic = self.with_topic(&entry.topic_name())?;
58        let file_location = self.source_root.join(topic.source_path(entry)?);
59        let data = &self.persistence.serialize(entry)?;
60        self.persistence.persist(&file_location, data)?;
61        Ok(file_location)
62    }
63
64    pub fn fetch_entry(&self, path: &Path) -> Result<Entry> {
65        self.persistence.fetch(path)
66    }
67
68    pub fn entries_for_topic<T>(&self, topic: &T) -> Result<Vec<Entry>>
69    where
70        T: AsRef<str>,
71    {
72        let mut entries = self
73            .persistence
74            .query(&Query::ForTopic(self.with_topic(topic)?))?;
75        self.hydrate_virtual_paths(&mut entries)?;
76        Ok(entries)
77    }
78
79    pub fn all_entries(&self) -> Result<Vec<Entry>> {
80        let mut entries = self.persistence.query(&Query::AllEntries)?;
81        self.hydrate_virtual_paths(&mut entries)?;
82        Ok(entries)
83    }
84
85    // PRIVATE METHODS
86
87    fn hydrate_virtual_paths(&self, entries: &mut [Entry]) -> Result<()> {
88        for entry in entries {
89            let path = self.with_topic(&entry.topic_name())?.virtual_path(entry)?;
90            entry.virtual_path = Some(path);
91        }
92
93        Ok(())
94    }
95}
96
97#[cfg(test)]
98mod test {
99    use crate::prelude::*;
100    use crate::support::prelude::*;
101    use pretty_assertions::assert_eq;
102
103    #[rstest]
104    fn full_generation() -> Result<()> {
105        let journal: Journal<MockJournalLoaderTrait> = Journal {
106            persistence: FilePersistence::new("/tmp/mdbook-journal-test"),
107            source_root: "/tmp/mdbook-journal-test".into(),
108            topics: TopicMap::default().insert(
109                Topic::builder("code-blog")
110                    .add_variable(Variable::new("title").required())
111                    .build(),
112            )?,
113        };
114
115        let topic = journal.with_topic(&"code-blog")?;
116        assert_eq!("code-blog", topic.name());
117
118        let mut adapter = MockEntryGenerationTrait::new();
119
120        adapter
121            .expect_created_at()
122            .returning(|| Ok(Utc.with_ymd_and_hms(2024, 10, 19, 16, 20, 0).unwrap()));
123
124        adapter
125            .expect_collect_value()
126            .withf(|var| var.key() == "title")
127            .returning(|_| Ok(Some(MetaValue::String("Test Entry".to_owned()))));
128
129        let entry = topic.generate_entry(&adapter)?;
130
131        assert_eq!(entry.topic_name(), "code-blog");
132        assert_eq!(entry.created_at().year(), 2024);
133        assert_eq!(entry.created_at().month(), 10);
134        assert_eq!(
135            entry.meta_value(&"title").unwrap(),
136            &MetaValue::String("Test Entry".to_owned())
137        );
138        assert_eq!(entry.content(), "");
139
140        let file_location = journal.persist_entry(&entry)?;
141        let reloaded = journal.fetch_entry(&file_location)?;
142
143        assert_eq!(entry.topic_name(), reloaded.topic_name());
144        assert_eq!(entry.created_at(), reloaded.created_at());
145        assert_eq!(entry.content(), reloaded.content());
146        assert_eq!(entry.meta_value(&"title"), reloaded.meta_value(&"title"));
147        assert_eq!(&file_location, reloaded.file_location().unwrap());
148
149        let entries = journal.entries_for_topic(&"code-blog")?;
150        assert_eq!(entry.meta_value(&"title"), entries[0].meta_value(&"title"));
151
152        let entries = journal.all_entries()?;
153        assert_eq!(entry.meta_value(&"title"), entries[0].meta_value(&"title"));
154        Ok(())
155    }
156}