kitt_score 0.1.0

Decision engine at the core of Project KITT — in-memory stateful matching with pluggable scoring backends.
Documentation
//! Newtype wrappers for the identifier types used across the crate.
//!
//! Using newtypes over raw integers and strings gives us two things:
//! 1. Compile-time prevention of "did I pass the `LocId` or the `ActionId`?" mix-ups.
//! 2. A place to hang documentation about the semantics of each identifier.

use std::fmt;
use std::sync::Arc;

/// Caller-assigned, stable identifier for a Location.
///
/// The library treats `LocId`s as opaque: it never interprets the bits. Callers
/// are free to use sequential integers, hashed strings, Snowflake IDs, etc.
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct LocId(pub u64);

impl fmt::Debug for LocId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "LocId({})", self.0)
    }
}

/// Caller-assigned identifier for an Action within a Location.
///
/// `ActionId` is always string-backed because integrations often use external
/// identifiers directly (for example a VAST creative URL). Uniqueness is
/// required only per-location. Different locations may reuse the same
/// `ActionId`.
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ActionId(Arc<str>);

impl ActionId {
    /// Build an `ActionId` from any owned or borrowed string input.
    #[must_use]
    pub fn new(id: impl Into<Arc<str>>) -> Self {
        Self(id.into())
    }

    /// Borrow the underlying action identifier as `&str`.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl From<&str> for ActionId {
    fn from(value: &str) -> Self {
        Self::new(value)
    }
}

impl From<String> for ActionId {
    fn from(value: String) -> Self {
        Self::new(value)
    }
}

impl From<Arc<str>> for ActionId {
    fn from(value: Arc<str>) -> Self {
        Self::new(value)
    }
}

impl AsRef<str> for ActionId {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl fmt::Debug for ActionId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("ActionId").field(&self.0).finish()
    }
}

impl fmt::Display for ActionId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Interned index of an event-kind name in the Schema.
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub struct KindId(pub u16);

/// Interned index of an attribute name in the Schema.
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub struct AttrId(pub u16);

/// Unix time in seconds since the epoch. Signed to align with
/// `std::time::SystemTime`'s internal representation.
pub type UnixTime = i64;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ids_are_debug_formatted_with_their_type_name() {
        assert_eq!(format!("{:?}", LocId(42)), "LocId(42)");
        assert_eq!(
            format!("{:?}", ActionId::from("creative-7")),
            "ActionId(\"creative-7\")"
        );
    }

    #[test]
    fn action_id_round_trips_as_a_string() {
        let id = ActionId::from("https://www.adserver/mycreative.mp4");
        assert_eq!(id.as_str(), "https://www.adserver/mycreative.mp4");
        assert_eq!(format!("{id}"), "https://www.adserver/mycreative.mp4");
    }

    #[test]
    fn scalar_ids_are_small() {
        // Guard against a future "helpful" refactor that puts heap data inside scalar IDs.
        use std::mem::size_of;
        assert_eq!(size_of::<LocId>(), 8);
        assert_eq!(size_of::<KindId>(), 2);
        assert_eq!(size_of::<AttrId>(), 2);
    }
}