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