scl_lib/api_objects/
separation_context.rs

1// SPDX-License-Identifier: EUPL-1.2
2use crate::api_objects::{
3    Error, MetaData, Result, SclName, SclObject, TransitionError, ValueSpaceExhaustedError,
4};
5use serde::{Deserialize, Serialize};
6use std::fmt::{Display, Formatter};
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 SeparationContext {
15    #[serde(flatten)]
16    pub metadata: MetaData,
17
18    /// Values provided by the client will be ignored as vlan tags will
19    /// be assigned by the server and may not change afterwards.
20    #[serde(default = "VlanTag::deserialization_default")]
21    pub vlan_tag: VlanTag,
22}
23
24impl SclObject for SeparationContext {
25    /// Prefix for all [SeparationContext]s (`"/scs"`).
26    const PREFIX: &'static str = "/scs";
27
28    fn name(&self) -> &SclName {
29        &self.metadata.name
30    }
31
32    /// Returns the [SclName] of the [SeparationContext], as each separation context is associated
33    /// with itself.
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use scl_lib::api_objects::{MetaData, SclName, SclObject, SeparationContext, VlanTag};
39    ///
40    /// let sc = SeparationContext {
41    ///   vlan_tag: VlanTag::try_from(1).unwrap(),
42    ///   metadata: MetaData::new(SclName::try_from("sc-01").unwrap()),
43    /// };
44    /// assert_eq!(sc.separation_context(), Some(sc.name()));
45    /// ```
46    fn separation_context(&self) -> Option<&SclName> {
47        Some(&self.metadata.name)
48    }
49
50    /// Overrides the default implementation of the [SclObject] trait to use either the `sc` or the
51    /// `name` argument for API endpoint construction.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use scl_lib::api_objects::{SeparationContext, SclObject};
57    ///
58    /// assert_eq!(SeparationContext::api_endpoint("sc-01", None), SeparationContext::api_endpoint(None, "sc-01"));
59    /// assert_eq!(SeparationContext::api_endpoint("sc-01", "sc-01"), SeparationContext::api_endpoint("sc-01", None));
60    /// assert_eq!(SeparationContext::api_endpoint(None, None), SeparationContext::PREFIX);
61    /// ```
62    fn api_endpoint<'a>(
63        sc: impl Into<Option<&'a str>>,
64        name: impl Into<Option<&'a str>>,
65    ) -> String {
66        if let Some(value) = sc.into().or_else(|| name.into()) {
67            format!("{}/{}", Self::PREFIX, value)
68        } else {
69            Self::PREFIX.into()
70        }
71    }
72
73    /// Overrides the default implementation of the [SclObject] trait to use either the `sc` or the
74    /// `name` argument for DB key construction.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use scl_lib::api_objects::{SeparationContext, SclObject};
80    ///
81    /// assert_eq!(SeparationContext::db_key("sc-01", None), SeparationContext::db_key(None, "sc-01"));
82    /// assert_eq!(SeparationContext::db_key("sc-01", "sc-01"), SeparationContext::db_key("sc-01", None));
83    /// assert_eq!(SeparationContext::db_key(None, None), SeparationContext::PREFIX);
84    /// ```
85    fn db_key<'a>(sc: impl Into<Option<&'a str>>, name: impl Into<Option<&'a str>>) -> String {
86        if let Some(value) = sc.into().or_else(|| name.into()) {
87            format!("{}/{}", Self::PREFIX, value)
88        } else {
89            Self::PREFIX.into()
90        }
91    }
92
93    fn metadata(&self) -> &MetaData {
94        &self.metadata
95    }
96
97    fn metadata_mut(&mut self) -> &mut MetaData {
98        &mut self.metadata
99    }
100
101    fn referenced_db_keys(&self) -> Vec<String> {
102        vec![]
103    }
104
105    fn validate_fields_before_create(&self) -> Result<()> {
106        self.metadata.validate_fields_before_create()
107    }
108
109    fn validate_fields_before_update(
110        _current_db_state: &Self,
111        _proposed_new_state: &Self,
112    ) -> Result<()> {
113        Err(TransitionError::Other("Regular updates are not supported!".to_string()).into())
114    }
115}
116
117#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
118#[cfg_attr(feature = "openapi", derive(JsonSchema))]
119#[serde(try_from = "u16")]
120pub struct VlanTag(#[cfg_attr(feature = "openapi", validate(range(min = 1, max = 4094)))] u16);
121
122impl VlanTag {
123    pub fn value(&self) -> u16 {
124        self.0
125    }
126
127    /// Can be used as default VlanTag value if a client request (e.g., create new SC)
128    /// does not specify it. Note that the value needs to be properly initialized by
129    /// the SCL API in any case.
130    fn deserialization_default() -> Self {
131        Self::try_from(1).unwrap()
132    }
133}
134
135impl TryFrom<u16> for VlanTag {
136    type Error = Error;
137
138    fn try_from(value: u16) -> Result<Self> {
139        if value > 0 && value < 4095 {
140            Ok(Self(value))
141        } else {
142            Err(Error::ParsingFailed(
143                "Out of range [0 < VlanTag < 4095]".to_string(),
144            ))
145        }
146    }
147}
148
149impl Display for VlanTag {
150    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
151        write!(f, "{}", self.0)
152    }
153}
154
155/// Provides a list of unused tags that may be used by new separation contexts.
156/// Internal management data that will not be exposed to users.
157#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
158#[cfg_attr(feature = "openapi", derive(JsonSchema))]
159pub struct AvailableVlanTags {
160    tags: Vec<VlanTag>,
161
162    // Only included for concurrency control.
163    pub metadata: MetaData,
164}
165
166impl AvailableVlanTags {
167    pub fn take(&mut self) -> Result<VlanTag> {
168        self.tags
169            .pop()
170            .ok_or_else(|| ValueSpaceExhaustedError::NoFreeVlanTagLeft.into())
171    }
172
173    pub fn free(&mut self, vlan_tag: VlanTag) {
174        assert!(!self.tags.contains(&vlan_tag)); // TODO avoid `assert`.
175        self.tags.push(vlan_tag);
176    }
177}
178
179impl Default for AvailableVlanTags {
180    fn default() -> Self {
181        Self {
182            tags: (1..=4094).map(|n| VlanTag::try_from(n).unwrap()).collect(),
183            metadata: MetaData::new(SclName::try_from("vlan-tags").unwrap()),
184        }
185    }
186}
187
188impl SclObject for AvailableVlanTags {
189    /// Prefix for all [AvailableVlanTags] (`"/vars/vlan-tags"`).
190    const PREFIX: &'static str = "/vars/vlan-tags";
191
192    fn name(&self) -> &SclName {
193        &self.metadata.name
194    }
195
196    /// Overrides the default implementation of the [SclObject] trait to ignore [SeparationContext]
197    /// and [SclName] and return the `PREFIX`.
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// use scl_lib::api_objects::{AvailableVlanTags, SclObject};
203    ///
204    /// assert_eq!(AvailableVlanTags::api_endpoint(None, None), AvailableVlanTags::PREFIX.to_string());
205    /// assert_eq!(AvailableVlanTags::api_endpoint("sc-01", "vlan"), AvailableVlanTags::PREFIX.to_string());
206    /// ```
207    fn api_endpoint<'a>(
208        _sc: impl Into<Option<&'a str>>,
209        _name: impl Into<Option<&'a str>>,
210    ) -> String {
211        Self::PREFIX.into()
212    }
213
214    /// Overrides the default implementation of the [SclObject] trait to ignore [SeparationContext]
215    /// and [SclName] and return the `PREFIX`.
216    ///
217    /// # Examples
218    ///
219    /// ```
220    /// use scl_lib::api_objects::{AvailableVlanTags, SclObject};
221    ///
222    /// assert_eq!(AvailableVlanTags::db_key(None, None), AvailableVlanTags::PREFIX.to_string());
223    /// assert_eq!(AvailableVlanTags::db_key("sc-01", "vlan"), AvailableVlanTags::PREFIX.to_string());
224    /// ```
225    fn db_key<'a>(_sc: impl Into<Option<&'a str>>, _name: impl Into<Option<&'a str>>) -> String {
226        Self::PREFIX.into()
227    }
228
229    fn metadata(&self) -> &MetaData {
230        &self.metadata
231    }
232
233    fn metadata_mut(&mut self) -> &mut MetaData {
234        &mut self.metadata
235    }
236
237    fn referenced_db_keys(&self) -> Vec<String> {
238        vec![]
239    }
240
241    fn validate_fields_before_create(&self) -> Result<()> {
242        if *self != Self::default() {
243            Err(Error::IllegalInitialValue(
244                "VLAN tags have been initialized in a wrong way!".to_string(),
245            ))
246        } else {
247            Ok(())
248        }
249    }
250
251    fn validate_fields_before_update(
252        _current_db_state: &Self,
253        _proposed_new_state: &Self,
254    ) -> Result<()> {
255        // Only via side effects during SC deletion.
256        Err(TransitionError::Other("VLAN tags may not be updated directly!".to_string()).into())
257    }
258}
259
260#[cfg(test)]
261mod test {
262    use super::*;
263    use crate::api_objects::test::{
264        detect_invalid_metadata_create_mutations, detect_invalid_metadata_update_mutations,
265        detect_invalid_update_mutations, parse_json_dir,
266    };
267    use std::path::Path;
268
269    #[test]
270    fn parse_sample_json() {
271        parse_json_dir::<SeparationContext>(Path::new("../test/sample_json/scs"));
272    }
273
274    #[test]
275    fn vlan_tag_try_from() {
276        assert!(VlanTag::try_from(0).is_err());
277        assert!(VlanTag::try_from(1).is_ok());
278        assert!(VlanTag::try_from(2).is_ok());
279        assert!(VlanTag::try_from(4093).is_ok());
280        assert!(VlanTag::try_from(4094).is_ok());
281        assert!(VlanTag::try_from(4095).is_err());
282    }
283
284    #[test]
285    fn vlan_tag_deserialization_default() {
286        let _ = VlanTag::deserialization_default(); // Should not panic.
287    }
288
289    fn example_sc() -> SeparationContext {
290        SeparationContext {
291            metadata: MetaData {
292                name: SclName::try_from("example").unwrap(),
293                resource_version: Default::default(),
294                deletion_mark: None,
295            },
296            vlan_tag: VlanTag::try_from(42).unwrap(),
297        }
298    }
299
300    #[test]
301    fn validate_sc_fields_before_create() {
302        let sc = example_sc();
303        assert!(sc.validate_fields_before_create().is_ok());
304    }
305
306    #[test]
307    fn validate_sc_fields_before_update() {
308        let current = example_sc();
309
310        #[allow(clippy::type_complexity)]
311        let invalid_mutations: Vec<Box<dyn Fn(&mut SeparationContext)>> = vec![
312            Box::new(|t| t.vlan_tag = VlanTag::try_from(999).unwrap()),
313            Box::new(|_| ()),
314        ];
315
316        detect_invalid_update_mutations(&current, &current, invalid_mutations);
317    }
318
319    #[test]
320    fn detect_invalid_metadata_mutations() {
321        let t = example_sc();
322        detect_invalid_metadata_create_mutations(&t);
323        detect_invalid_metadata_update_mutations(&t);
324    }
325
326    #[test]
327    fn validate_avt_fields_before_create() {
328        let mut tags = AvailableVlanTags::default();
329        assert!(tags.validate_fields_before_create().is_ok());
330        let _ = tags.take().unwrap();
331        assert!(tags.validate_fields_before_create().is_err());
332    }
333
334    #[test]
335    fn validate_avt_fields_before_update() {
336        let mut current = AvailableVlanTags::default();
337        let _ = current.take().unwrap();
338
339        #[allow(clippy::type_complexity)]
340        let invalid_mutations: Vec<Box<dyn Fn(&mut AvailableVlanTags)>> = vec![
341            Box::new(|t| {
342                let _ = t.take().unwrap();
343            }),
344            Box::new(|t| t.free(VlanTag::try_from(4094).unwrap())),
345            Box::new(|_| ()),
346        ];
347
348        detect_invalid_update_mutations(&current, &current, invalid_mutations);
349    }
350}