kmp-domain 0.1.5

Domain model of the Kernel Memory Protocol: aggregates, value objects, repositories and projections, with no IO
Documentation
use crate::{DomainError, RelationExplanation};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TemporalCoordinate {
    dimension: String,
    scope_id: String,
    sequence: Option<u32>,
    rank: Option<u32>,
    occurred_at: Option<String>,
    observed_at: Option<String>,
    ingested_at: Option<String>,
    valid_from: Option<String>,
    valid_until: Option<String>,
}

impl TemporalCoordinate {
    pub fn from_relation_explanation(
        explanation: &RelationExplanation,
    ) -> Result<Option<Self>, DomainError> {
        let Some(dimension) = normalize_optional(explanation.dimension()) else {
            return Ok(None);
        };
        let Some(scope_id) = normalize_optional(explanation.scope_id()) else {
            return Ok(None);
        };

        Ok(Some(Self {
            dimension,
            scope_id,
            sequence: explanation.sequence(),
            rank: explanation.rank(),
            occurred_at: normalize_optional(explanation.occurred_at()),
            observed_at: normalize_optional(explanation.observed_at()),
            ingested_at: normalize_optional(explanation.ingested_at()),
            valid_from: normalize_optional(explanation.valid_from()),
            valid_until: normalize_optional(explanation.valid_until()),
        }))
    }

    pub fn cursor_time(value: impl Into<String>) -> Result<Self, DomainError> {
        let value = normalize_required(value.into(), "temporal cursor time")?;
        Ok(Self {
            dimension: String::new(),
            scope_id: String::new(),
            sequence: None,
            rank: None,
            occurred_at: Some(value),
            observed_at: None,
            ingested_at: None,
            valid_from: None,
            valid_until: None,
        })
    }

    pub fn cursor_sequence(value: u32) -> Result<Self, DomainError> {
        if value == 0 {
            return Err(DomainError::InvalidState(
                "temporal cursor sequence must be greater than zero".to_string(),
            ));
        }
        Ok(Self {
            dimension: String::new(),
            scope_id: String::new(),
            sequence: Some(value),
            rank: None,
            occurred_at: None,
            observed_at: None,
            ingested_at: None,
            valid_from: None,
            valid_until: None,
        })
    }

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

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

    pub fn sequence(&self) -> Option<u32> {
        self.sequence
    }

    pub fn rank(&self) -> Option<u32> {
        self.rank
    }

    pub fn occurred_at(&self) -> Option<&str> {
        self.occurred_at.as_deref()
    }

    pub fn observed_at(&self) -> Option<&str> {
        self.observed_at.as_deref()
    }

    pub fn ingested_at(&self) -> Option<&str> {
        self.ingested_at.as_deref()
    }

    pub fn valid_from(&self) -> Option<&str> {
        self.valid_from.as_deref()
    }

    pub fn valid_until(&self) -> Option<&str> {
        self.valid_until.as_deref()
    }
}

fn normalize_optional(value: Option<&str>) -> Option<String> {
    value
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
}

fn normalize_required(value: String, field: &'static str) -> Result<String, DomainError> {
    let value = value.trim();
    if value.is_empty() {
        Err(DomainError::EmptyValue(field))
    } else {
        Ok(value.to_string())
    }
}