hdb/
lib.rs

1/// MIT License
2///
3/// Copyright (c) 2023 Chris Varga
4///
5/// Permission is hereby granted, free of charge, to any person obtaining a copy
6/// of this software and associated documentation files (the "Software"), to deal
7/// in the Software without restriction, including without limitation the rights
8/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9/// copies of the Software, and to permit persons to whom the Software is
10/// furnished to do so, subject to the following conditions:
11///
12/// The above copyright notice and this permission notice shall be included in all
13/// copies or substantial portions of the Software.
14///
15/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21/// SOFTWARE.
22
23static HOBBIT_STORAGE: &str = "/var/tmp/hdb/";
24
25macro_rules! unwrap_or_return {
26    ( $e:expr ) => {
27        match $e {
28            Ok(x) => x,
29            Err(e) => return Err(e.to_string()),
30        }
31    };
32}
33
34macro_rules! some_or_return {
35    ( $e:expr ) => {
36        match $e {
37            Some(x) => x.to_string(),
38            _ => return Err("None".to_string()),
39        }
40    };
41}
42
43/// Trim everything after the last slash of a string, e.g.,
44///
45///     /hobbit/test/hello -> /hobbit/test/
46fn trim_slash(s: &str) -> String {
47    let trim_len = match s.rfind('/') {
48        None => 0,
49        Some(i) => {
50            // if the "/" is the last item in the string don't increment it
51            // so that we don't create an out of bounds slice.
52            if i == s.len() - 1 {
53                i
54            } else {
55                // Move past the /
56                i + 1
57            }
58        }
59    };
60    let trimmed = &s[..trim_len];
61    trimmed.to_string()
62}
63
64/// Retreive a `key` from `table`
65pub fn get(table: &str, key: &str) -> Result<String, String> {
66    let path = format!("{}{}", HOBBIT_STORAGE, table);
67    if let Ok(data) = std::fs::read_to_string(&path) {
68        let json: serde_json::Value = unwrap_or_return!(serde_json::from_str(&data));
69        return Ok(some_or_return!(json[key].as_str()));
70    }
71    let error = format!("Error reading table '{}'", table);
72    Err(error)
73}
74
75/// Set a `key` in a `table` to a `value`
76pub fn set(table: &str, key: &str, value: &str) -> Result<(), String> {
77    // If the table doesn't yet exist, just create it while we're at it.
78    let _ = make(table);
79    let path = format!("{}{}", HOBBIT_STORAGE, table);
80    if let Ok(data) = std::fs::read_to_string(&path) {
81        let mut json: serde_json::Value = unwrap_or_return!(serde_json::from_str(&data));
82        json[key] = serde_json::Value::String(value.to_string());
83        return match std::fs::write(&path, json.to_string()) {
84            Ok(_) => Ok(()),
85            Err(e) => Err(e.to_string()),
86        };
87    }
88    let error = format!("Error reading table '{}'", table);
89    Err(error)
90}
91
92/// Delete a `key` from a `table`
93pub fn del(table: &str, key: &str) -> Result<(), String> {
94    let path = format!("{}{}", HOBBIT_STORAGE, table);
95    if let Ok(data) = std::fs::read_to_string(&path) {
96        let json: serde_json::Value = unwrap_or_return!(serde_json::from_str(&data));
97        match json {
98            serde_json::Value::Object(mut map) => {
99                map.remove(key);
100                // If the table is now empty, just remove the file too.
101                if map.len() == 0 {
102                    let _ = std::fs::remove_file(&path);
103                    // If the directory is now empty, then clean it up as well.
104                    let _ = std::fs::remove_dir(trim_slash(&path));
105                    return Ok(());
106                } else {
107                    let v: serde_json::Value = map.into();
108                    return match std::fs::write(&path, v.to_string()) {
109                        Ok(_) => Ok(()),
110                        Err(e) => Err(e.to_string()),
111                    };
112                }
113            }
114            _ => return Err("Key not found".to_string()),
115        }
116    }
117    let error = format!("Error reading table '{}'", table);
118    Err(error)
119}
120
121fn make(table: &str) -> Result<(), String> {
122    let _ = std::fs::create_dir_all(HOBBIT_STORAGE);
123    let path = format!("{}{}", HOBBIT_STORAGE, table);
124    // If the table contains a slash, create the subdirectory first.
125    let _ = std::fs::create_dir_all(trim_slash(&path));
126    // If the table already exists, return ok. If not, create empty json.
127    if !std::fs::metadata(path.to_string()).is_ok() {
128        let data = serde_json::json!({});
129        match std::fs::write(path.to_string(), data.to_string()) {
130            Ok(_) => return Ok(()),
131            Err(e) => return Err(e.to_string()),
132        }
133    }
134    Ok(())
135}
136
137/// Load an entire `table` as a `serde_json::Map`
138pub fn map(table: &str) -> Result<serde_json::Map<String, serde_json::Value>, String> {
139    let path = format!("{}{}", HOBBIT_STORAGE, table);
140    if let Ok(data) = std::fs::read_to_string(&path) {
141        let json: serde_json::Value = unwrap_or_return!(serde_json::from_str(&data));
142        match json {
143            serde_json::Value::Object(map) => {
144                return Ok(map);
145            }
146            _ => return Err("Could not parse table as Map".to_string()),
147        }
148    }
149    let error = format!("Error reading table '{}'", table);
150    Err(error)
151}