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(
51                trimmed,
52                "provider names must be ASCII",
53            ));
54        }
55        if !trimmed.bytes().all(is_allowed_provider_name_byte) {
56            return Err(invalid_provider_name(
57                trimmed,
58                "provider names may contain only ASCII letters, digits, '_' or '-'",
59            ));
60        }
61        let bytes = trimmed.as_bytes();
62        let first = bytes[0];
63        let last = bytes[bytes.len() - 1];
64        if !first.is_ascii_alphanumeric() {
65            return Err(invalid_provider_name(
66                trimmed,
67                "provider names must start with an ASCII letter or digit",
68            ));
69        }
70        if !last.is_ascii_alphanumeric() {
71            return Err(invalid_provider_name(
72                trimmed,
73                "provider names must end with an ASCII letter or digit",
74            ));
75        }
76        if has_consecutive_separators(trimmed) {
77            return Err(invalid_provider_name(
78                trimmed,
79                "provider names must not contain consecutive separators",
80            ));
81        }
82        Ok(Self(trimmed.to_ascii_lowercase()))
83    }
84
85    /// Gets the normalized provider name.
86    ///
87    /// # Returns
88    /// Normalized provider name string.
89    #[inline]
90    pub fn as_str(&self) -> &str {
91        &self.0
92    }
93}
94
95impl AsRef<str> for ProviderName {
96    #[inline]
97    fn as_ref(&self) -> &str {
98        self.as_str()
99    }
100}
101
102impl Display for ProviderName {
103    #[inline]
104    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
105        formatter.write_str(self.as_str())
106    }
107}
108
109impl FromStr for ProviderName {
110    type Err = ProviderRegistryError;
111
112    #[inline]
113    fn from_str(name: &str) -> Result<Self, Self::Err> {
114        Self::new(name)
115    }
116}
117
118/// Tells whether one ASCII byte is allowed in a provider name.
119///
120/// # Parameters
121/// - `byte`: Byte to validate.
122///
123/// # Returns
124/// `true` when the byte is accepted by the provider-name grammar.
125fn is_allowed_provider_name_byte(byte: u8) -> bool {
126    byte.is_ascii_alphanumeric() || is_separator_provider_name_byte(byte)
127}
128
129/// Tells whether one ASCII byte is a provider-name separator.
130///
131/// # Parameters
132/// - `byte`: Byte to validate.
133///
134/// # Returns
135/// `true` when the byte is a provider-name separator.
136fn is_separator_provider_name_byte(byte: u8) -> bool {
137    matches!(byte, b'_' | b'-')
138}
139
140/// Tells whether a provider name contains consecutive separators.
141///
142/// # Parameters
143/// - `name`: Provider name after trimming and character validation.
144///
145/// # Returns
146/// `true` when any two adjacent bytes are separators.
147fn has_consecutive_separators(name: &str) -> bool {
148    name.as_bytes().windows(2).any(|bytes| {
149        is_separator_provider_name_byte(bytes[0]) && is_separator_provider_name_byte(bytes[1])
150    })
151}
152
153/// Builds an invalid provider-name error.
154///
155/// # Parameters
156/// - `name`: Invalid provider name after trimming.
157/// - `reason`: Human-readable validation failure reason.
158///
159/// # Returns
160/// Invalid provider-name error.
161fn invalid_provider_name(name: &str, reason: &str) -> ProviderRegistryError {
162    ProviderRegistryError::InvalidProviderName {
163        name: name.to_owned(),
164        reason: reason.to_owned(),
165    }
166}