hdb 0.1.3

A small, hobbit-sized database
Documentation
/// MIT License
///
/// Copyright (c) 2023 Chris Varga
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in all
/// copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
/// SOFTWARE.

static HOBBIT_STORAGE: &str = "/var/tmp/hdb/";

macro_rules! unwrap_or_return {
    ( $e:expr ) => {
        match $e {
            Ok(x) => x,
            Err(e) => return Err(e.to_string()),
        }
    };
}

macro_rules! some_or_return {
    ( $e:expr ) => {
        match $e {
            Some(x) => x.to_string(),
            _ => return Err("None".to_string()),
        }
    };
}

/// Trim everything after the last slash of a string, e.g.,
///
///     /hobbit/test/hello -> /hobbit/test/
fn trim_slash(s: &str) -> String {
    let trim_len = match s.rfind('/') {
        None => 0,
        Some(i) => {
            // if the "/" is the last item in the string don't increment it
            // so that we don't create an out of bounds slice.
            if i == s.len() - 1 {
                i
            } else {
                // Move past the /
                i + 1
            }
        }
    };
    let trimmed = &s[..trim_len];
    trimmed.to_string()
}

/// Retreive a `key` from `table`
pub fn get(table: &str, key: &str) -> Result<String, String> {
    let path = format!("{}{}", HOBBIT_STORAGE, table);
    if let Ok(data) = std::fs::read_to_string(&path) {
        let json: serde_json::Value = unwrap_or_return!(serde_json::from_str(&data));
        return Ok(some_or_return!(json[key].as_str()));
    }
    let error = format!("Error reading table '{}'", table);
    Err(error)
}

/// Set a `key` in a `table` to a `value`
pub fn set(table: &str, key: &str, value: &str) -> Result<(), String> {
    // If the table doesn't yet exist, just create it while we're at it.
    let _ = make(table);
    let path = format!("{}{}", HOBBIT_STORAGE, table);
    if let Ok(data) = std::fs::read_to_string(&path) {
        let mut json: serde_json::Value = unwrap_or_return!(serde_json::from_str(&data));
        json[key] = serde_json::Value::String(value.to_string());
        return match std::fs::write(&path, json.to_string()) {
            Ok(_) => Ok(()),
            Err(e) => Err(e.to_string()),
        };
    }
    let error = format!("Error reading table '{}'", table);
    Err(error)
}

/// Delete a `key` from a `table`
pub fn del(table: &str, key: &str) -> Result<(), String> {
    let path = format!("{}{}", HOBBIT_STORAGE, table);
    if let Ok(data) = std::fs::read_to_string(&path) {
        let json: serde_json::Value = unwrap_or_return!(serde_json::from_str(&data));
        match json {
            serde_json::Value::Object(mut map) => {
                map.remove(key);
                // If the table is now empty, just remove the file too.
                if map.len() == 0 {
                    let _ = std::fs::remove_file(&path);
                    // If the directory is now empty, then clean it up as well.
                    let _ = std::fs::remove_dir(trim_slash(&path));
                    return Ok(());
                } else {
                    let v: serde_json::Value = map.into();
                    return match std::fs::write(&path, v.to_string()) {
                        Ok(_) => Ok(()),
                        Err(e) => Err(e.to_string()),
                    };
                }
            }
            _ => return Err("Key not found".to_string()),
        }
    }
    let error = format!("Error reading table '{}'", table);
    Err(error)
}

fn make(table: &str) -> Result<(), String> {
    let _ = std::fs::create_dir_all(HOBBIT_STORAGE);
    let path = format!("{}{}", HOBBIT_STORAGE, table);
    // If the table contains a slash, create the subdirectory first.
    let _ = std::fs::create_dir_all(trim_slash(&path));
    // If the table already exists, return ok. If not, create empty json.
    if !std::fs::metadata(path.to_string()).is_ok() {
        let data = serde_json::json!({});
        match std::fs::write(path.to_string(), data.to_string()) {
            Ok(_) => return Ok(()),
            Err(e) => return Err(e.to_string()),
        }
    }
    Ok(())
}

/// Load an entire `table` as a `serde_json::Map`
pub fn map(table: &str) -> Result<serde_json::Map<String, serde_json::Value>, String> {
    let path = format!("{}{}", HOBBIT_STORAGE, table);
    if let Ok(data) = std::fs::read_to_string(&path) {
        let json: serde_json::Value = unwrap_or_return!(serde_json::from_str(&data));
        match json {
            serde_json::Value::Object(map) => {
                return Ok(map);
            }
            _ => return Err("Could not parse table as Map".to_string()),
        }
    }
    let error = format!("Error reading table '{}'", table);
    Err(error)
}