main-spex 0.19.1

AI-first development framework that turns specifications into deterministic, verifiable code
use std::fmt;
use std::sync::LazyLock;

use derive_more::Display;
use regex::Regex;
use serde::{Deserialize, Serialize};

#[cfg(test)]
#[path = "id_test.rs"]
mod id_test;

const ID_PATTERN: &str = "^[a-z0-9]+(-[a-z0-9]+)*$";
const ID_MAX_LENGTH: usize = 40;

static ID: LazyLock<Regex> = LazyLock::new(|| Regex::new(ID_PATTERN).expect("valid id regex"));

#[derive(Debug, PartialEq, Eq)]
pub struct InvalidId {
    kind: &'static str,
    value: String,
}

impl fmt::Display for InvalidId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "invalid {} `{}`: use lowercase letters and digits separated by single hyphens, at most {ID_MAX_LENGTH} characters",
            self.kind, self.value
        )
    }
}

impl std::error::Error for InvalidId {}

fn validate_id(kind: &'static str, value: &str) -> Result<(), InvalidId> {
    if value.len() <= ID_MAX_LENGTH && ID.is_match(value) {
        Ok(())
    } else {
        Err(InvalidId {
            kind,
            value: value.to_string(),
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, Deserialize, Serialize)]
#[serde(try_from = "String")]
pub struct SpecId(String);

impl SpecId {
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl TryFrom<String> for SpecId {
    type Error = InvalidId;

    fn try_from(value: String) -> Result<Self, InvalidId> {
        validate_id("spec id", &value)?;
        Ok(Self(value))
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, Deserialize, Serialize)]
#[serde(try_from = "String")]
pub struct AcId(String);

impl AcId {
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl TryFrom<String> for AcId {
    type Error = InvalidId;

    fn try_from(value: String) -> Result<Self, InvalidId> {
        validate_id("acceptance criterion id", &value)?;
        Ok(Self(value))
    }
}