spin_doctor/manifest/
version.rs1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use serde::Deserialize;
4use toml::Value;
5use toml_edit::{de::from_document, DocumentMut, Item};
6
7use crate::{Diagnosis, Diagnostic, PatientApp, Treatment};
8
9use super::ManifestTreatment;
10
11const SPIN_MANIFEST_VERSION: &str = "spin_manifest_version";
12const SPIN_VERSION: &str = "spin_version";
13
14#[derive(Default)]
16pub struct VersionDiagnostic;
17
18#[async_trait]
19impl Diagnostic for VersionDiagnostic {
20 type Diagnosis = VersionDiagnosis;
21
22 async fn diagnose(&self, patient: &PatientApp) -> Result<Vec<Self::Diagnosis>> {
23 let doc = &patient.manifest_doc;
24 let test: VersionProbe =
25 from_document(doc.clone()).context("failed to decode VersionProbe")?;
26
27 if let Some(value) = test.spin_manifest_version {
28 if corrected_version(&value).is_some() {
29 return Ok(vec![VersionDiagnosis::WrongValue(value)]);
30 }
31 } else if test.spin_version.is_some() {
32 return Ok(vec![VersionDiagnosis::OldVersionKey]);
33 } else {
34 return Ok(vec![VersionDiagnosis::MissingVersion]);
35 }
36 Ok(vec![])
37 }
38}
39
40fn corrected_version(value: &Value) -> Option<toml_edit::Value> {
41 match value {
42 Value::String(s) if s == "1" => None,
43 Value::Integer(2) => None,
44 Value::Integer(1) => Some("1".into()),
45 _ => Some(2.into()),
46 }
47}
48
49#[derive(Debug, Deserialize)]
50struct VersionProbe {
51 spin_manifest_version: Option<Value>,
52 spin_version: Option<Value>,
53}
54
55#[derive(Debug)]
57pub enum VersionDiagnosis {
58 MissingVersion,
60 OldVersionKey,
62 WrongValue(Value),
64}
65
66impl Diagnosis for VersionDiagnosis {
67 fn description(&self) -> String {
68 match self {
69 Self::MissingVersion => "Manifest missing 'spin_manifest_version' key".into(),
70 Self::OldVersionKey => "Manifest using old 'spin_version' key".into(),
71 Self::WrongValue(val) => {
72 format!(r#"Manifest 'spin_manifest_version' must be "1" or 2, not {val}"#)
73 }
74 }
75 }
76
77 fn is_critical(&self) -> bool {
78 !matches!(self, Self::OldVersionKey)
79 }
80
81 fn treatment(&self) -> Option<&dyn Treatment> {
82 Some(self)
83 }
84}
85
86#[async_trait]
87impl ManifestTreatment for VersionDiagnosis {
88 fn summary(&self) -> String {
89 match self {
90 Self::MissingVersion => "Add spin_manifest_version to manifest".into(),
91 Self::OldVersionKey => "Replace 'spin_version' with 'spin_manifest_version'".into(),
92 Self::WrongValue(value) => format!(
93 "Set manifest version to {}",
94 corrected_version(value).unwrap()
95 ),
96 }
97 }
98
99 async fn treat_manifest(&self, doc: &mut DocumentMut) -> anyhow::Result<()> {
100 doc.remove(SPIN_VERSION);
101
102 let item = Item::Value(match self {
103 Self::MissingVersion => 2.into(),
104 Self::OldVersionKey => "1".into(),
105 Self::WrongValue(value) => corrected_version(value).unwrap(),
106 });
107 if let Some(existing) = doc.get_mut(SPIN_MANIFEST_VERSION) {
108 *existing = item;
109 } else {
110 doc.insert(SPIN_MANIFEST_VERSION, item);
111 doc.sort_values_by(|k1, _, k2, _| {
113 let k1_is_version = k1.get() == SPIN_MANIFEST_VERSION;
114 let k2_is_version = k2.get() == SPIN_MANIFEST_VERSION;
115 k2_is_version.cmp(&k1_is_version)
117 })
118 }
119 Ok(())
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use crate::test::{run_broken_test, run_correct_test};
126
127 use super::*;
128
129 #[tokio::test]
130 async fn test_correct() {
131 run_correct_test::<VersionDiagnostic>("manifest_version").await;
132 }
133
134 #[tokio::test]
135 async fn test_old_key() {
136 let diag = run_broken_test::<VersionDiagnostic>("manifest_version", "old_key").await;
137 assert!(matches!(diag, VersionDiagnosis::OldVersionKey));
138 }
139
140 #[tokio::test]
141 async fn test_wrong_value() {
142 let diag = run_broken_test::<VersionDiagnostic>("manifest_version", "wrong_value").await;
143 assert!(matches!(diag, VersionDiagnosis::WrongValue(_)));
144 }
145}