xianyun_core/config/
registry.rs1use super::errors::ConfigError;
5use super::latest::CaproniConfig;
6use super::version::ConfigVersion;
7use super::versions::Upgrader;
8
9pub const LATEST_VERSION: ConfigVersion = ConfigVersion::V2;
11
12const REGISTERED_VERSION_COUNT: usize = 1;
19
20pub fn supported_versions() -> String {
23 "2".to_string()
24}
25
26pub fn walk(cfg: Box<dyn Upgrader>) -> Result<Box<dyn Upgrader>, ConfigError> {
32 let mut current = cfg;
33
34 for _ in 0..=REGISTERED_VERSION_COUNT {
35 if current.is_terminal() {
36 return Ok(current);
37 }
38
39 current = current
40 .upgrade()
41 .map_err(|err| ConfigError::SemanticValidation(format!("upgrade step: {err}")))?;
42 }
43
44 Err(ConfigError::UpgradeLoop)
45}
46
47pub fn load_and_upgrade(
58 declared: ConfigVersion,
59 data: &[u8],
60 apply_latest_defaults: bool,
61) -> Result<CaproniConfig, ConfigError> {
62 if declared != LATEST_VERSION {
63 return Err(ConfigError::UnsupportedVersion);
64 }
65
66 let mut cfg: CaproniConfig =
67 serde_yaml::from_slice(data).map_err(|err| ConfigError::InvalidYAML(err.to_string()))?;
68
69 if declared != LATEST_VERSION || apply_latest_defaults {
70 cfg.apply_defaults()
71 .map_err(|err| ConfigError::SemanticValidation(format!("applying defaults: {err}")))?;
72 }
73
74 let upgraded = walk(Box::new(cfg)).map_err(|err| {
75 ConfigError::SemanticValidation(format!(
76 "upgrading config declared as version {declared}: {err}"
77 ))
78 })?;
79
80 upgraded
81 .as_any()
82 .downcast_ref::<CaproniConfig>()
83 .cloned()
84 .ok_or(ConfigError::UpgradeLoop)
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 struct TerminalUpgrader;
95
96 impl Upgrader for TerminalUpgrader {
97 fn upgrade(&self) -> Result<Box<dyn Upgrader>, super::super::versions::UpgraderError> {
98 unreachable!("walk must not call upgrade on a terminal value")
99 }
100
101 fn is_terminal(&self) -> bool {
102 true
103 }
104
105 fn as_any(&self) -> &dyn std::any::Any {
106 self
107 }
108 }
109
110 struct OneStepUpgrader;
112
113 impl Upgrader for OneStepUpgrader {
114 fn upgrade(&self) -> Result<Box<dyn Upgrader>, super::super::versions::UpgraderError> {
115 Ok(Box::new(TerminalUpgrader))
116 }
117
118 fn as_any(&self) -> &dyn std::any::Any {
119 self
120 }
121 }
122
123 struct RunawayUpgrader {
126 step: u32,
127 }
128
129 impl Upgrader for RunawayUpgrader {
130 fn upgrade(&self) -> Result<Box<dyn Upgrader>, super::super::versions::UpgraderError> {
131 Ok(Box::new(RunawayUpgrader {
132 step: self.step + 1,
133 }))
134 }
135
136 fn as_any(&self) -> &dyn std::any::Any {
137 self
138 }
139 }
140
141 #[test]
143 fn walk_native_latest_is_noop() {
144 let data = b"version: 2\ncluster:\n driver: mock\n";
145 let cfg = load_and_upgrade(ConfigVersion::V2, data, true).expect("load should succeed");
146 assert_eq!(cfg.version, 2);
147 assert!(
148 cfg.cluster.name.is_some(),
149 "latest defaults applied for a native-latest config"
150 );
151 }
152
153 #[test]
155 fn load_and_upgrade_unsupported_version() {
156 let err = load_and_upgrade(ConfigVersion::Unknown, b"version: 99\n", true).unwrap_err();
157 assert!(matches!(err, ConfigError::UnsupportedVersion));
158 }
159
160 #[test]
162 fn seed_skips_latest_defaults() {
163 let data = b"version: 2\ncluster:\n driver: mock\n";
164 let cfg = load_and_upgrade(ConfigVersion::V2, data, false).expect("load should succeed");
165 assert!(cfg.cluster.name.is_none());
166 }
167
168 #[test]
170 fn walk_converges_after_upgrade_step() {
171 let start: Box<dyn Upgrader> = Box::new(OneStepUpgrader);
172 let got = walk(start).expect("walk should succeed");
173 let _ = got; }
175
176 #[test]
178 fn walk_stops_runaway_upgrade_with_bound() {
179 let start: Box<dyn Upgrader> = Box::new(RunawayUpgrader { step: 0 });
180 match walk(start) {
181 Err(err) => assert!(matches!(err, ConfigError::UpgradeLoop)),
182 Ok(_) => panic!("expected ErrUpgradeLoop"),
183 }
184 }
185}