qubit_codec/value/value_codec_id.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8
9//! Stable value-codec identifiers.
10
11use core::borrow::Borrow;
12
13use crate::ValueCodecIdError;
14
15/// A validated, process-independent value-codec identifier.
16///
17/// # Examples
18///
19/// ```
20/// use qubit_codec::ValueCodecId;
21///
22/// let id = ValueCodecId::new("example.u16");
23/// assert_eq!(id.as_str(), "example.u16");
24/// ```
25#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
26pub struct ValueCodecId(
27 /// Validated static identifier, borrowed without allocation.
28 &'static str,
29);
30
31impl ValueCodecId {
32 /// Creates an ID from a static string.
33 ///
34 /// # Parameters
35 ///
36 /// - `value`: Non-empty dot-separated ASCII segments; each starts with a
37 /// letter and continues with letters, digits, or underscores.
38 ///
39 /// # Returns
40 ///
41 /// An ID borrowing the validated static string without allocation.
42 ///
43 /// # Panics
44 ///
45 /// Panics when `value` violates the dot-separated ASCII protocol.
46 #[must_use]
47 pub const fn new(value: &'static str) -> Self {
48 match Self::try_new(value) {
49 Ok(id) => id,
50 Err(_) => panic!("invalid value codec ID"),
51 }
52 }
53
54 /// Validates and creates an ID.
55 ///
56 /// # Parameters
57 ///
58 /// - `value`: Non-empty dot-separated ASCII segments; each starts with a
59 /// letter and continues with letters, digits, or underscores.
60 ///
61 /// # Returns
62 ///
63 /// An ID borrowing the validated static string without allocation.
64 ///
65 /// # Errors
66 ///
67 /// Returns Empty for an empty ID, EmptySegment for leading/trailing or
68 /// consecutive dots, or InvalidSegment for an invalid ASCII segment.
69 pub const fn try_new(value: &'static str) -> Result<Self, ValueCodecIdError> {
70 match validate(value) {
71 Ok(()) => Ok(Self(value)),
72 Err(error) => Err(error),
73 }
74 }
75
76 /// Returns the complete stable ID.
77 ///
78 /// # Returns
79 ///
80 /// The original static string; consuming this Copy ID does not invalidate
81 /// it.
82 #[inline]
83 #[must_use]
84 pub const fn as_str(self) -> &'static str {
85 self.0
86 }
87}
88
89impl Borrow<str> for ValueCodecId {
90 /// Borrows the stable identifier for lookup without allocating.
91 ///
92 /// # Returns
93 ///
94 /// The complete ID string borrowed through this lookup key.
95 #[inline]
96 fn borrow(&self) -> &str {
97 self.0
98 }
99}
100
101/// Validates one dot-separated ASCII identifier in linear time.
102///
103/// # Parameters
104///
105/// - `value`: Candidate identifier, borrowed without allocating.
106///
107/// # Returns
108///
109/// Unit when every segment follows the ASCII protocol.
110///
111/// # Errors
112///
113/// Returns Empty for no bytes, EmptySegment for a dot-delimited empty part,
114/// or InvalidSegment for a non-letter start or unsupported continuation.
115const fn validate(value: &str) -> Result<(), ValueCodecIdError> {
116 let bytes = value.as_bytes();
117 if bytes.is_empty() {
118 return Err(ValueCodecIdError::Empty);
119 }
120 let mut start = 0;
121 let mut index = 0;
122 while index < bytes.len() {
123 if bytes[index] == b'.' {
124 if start == index || index + 1 == bytes.len() {
125 return Err(ValueCodecIdError::EmptySegment);
126 }
127 if let Err(error) = validate_segment(bytes, start, index) {
128 return Err(error);
129 }
130 start = index + 1;
131 }
132 index += 1;
133 }
134 validate_segment(bytes, start, bytes.len())
135}
136
137/// Validates one non-empty ID segment in linear time.
138///
139/// # Parameters
140///
141/// - `bytes`: Complete identifier storage.
142/// - `start`: Inclusive segment boundary, no greater than `end`.
143/// - `end`: Exclusive boundary, no greater than `bytes.len()`.
144///
145/// # Returns
146///
147/// Unit for an ASCII letter followed by ASCII letters, digits, or underscores.
148///
149/// # Errors
150///
151/// Returns InvalidSegment for an empty segment or a disallowed byte.
152///
153/// # Panics
154///
155/// May panic if the caller violates the documented index bounds.
156const fn validate_segment(bytes: &[u8], start: usize, end: usize) -> Result<(), ValueCodecIdError> {
157 if start == end || !bytes[start].is_ascii_alphabetic() {
158 return Err(ValueCodecIdError::InvalidSegment);
159 }
160 let mut index = start + 1;
161 while index < end {
162 let byte = bytes[index];
163 if !(byte.is_ascii_alphanumeric() || byte == b'_') {
164 return Err(ValueCodecIdError::InvalidSegment);
165 }
166 index += 1;
167 }
168 Ok(())
169}