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 `-`.
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}