use serde::Serialize;
use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Event {
InstallFirstRun,
InstallUpdated,
CliCommand,
ServeStarted,
ServeStopped,
ModelLoaded,
ModelDownload,
HardwareProfile,
}
impl Event {
pub const fn name(self) -> &'static str {
match self {
Self::InstallFirstRun => "install_first_run",
Self::InstallUpdated => "install_updated",
Self::CliCommand => "cli_command",
Self::ServeStarted => "serve_started",
Self::ServeStopped => "serve_stopped",
Self::ModelLoaded => "model_loaded",
Self::ModelDownload => "model_download",
Self::HardwareProfile => "hardware_profile",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)]
pub struct Label(String);
const MAX_LABEL_LEN: usize = 96;
const MAX_SLUG_LEN: usize = 48;
const MAX_SLUG_SEGMENTS: usize = 8;
impl Label {
pub fn sanitize(value: &str) -> Option<Self> {
if value.is_empty() || value.len() > MAX_LABEL_LEN {
return None;
}
if value.contains("..") {
return None;
}
if value.matches('/').count() > 1 {
return None;
}
if value.starts_with(['/', '-', '.']) || value.ends_with('/') {
return None;
}
let permitted = value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_' | '+' | '/'));
permitted.then(|| Self(value.to_owned()))
}
pub fn slug(value: &str) -> Option<Self> {
if value.contains('/') || value.contains('\\') {
return None;
}
let mut slugged = String::with_capacity(value.len());
let mut pending_separator = false;
for ch in value.trim().chars() {
if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '+') {
if pending_separator && !slugged.is_empty() {
slugged.push('-');
}
pending_separator = false;
slugged.extend(ch.to_lowercase());
} else {
pending_separator = true;
}
}
if slugged.len() > MAX_SLUG_LEN || slugged.split('-').count() > MAX_SLUG_SEGMENTS {
return None;
}
Self::sanitize(&slugged)
}
#[must_use]
pub fn redacted() -> Self {
Self("redacted".to_owned())
}
#[must_use]
pub fn sanitize_or_redact(value: &str) -> Self {
Self::sanitize(value).unwrap_or_else(Self::redacted)
}
#[must_use]
pub fn slug_or_redact(value: &str) -> Self {
Self::slug(value).unwrap_or_else(Self::redacted)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Value {
Bool(bool),
Int(i64),
Text(Label),
Static(&'static str),
}
impl From<bool> for Value {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl From<i64> for Value {
fn from(value: i64) -> Self {
Self::Int(value)
}
}
impl From<u64> for Value {
fn from(value: u64) -> Self {
Self::Int(i64::try_from(value).unwrap_or(i64::MAX))
}
}
impl From<usize> for Value {
fn from(value: usize) -> Self {
Self::Int(i64::try_from(value).unwrap_or(i64::MAX))
}
}
impl From<Label> for Value {
fn from(value: Label) -> Self {
Self::Text(value)
}
}
impl From<&'static str> for Value {
fn from(value: &'static str) -> Self {
Self::Static(value)
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Properties(BTreeMap<&'static str, Value>);
impl Properties {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, key: &'static str, value: impl Into<Value>) -> Self {
self.0.insert(key, value.into());
self
}
pub(crate) fn entries(&self) -> impl Iterator<Item = (&'static str, &Value)> {
self.0.iter().map(|(key, value)| (*key, value))
}
}
#[must_use]
pub fn bucket_count(count: u64) -> &'static str {
match count {
0 => "0",
1 => "1",
2 => "2",
3..=4 => "3-4",
5..=8 => "5-8",
9..=16 => "9-16",
17..=32 => "17-32",
_ => "33+",
}
}
#[must_use]
pub fn bucket_gigabytes(bytes: u64) -> &'static str {
const GB: u64 = 1024 * 1024 * 1024;
match bytes / GB {
0..=7 => "0-8",
8..=15 => "8-16",
16..=31 => "16-32",
32..=63 => "32-64",
64..=127 => "64-128",
128..=255 => "128-256",
_ => "256+",
}
}
#[must_use]
pub fn bucket_duration_secs(secs: u64) -> &'static str {
match secs {
0..=59 => "under_1m",
60..=899 => "1m-15m",
900..=3599 => "15m-1h",
3600..=21599 => "1h-6h",
21600..=86399 => "6h-24h",
_ => "over_24h",
}
}
#[cfg(test)]
#[path = "event/tests.rs"]
mod tests;