use std::path::PathBuf;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const SCHEMA_VERSION: u32 = 1;
fn schema_v1() -> u32 {
SCHEMA_VERSION
}
fn de_datetime<'de, D: serde::Deserializer<'de>>(d: D) -> Result<DateTime<Utc>, D::Error> {
use serde::de::{Error, MapAccess, Visitor};
struct V;
impl<'de> Visitor<'de> for V {
type Value = DateTime<Utc>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("an RFC 3339 timestamp (quoted or a native TOML datetime)")
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
DateTime::parse_from_rfc3339(v)
.map(|t| t.with_timezone(&Utc))
.map_err(E::custom)
}
fn visit_map<A: MapAccess<'de>>(self, mut m: A) -> Result<Self::Value, A::Error> {
let (_k, v): (String, String) = m
.next_entry()?
.ok_or_else(|| A::Error::custom("empty datetime"))?;
self.visit_str(&v)
}
}
d.deserialize_any(V)
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WeaveSection {
#[serde(default)]
pub master: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProjectToml {
#[serde(rename = "schema", default = "schema_v1")]
pub schema_version: u32,
pub id: String,
pub name: String,
#[serde(deserialize_with = "de_datetime")]
pub created: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub governance: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weave: Option<WeaveSection>,
#[serde(flatten)]
pub extra: toml::Table,
}
impl ProjectToml {
pub fn is_weave_master(&self) -> bool {
self.weave.as_ref().is_some_and(|w| w.master)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProjectState {
#[default]
Active,
Missing,
Archived,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProjectTomlPresence {
#[default]
Present,
Pending,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SeedSection {
pub source: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub legacy_name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct LegacySection {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ServeVia {
#[default]
UserDaemon,
ChildKernel,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProjectSandbox {
#[default]
#[serde(alias = "native")]
Logical,
Seatbelt,
LinuxContainer,
Wasmtime,
}
fn is_logical_sandbox(value: &ProjectSandbox) -> bool {
*value == ProjectSandbox::Logical
}
pub const DEFAULT_IDLE_STOP_SECS: u64 = 1800;
pub const DEFAULT_RESTART_MAX: u32 = 5;
pub const DEFAULT_RESTART_WINDOW_SECS: u64 = 60;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ChildState {
#[default]
Stopped,
Starting,
Running,
IdleStopping,
Failed,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServeSection {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub adapter: Option<String>,
#[serde(default)]
pub via: ServeVia,
#[serde(default, skip_serializing_if = "is_logical_sandbox")]
pub sandbox: ProjectSandbox,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idle_stop_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub restart_max: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub restart_window_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lost_heartbeat_busy_ceiling_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel_sha: Option<String>,
}
impl ServeSection {
pub fn restart_max(&self) -> u32 {
self.restart_max.unwrap_or(DEFAULT_RESTART_MAX)
}
pub fn restart_window_secs(&self) -> u64 {
self.restart_window_secs
.unwrap_or(DEFAULT_RESTART_WINDOW_SECS)
}
pub fn idle_stop_secs(&self) -> u64 {
match (self.idle_stop_secs, self.via) {
(Some(n), _) => n,
(None, ServeVia::ChildKernel) => DEFAULT_IDLE_STOP_SECS,
(None, _) => 0,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChainSection {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dir: Option<PathBuf>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BinaryInfo {
#[serde(default)]
pub path: String,
#[serde(default)]
pub sha: String,
#[serde(default)]
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProjectManifest {
#[serde(rename = "schema", default = "schema_v1")]
pub schema_version: u32,
pub id: String,
pub name: String,
pub root: PathBuf,
#[serde(default)]
pub state: ProjectState,
#[serde(deserialize_with = "de_datetime")]
pub created: DateTime<Utc>,
#[serde(deserialize_with = "de_datetime")]
pub last_seen: DateTime<Utc>,
#[serde(default)]
pub project_toml: ProjectTomlPresence,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seed: Option<SeedSection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub legacy: Option<LegacySection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub serve: Option<ServeSection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chain: Option<ChainSection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub binary: Option<BinaryInfo>,
#[serde(flatten)]
pub extra: toml::Table,
}
impl ProjectManifest {
pub fn runtime_dir_override(&self) -> Option<&std::path::Path> {
self.serve.as_ref().and_then(|s| s.runtime_dir.as_deref())
}
pub fn chain_dir(&self) -> PathBuf {
self.chain
.as_ref()
.and_then(|c| c.dir.clone())
.unwrap_or_else(|| self.root.join(".weftos").join("chain"))
}
}