relay_base_schema/metrics/
units.rsuse std::fmt;
use relay_protocol::{Annotated, Empty, ErrorKind, FromValue, IntoValue, SkipSerialization, Value};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
pub enum MetricUnit {
Duration(DurationUnit),
Information(InformationUnit),
Fraction(FractionUnit),
Custom(CustomUnit),
#[default]
None,
}
impl MetricUnit {
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
pub fn as_str(&self) -> &str {
match self {
MetricUnit::Duration(u) => u.as_str(),
MetricUnit::Information(u) => u.as_str(),
MetricUnit::Fraction(u) => u.as_str(),
MetricUnit::Custom(u) => u.as_str(),
MetricUnit::None => "none",
}
}
}
impl fmt::Display for MetricUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MetricUnit::Duration(u) => u.fmt(f),
MetricUnit::Information(u) => u.fmt(f),
MetricUnit::Fraction(u) => u.fmt(f),
MetricUnit::Custom(u) => u.fmt(f),
MetricUnit::None => f.write_str("none"),
}
}
}
impl std::str::FromStr for MetricUnit {
type Err = ParseMetricUnitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"nanosecond" | "ns" => Self::Duration(DurationUnit::NanoSecond),
"microsecond" => Self::Duration(DurationUnit::MicroSecond),
"millisecond" | "ms" => Self::Duration(DurationUnit::MilliSecond),
"second" | "s" => Self::Duration(DurationUnit::Second),
"minute" => Self::Duration(DurationUnit::Minute),
"hour" => Self::Duration(DurationUnit::Hour),
"day" => Self::Duration(DurationUnit::Day),
"week" => Self::Duration(DurationUnit::Week),
"bit" => Self::Information(InformationUnit::Bit),
"byte" => Self::Information(InformationUnit::Byte),
"kilobyte" => Self::Information(InformationUnit::KiloByte),
"kibibyte" => Self::Information(InformationUnit::KibiByte),
"megabyte" => Self::Information(InformationUnit::MegaByte),
"mebibyte" => Self::Information(InformationUnit::MebiByte),
"gigabyte" => Self::Information(InformationUnit::GigaByte),
"gibibyte" => Self::Information(InformationUnit::GibiByte),
"terabyte" => Self::Information(InformationUnit::TeraByte),
"tebibyte" => Self::Information(InformationUnit::TebiByte),
"petabyte" => Self::Information(InformationUnit::PetaByte),
"pebibyte" => Self::Information(InformationUnit::PebiByte),
"exabyte" => Self::Information(InformationUnit::ExaByte),
"exbibyte" => Self::Information(InformationUnit::ExbiByte),
"ratio" => Self::Fraction(FractionUnit::Ratio),
"percent" => Self::Fraction(FractionUnit::Percent),
"" | "none" => Self::None,
_ => Self::Custom(CustomUnit::parse(s)?),
})
}
}
relay_common::impl_str_serde!(MetricUnit, "a metric unit string");
impl Empty for MetricUnit {
#[inline]
fn is_empty(&self) -> bool {
false
}
}
impl FromValue for MetricUnit {
fn from_value(value: Annotated<Value>) -> Annotated<Self> {
match String::from_value(value) {
Annotated(Some(value), mut meta) => match value.parse() {
Ok(unit) => Annotated(Some(unit), meta),
Err(_) => {
meta.add_error(ErrorKind::InvalidData);
meta.set_original_value(Some(value));
Annotated(None, meta)
}
},
Annotated(None, meta) => Annotated(None, meta),
}
}
}
impl IntoValue for MetricUnit {
fn into_value(self) -> Value
where
Self: Sized,
{
Value::String(self.to_string())
}
fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
where
Self: Sized,
S: serde::Serializer,
{
serde::Serialize::serialize(self.as_str(), s)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum DurationUnit {
NanoSecond,
MicroSecond,
MilliSecond,
Second,
Minute,
Hour,
Day,
Week,
}
impl DurationUnit {
pub fn as_str(&self) -> &'static str {
match self {
Self::NanoSecond => "nanosecond",
Self::MicroSecond => "microsecond",
Self::MilliSecond => "millisecond",
Self::Second => "second",
Self::Minute => "minute",
Self::Hour => "hour",
Self::Day => "day",
Self::Week => "week",
}
}
}
impl Default for DurationUnit {
fn default() -> Self {
Self::MilliSecond
}
}
impl fmt::Display for DurationUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug)]
pub struct ParseMetricUnitError(());
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum InformationUnit {
Bit,
Byte,
KiloByte,
KibiByte,
MegaByte,
MebiByte,
GigaByte,
GibiByte,
TeraByte,
TebiByte,
PetaByte,
PebiByte,
ExaByte,
ExbiByte,
}
impl InformationUnit {
pub fn as_str(&self) -> &'static str {
match self {
Self::Bit => "bit",
Self::Byte => "byte",
Self::KiloByte => "kilobyte",
Self::KibiByte => "kibibyte",
Self::MegaByte => "megabyte",
Self::MebiByte => "mebibyte",
Self::GigaByte => "gigabyte",
Self::GibiByte => "gibibyte",
Self::TeraByte => "terabyte",
Self::TebiByte => "tebibyte",
Self::PetaByte => "petabyte",
Self::PebiByte => "pebibyte",
Self::ExaByte => "exabyte",
Self::ExbiByte => "exbibyte",
}
}
}
impl Default for InformationUnit {
fn default() -> Self {
Self::Byte
}
}
impl fmt::Display for InformationUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum FractionUnit {
Ratio,
Percent,
}
impl FractionUnit {
pub fn as_str(&self) -> &'static str {
match self {
Self::Ratio => "ratio",
Self::Percent => "percent",
}
}
}
impl Default for FractionUnit {
fn default() -> Self {
Self::Ratio
}
}
impl fmt::Display for FractionUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
const CUSTOM_UNIT_MAX_SIZE: usize = 15;
fn is_word_char(c: u8) -> bool {
matches!(c, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_')
}
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub struct CustomUnit([u8; CUSTOM_UNIT_MAX_SIZE]);
impl CustomUnit {
pub fn parse(s: &str) -> Result<Self, ParseMetricUnitError> {
if s.is_empty() || s.bytes().any(|c| !is_word_char(c)) {
return Err(ParseMetricUnitError(()));
}
let mut unit = Self([0; CUSTOM_UNIT_MAX_SIZE]);
let slice = unit.0.get_mut(..s.len()).ok_or(ParseMetricUnitError(()))?;
slice.copy_from_slice(s.as_bytes());
unit.0.make_ascii_lowercase();
Ok(unit)
}
#[inline]
pub fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(&self.0).trim_end_matches('\0') }
}
}
impl fmt::Debug for CustomUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}
impl fmt::Display for CustomUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}
impl std::str::FromStr for CustomUnit {
type Err = ParseMetricUnitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl std::ops::Deref for CustomUnit {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_unit() {
assert_eq!(MetricUnit::None, "".parse().unwrap());
}
#[test]
fn test_custom_unit_parse() {
assert_eq!("foo", CustomUnit::parse("Foo").unwrap().as_str());
assert_eq!(
"0123456789abcde",
CustomUnit::parse("0123456789abcde").unwrap().as_str()
);
assert!(CustomUnit::parse("this_is_a_unit_that_is_too_long").is_err());
}
#[test]
fn test_custom_unit_invalid_char() {
assert!(CustomUnit::parse("").is_err()); assert!(CustomUnit::parse("foo bar").is_err());
assert!(CustomUnit::parse("foo/bar").is_err());
assert!(CustomUnit::parse("foo-bar").is_err());
assert!(CustomUnit::parse("föö").is_err());
}
}