Skip to main content

provide_telemetry/
schema.rs

1// SPDX-FileCopyrightText: Copyright (C) 2026 provide.io llc
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-Comment: Part of provide-telemetry.
4//
5
6use regex::Regex;
7use std::sync::{
8    atomic::{AtomicBool, Ordering},
9    OnceLock,
10};
11
12use crate::errors::EventSchemaError;
13
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct Event {
16    pub event: String,
17    pub domain: String,
18    pub action: String,
19    pub resource: Option<String>,
20    pub status: String,
21}
22
23static STRICT_SCHEMA: AtomicBool = AtomicBool::new(false);
24
25/// Enable or disable strict segment-format validation for `event()`. Thread-safe.
26pub fn set_strict_schema(enabled: bool) {
27    STRICT_SCHEMA.store(enabled, Ordering::Relaxed);
28}
29
30/// Return the current strict-schema flag value.
31pub fn get_strict_schema() -> bool {
32    STRICT_SCHEMA.load(Ordering::Relaxed)
33}
34
35fn segment_re() -> &'static Regex {
36    static SEGMENT_RE: OnceLock<Regex> = OnceLock::new();
37    SEGMENT_RE.get_or_init(|| Regex::new(r"^[a-z][a-z0-9_]*$").expect("valid regex"))
38}
39
40pub fn event(segments: &[&str]) -> Result<Event, EventSchemaError> {
41    if !(3..=4).contains(&segments.len()) {
42        return Err(EventSchemaError::new(format!(
43            "event() requires 3 or 4 segments (DA[R]S), got {}",
44            segments.len()
45        )));
46    }
47
48    for (idx, segment) in segments.iter().enumerate() {
49        if !segment_re().is_match(segment) {
50            return Err(EventSchemaError::new(format!(
51                "invalid event segment: segment[{idx}]={segment}"
52            )));
53        }
54    }
55
56    let event = segments.join(".");
57    Ok(Event {
58        event,
59        domain: segments[0].to_string(),
60        action: segments[1].to_string(),
61        resource: if segments.len() == 4 {
62            Some(segments[2].to_string())
63        } else {
64            None
65        },
66        status: segments[segments.len() - 1].to_string(),
67    })
68}
69
70pub fn event_name(segments: &[&str]) -> Result<String, EventSchemaError> {
71    let strict = get_strict_schema();
72    if strict {
73        if !(3..=5).contains(&segments.len()) {
74            return Err(EventSchemaError::new(format!(
75                "expected 3-5 segments, got {}",
76                segments.len()
77            )));
78        }
79        for (idx, segment) in segments.iter().enumerate() {
80            if !segment_re().is_match(segment) {
81                return Err(EventSchemaError::new(format!(
82                    "invalid event segment: segment[{idx}]={segment}"
83                )));
84            }
85        }
86    } else if segments.is_empty() {
87        return Err(EventSchemaError::new(
88            "event_name requires at least 1 segment",
89        ));
90    }
91    Ok(segments.join("."))
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn schema_test_event_name_returns_exact_joined_value() {
100        set_strict_schema(false);
101        assert_eq!(
102            event_name(&["auth", "login", "ok"]).expect("name should build"),
103            "auth.login.ok"
104        );
105        set_strict_schema(true);
106        assert_eq!(
107            event_name(&["a", "b", "c", "d", "e"]).expect("strict name should build"),
108            "a.b.c.d.e"
109        );
110        set_strict_schema(false);
111    }
112
113    #[test]
114    fn schema_test_event_name_validates_empty_and_invalid_strict_inputs() {
115        set_strict_schema(false);
116        let err = event_name(&[]).expect_err("empty non-strict name should fail");
117        assert_eq!(err.message, "event_name requires at least 1 segment");
118
119        set_strict_schema(true);
120        let err = event_name(&["a", "b"]).expect_err("strict arity should fail");
121        assert_eq!(err.message, "expected 3-5 segments, got 2");
122
123        let err = event_name(&["valid", "not-valid", "ok"]).expect_err("strict syntax should fail");
124        assert_eq!(err.message, "invalid event segment: segment[1]=not-valid");
125        set_strict_schema(false);
126    }
127}