use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub features: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub grace_period_ends_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub warning: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bandwidth_used_bytes: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bandwidth_limit_bytes: Option<i64>,
}
impl ValidationResult {
pub fn has_grace_period_warning(&self) -> bool {
self.grace_period_ends_at.is_some()
}
pub fn has_warning(&self) -> bool {
self.warning.is_some()
}
pub fn has_feature(&self, feature: &str) -> bool {
self.features.iter().any(|f| f == feature)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BindResult {
pub license_id: String,
pub features: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
}
impl BindResult {
pub fn has_feature(&self, feature: &str) -> bool {
self.features.iter().any(|f| f == feature)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureResult {
pub allowed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tier: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeartbeatResult {
pub server_time: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub grace_period_ends_at: Option<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub(crate) struct ServerBindResponse {
pub success: bool,
pub license_id: String,
pub features: Vec<String>,
pub tier: Option<String>,
pub expires_at: Option<String>,
}
impl From<ServerBindResponse> for BindResult {
fn from(resp: ServerBindResponse) -> Self {
Self {
license_id: resp.license_id,
features: resp.features,
tier: resp.tier,
expires_at: resp.expires_at,
}
}
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub(crate) struct ServerReleaseResponse {
pub success: bool,
pub message: String,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub(crate) struct ServerValidateResponse {
pub valid: bool,
pub license_id: Option<String>,
pub features: Option<Vec<String>>,
pub tier: Option<String>,
pub expires_at: Option<String>,
pub grace_period_ends_at: Option<String>,
pub warning: Option<String>,
pub bandwidth_used_bytes: Option<i64>,
pub bandwidth_limit_bytes: Option<i64>,
}
impl From<ServerValidateResponse> for ValidationResult {
fn from(resp: ServerValidateResponse) -> Self {
Self {
features: resp.features.unwrap_or_default(),
tier: resp.tier,
expires_at: resp.expires_at,
grace_period_ends_at: resp.grace_period_ends_at,
warning: resp.warning,
bandwidth_used_bytes: resp.bandwidth_used_bytes,
bandwidth_limit_bytes: resp.bandwidth_limit_bytes,
}
}
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub(crate) struct ServerHeartbeatResponse {
pub success: bool,
pub server_time: String,
}
impl From<ServerHeartbeatResponse> for HeartbeatResult {
fn from(resp: ServerHeartbeatResponse) -> Self {
Self {
server_time: resp.server_time,
grace_period_ends_at: None,
}
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct ServerFeatureResponse {
pub allowed: bool,
pub message: Option<String>,
pub tier: Option<String>,
}
impl From<ServerFeatureResponse> for FeatureResult {
fn from(resp: ServerFeatureResponse) -> Self {
Self {
allowed: resp.allowed,
message: resp.message,
tier: resp.tier,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validation_result_has_feature() {
let result = ValidationResult {
features: vec!["feature_a".to_string(), "feature_b".to_string()],
tier: Some("pro".to_string()),
expires_at: None,
grace_period_ends_at: None,
warning: None,
bandwidth_used_bytes: None,
bandwidth_limit_bytes: None,
};
assert!(result.has_feature("feature_a"));
assert!(result.has_feature("feature_b"));
assert!(!result.has_feature("feature_c"));
}
#[test]
fn validation_result_grace_period_warning() {
let with_grace = ValidationResult {
features: vec![],
tier: None,
expires_at: None,
grace_period_ends_at: Some("2024-12-31T23:59:59Z".to_string()),
warning: Some("Must connect by 2024-12-31".to_string()),
bandwidth_used_bytes: None,
bandwidth_limit_bytes: None,
};
assert!(with_grace.has_grace_period_warning());
assert!(with_grace.has_warning());
let without_grace = ValidationResult {
features: vec![],
tier: None,
expires_at: None,
grace_period_ends_at: None,
warning: None,
bandwidth_used_bytes: None,
bandwidth_limit_bytes: None,
};
assert!(!without_grace.has_grace_period_warning());
assert!(!without_grace.has_warning());
}
#[test]
fn parse_server_bind_response() {
let json = r#"{
"success": true,
"license_id": "550e8400-e29b-41d4-a716-446655440000",
"features": ["feature_a", "feature_b"],
"tier": "pro",
"expires_at": "2025-12-31T23:59:59Z"
}"#;
let resp: ServerBindResponse = serde_json::from_str(json).unwrap();
let result: BindResult = resp.into();
assert_eq!(result.license_id, "550e8400-e29b-41d4-a716-446655440000");
assert_eq!(result.features.len(), 2);
assert_eq!(result.tier, Some("pro".to_string()));
}
#[test]
fn parse_server_validate_response() {
let json = r#"{
"valid": true,
"license_id": "550e8400-e29b-41d4-a716-446655440000",
"features": ["feature_a"],
"tier": "enterprise",
"expires_at": "2025-12-31T23:59:59Z",
"grace_period_ends_at": "2025-01-15T12:00:00Z",
"warning": "System must connect by January 15"
}"#;
let resp: ServerValidateResponse = serde_json::from_str(json).unwrap();
let result: ValidationResult = resp.into();
assert!(result.has_feature("feature_a"));
assert!(result.has_grace_period_warning());
assert!(result.has_warning());
}
}