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}