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