use serde::{Deserialize, Serialize};
use super::{require_schema, validate_id};
use crate::{UtcTimestamp, ValidationError};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum NetworkV1 {
Org { channel: String },
}
impl NetworkV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
match self {
Self::Org { channel } => validate_channel(channel),
}
}
}
fn validate_channel(channel: &str) -> Result<(), ValidationError> {
let mut parts = channel.splitn(3, '/');
let org = parts.next().and_then(|part| part.strip_prefix("org:"));
let vault = parts.next().and_then(|part| part.strip_prefix("vault:"));
let name = parts.next();
match (org, vault, name) {
(Some(org), Some(vault), Some(name)) => {
validate_id(org, "channel org")?;
super::validate_vault_id(vault)?;
validate_id(name, "channel name")
}
_ => Err(ValidationError::new(
"channel must be org:<org>/vault:<vault>/<name>",
)),
}
}
pub const ENGINE_OPTIN_SCHEMA: &str = "leanctx.hive.instance-optin/v1";
pub const ENGINE_OPTIN_STOP_SCHEMA: &str = "leanctx.hive.instance-optin-stop/v1";
pub const DEFAULT_DAILY_TASKS: u32 = 5;
pub const MAX_DAILY_TASKS: u32 = 100;
pub const MAX_OPTIN_VAULTS: usize = 200;
pub const MAX_OPTIN_NETWORKS: usize = 200;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct DailyBudgetV1 {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tasks: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usd_micros: Option<u64>,
}
impl Default for DailyBudgetV1 {
fn default() -> Self {
Self {
tasks: Some(DEFAULT_DAILY_TASKS),
tokens: None,
usd_micros: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DailyUsageV1 {
pub day: String,
pub tasks: u32,
pub tokens: u64,
pub usd_micros: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineOptinV1 {
pub schema: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub optin_id: Option<String>,
pub active: bool,
#[serde(default)]
pub vaults: Vec<String>,
#[serde(default)]
pub networks: Vec<NetworkV1>,
pub daily_budget: DailyBudgetV1,
pub used_today: DailyUsageV1,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stopped_at: Option<UtcTimestamp>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<UtcTimestamp>,
}
fn day_ok(day: &str) -> bool {
let b = day.as_bytes();
b.len() == 10
&& b[4] == b'-'
&& b[7] == b'-'
&& [0..4, 5..7, 8..10]
.iter()
.all(|r| b[r.clone()].iter().all(u8::is_ascii_digit))
}
impl EngineOptinV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, ENGINE_OPTIN_SCHEMA)?;
match &self.optin_id {
Some(id) => validate_id(id, "optin_id")?,
None if self.active || !self.vaults.is_empty() || !self.networks.is_empty() => {
return Err(ValidationError::new(
"an engine without opt-in is inactive with empty scope",
));
}
None => {}
}
if self.vaults.len() > MAX_OPTIN_VAULTS || self.networks.len() > MAX_OPTIN_NETWORKS {
return Err(ValidationError::new(format!(
"at most {MAX_OPTIN_VAULTS} vaults and {MAX_OPTIN_NETWORKS} networks"
)));
}
for (i, vault) in self.vaults.iter().enumerate() {
super::validate_vault_id(vault)?;
if i > 0 && self.vaults[i - 1] >= *vault {
return Err(ValidationError::new(
"vaults must be sorted without repeats",
));
}
}
if self
.daily_budget
.tasks
.is_some_and(|t| !(1..=MAX_DAILY_TASKS).contains(&t))
|| self.daily_budget.tokens == Some(0)
{
return Err(ValidationError::new(
"daily_budget is 1-100 tasks or none, and a non-zero token limit",
));
}
if !day_ok(&self.used_today.day) {
return Err(ValidationError::new("used_today.day is YYYY-MM-DD"));
}
if self.active == self.stopped_at.is_some() && self.optin_id.is_some() {
return Err(ValidationError::new(
"stopped_at is set exactly while the opt-in is inactive",
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EngineOptinStopV1 {
pub schema: String,
pub active: bool,
}
impl EngineOptinStopV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, ENGINE_OPTIN_STOP_SCHEMA)?;
if self.active {
return Err(ValidationError::new("an engine can only stop its opt-in"));
}
Ok(())
}
}