use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use crate::message::{PostcardReader, PostcardValue};
use crate::schema::declaration::{Field, Kind};
use crate::schema::{Cardinality, Described, declaration};
use crate::{MemorySize, MessageError, RowMetadata};
const COUNT: u32 = 31_971_292;
const SUM: u32 = 534_305_306;
const MEAN: u32 = 96_322_653;
const MEDIAN: u32 = 148_660_223;
const MIN: u32 = 244_600_265;
const MAX: u32 = 480_766_859;
const RMS: u32 = 43_272_471;
const STD: u32 = 450_256_219;
const PERCENTILE: u32 = 455_936_439;
const RANK: u32 = 211_531_165;
struct Rank;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Aggregate {
Count,
Sum,
Mean,
Median,
Min,
Max,
Rms,
Std,
Percentile(#[serde(with = "rank")] u8),
}
impl Aggregate {
pub fn name(&self) -> String {
match self {
Self::Count => "count".to_owned(),
Self::Sum => "sum".to_owned(),
Self::Mean => "mean".to_owned(),
Self::Median => "median".to_owned(),
Self::Min => "min".to_owned(),
Self::Max => "max".to_owned(),
Self::Rms => "rms".to_owned(),
Self::Std => "std".to_owned(),
Self::Percentile(rank) => format!("p{rank}"),
}
}
pub fn parse(name: &str) -> Option<Self> {
let named = match name {
"count" => Self::Count,
"sum" => Self::Sum,
"mean" => Self::Mean,
"median" => Self::Median,
"min" => Self::Min,
"max" => Self::Max,
"rms" => Self::Rms,
"std" => Self::Std,
_ => {
let rank = name.strip_prefix('p')?;
if rank.is_empty() || !rank.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
Self::Percentile(rank.parse().ok()?)
}
};
named.answerable().then_some(named)
}
pub fn answerable(&self) -> bool {
match self {
Self::Percentile(rank) => *rank <= 100,
_ => true,
}
}
pub fn mergeable(&self) -> bool {
!matches!(self, Self::Median | Self::Percentile(_))
}
fn ordinal(&self) -> u32 {
match self {
Self::Count => 0,
Self::Sum => 1,
Self::Mean => 2,
Self::Median => 3,
Self::Min => 4,
Self::Max => 5,
Self::Rms => 6,
Self::Std => 7,
Self::Percentile(_) => 8,
}
}
fn unit(ordinal: u32) -> Option<Self> {
Some(match ordinal {
0 => Self::Count,
1 => Self::Sum,
2 => Self::Mean,
3 => Self::Median,
4 => Self::Min,
5 => Self::Max,
6 => Self::Rms,
7 => Self::Std,
_ => return None,
})
}
fn read_postcard(input: &mut PostcardReader<'_>) -> Result<Self, MessageError> {
let ordinal: u32 = input.value()?;
if ordinal == 8 {
let rank: u32 = input.value()?;
return u8::try_from(rank).map(Self::Percentile).map_err(|_| MessageError::borrowed("percentile rank exceeds 255"));
}
Self::unit(ordinal).ok_or(MessageError::borrowed("unknown Postcard enum variant"))
}
}
impl Described for Aggregate {
const NAME: &'static str = "harmos::Aggregate";
const FIELDS: &'static [Field] = &[
variant("Count", COUNT, <()>::DEFINITION),
variant("Sum", SUM, <()>::DEFINITION),
variant("Mean", MEAN, <()>::DEFINITION),
variant("Median", MEDIAN, <()>::DEFINITION),
variant("Min", MIN, <()>::DEFINITION),
variant("Max", MAX, <()>::DEFINITION),
variant("Rms", RMS, <()>::DEFINITION),
variant("Std", STD, <()>::DEFINITION),
variant("Percentile", PERCENTILE, Rank::DEFINITION),
];
const MARKER: &'static [u8] = &declaration::encode::<{ declaration::encoded_len(Self::NAME, Self::FIELDS) }>(Self::NAME, Self::FIELDS);
}
impl Described for Rank {
const NAME: &'static str = "harmos::Aggregate::Percentile";
const FIELDS: &'static [Field] =
&[Field { name: Cow::Borrowed("rank"), tag: RANK, kind: Kind::U32, cardinality: Cardinality::Required }];
const MARKER: &'static [u8] = &declaration::encode::<{ declaration::encoded_len(Self::NAME, Self::FIELDS) }>(Self::NAME, Self::FIELDS);
}
impl PostcardValue for Aggregate {
fn write(&self, bytes: &mut Vec<u8>) {
self.ordinal().write(bytes);
if let Self::Percentile(rank) = self {
u32::from(*rank).write(bytes);
}
}
fn size(&self) -> usize {
let rank = match self {
Self::Percentile(rank) => u32::from(*rank).size(),
_ => 0,
};
self.ordinal().size().saturating_add(rank)
}
fn memory(input: &mut PostcardReader<'_>) -> Result<usize, MessageError> {
input.nested(|input| {
Self::read_postcard(input)?;
Ok(std::mem::size_of::<Self>())
})
}
fn read(input: &mut PostcardReader<'_>) -> Result<Self, MessageError> {
input.nested(Self::read_postcard)
}
}
impl MemorySize for Aggregate {
fn memory_size(&self) -> usize {
std::mem::size_of::<Self>()
}
}
impl RowMetadata for Aggregate {}
mod rank {
pub(super) fn serialize<S: serde::Serializer>(rank: &u8, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u32(u32::from(*rank))
}
pub(super) fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<u8, D::Error> {
let rank = <u32 as serde::Deserialize>::deserialize(deserializer)?;
u8::try_from(rank).map_err(|_| serde::de::Error::custom("percentile rank exceeds 255"))
}
}
const fn variant(name: &'static str, tag: u32, payload: declaration::Definition) -> Field {
Field { name: Cow::Borrowed(name), tag, kind: Kind::Message { definition: payload }, cardinality: Cardinality::Variant }
}
#[cfg(test)]
mod tests {
use super::Aggregate;
const EVERY: [Aggregate; 11] = [
Aggregate::Count,
Aggregate::Sum,
Aggregate::Mean,
Aggregate::Median,
Aggregate::Min,
Aggregate::Max,
Aggregate::Rms,
Aggregate::Std,
Aggregate::Percentile(0),
Aggregate::Percentile(25),
Aggregate::Percentile(100),
];
#[test]
fn wire_names() {
for kind in EVERY {
assert_eq!(Aggregate::parse(&kind.name()), Some(kind));
let json = serde_json::to_string(&kind).unwrap();
assert_eq!(serde_json::from_str::<Aggregate>(&json).unwrap(), kind);
}
assert_eq!(serde_json::to_string(&Aggregate::Count).unwrap(), "\"count\"");
assert_eq!(serde_json::to_string(&Aggregate::Rms).unwrap(), "\"rms\"");
assert_eq!(serde_json::to_string(&Aggregate::Percentile(95)).unwrap(), "{\"percentile\":95}");
assert_eq!(Aggregate::Percentile(95).name(), "p95");
assert_eq!(Aggregate::parse("p95"), Some(Aggregate::Percentile(95)));
for refused in ["p101", "p", "p+5", "variance"] {
assert_eq!(Aggregate::parse(refused), None, "{refused}");
}
assert!(!Aggregate::Percentile(101).answerable());
}
}