use std::collections::BTreeSet;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::guest::Guest;
use crate::identity::{Cloud, Driver, HostOs, Identity, Target, Tool};
use crate::lifecycle::{Condition, Expiry, Lifecycle, Recovery};
use crate::machine::{Machine, Network};
use crate::outputs::{Checks, Output};
use crate::provenance::{EnvironmentRef, Generator, ManifestVersion, SUPPORTED_VERSION};
use crate::slots::{Param, SecretSlot};
use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(title = "Isoloom run manifest, version 0")]
pub struct Manifest {
#[serde(rename = "$schema", default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
pub manifest_version: ManifestVersion,
pub generator: Generator,
pub environment: EnvironmentRef,
pub target: Target,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cloud: Option<Cloud>,
pub driver: Driver,
#[schemars(extend("minItems" = 1))]
pub hosts: BTreeSet<HostOs>,
pub tools: Vec<Tool>,
pub identity: Identity,
pub params: Vec<Param>,
pub secrets: Vec<SecretSlot>,
pub networks: Vec<Network>,
pub machines: Vec<Machine>,
pub lifecycle: Lifecycle,
pub ready: Vec<Condition>,
pub outputs: Vec<Output>,
pub checks: Checks,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recovery: Option<Recovery>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expiry: Option<Expiry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guest: Option<Guest>,
}
pub fn parse(text: &str) -> Result<Manifest> {
let value: serde_json::Value = serde_json::from_str(text).map_err(|e| Error::Syntax {
line: e.line(),
column: e.column(),
reason: e.to_string(),
})?;
if let Some(found) = value.get("manifest_version").filter(|v| **v != SUPPORTED_VERSION) {
return Err(Error::Version { found: found.to_string() });
}
let manifest: Manifest = serde_path_to_error::deserialize(value).map_err(|e| Error::Field {
path: e.path().to_string(),
reason: e.into_inner().to_string(),
})?;
manifest.validate()?;
Ok(manifest)
}
pub fn to_json(manifest: &Manifest) -> Result<String> {
serde_json::to_string_pretty(manifest).map(|text| text + "\n").map_err(|e| Error::Field {
path: ".".into(),
reason: e.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_manifest_crosses_threads() {
fn shared<T: Send + Sync>() {}
shared::<Manifest>();
shared::<Error>();
}
#[test]
fn another_version_is_refused_before_its_fields() {
let err = parse(r#"{ "manifest_version": 1, "renamed": true }"#).unwrap_err();
assert_eq!(err.code(), "version");
assert_eq!(parse("{ not json").unwrap_err().code(), "syntax");
let field = parse(r#"{ "manifest_version": 0 }"#).unwrap_err();
assert!(matches!(&field, Error::Field { path, .. } if path == "."), "{field}");
}
#[test]
fn a_written_manifest_ends_with_a_newline_and_reads_back() {
let manifest = parse(include_str!("../fixtures/minimal.run.json")).unwrap();
let written = to_json(&manifest).unwrap();
assert!(written.ends_with("}\n") && written.starts_with("{\n \"manifest_version\": 0,"), "{written}");
assert_eq!(parse(&written).unwrap(), manifest);
}
}