use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::{PendingHostEventPublication, SessionStoreError, SessionStoreResult};
pub const MODEL_SELECTION_OPERATION: &str = "model_selection.select";
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DurableModelSelection {
pub authority_binding: String,
pub selected_profile: String,
pub model_id: String,
pub revision: u64,
pub updated_at: DateTime<Utc>,
}
impl starweaver_core::VersionedRecord for DurableModelSelection {
const SCHEMA: &'static str = "starweaver.session.model_selection";
}
impl DurableModelSelection {
pub fn validate(&self) -> SessionStoreResult<()> {
require_non_empty("model selection authority binding", &self.authority_binding)?;
require_non_empty("model selection profile", &self.selected_profile)?;
require_non_empty("model selection model id", &self.model_id)?;
if self.revision == 0 {
return Err(SessionStoreError::Failed(
"model selection revision must be greater than zero".to_string(),
));
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InitializeModelSelection {
pub authority_binding: String,
pub selected_profile: String,
pub model_id: String,
pub initialized_at: DateTime<Utc>,
}
impl InitializeModelSelection {
pub fn validate(&self) -> SessionStoreResult<()> {
require_non_empty("model selection authority binding", &self.authority_binding)?;
require_non_empty("model selection profile", &self.selected_profile)?;
require_non_empty("model selection model id", &self.model_id)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct MutationReceipt {
pub receipt_id: String,
pub idempotency_key: String,
pub fingerprint: String,
pub operation: String,
pub state: String,
pub target_ref: String,
pub reconciliation_required: bool,
pub replayed: bool,
pub created_at: DateTime<Utc>,
}
impl starweaver_core::VersionedRecord for MutationReceipt {
const SCHEMA: &'static str = "starweaver.session.mutation_receipt";
}
impl MutationReceipt {
#[must_use]
pub fn replayed_projection(&self) -> Self {
let mut projection = self.clone();
projection.replayed = true;
projection
}
pub fn validate(&self) -> SessionStoreResult<()> {
require_non_empty("mutation receipt id", &self.receipt_id)?;
require_non_empty("mutation receipt idempotency key", &self.idempotency_key)?;
require_non_empty("mutation receipt fingerprint", &self.fingerprint)?;
require_non_empty("mutation receipt operation", &self.operation)?;
require_non_empty("mutation receipt state", &self.state)?;
require_non_empty("mutation receipt target", &self.target_ref)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SelectModel {
pub authority_binding: String,
pub selected_profile: String,
pub model_id: String,
pub idempotency_key: String,
pub command_fingerprint: String,
pub occurred_at: DateTime<Utc>,
pub host_event_publication: Option<PendingHostEventPublication>,
}
impl SelectModel {
pub fn validate(&self) -> SessionStoreResult<()> {
require_non_empty("model selection authority binding", &self.authority_binding)?;
require_non_empty("model selection profile", &self.selected_profile)?;
require_non_empty("model selection model id", &self.model_id)?;
require_non_empty("model selection idempotency key", &self.idempotency_key)?;
require_non_empty(
"model selection command fingerprint",
&self.command_fingerprint,
)?;
if let Some(publication) = &self.host_event_publication {
publication.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ModelSelectionMutationReceipt {
pub selection: DurableModelSelection,
pub receipt: MutationReceipt,
}
impl starweaver_core::VersionedRecord for ModelSelectionMutationReceipt {
const SCHEMA: &'static str = "starweaver.session.model_selection_mutation_receipt";
}
impl ModelSelectionMutationReceipt {
#[must_use]
pub fn replayed_projection(&self) -> Self {
Self {
selection: self.selection.clone(),
receipt: self.receipt.replayed_projection(),
}
}
pub fn validate(&self) -> SessionStoreResult<()> {
self.selection.validate()?;
self.receipt.validate()?;
if self.receipt.target_ref != self.selection.authority_binding {
return Err(SessionStoreError::Conflict(
"model selection receipt target does not match selection authority".to_string(),
));
}
if self.receipt.operation != MODEL_SELECTION_OPERATION {
return Err(SessionStoreError::Conflict(
"model selection receipt operation is invalid".to_string(),
));
}
Ok(())
}
}
fn require_non_empty(label: &str, value: &str) -> SessionStoreResult<()> {
if value.is_empty() {
return Err(SessionStoreError::Failed(format!(
"{label} cannot be empty"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use chrono::Utc;
use super::{DurableModelSelection, ModelSelectionMutationReceipt, MutationReceipt};
#[test]
fn replay_projection_does_not_mutate_durable_original() {
let original = ModelSelectionMutationReceipt {
selection: DurableModelSelection {
authority_binding: "authority-a".to_string(),
selected_profile: "coding".to_string(),
model_id: "model-a".to_string(),
revision: 2,
updated_at: Utc::now(),
},
receipt: MutationReceipt {
receipt_id: "receipt-a".to_string(),
idempotency_key: "key-a".to_string(),
fingerprint: "sha256:a".to_string(),
operation: super::MODEL_SELECTION_OPERATION.to_string(),
state: "applied".to_string(),
target_ref: "authority-a".to_string(),
reconciliation_required: false,
replayed: false,
created_at: Utc::now(),
},
};
let replay = original.replayed_projection();
assert!(replay.receipt.replayed);
assert!(!original.receipt.replayed);
assert_eq!(replay.selection, original.selection);
}
}