Skip to main content

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}