spin_doctor/manifest/
version.rs

1use 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/// VersionDiagnostic detects problems with the app manifest version field.
15#[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/// VersionDiagnosis represents a problem with the app manifest version field.
56#[derive(Debug)]
57pub enum VersionDiagnosis {
58    /// Missing any known version key
59    MissingVersion,
60    /// Using old spin_version key
61    OldVersionKey,
62    /// Wrong version value
63    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            // (ab)use stable sorting to move the inserted item to the top
112            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                // true > false
116                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}