quil_rs/validation/
identifier.rs

1//! Types and functions related to validating Quil identifiers
2use std::str::FromStr;
3
4use once_cell::sync::Lazy;
5use regex::Regex;
6use thiserror;
7
8#[cfg(feature = "stubs")]
9use pyo3_stub_gen::derive::gen_stub_pyfunction;
10
11use crate::reserved::ReservedToken;
12
13#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
14pub enum IdentifierValidationError {
15    #[error("{0} is a reserved token")]
16    Reserved(ReservedToken),
17
18    #[error("{0} is not a valid identifier")]
19    Invalid(String),
20}
21
22/// A regex that matches only valid Quil identifiers
23const IDENTIFIER_REGEX_STRING: &str = r"^([A-Za-z_]|[A-Za-z_][A-Za-z0-9\-_]*[A-Za-z0-9_])$";
24
25static IDENTIFIER_REGEX: Lazy<Regex> =
26    Lazy::new(|| Regex::new(IDENTIFIER_REGEX_STRING).expect("regex should be valid"));
27
28/// Returns an error if the given identifier is not a valid Quil Identifier.
29#[cfg_attr(
30    feature = "stubs",
31    gen_stub_pyfunction(module = "quil.validation.identifier")
32)]
33#[cfg_attr(feature = "python", pyo3::pyfunction)]
34pub fn validate_identifier(ident: &str) -> Result<(), IdentifierValidationError> {
35    match IDENTIFIER_REGEX.is_match(ident) {
36        true => Ok(()),
37        false => Err(IdentifierValidationError::Invalid(ident.to_string())),
38    }
39}
40
41/// Returns an error if the given identifier is reserved, or if it is not a valid Quil identifier
42#[cfg_attr(
43    feature = "stubs",
44    gen_stub_pyfunction(module = "quil.validation.identifier")
45)]
46#[cfg_attr(feature = "python", pyo3::pyfunction)]
47pub fn validate_user_identifier(ident: &str) -> Result<(), IdentifierValidationError> {
48    ReservedToken::from_str(ident).map_or(validate_identifier(ident), |t| {
49        Err(IdentifierValidationError::Reserved(t))
50    })
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use rstest::rstest;
57
58    #[rstest]
59    #[case("Good_Ident1f1er-0", true)]
60    #[case("H", true)]
61    #[case("-Cant-start-with-dash", false)]
62    #[case("Cant-end-with-dash-", false)]
63    #[case("1-Cant-start-with-number", false)]
64    fn test_validate_identifier(#[case] input: &str, #[case] ok: bool) {
65        assert_eq!(validate_identifier(input).is_ok(), ok)
66    }
67
68    #[rstest]
69    #[case("Good_Ident1f1er-0", true)]
70    #[case("DEFGATE", false)]
71    #[case("AS", false)]
72    #[case("pi", false)]
73    #[case("PAULI-SUM", false)]
74    #[case("H", false)]
75    #[case("G", true)]
76    fn test_validate_user_identifier(#[case] input: &str, #[case] ok: bool) {
77        assert_eq!(validate_user_identifier(input).is_ok(), ok)
78    }
79}