use super::{ValidationError, ValidationResult};
use crate::model::v1::{
DeploymentPolicy, DeploymentPriority, DeploymentState, ModelDeployment, ModelDeploymentStatus,
ModelType,
};
pub fn validate_model_deployment(deployment: &ModelDeployment) -> ValidationResult<()> {
if deployment.deployment_id.is_empty() {
return Err(ValidationError::MissingField("deployment_id".to_string()));
}
if deployment.model_id.is_empty() {
return Err(ValidationError::MissingField("model_id".to_string()));
}
if deployment.model_version.is_empty() {
return Err(ValidationError::MissingField("model_version".to_string()));
}
if deployment.model_type == ModelType::Unspecified as i32 {
return Err(ValidationError::InvalidValue(
"model_type must be specified".to_string(),
));
}
if deployment.model_url.is_empty() {
return Err(ValidationError::MissingField("model_url".to_string()));
}
if !deployment.model_url.contains("://") {
return Err(ValidationError::InvalidValue(
"model_url must be a valid URL with scheme".to_string(),
));
}
if deployment.checksum_sha256.is_empty() {
return Err(ValidationError::MissingField("checksum_sha256".to_string()));
}
if deployment.checksum_sha256.len() != 64 {
return Err(ValidationError::InvalidValue(format!(
"checksum_sha256 must be 64 hex characters, got {}",
deployment.checksum_sha256.len()
)));
}
if !deployment
.checksum_sha256
.chars()
.all(|c| c.is_ascii_hexdigit())
{
return Err(ValidationError::InvalidValue(
"checksum_sha256 must contain only hex characters".to_string(),
));
}
if deployment.file_size_bytes == 0 {
return Err(ValidationError::InvalidValue(
"file_size_bytes must be non-zero".to_string(),
));
}
if deployment.target_platforms.is_empty() {
return Err(ValidationError::MissingField(
"target_platforms (at least one required)".to_string(),
));
}
if deployment.deployment_policy == DeploymentPolicy::Unspecified as i32 {
return Err(ValidationError::InvalidValue(
"deployment_policy must be specified".to_string(),
));
}
if deployment.priority == DeploymentPriority::Unspecified as i32 {
return Err(ValidationError::InvalidValue(
"priority must be specified".to_string(),
));
}
if deployment.deployed_at.is_none() {
return Err(ValidationError::MissingField("deployed_at".to_string()));
}
if deployment.deployed_by.is_empty() {
return Err(ValidationError::MissingField("deployed_by".to_string()));
}
Ok(())
}
pub fn validate_model_deployment_status(status: &ModelDeploymentStatus) -> ValidationResult<()> {
if status.deployment_id.is_empty() {
return Err(ValidationError::MissingField("deployment_id".to_string()));
}
if status.platform_id.is_empty() {
return Err(ValidationError::MissingField("platform_id".to_string()));
}
if status.state == DeploymentState::Unspecified as i32 {
return Err(ValidationError::InvalidValue(
"state must be specified".to_string(),
));
}
if status.progress_percent > 100 {
return Err(ValidationError::InvalidValue(format!(
"progress_percent {} must be between 0 and 100",
status.progress_percent
)));
}
if status.updated_at.is_none() {
return Err(ValidationError::MissingField("updated_at".to_string()));
}
if status.state == DeploymentState::Failed as i32 && status.error_message.is_empty() {
return Err(ValidationError::MissingField(
"error_message (required when state is FAILED)".to_string(),
));
}
if (status.state == DeploymentState::Complete as i32
|| status.state == DeploymentState::Verifying as i32)
&& status.downloaded_hash.is_empty()
{
return Err(ValidationError::MissingField(
"downloaded_hash (required for COMPLETE or VERIFYING state)".to_string(),
));
}
if !status.downloaded_hash.is_empty() {
if status.downloaded_hash.len() != 64 {
return Err(ValidationError::InvalidValue(format!(
"downloaded_hash must be 64 hex characters, got {}",
status.downloaded_hash.len()
)));
}
if !status
.downloaded_hash
.chars()
.all(|c| c.is_ascii_hexdigit())
{
return Err(ValidationError::InvalidValue(
"downloaded_hash must contain only hex characters".to_string(),
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::v1::Timestamp;
fn valid_model_deployment() -> ModelDeployment {
ModelDeployment {
deployment_id: "deploy-2025-001".to_string(),
model_id: "yolov8-poi-v2.1".to_string(),
model_version: "2.1.0".to_string(),
model_type: ModelType::Detector as i32,
model_url: "https://models.example.com/yolov8-poi-v2.1.onnx".to_string(),
checksum_sha256: "a".repeat(64), file_size_bytes: 45_000_000,
target_platforms: vec!["Alpha-3".to_string(), "Bravo-1".to_string()],
deployment_policy: DeploymentPolicy::Rolling as i32,
priority: DeploymentPriority::Normal as i32,
deployed_at: Some(Timestamp {
seconds: 1702000000,
nanos: 0,
}),
deployed_by: "MLOps-Pipeline".to_string(),
rollback_model_id: String::new(),
metadata: None,
}
}
fn valid_deployment_status() -> ModelDeploymentStatus {
ModelDeploymentStatus {
deployment_id: "deploy-2025-001".to_string(),
platform_id: "Alpha-3".to_string(),
state: DeploymentState::Downloading as i32,
progress_percent: 45,
error_message: String::new(),
updated_at: Some(Timestamp {
seconds: 1702000100,
nanos: 0,
}),
downloaded_hash: String::new(),
previous_version: "2.0.0".to_string(),
}
}
#[test]
fn test_valid_model_deployment() {
let deployment = valid_model_deployment();
assert!(validate_model_deployment(&deployment).is_ok());
}
#[test]
fn test_missing_deployment_id() {
let mut deployment = valid_model_deployment();
deployment.deployment_id = String::new();
let err = validate_model_deployment(&deployment).unwrap_err();
assert!(matches!(err, ValidationError::MissingField(f) if f == "deployment_id"));
}
#[test]
fn test_missing_model_id() {
let mut deployment = valid_model_deployment();
deployment.model_id = String::new();
let err = validate_model_deployment(&deployment).unwrap_err();
assert!(matches!(err, ValidationError::MissingField(f) if f == "model_id"));
}
#[test]
fn test_unspecified_model_type() {
let mut deployment = valid_model_deployment();
deployment.model_type = ModelType::Unspecified as i32;
let err = validate_model_deployment(&deployment).unwrap_err();
assert!(matches!(err, ValidationError::InvalidValue(_)));
}
#[test]
fn test_invalid_model_url() {
let mut deployment = valid_model_deployment();
deployment.model_url = "not-a-valid-url".to_string();
let err = validate_model_deployment(&deployment).unwrap_err();
assert!(matches!(err, ValidationError::InvalidValue(_)));
}
#[test]
fn test_invalid_checksum_length() {
let mut deployment = valid_model_deployment();
deployment.checksum_sha256 = "abc123".to_string(); let err = validate_model_deployment(&deployment).unwrap_err();
assert!(matches!(err, ValidationError::InvalidValue(_)));
}
#[test]
fn test_invalid_checksum_chars() {
let mut deployment = valid_model_deployment();
deployment.checksum_sha256 = "g".repeat(64); let err = validate_model_deployment(&deployment).unwrap_err();
assert!(matches!(err, ValidationError::InvalidValue(_)));
}
#[test]
fn test_zero_file_size() {
let mut deployment = valid_model_deployment();
deployment.file_size_bytes = 0;
let err = validate_model_deployment(&deployment).unwrap_err();
assert!(matches!(err, ValidationError::InvalidValue(_)));
}
#[test]
fn test_empty_target_platforms() {
let mut deployment = valid_model_deployment();
deployment.target_platforms = vec![];
let err = validate_model_deployment(&deployment).unwrap_err();
assert!(matches!(err, ValidationError::MissingField(_)));
}
#[test]
fn test_unspecified_deployment_policy() {
let mut deployment = valid_model_deployment();
deployment.deployment_policy = DeploymentPolicy::Unspecified as i32;
let err = validate_model_deployment(&deployment).unwrap_err();
assert!(matches!(err, ValidationError::InvalidValue(_)));
}
#[test]
fn test_missing_deployed_at() {
let mut deployment = valid_model_deployment();
deployment.deployed_at = None;
let err = validate_model_deployment(&deployment).unwrap_err();
assert!(matches!(err, ValidationError::MissingField(f) if f == "deployed_at"));
}
#[test]
fn test_valid_deployment_status() {
let status = valid_deployment_status();
assert!(validate_model_deployment_status(&status).is_ok());
}
#[test]
fn test_status_missing_deployment_id() {
let mut status = valid_deployment_status();
status.deployment_id = String::new();
let err = validate_model_deployment_status(&status).unwrap_err();
assert!(matches!(err, ValidationError::MissingField(f) if f == "deployment_id"));
}
#[test]
fn test_missing_platform_id() {
let mut status = valid_deployment_status();
status.platform_id = String::new();
let err = validate_model_deployment_status(&status).unwrap_err();
assert!(matches!(err, ValidationError::MissingField(f) if f == "platform_id"));
}
#[test]
fn test_unspecified_state() {
let mut status = valid_deployment_status();
status.state = DeploymentState::Unspecified as i32;
let err = validate_model_deployment_status(&status).unwrap_err();
assert!(matches!(err, ValidationError::InvalidValue(_)));
}
#[test]
fn test_invalid_progress_percent() {
let mut status = valid_deployment_status();
status.progress_percent = 150; let err = validate_model_deployment_status(&status).unwrap_err();
assert!(matches!(err, ValidationError::InvalidValue(_)));
}
#[test]
fn test_missing_updated_at() {
let mut status = valid_deployment_status();
status.updated_at = None;
let err = validate_model_deployment_status(&status).unwrap_err();
assert!(matches!(err, ValidationError::MissingField(f) if f == "updated_at"));
}
#[test]
fn test_failed_state_requires_error_message() {
let mut status = valid_deployment_status();
status.state = DeploymentState::Failed as i32;
status.error_message = String::new();
let err = validate_model_deployment_status(&status).unwrap_err();
assert!(matches!(err, ValidationError::MissingField(_)));
}
#[test]
fn test_complete_state_requires_hash() {
let mut status = valid_deployment_status();
status.state = DeploymentState::Complete as i32;
status.downloaded_hash = String::new();
let err = validate_model_deployment_status(&status).unwrap_err();
assert!(matches!(err, ValidationError::MissingField(_)));
}
#[test]
fn test_valid_complete_status() {
let mut status = valid_deployment_status();
status.state = DeploymentState::Complete as i32;
status.downloaded_hash = "a".repeat(64);
status.progress_percent = 100;
assert!(validate_model_deployment_status(&status).is_ok());
}
#[test]
fn test_invalid_downloaded_hash_length() {
let mut status = valid_deployment_status();
status.state = DeploymentState::Complete as i32;
status.downloaded_hash = "abc123".to_string(); let err = validate_model_deployment_status(&status).unwrap_err();
assert!(matches!(err, ValidationError::InvalidValue(_)));
}
}