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}