1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Defines the [`Data`] struct. A type that compiles and stores [`Entry`]s.

use std::collections::HashMap;
use std::path::Path;

use crate::applebooks::database::{ABDatabase, ABDatabaseName};
use crate::result::Result;

use super::annotation::Annotation;
use super::book::Book;
use super::entry::Entry;

/// Defines the `Entries` type alias.
///
/// A `Entries` is a `HashMap` composed of `key:value` pairs of `ID:Entry`.
///
/// For example:
///
/// ```plaintext
/// Entries
///  │
///  ├─ ID: Entry
///  ├─ ID: Entry
///  └─ ...
/// ```
///
/// The `ID` for each `Entry` is taken from its `Book`'s `BookMetadata::id`.
/// field. See [`From<Book> for Entry`](struct@Entry#impl-From<Book>) for more
/// information.
type Entries = HashMap<String, Entry>;

/// Defines a thin wrapper around the [`Entries`] type alias.
///
/// Allows default `HashMap` interaction via `Deref` and `DerefMut`, but also
/// provides a few specific convenience methods.
///
/// The basic structure is as follows:
///
/// ```plaintext
/// Data
///  │
///  └─ Entries
///      │
///      ├─ ID: Entry
///      │       ├─ Book
///      │       └─ Annotation
///      ├─ ID: Entry
///      │       ├─ Book
///      │       └─ Annotation
///      └─ ...
/// ```
#[derive(Debug, Default)]
pub struct Data(Entries);

impl Data {
    /// Builds [`Entry`]s from the Apple Books databases.
    ///
    /// # Errors
    ///
    /// See [`ABDatabase::query()`] for information on errors as these are the
    /// only sources of possible errors.
    pub fn init(&mut self, path: &Path) -> Result<()> {
        let books = ABDatabase::query::<Book>(path)?;
        let annotations = ABDatabase::query::<Annotation>(path)?;

        log::debug!(
            "found {} book(s) in {}.",
            books.len(),
            ABDatabaseName::Books.to_string()
        );

        log::debug!(
            "found {} annotation(s) in {}.",
            annotations.len(),
            ABDatabaseName::Annotations.to_string()
        );

        // `Entry`s are created from `Book`s. Note that `book.metadata.id` is
        // set as the key for each entry into the `Data`. This is later used to
        // compare with each `Annotation` to determine if the `Annotation`
        // belongs to a `Book` and therefore its `Entry`.
        //
        // See https://stackoverflow.com/q/69274529/16968574
        let mut data: Entries = books
            .into_iter()
            .map(|book| (book.metadata.id.clone(), Entry::from(book)))
            .collect();

        // `Annotation`s are pushed onto an `Entry` based on their `book_id`.
        for annotation in annotations {
            if let Some(entry) = data.get_mut(&annotation.metadata.book_id) {
                entry.annotations.push(annotation);
            }
        }

        // Remove `Entry`s that have no `Annotation`s.
        data.retain(|_, entry| !entry.annotations.is_empty());

        self.0 = data;

        log::debug!("created {} Book's", self.count_books());
        log::debug!("created {} Annotation's", self.count_annotations());

        Ok(())
    }

    /// Returns an iterator over all [`Entry`]s.
    pub fn entries(&self) -> impl Iterator<Item = &Entry> {
        self.0.values()
    }

    /// Returns a mutable iterator over all [`Entry`]s.
    pub fn entries_mut(&mut self) -> impl Iterator<Item = &mut Entry> {
        self.0.values_mut()
    }

    /// Returns the number of books.
    #[must_use]
    pub fn count_books(&self) -> usize {
        self.0.len()
    }

    /// Returns the number of annotations.
    #[must_use]
    pub fn count_annotations(&self) -> usize {
        self.0.values().map(|entry| entry.annotations.len()).sum()
    }
}