1use crate::api_objects::{
3 Error, MetaData, Result, SclName, SclObject, SeparationContext, TransitionError, Url,
4};
5use serde::{Deserialize, Serialize};
6use std::path::{Path, PathBuf};
7
8#[cfg(feature = "openapi")]
9use aide::OperationIo;
10#[cfg(feature = "openapi")]
11use schemars::JsonSchema;
12
13#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
14#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
15#[cfg_attr(feature = "openapi", aide(output))]
16#[serde(rename_all = "camelCase")]
17pub struct Volume {
18 #[serde(flatten)]
19 pub metadata: MetaData,
20
21 pub separation_context: SclName,
23
24 #[cfg_attr(feature = "openapi", validate(range(min = 1)))]
26 #[serde(rename = "sizeMiB")]
27 pub size_mib: u64,
28
29 #[serde(default)]
32 pub status: VolumeStatus,
33
34 pub url: Option<Url>,
37}
38
39impl Volume {
40 pub fn file_path(&self, root: &Path) -> PathBuf {
63 derive_volume_file_path(root, &self.separation_context, self.name())
64 }
65}
66
67pub fn derive_volume_file_path(root: &Path, sc_name: &SclName, vol_name: &SclName) -> PathBuf {
82 let mut path = PathBuf::from(root);
83 path.push(format!("{}", sc_name));
84 path.push("volumes");
85 path.push(format!("{}", vol_name));
86 path
87}
88
89#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
90#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
91#[cfg_attr(feature = "openapi", aide(output))]
92#[serde(rename_all = "camelCase")]
93pub enum VolumeStatus {
94 #[default]
96 Pending,
97
98 Active,
100
101 Failed(String),
107}
108
109impl VolumeStatus {
110 fn transition_is_valid(old: &Self, new: &Self) -> bool {
111 use VolumeStatus::*;
112 matches!(
113 (old, new),
114 (&Pending, &Active) | (&Pending, &Failed(_)) | (&Active, &Failed(_))
115 )
116 }
117}
118
119impl SclObject for Volume {
120 const PREFIX: &'static str = "/volumes";
122
123 fn name(&self) -> &SclName {
124 &self.metadata.name
125 }
126
127 fn separation_context(&self) -> Option<&SclName> {
128 Some(&self.separation_context)
129 }
130
131 fn metadata(&self) -> &MetaData {
132 &self.metadata
133 }
134
135 fn metadata_mut(&mut self) -> &mut MetaData {
136 &mut self.metadata
137 }
138
139 fn referenced_db_keys(&self) -> Vec<String> {
140 vec![SeparationContext::db_key(
141 self.separation_context.as_str(),
142 None,
143 )]
144 }
145
146 fn validate_fields_before_create(&self) -> Result<()> {
147 self.metadata.validate_fields_before_create()?;
148
149 if self.status != VolumeStatus::default() {
150 return Err(Error::IllegalInitialValue(
151 "Invalid initial volume status!".to_string(),
152 ));
153 }
154
155 if self.size_mib < 1 {
156 return Err(Error::IllegalInitialValue(
157 "Volume size must be greater than 0 MB!".to_string(),
158 ));
159 }
160
161 Ok(())
165 }
166
167 fn validate_fields_before_update(
168 current_db_state: &Self,
169 proposed_new_state: &Self,
170 ) -> Result<()> {
171 MetaData::validate_fields_before_regular_update(
172 ¤t_db_state.metadata,
173 &proposed_new_state.metadata,
174 )?;
175
176 if current_db_state.separation_context != proposed_new_state.separation_context {
177 return Err(TransitionError::Other(
178 "Field separation_context may not be changed!".to_string(),
179 )
180 .into());
181 }
182
183 if current_db_state.size_mib != proposed_new_state.size_mib {
184 return Err(
185 TransitionError::Other("Field size may not be changed!".to_string()).into(),
186 );
187 }
188
189 if current_db_state.url != proposed_new_state.url {
190 return Err(TransitionError::Other("Field url may not be changed!".to_string()).into());
191 }
192
193 if !VolumeStatus::transition_is_valid(¤t_db_state.status, &proposed_new_state.status)
194 {
195 return Err(TransitionError::Other(format!(
196 "Invalid status transition: Old({:?}) / New({:?})",
197 ¤t_db_state.status, &proposed_new_state.status
198 ))
199 .into());
200 }
201
202 Ok(())
203 }
204}
205
206#[cfg(test)]
207mod test {
208 use crate::api_objects::test::{
209 detect_invalid_metadata_create_mutations, detect_invalid_metadata_update_mutations,
210 detect_invalid_update_mutations, parse_json_dir,
211 };
212 use crate::api_objects::{MetaData, SclName, SclObject, Url, Volume, VolumeStatus};
213 use std::path::Path;
214
215 #[test]
216 fn parse_sample_json() {
217 parse_json_dir::<Volume>(Path::new("../test/sample_json/volumes"));
218 }
219
220 fn volume_status_transition_test_data() -> Vec<(VolumeStatus, VolumeStatus, bool)> {
221 use VolumeStatus::*;
222 vec![
223 (Pending, Pending, false),
224 (Pending, Active, true),
225 (Pending, Failed("foo".into()), true),
226 (Active, Pending, false),
227 (Active, Active, false),
228 (Active, Failed("foo".into()), true),
229 (Failed("foo".into()), Pending, false),
230 (Failed("foo".into()), Active, false),
231 (Failed("foo".into()), Failed("foo".into()), false),
232 ]
233 }
234
235 #[test]
236 fn test_volume_status_transition() {
237 for (old, new, expected_result) in volume_status_transition_test_data() {
238 assert_eq!(
239 VolumeStatus::transition_is_valid(&old, &new),
240 expected_result
241 );
242 }
243 }
244
245 fn example_volume() -> Volume {
246 Volume {
247 metadata: MetaData::new(SclName::try_from("example".to_string()).unwrap()),
248 separation_context: SclName::try_from("example".to_string()).unwrap(),
249 size_mib: 1,
250 status: Default::default(),
251 url: None,
252 }
253 }
254
255 #[test]
256 fn validate_fields_before_create() {
257 let mut vol = example_volume();
258 assert!(vol.validate_fields_before_create().is_ok());
259
260 vol.status = VolumeStatus::Active;
261 assert!(vol.validate_fields_before_create().is_err()); let mut vol = example_volume();
264 vol.size_mib = 0;
265 assert!(vol.validate_fields_before_create().is_err()); }
267
268 #[test]
269 fn validate_fields_before_update() {
270 let mut current = example_volume();
271 let mut proposed = example_volume();
272 proposed.status = VolumeStatus::Active;
273 assert!(Volume::validate_fields_before_update(¤t, &proposed).is_ok());
274
275 #[allow(clippy::type_complexity)]
276 let invalid_mutations: Vec<Box<dyn Fn(&mut Volume)>> = vec![
277 Box::new(|t| t.separation_context = SclName::try_from("different").unwrap()),
278 Box::new(|t| t.size_mib = 9999),
279 Box::new(|t| t.url = Some(Url::try_from("http://d3tn.com").unwrap())),
280 ];
281
282 detect_invalid_update_mutations(¤t, &proposed, invalid_mutations);
283
284 for (old, new, expected_result) in volume_status_transition_test_data() {
286 current.status = old;
287 proposed.status = new;
288 assert_eq!(
289 Volume::validate_fields_before_update(¤t, &proposed).is_ok(),
290 expected_result
291 );
292 }
293 }
294
295 #[test]
296 fn detect_invalid_metadata_mutations() {
297 let t = example_volume();
298 detect_invalid_metadata_create_mutations(&t);
299 detect_invalid_metadata_update_mutations(&t);
300 }
301}