gadget_sdk/store/
local_database.rs

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
129
130
131
132
133
134
135
136
137
use serde::{de::DeserializeOwned, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;

/// A local database for storing key-value pairs.
///
/// The database is stored in a JSON file, which is updated every time a key-value pair is added or updated.
///
/// # Example
///
/// ```no_run
/// use gadget_sdk::store::LocalDatabase;
///
/// let db = LocalDatabase::<u64>::open("data.json");
///
/// db.set("key", 42);
/// assert_eq!(db.get("key"), Some(42));
/// ```
#[derive(Debug)]
pub struct LocalDatabase<T> {
    path: PathBuf,
    data: Mutex<HashMap<String, T>>,
}

impl<T> LocalDatabase<T>
where
    T: Serialize + DeserializeOwned + Clone + Default,
{
    /// Reads a `LocalDatabase` from the given path.
    ///
    /// If the file does not exist, an empty database is created.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use gadget_sdk::store::LocalDatabase;
    ///
    /// let db = LocalDatabase::<u64>::open("data.json");
    /// assert!(db.is_empty());
    /// ```
    #[must_use]
    pub fn open<P: AsRef<Path>>(path: P) -> Self {
        let path = path.as_ref();
        let parent_dir = path.parent().expect("Failed to get parent directory");

        // Create the parent directory if it doesn't exist
        fs::create_dir_all(parent_dir).expect("Failed to create parent directory");

        let data = if path.exists() {
            let content = fs::read_to_string(path).expect("Failed to read the file");
            serde_json::from_str(&content).unwrap_or_default()
        } else {
            HashMap::new()
        };

        Self {
            path: path.to_owned(),
            data: Mutex::new(data),
        }
    }

    /// Returns the number of key-value pairs in the database.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use gadget_sdk::store::LocalDatabase;
    ///
    /// let db = LocalDatabase::<u64>::open("data.json");
    /// assert_eq!(db.len(), 0);
    ///
    /// db.set("key", 42);
    /// assert_eq!(db.len(), 1);
    /// ```
    pub fn len(&self) -> usize {
        let data = self.data.lock().unwrap();
        data.len()
    }

    /// Checks if the database is empty.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use gadget_sdk::store::LocalDatabase;
    ///
    /// let db = LocalDatabase::<u64>::open("data.json");
    /// assert!(db.is_empty());
    ///
    /// db.set("key", 42);
    /// assert!(!db.is_empty());
    /// ```
    pub fn is_empty(&self) -> bool {
        let data = self.data.lock().unwrap();
        data.is_empty()
    }

    /// Adds or updates a key-value pair in the database.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use gadget_sdk::store::LocalDatabase;
    ///
    /// let db = LocalDatabase::<u64>::open("data.json");
    ///
    /// db.set("key", 42);
    /// assert_eq!(db.get("key"), Some(42));
    /// ```
    pub fn set(&self, key: &str, value: T) {
        let mut data = self.data.lock().unwrap();
        let _old = data.insert(key.to_string(), value);

        // Save the data while the lock is held
        let json_string = serde_json::to_string(&*data).expect("Failed to serialize data to JSON");
        fs::write(&self.path, json_string).expect("Failed to write to the file");
    }

    /// Retrieves a value associated with the given key.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use gadget_sdk::store::LocalDatabase;
    ///
    /// let db = LocalDatabase::<u64>::open("data.json");
    ///
    /// db.set("key", 42);
    /// assert_eq!(db.get("key"), Some(42));
    /// ```
    pub fn get(&self, key: &str) -> Option<T> {
        let data = self.data.lock().unwrap();
        data.get(key).cloned()
    }
}