integresql 0.1.1

Rust client for the IntegreSQL Postgres testing tool
Documentation
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::{fmt::Display, str::FromStr};

use serde::{Deserialize, Serialize};
use xxhash_rust::xxh64::Xxh64;

/// Connection settings for a Postgres database.
/// 
/// DatabaseConfig values are passed to DB initialization functions to provide
/// the necessary connection settings.
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ConnectionSettings {
    pub host: String,
    pub port: u16,
    pub username: String,
    pub password: String,
    pub database: String,
    pub additional_params: Option<HashMap<String, String>>,
}

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Database {
    #[serde(rename = "templateHash")]
    pub template_hash: TemplateHash,
    pub config: ConnectionSettings,
}

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct GetTestDbResponse {
    pub database: Database,
    pub id: i32,
}

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct TemplateRequest {
    pub hash: TemplateHash,
}

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct InitializeTemplateResponse {
    pub database: Database,
}

/// Represents a unique ID for a template database, which should be created by
/// hashing the template's definition.
///
/// A TemplateHash wraps a u64 value, and in JSON is represented as a
/// hexadecimal string of that u64.
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)]
pub(crate) struct TemplateHash(u64);

impl TemplateHash {
    pub const fn new(id: u64) -> Self {
        TemplateHash(id)
    }

    /// Creates a new TemplateHash by hashing a value using ZwoHasher.
    pub fn from_hash(hashable: impl Hash) -> Self {
        let mut hasher = Xxh64::new(0);
        hashable.hash(&mut hasher);
        let hash = hasher.finish();
        TemplateHash(hash)
    }
}

/// Renders the TemplateHash as a 16 character hexadecimal string.
impl Display for TemplateHash {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:016x}", self.0)
    }
}

impl Serialize for TemplateHash {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

/// Parses a hexadecimal string into a TemplateHash.
impl FromStr for TemplateHash {
    type Err = std::num::ParseIntError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        u64::from_str_radix(s, 16).map(TemplateHash::new)
    }
}

impl<'de> Deserialize<'de> for TemplateHash {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct TemplateIdVisitor;

        impl<'de> serde::de::Visitor<'de> for TemplateIdVisitor {
            type Value = TemplateHash;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a string containing a u64 value as hexadecimal digits")
            }

            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                u64::from_str_radix(v, 16)
                    .map(TemplateHash::new)
                    .map_err(|_| {
                        serde::de::Error::custom("value is not a valid hexadecimal string")
                    })
            }
        }

        deserializer.deserialize_str(TemplateIdVisitor)
    }
}

mod tests {
    // not sure why this is needed
    #[allow(unused_imports)]
    use crate::server_models::TemplateHash;

    #[test]
    fn test_template_hash_new() {
        let hash = TemplateHash::new(67890);
        assert_eq!(hash.0, 67890);
    }

    #[test]
    fn test_to_string() {
        let hash = TemplateHash::new(0x1234);
        assert_eq!(hash.to_string(), "0000000000001234");

        let hash = TemplateHash::new(255);
        assert_eq!(hash.to_string(), "00000000000000ff");

        let hash = TemplateHash::new(0);
        assert_eq!(hash.to_string(), "0000000000000000");
    }

    #[test]
    fn test_json_serialization() {
        let hash = TemplateHash::new(0x1234);
        let json = serde_json::to_string(&hash).unwrap();
        assert_eq!(json, "\"0000000000001234\"");

        let deserialized: TemplateHash = serde_json::from_str("\"0000000000005678\"").unwrap();
        assert_eq!(deserialized.0, 0x5678);
    }

    #[test]
    fn test_parse() {
        let result = "12345678".parse::<TemplateHash>();
        assert!(result.is_ok());
        assert_eq!(result.unwrap().0, 0x12345678);

        let result = "invalid".parse::<TemplateHash>();
        assert!(result.is_err());
    }
}