Skip to main content

qubit_spi/
provider_name.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Strongly typed provider names.
11
12use std::fmt::{
13    Display,
14    Formatter,
15    Result as FmtResult,
16};
17use std::str::FromStr;
18
19use crate::ProviderRegistryError;
20
21/// Stable provider id or alias accepted by a registry.
22///
23/// Provider names are normalized by trimming surrounding whitespace and folding
24/// ASCII letters to lowercase. Valid names may contain ASCII letters, digits,
25/// `_`, and `-`, must start and end with an ASCII letter or digit, and must not
26/// contain consecutive separators.
27#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
28pub struct ProviderName(String);
29
30impl ProviderName {
31    /// Creates a normalized provider name.
32    ///
33    /// # Parameters
34    /// - `name`: Raw provider id, alias, or selector.
35    ///
36    /// # Returns
37    /// Normalized provider name.
38    ///
39    /// # Errors
40    /// Returns [`ProviderRegistryError::EmptyProviderName`] when `name` is empty
41    /// after trimming. Returns [`ProviderRegistryError::InvalidProviderName`]
42    /// when `name` is non-ASCII or contains unsupported characters.
43    #[inline]
44    pub fn new(name: &str) -> Result<Self, ProviderRegistryError> {
45        let trimmed = name.trim();
46        if trimmed.is_empty() {
47            return Err(ProviderRegistryError::EmptyProviderName);
48        }
49        if !trimmed.is_ascii() {
50            return Err(invalid_provider_name(trimmed, "provider names must be ASCII"));
51        }
52        if !trimmed.bytes().all(is_allowed_provider_name_byte) {
53            return Err(invalid_provider_name(
54                trimmed,
55                "provider names may contain only ASCII letters, digits, '_' or '-'",
56            ));
57        }
58        let bytes = trimmed.as_bytes();
59        let first = bytes[0];
60        let last = bytes[bytes.len() - 1];
61        if !first.is_ascii_alphanumeric() {
62            return Err(invalid_provider_name(
63                trimmed,
64                "provider names must start with an ASCII letter or digit",
65            ));
66        }
67        if !last.is_ascii_alphanumeric() {
68            return Err(invalid_provider_name(
69                trimmed,
70                "provider names must end with an ASCII letter or digit",
71            ));
72        }
73        if has_consecutive_separators(trimmed) {
74            return Err(invalid_provider_name(
75                trimmed,
76                "provider names must not contain consecutive separators",
77            ));
78        }
79        Ok(Self(trimmed.to_ascii_lowercase()))
80    }
81
82    /// Gets the normalized provider name.
83    ///
84    /// # Returns
85    /// Normalized provider name string.
86    #[inline]
87    pub fn as_str(&self) -> &str {
88        &self.0
89    }
90}
91
92impl AsRef<str> for ProviderName {
93    #[inline]
94    fn as_ref(&self) -> &str {
95        self.as_str()
96    }
97}
98
99impl Display for ProviderName {
100    #[inline]
101    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
102        formatter.write_str(self.as_str())
103    }
104}
105
106impl FromStr for ProviderName {
107    type Err = ProviderRegistryError;
108
109    #[inline]
110    fn from_str(name: &str) -> Result<Self, Self::Err> {
111        Self::new(name)
112    }
113}
114
115/// Tells whether one ASCII byte is allowed in a provider name.
116///
117/// # Parameters
118/// - `byte`: Byte to validate.
119///
120/// # Returns
121/// `true` when the byte is accepted by the provider-name grammar.
122fn is_allowed_provider_name_byte(byte: u8) -> bool {
123    byte.is_ascii_alphanumeric() || is_separator_provider_name_byte(byte)
124}
125
126/// Tells whether one ASCII byte is a provider-name separator.
127///
128/// # Parameters
129/// - `byte`: Byte to validate.
130///
131/// # Returns
132/// `true` when the byte is a provider-name separator.
133fn is_separator_provider_name_byte(byte: u8) -> bool {
134    matches!(byte, b'_' | b'-')
135}
136
137/// Tells whether a provider name contains consecutive separators.
138///
139/// # Parameters
140/// - `name`: Provider name after trimming and character validation.
141///
142/// # Returns
143/// `true` when any two adjacent bytes are separators.
144fn has_consecutive_separators(name: &str) -> bool {
145    name.as_bytes()
146        .windows(2)
147        .any(|bytes| is_separator_provider_name_byte(bytes[0]) && is_separator_provider_name_byte(bytes[1]))
148}
149
150/// Builds an invalid provider-name error.
151///
152/// # Parameters
153/// - `name`: Invalid provider name after trimming.
154/// - `reason`: Human-readable validation failure reason.
155///
156/// # Returns
157/// Invalid provider-name error.
158fn invalid_provider_name(name: &str, reason: &str) -> ProviderRegistryError {
159    ProviderRegistryError::InvalidProviderName {
160        name: name.to_owned(),
161        reason: reason.to_owned(),
162    }
163}