linguasteg-core 0.2.0

Core domain contracts and pipeline abstractions for LinguaSteg
Documentation
use crate::{CoreError, CoreResult};

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct LanguageTag(String);

impl LanguageTag {
    pub fn new(value: impl Into<String>) -> CoreResult<Self> {
        let value = normalize_identifier(value.into())?;
        Ok(Self(value))
    }

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

impl core::fmt::Display for LanguageTag {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct StrategyId(String);

impl StrategyId {
    pub fn new(value: impl Into<String>) -> CoreResult<Self> {
        let value = normalize_identifier(value.into())?;
        Ok(Self(value))
    }

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

impl core::fmt::Display for StrategyId {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ProviderId(String);

impl ProviderId {
    pub fn new(value: impl Into<String>) -> CoreResult<Self> {
        let value = normalize_identifier(value.into())?;
        Ok(Self(value))
    }

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

impl core::fmt::Display for ProviderId {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ModelId(String);

impl ModelId {
    pub fn new(value: impl Into<String>) -> CoreResult<Self> {
        let value = normalize_identifier(value.into())?;
        Ok(Self(value))
    }

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

impl core::fmt::Display for ModelId {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct StyleProfileId(String);

impl StyleProfileId {
    pub fn new(value: impl Into<String>) -> CoreResult<Self> {
        let value = normalize_identifier(value.into())?;
        Ok(Self(value))
    }

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

impl core::fmt::Display for StyleProfileId {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TemplateId(String);

impl TemplateId {
    pub fn new(value: impl Into<String>) -> CoreResult<Self> {
        let value = normalize_identifier(value.into())?;
        Ok(Self(value))
    }

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

impl core::fmt::Display for TemplateId {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SlotId(String);

impl SlotId {
    pub fn new(value: impl Into<String>) -> CoreResult<Self> {
        let value = normalize_identifier(value.into())?;
        Ok(Self(value))
    }

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

impl core::fmt::Display for SlotId {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.as_str())
    }
}

fn normalize_identifier(value: String) -> CoreResult<String> {
    let normalized = value.trim().to_ascii_lowercase();
    let is_valid = !normalized.is_empty()
        && normalized
            .bytes()
            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-');

    if is_valid {
        Ok(normalized)
    } else {
        Err(CoreError::InvalidIdentifier(value))
    }
}

#[cfg(test)]
mod tests {
    use super::{LanguageTag, ModelId, ProviderId, SlotId, StrategyId, StyleProfileId, TemplateId};

    #[test]
    fn language_tag_normalizes_ascii_input() {
        let tag = LanguageTag::new(" FA ").expect("tag should normalize");
        assert_eq!(tag.as_str(), "fa");
    }

    #[test]
    fn strategy_id_rejects_invalid_characters() {
        let strategy = StrategyId::new("synonym_v1");
        assert!(strategy.is_err());
    }

    #[test]
    fn provider_id_normalizes_ascii_input() {
        let provider = ProviderId::new(" OpenAI ").expect("provider should normalize");
        assert_eq!(provider.as_str(), "openai");
    }

    #[test]
    fn model_id_accepts_dash_and_digits() {
        let model = ModelId::new("gpt-4o-mini").expect("model id should be valid");
        assert_eq!(model.as_str(), "gpt-4o-mini");
    }

    #[test]
    fn style_profile_id_normalizes_ascii_input() {
        let profile = StyleProfileId::new(" FA-Formal ").expect("profile id should normalize");
        assert_eq!(profile.as_str(), "fa-formal");
    }

    #[test]
    fn template_id_normalizes_ascii_input() {
        let template = TemplateId::new(" FA-Template-01 ").expect("template id should normalize");
        assert_eq!(template.as_str(), "fa-template-01");
    }

    #[test]
    fn slot_id_normalizes_ascii_input() {
        let slot = SlotId::new(" Subject-Main ").expect("slot id should normalize");
        assert_eq!(slot.as_str(), "subject-main");
    }
}