Skip to main content

gts_id/
prefix.rs

1//! Configuration of the GTS identifier prefix.
2//!
3//! The prefix is resolved once, at compile time, from the [`GTS_ID_PREFIX_ENV`]
4//! environment variable (falling back to [`DEFAULT_GTS_ID_PREFIX`]). It is
5//! validated by [`validate_gts_id_prefix`] so an invalid override fails the
6//! build instead of silently producing malformed identifiers.
7//!
8//! Because the value is baked in at compile time, the crate's `build.rs` emits
9//! `rerun-if-env-changed` so Cargo rebuilds when the variable changes.
10
11/// The default identifier prefix for all GTS identifiers.
12#[allow(unknown_lints, gts_id_hardcoded_prefix)]
13pub const DEFAULT_GTS_ID_PREFIX: &str = "gts.";
14
15/// Environment variable used to override the GTS identifier prefix at compile time.
16///
17/// `option_env!` requires a string *literal*, so the name is repeated in
18/// [`GTS_ID_PREFIX`]'s initializer below; `build.rs` likewise hardcodes it for
19/// its `rerun-if-env-changed` hint. Keep all three occurrences in sync.
20pub const GTS_ID_PREFIX_ENV: &str = "GTS_ID_PREFIX";
21
22/// The configured prefix for all GTS identifiers.
23///
24/// Defaults to [`DEFAULT_GTS_ID_PREFIX`] and can be overridden at compile time
25/// via the [`GTS_ID_PREFIX_ENV`] environment variable. The override must be a
26/// single lowercase token (`[a-z][a-z0-9_]*`) terminated by a single `.`
27/// (e.g. `acme.`); multi-segment prefixes such as `my.org.` are rejected at
28/// compile time by [`validate_gts_id_prefix`].
29pub const GTS_ID_PREFIX: &str = validate_gts_id_prefix(match option_env!("GTS_ID_PREFIX") {
30    Some(prefix) => prefix,
31    None => DEFAULT_GTS_ID_PREFIX,
32});
33
34/// Validates a configured GTS identifier prefix at compile time.
35///
36/// A valid prefix is a single lowercase token (`[a-z][a-z0-9_]*`) followed by
37/// a single trailing `.`. Multi-segment prefixes (`my.org.`), uppercase, and
38/// other punctuation are rejected.
39#[allow(clippy::manual_is_ascii_check)]
40const fn validate_gts_id_prefix(prefix: &str) -> &str {
41    let bytes = prefix.as_bytes();
42    assert!(!bytes.is_empty(), "GTS_ID_PREFIX must not be empty");
43    assert!(
44        bytes[bytes.len() - 1] == b'.',
45        "GTS_ID_PREFIX must end with '.'"
46    );
47    assert!(
48        bytes.len() != 1,
49        "GTS_ID_PREFIX must contain a token before the final dot"
50    );
51
52    let first = bytes[0];
53    assert!(
54        first >= b'a' && first <= b'z',
55        "GTS_ID_PREFIX must start with a lowercase ASCII letter"
56    );
57
58    let mut i = 1;
59    while i < bytes.len() - 1 {
60        let b = bytes[i];
61        assert!(
62            (b >= b'a' && b <= b'z') || (b >= b'0' && b <= b'9') || b == b'_',
63            "GTS_ID_PREFIX must be a lowercase ASCII token followed by '.'"
64        );
65        i += 1;
66    }
67
68    prefix
69}
70
71#[cfg(test)]
72#[allow(clippy::unwrap_used, clippy::expect_used)]
73mod tests {
74    use super::*;
75    use std::panic::catch_unwind;
76
77    #[test]
78    fn test_valid_prefixes() {
79        assert_eq!(validate_gts_id_prefix("gts."), "gts.");
80        assert_eq!(validate_gts_id_prefix("a."), "a.");
81        assert_eq!(validate_gts_id_prefix("acme."), "acme.");
82        assert_eq!(validate_gts_id_prefix("a1."), "a1.");
83        assert_eq!(validate_gts_id_prefix("a_b."), "a_b.");
84        assert_eq!(validate_gts_id_prefix("abc123_."), "abc123_.");
85    }
86
87    #[test]
88    fn test_default_prefix_is_in_effect() {
89        // When no custom prefix is configured via GTS_ID_PREFIX, the resolved
90        // prefix must equal the default. When a custom prefix *is* configured,
91        // the resolved prefix differs from the default — that's expected and
92        // tested by the compile-time validation above.
93        if option_env!("GTS_ID_PREFIX").is_none() {
94            assert_eq!(GTS_ID_PREFIX, DEFAULT_GTS_ID_PREFIX);
95            assert_eq!(GTS_ID_PREFIX, "gts.");
96        }
97    }
98
99    #[test]
100    fn test_invalid_prefixes_rejected() {
101        let invalid = [
102            "",           // empty
103            "acme",       // no trailing dot
104            "Acme.",      // uppercase
105            "acme-prod.", // hyphen
106            "my.org.",    // multi-segment (dot in middle)
107            ".",          // bare dot, no token
108            "1bad.",      // starts with digit
109            "_.",         // starts with underscore
110        ];
111        for prefix in invalid {
112            let result = catch_unwind(|| validate_gts_id_prefix(prefix));
113            assert!(
114                result.is_err(),
115                "prefix {prefix:?} should be rejected but was accepted"
116            );
117        }
118    }
119}