oxicode/foundation/
compatibility.rs1use std::collections::BTreeMap;
4use std::path::Path;
5
6use serde::{Deserialize, Serialize};
7
8use super::FoundationError;
9
10const SUPPORTED_SCHEMA_VERSION: u32 = 1;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct FoundationManifest {
15 pub schema_version: u32,
17 #[serde(default)]
19 pub host_compatibility: BTreeMap<String, String>,
20}
21
22impl FoundationManifest {
23 pub fn validate_oxicode(&self) -> Result<(), FoundationError> {
27 if self.schema_version != SUPPORTED_SCHEMA_VERSION {
28 return Err(FoundationError::UnsupportedSchema(self.schema_version));
29 }
30 let Some(spec) = self.host_compatibility.get("oxicode") else {
31 return Ok(());
33 };
34 let required = parse_minimum_version(spec).ok_or_else(|| {
35 FoundationError::IncompatibleHost(format!("unparsable spec {spec:?}"))
36 })?;
37 let current = current_pkg_version();
38 if current_compare(¤t, &required) {
39 Ok(())
40 } else {
41 Err(FoundationError::IncompatibleHost(format!(
42 "oxicode {}.{}.{} < required {spec}",
43 current.0, current.1, current.2
44 )))
45 }
46 }
47}
48
49pub fn read(path: &Path) -> Result<FoundationManifest, FoundationError> {
52 let manifest: FoundationManifest = serde_json::from_slice(&std::fs::read(path)?)?;
53 manifest.validate_oxicode()?;
54 Ok(manifest)
55}
56
57fn parse_minimum_version(spec: &str) -> Option<(u64, u64, u64)> {
59 let trimmed = spec.trim();
60 let rest = trimmed.strip_prefix(">=")?.trim();
61 let mut parts = rest.split('.');
62 let major = parts.next()?.parse().ok()?;
63 let minor = parts.next()?.parse().ok()?;
64 let patch = parts.next()?.parse().ok()?;
65 if parts.next().is_some() {
66 return None;
67 }
68 Some((major, minor, patch))
69}
70
71fn current_pkg_version() -> (u64, u64, u64) {
73 let s = env!("CARGO_PKG_VERSION");
74 let mut parts = s.split('.');
75 let major = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
76 let minor = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
77 let patch = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
78 (major, minor, patch)
79}
80
81fn current_compare(current: &(u64, u64, u64), required: &(u64, u64, u64)) -> bool {
83 current.0 > required.0
84 || (current.0 == required.0 && current.1 > required.1)
85 || (current.0 == required.0 && current.1 == required.1 && current.2 >= required.2)
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn parse_min_version_accepts_supported() {
94 assert_eq!(parse_minimum_version(">=0.75.0"), Some((0, 75, 0)));
95 assert_eq!(parse_minimum_version(">=1.0.0"), Some((1, 0, 0)));
96 assert_eq!(parse_minimum_version(">= 0.75.0 "), Some((0, 75, 0)));
97 }
98
99 #[test]
100 fn parse_min_version_rejects_other_forms() {
101 assert_eq!(parse_minimum_version("^0.75.0"), None);
102 assert_eq!(parse_minimum_version("~0.75.0"), None);
103 assert_eq!(parse_minimum_version("0.75.0"), None);
104 assert_eq!(parse_minimum_version(">=0.75"), None);
105 assert_eq!(parse_minimum_version(">=0.75.0.1"), None);
106 }
107
108 #[test]
109 fn read_rejects_missing_schema_version() {
110 let tmp = tempfile::tempdir().unwrap();
111 let path = tmp.path().join("foundation.json");
112 std::fs::write(&path, "{}").unwrap();
113 let err = read(&path).unwrap_err();
114 assert!(matches!(err, FoundationError::Parse(_)));
115 }
116
117 #[test]
118 fn read_rejects_unsupported_schema() {
119 let tmp = tempfile::tempdir().unwrap();
120 let path = tmp.path().join("foundation.json");
121 std::fs::write(&path, r#"{"schema_version": 99}"#).unwrap();
122 let err = read(&path).unwrap_err();
123 assert!(matches!(err, FoundationError::UnsupportedSchema(99)));
124 }
125
126 #[test]
127 fn read_accepts_compatible_host() {
128 let tmp = tempfile::tempdir().unwrap();
129 let path = tmp.path().join("foundation.json");
130 std::fs::write(
131 &path,
132 r#"{"schema_version": 1, "host_compatibility": {"oxicode": ">=0.50.0"}}"#,
133 )
134 .unwrap();
135 let m = read(&path).unwrap();
136 assert_eq!(m.schema_version, 1);
137 }
138
139 #[test]
140 fn read_rejects_incompatible_host() {
141 let tmp = tempfile::tempdir().unwrap();
142 let path = tmp.path().join("foundation.json");
143 std::fs::write(
144 &path,
145 r#"{"schema_version": 1, "host_compatibility": {"oxicode": ">=999.0.0"}}"#,
146 )
147 .unwrap();
148 let err = read(&path).unwrap_err();
149 assert!(matches!(err, FoundationError::IncompatibleHost(_)));
150 }
151}