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}