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 `-`.
26#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct ProviderName(String);
28
29impl ProviderName {
30 /// Creates a normalized provider name.
31 ///
32 /// # Parameters
33 /// - `name`: Raw provider id, alias, or selector.
34 ///
35 /// # Returns
36 /// Normalized provider name.
37 ///
38 /// # Errors
39 /// Returns [`ProviderRegistryError::EmptyProviderName`] when `name` is empty
40 /// after trimming. Returns [`ProviderRegistryError::InvalidProviderName`]
41 /// when `name` is non-ASCII or contains unsupported characters.
42 #[inline]
43 pub fn new(name: &str) -> Result<Self, ProviderRegistryError> {
44 let trimmed = name.trim();
45 if trimmed.is_empty() {
46 return Err(ProviderRegistryError::EmptyProviderName);
47 }
48 if !trimmed.is_ascii() {
49 return Err(invalid_provider_name(
50 trimmed,
51 "provider names must be ASCII",
52 ));
53 }
54 if !trimmed.bytes().all(is_allowed_provider_name_byte) {
55 return Err(invalid_provider_name(
56 trimmed,
57 "provider names may contain only ASCII letters, digits, '.', '_' or '-'",
58 ));
59 }
60 Ok(Self(trimmed.to_ascii_lowercase()))
61 }
62
63 /// Gets the normalized provider name.
64 ///
65 /// # Returns
66 /// Normalized provider name string.
67 #[inline]
68 pub fn as_str(&self) -> &str {
69 &self.0
70 }
71}
72
73impl AsRef<str> for ProviderName {
74 #[inline]
75 fn as_ref(&self) -> &str {
76 self.as_str()
77 }
78}
79
80impl Display for ProviderName {
81 #[inline]
82 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
83 formatter.write_str(self.as_str())
84 }
85}
86
87impl FromStr for ProviderName {
88 type Err = ProviderRegistryError;
89
90 #[inline]
91 fn from_str(name: &str) -> Result<Self, Self::Err> {
92 Self::new(name)
93 }
94}
95
96/// Tells whether one ASCII byte is allowed in a provider name.
97///
98/// # Parameters
99/// - `byte`: Byte to validate.
100///
101/// # Returns
102/// `true` when the byte is accepted by the provider-name grammar.
103fn is_allowed_provider_name_byte(byte: u8) -> bool {
104 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')
105}
106
107/// Builds an invalid provider-name error.
108///
109/// # Parameters
110/// - `name`: Invalid provider name after trimming.
111/// - `reason`: Human-readable validation failure reason.
112///
113/// # Returns
114/// Invalid provider-name error.
115fn invalid_provider_name(name: &str, reason: &str) -> ProviderRegistryError {
116 ProviderRegistryError::InvalidProviderName {
117 name: name.to_owned(),
118 reason: reason.to_owned(),
119 }
120}