fastmetrics 0.7.1

OpenMetrics / Prometheus client library in Rust.
Documentation
use std::{borrow::Cow, fmt::Write as _};

use super::{EscapingScheme, config::NamePolicy};
use crate::{
    error::{Error, Result},
    registry::{is_legacy_label_name, is_legacy_metric_name},
};

#[derive(Clone, Copy)]
enum NameKind {
    Metric,
    Label,
}

impl NameKind {
    fn as_str(self) -> &'static str {
        match self {
            Self::Metric => "metric",
            Self::Label => "label",
        }
    }

    fn is_legacy_name(self, name: &str) -> bool {
        match self {
            Self::Metric => is_legacy_metric_name(name, true),
            Self::Label => is_legacy_label_name(name),
        }
    }

    fn is_legacy_char(self, ch: char, is_first: bool) -> bool {
        match self {
            Self::Metric => {
                if is_first {
                    is_legacy_metric_initial_char(ch)
                } else {
                    is_legacy_metric_char(ch)
                }
            },
            Self::Label => {
                if is_first {
                    is_legacy_label_initial_char(ch)
                } else {
                    is_legacy_label_char(ch)
                }
            },
        }
    }
}

pub(super) fn escape_metric_name<'a>(
    name: Cow<'a, str>,
    policy: NamePolicy,
) -> Result<Cow<'a, str>> {
    escape_name(name, policy, NameKind::Metric)
}

pub(super) fn escape_label_name<'a>(name: &'a str, policy: NamePolicy) -> Result<Cow<'a, str>> {
    escape_name(Cow::Borrowed(name), policy, NameKind::Label)
}

fn escape_name<'a>(name: Cow<'a, str>, policy: NamePolicy, kind: NameKind) -> Result<Cow<'a, str>> {
    match policy {
        NamePolicy::Legacy => {
            if kind.is_legacy_name(name.as_ref()) {
                Ok(name)
            } else {
                Err(Error::invalid(format!(
                    "{kind_name} name `{name}` is not valid for legacy text profiles",
                    kind_name = kind.as_str()
                )))
            }
        },
        NamePolicy::V1Escaping(scheme) => match scheme {
            EscapingScheme::AllowUtf8 => Ok(name),
            EscapingScheme::Underscores => Ok(escape_underscores(name, kind)),
            EscapingScheme::Dots => Ok(escape_dots(name, kind)),
            EscapingScheme::Values => Ok(escape_values(name, kind)),
        },
    }
}

fn escape_underscores<'a>(name: Cow<'a, str>, kind: NameKind) -> Cow<'a, str> {
    if kind.is_legacy_name(name.as_ref()) {
        return name;
    }

    let escaped = name
        .chars()
        .enumerate()
        .map(|(idx, ch)| if kind.is_legacy_char(ch, idx == 0) { ch } else { '_' })
        .collect::<String>();

    Cow::Owned(escaped)
}

fn escape_dots<'a>(name: Cow<'a, str>, kind: NameKind) -> Cow<'a, str> {
    let needs_rewrite = name
        .chars()
        .enumerate()
        .any(|(idx, ch)| ch == '.' || ch == '_' || !kind.is_legacy_char(ch, idx == 0));

    if !needs_rewrite {
        return name;
    }

    let mut escaped = String::with_capacity(name.len() + 8);
    for (idx, ch) in name.chars().enumerate() {
        match ch {
            '.' => escaped.push_str("_dot_"),
            '_' => escaped.push_str("__"),
            _ if kind.is_legacy_char(ch, idx == 0) => escaped.push(ch),
            _ => escaped.push('_'),
        }
    }

    Cow::Owned(escaped)
}

fn escape_values<'a>(name: Cow<'a, str>, kind: NameKind) -> Cow<'a, str> {
    let mut escaped = String::with_capacity(name.len() + 8);
    escaped.push_str("U__");

    for (idx, ch) in name.chars().enumerate() {
        match ch {
            '_' => escaped.push_str("__"),
            _ if kind.is_legacy_char(ch, idx == 0) => escaped.push(ch),
            _ => {
                escaped.push('_');
                write!(&mut escaped, "{:X}", ch as u32)
                    .expect("writing UTF-8 codepoint into String should not fail");
                escaped.push('_');
            },
        }
    }

    Cow::Owned(escaped)
}

const fn is_legacy_metric_initial_char(ch: char) -> bool {
    ch.is_ascii_alphabetic() || matches!(ch, '_' | ':')
}

const fn is_legacy_metric_char(ch: char) -> bool {
    is_legacy_metric_initial_char(ch) || ch.is_ascii_digit()
}

const fn is_legacy_label_initial_char(ch: char) -> bool {
    ch.is_ascii_alphabetic() || ch == '_'
}

const fn is_legacy_label_char(ch: char) -> bool {
    is_legacy_label_initial_char(ch) || ch.is_ascii_digit()
}