telemetry-safe 0.1.0

Compile-time safe telemetry formatting facade crate
Documentation
// `cargo package` verifies the crate from the packaged tarball, so the doc
// source must live inside this crate rather than only at the workspace root.
#![doc = include_str!("../README.md")]

// Derive macros refer to the public crate path so downstream crates and this
// crate's own tests expand identically.
extern crate self as telemetry_safe;

pub use telemetry_safe_core::{
    TelemetryDebug, TelemetryDisplay, ToTelemetry, prelude, telemetry, telemetry_debug,
};
pub use telemetry_safe_derive::ToTelemetry;

#[cfg(test)]
mod tests {
    #![allow(dead_code)]

    use super::{ToTelemetry, telemetry};
    use std::collections::{BTreeMap, BTreeSet};
    use std::fmt::{self, Formatter};

    #[derive(ToTelemetry)]
    struct AccountSnapshot {
        id: u64,
        state: AccountState,
        #[telemetry(skip)]
        secret_note: &'static str,
    }

    #[derive(ToTelemetry)]
    enum Outcome {
        Accepted { account: AccountSnapshot },
        Rejected(RejectReason),
    }

    #[derive(ToTelemetry)]
    struct RejectReason {
        code: RejectCode,
    }

    struct AccountState(&'static str);

    impl ToTelemetry for AccountState {
        fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
            f.write_str(self.0)
        }
    }

    struct RejectCode(&'static str);

    impl ToTelemetry for RejectCode {
        fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
            f.write_str(self.0)
        }
    }

    #[test]
    fn derived_struct_skips_sensitive_fields() {
        let snapshot = AccountSnapshot {
            id: 42,
            state: AccountState("active"),
            secret_note: "pii",
        };

        assert_eq!(
            telemetry(&snapshot).to_string(),
            r#"AccountSnapshot { id: 42, state: active }"#
        );
    }

    #[test]
    fn derived_enum_formats_variants() {
        let outcome = Outcome::Rejected(RejectReason {
            code: RejectCode("policy"),
        });

        assert_eq!(
            telemetry(&outcome).to_string(),
            r#"Rejected(RejectReason { code: policy })"#
        );
    }

    #[test]
    fn collections_require_safe_elements() {
        let mut map = BTreeMap::new();
        map.insert(
            1_u64,
            AccountSnapshot {
                id: 7,
                state: AccountState("pending"),
                secret_note: "hidden",
            },
        );

        let mut set = BTreeSet::new();
        set.insert(1_u64);

        assert_eq!(
            telemetry(&map).to_string(),
            r#"{1: AccountSnapshot { id: 7, state: pending }}"#
        );
        assert_eq!(telemetry(&set).to_string(), "{1}");
    }

    #[test]
    fn display_wrapper_can_be_used_in_format_args() {
        let snapshot = AccountSnapshot {
            id: 1,
            state: AccountState("active"),
            secret_note: "hidden",
        };

        let rendered = format!("account={}", telemetry(&snapshot));

        assert_eq!(rendered, "account=AccountSnapshot { id: 1, state: active }");
    }
}