scepter 0.1.1

Composable primitives for planet-scale time-series routing, indexing, and aggregation.
Documentation
use std::collections::HashMap;

use crate::key::{KeyEncoder, LexicographicKey};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValueKind {
    Bool,
    I64,
    F64,
    String,
    Distribution,
    Tuple(Vec<ValueKind>),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetricKind {
    Gauge,
    Cumulative,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
    pub name: String,
    pub kind: ValueKind,
}

impl Field {
    pub fn new(name: impl Into<String>, kind: ValueKind) -> Self {
        Self {
            name: name.into(),
            kind,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TargetSchema {
    pub name: String,
    pub fields: Vec<Field>,
    pub location_field: String,
}

impl TargetSchema {
    pub fn new(
        name: impl Into<String>,
        fields: Vec<Field>,
        location_field: impl Into<String>,
    ) -> Self {
        Self {
            name: name.into(),
            fields,
            location_field: location_field.into(),
        }
    }

    pub fn has_field(&self, name: &str) -> bool {
        self.fields.iter().any(|field| field.name == name)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetricSchema {
    pub name: String,
    pub fields: Vec<Field>,
    pub value_kind: ValueKind,
    pub metric_kind: MetricKind,
}

impl MetricSchema {
    pub fn new(
        name: impl Into<String>,
        fields: Vec<Field>,
        value_kind: ValueKind,
        metric_kind: MetricKind,
    ) -> Self {
        Self {
            name: name.into(),
            fields,
            value_kind,
            metric_kind,
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum FieldValue {
    Bool(bool),
    I64(i64),
    U64(u64),
    F64(f64),
    String(String),
}

impl LexicographicKey for FieldValue {
    fn encode_key(&self, out: &mut Vec<u8>) {
        match self {
            Self::Bool(value) => value.encode_key(out),
            Self::I64(value) => value.encode_key(out),
            Self::U64(value) => value.encode_key(out),
            Self::F64(value) => value.encode_key(out),
            Self::String(value) => value.encode_key(out),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct TimeSeriesKey {
    pub target_schema: String,
    pub target_fields: Vec<FieldValue>,
    pub metric_name: String,
    pub metric_fields: Vec<FieldValue>,
}

impl TimeSeriesKey {
    pub fn target_key(&self) -> Vec<u8> {
        let mut encoder = KeyEncoder::new();
        encoder.push_field(self.target_schema.as_str());
        for field in &self.target_fields {
            encoder.push_field(field);
        }
        encoder.finish()
    }

    pub fn series_key(&self) -> Vec<u8> {
        let mut encoder = KeyEncoder::new();
        encoder.push_field(self.target_schema.as_str());
        for field in &self.target_fields {
            encoder.push_field(field);
        }
        encoder.push_field(self.metric_name.as_str());
        for field in &self.metric_fields {
            encoder.push_field(field);
        }
        encoder.finish()
    }
}

/// Maps target location values to storage/query zones.
#[derive(Debug, Clone, Default)]
pub struct LocationResolver {
    zones: HashMap<String, String>,
}

impl LocationResolver {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn insert(&mut self, location: impl Into<String>, zone: impl Into<String>) {
        self.zones.insert(location.into(), zone.into());
    }

    pub fn zone_for(&self, location: &str) -> Option<&str> {
        self.zones.get(location).map(String::as_str)
    }
}

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

    #[test]
    fn time_series_key_has_target_and_full_series_forms() {
        let key = TimeSeriesKey {
            target_schema: "ComputeTask".to_owned(),
            target_fields: vec![FieldValue::String("aa".to_owned())],
            metric_name: "/rpc/server/latency".to_owned(),
            metric_fields: vec![FieldValue::String("Query".to_owned())],
        };

        assert!(key.target_key() < key.series_key());
    }

    #[test]
    fn location_resolver_maps_locations_to_zones() {
        let mut resolver = LocationResolver::new();
        resolver.insert("aa", "zone-west");

        assert_eq!(resolver.zone_for("aa"), Some("zone-west"));
    }
}