opsview/config/
tenancy.rs

1use super::{Role, RoleRef};
2use crate::{prelude::*, util::*};
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6/// Represents a [Tenancy](https://docs.itrsgroup.com/docs/opsview/6.8.9/configuration/users-and-roles/multi-tenancy/index.html) entity in Opsview.
7#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
8pub struct Tenancy {
9    // Required fields ---------------------------------------------------------------------------//
10    /// The name of the `Tenancy`.
11    pub name: String,
12
13    // Semi-optional fields ----------------------------------------------------------------------//
14    /// Primary role of the `Tenancy`.
15    pub primary_role: Option<RoleRef>,
16
17    // Optional fields ---------------------------------------------------------------------------//
18    /// Optional description of the `Tenancy`.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub description: Option<String>,
21
22    // Read-only fields --------------------------------------------------------------------------//
23    /// The unique identifier of the `Tenancy`.
24    #[serde(
25        skip_serializing_if = "Option::is_none",
26        deserialize_with = "deserialize_string_or_number_to_u64",
27        default
28    )]
29    pub id: Option<u64>,
30
31    /// A reference string unique to this Tenancy.
32    #[serde(
33        rename = "ref",
34        skip_serializing_if = "Option::is_none",
35        deserialize_with = "deserialize_readonly",
36        default
37    )]
38    pub ref_: Option<String>,
39
40    /// Optional priority of the `Tenancy`.
41    #[serde(
42        skip_serializing_if = "Option::is_none",
43        deserialize_with = "deserialize_string_or_number_to_u64",
44        default
45    )]
46    pub priority: Option<u64>,
47
48    /// A boolean indicating whether the `Tenancy` is uncommitted.
49    #[serde(
50        skip_serializing_if = "Option::is_none",
51        deserialize_with = "deserialize_string_or_number_to_option_bool",
52        serialize_with = "serialize_option_bool_as_string",
53        default
54    )]
55    pub uncommitted: Option<bool>,
56}
57
58/// Enables the creation of a [`Tenancy`] instance from a JSON representation.
59/// Typically used when parsing JSON data from the Opsview API.
60impl CreateFromJson for Tenancy {}
61
62impl ConfigObject for Tenancy {
63    type Builder = TenancyBuilder;
64
65    /// Returns a builder for constructing a [`Tenancy`] object.
66    ///
67    /// # Returns
68    /// A [`TenancyBuilder`] object.
69    fn builder() -> Self::Builder {
70        TenancyBuilder::new()
71    }
72
73    /// Provides the configuration path for a [`Tenancy`] object within the Opsview system.
74    ///
75    /// # Returns
76    /// A string representing the API path where Tenancy are configured.
77    fn config_path() -> Option<String> {
78        Some("/config/tenancy".to_string())
79    }
80
81    /// Returns the unique name of the [`Tenancy`] object.
82    ///
83    /// This name is used to identify the `Tenancy` when building the `HashMap` for a
84    /// [`ConfigObjectMap`].
85    ///
86    /// # Returns
87    /// A string representing the unique name of the `Tenancy` entity.
88    fn unique_name(&self) -> String {
89        self.name.clone()
90    }
91
92    fn minimal(name: &str) -> Result<Self, OpsviewConfigError> {
93        Ok(Self {
94            name: validate_and_trim_tenancy_name(name)?,
95            ..Default::default()
96        })
97    }
98}
99
100impl Persistent for Tenancy {
101    /// Returns the unique identifier.
102    fn id(&self) -> Option<u64> {
103        self.id
104    }
105
106    /// Returns the reference string if it's not empty.
107    fn ref_(&self) -> Option<String> {
108        if self.ref_.as_ref().is_some_and(|x| !x.is_empty()) {
109            self.ref_.clone()
110        } else {
111            None
112        }
113    }
114
115    /// Returns the name if it's not empty.
116    fn name(&self) -> Option<String> {
117        if self.name.is_empty() {
118            None
119        } else {
120            Some(self.name.clone())
121        }
122    }
123
124    fn name_regex(&self) -> Option<String> {
125        Some(TENANCY_NAME_REGEX_STR.to_string())
126    }
127
128    fn validated_name(&self, name: &str) -> Result<String, OpsviewConfigError> {
129        validate_and_trim_tenancy_name(name)
130    }
131
132    fn set_name(&mut self, new_name: &str) -> Result<String, OpsviewConfigError> {
133        self.name = self.validated_name(new_name)?;
134        Ok(self.name.clone())
135    }
136
137    fn clear_readonly(&mut self) {
138        self.id = None;
139        self.priority = None;
140        self.ref_ = None;
141        self.uncommitted = None;
142    }
143}
144
145impl PersistentMap for ConfigObjectMap<Tenancy> {
146    fn config_path() -> Option<String> {
147        Some("/config/tenancy".to_string())
148    }
149}
150
151/// Builder for [`Tenancy`] objects, used to simplify the creation of new instances.
152///
153/// # Example
154/// ```rust
155/// use opsview::config::{Role, Tenancy};
156/// use opsview::prelude::*;
157///
158/// let my_role = Role::minimal("My Role").unwrap();
159///    
160/// let tenancy = Tenancy::builder()
161///    .name("My Tenancy")
162///    .primary_role(my_role)
163///    .build()
164///    .unwrap();
165///
166/// assert_eq!(tenancy.name, "My Tenancy".to_string());
167/// ```
168#[derive(Clone, Debug, Default)]
169pub struct TenancyBuilder {
170    // Required fields ---------------------------------------------------------------------------//
171    name: Option<String>,
172    primary_role: Option<RoleRef>,
173    // Optional fields ---------------------------------------------------------------------------//
174    description: Option<String>,
175}
176
177impl Builder for TenancyBuilder {
178    type ConfigObject = Tenancy;
179
180    /// Creates a new [`TenancyBuilder`] instance with default values.
181    ///
182    /// # Returns
183    /// A `TenancyBuilder` instance.
184    fn new() -> Self {
185        TenancyBuilder::default()
186    }
187
188    /// Sets the name field.
189    ///
190    /// # Arguments
191    /// * `name` - The name of the `Tenancy`.
192    fn name(mut self, name: &str) -> Self {
193        self.name = Some(name.to_string());
194        self
195    }
196
197    /// Consumes the builder and returns a [`Tenancy`] object.
198    ///
199    /// # Returns
200    /// A `Tenancy` object.
201    ///
202    /// # Errors
203    /// If the `name` field is not set or invalid, an `Error` will be returned.
204    /// If the `primary_role` field is not set, an `Error` will be returned.
205    fn build(self) -> Result<Self::ConfigObject, OpsviewConfigError> {
206        let name = require_field(&self.name, "name")?;
207        let primary_role = require_field(&self.primary_role, "primary_role")?;
208        let validated_description =
209            validate_opt_string(self.description, validate_and_trim_description)?;
210
211        Ok(Tenancy {
212            name: validate_and_trim_tenancy_name(&name)?,
213            primary_role: Some(primary_role),
214            description: validated_description,
215            id: None,
216            priority: None,
217            ref_: None,
218            uncommitted: None,
219        })
220    }
221}
222
223impl TenancyBuilder {
224    /// Clears the name field.
225    pub fn clear_name(mut self) -> Self {
226        self.name = None;
227        self
228    }
229
230    /// Clears the description field.
231    pub fn clear_description(mut self) -> Self {
232        self.description = None;
233        self
234    }
235
236    /// Clears the primary_role field.
237    pub fn clear_primary_role(mut self) -> Self {
238        self.primary_role = None;
239        self
240    }
241
242    /// Sets the description field.
243    ///
244    /// # Arguments
245    /// * `description` - The description of the `Tenancy`.
246    pub fn description(mut self, description: &str) -> Self {
247        self.description = Some(description.to_string());
248        self
249    }
250
251    /// Sets the primary_role field.
252    ///
253    /// # Arguments
254    /// * `primary_role` - The primary [`Role`] of the `Tenancy`.
255    pub fn primary_role(mut self, primary_role: Role) -> Self {
256        self.primary_role = Some(RoleRef::from(primary_role));
257        self
258    }
259}
260
261/// A reference version of [`Tenancy`] that is used when passing or retrieving a
262/// [`Tenancy`] object as part of another object.
263#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
264pub struct TenancyRef {
265    name: String,
266    #[serde(
267        rename = "ref",
268        skip_serializing_if = "Option::is_none",
269        deserialize_with = "deserialize_readonly",
270        default
271    )]
272    ref_: Option<String>,
273}
274
275/// Enables the creation of a [`TenancyRef`] instance from a JSON representation.
276/// Typically used when parsing JSON data from the Opsview API.
277impl CreateFromJson for TenancyRef {}
278
279impl ConfigRef for TenancyRef {
280    type FullObject = Tenancy;
281
282    /// Returns the reference string of the [`TenancyRef`] object.
283    fn ref_(&self) -> Option<String> {
284        self.ref_.clone()
285    }
286
287    /// Returns the name of the [`TenancyRef`] object.
288    fn name(&self) -> String {
289        self.name.clone()
290    }
291
292    /// Returns the unique name of the [`TenancyRef`] object.
293    ///
294    /// This name is used to identify the `TenancyRef` when building the `HashMap` for a
295    /// [`ConfigRefMap`].
296    fn unique_name(&self) -> String {
297        self.name.clone()
298    }
299}
300
301impl From<Tenancy> for TenancyRef {
302    /// Creates a [`TenancyRef`] object from a [`Tenancy`] object.
303    ///
304    /// # Arguments
305    /// * `tenancy` - A [`Tenancy`] object.
306    ///
307    /// # Returns
308    /// A [`TenancyRef`] object.
309    fn from(tenancy: Tenancy) -> Self {
310        Self {
311            name: tenancy.name.clone(),
312            ref_: tenancy.ref_.clone(),
313        }
314    }
315}
316
317impl From<Arc<Tenancy>> for TenancyRef {
318    fn from(item: Arc<Tenancy>) -> Self {
319        let cmd: Tenancy = Arc::try_unwrap(item).unwrap_or_else(|arc| (*arc).clone());
320        TenancyRef::from(cmd)
321    }
322}
323
324impl From<&ConfigObjectMap<Tenancy>> for ConfigRefMap<TenancyRef> {
325    fn from(tenancies: &ConfigObjectMap<Tenancy>) -> Self {
326        ref_map_from(tenancies)
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_tenancy_default() {
336        let tenancy = Tenancy::default();
337
338        assert_eq!(tenancy.name, "".to_string());
339        assert_eq!(tenancy.description, None);
340        assert_eq!(tenancy.id, None);
341        assert_eq!(tenancy.priority, None);
342        assert_eq!(tenancy.ref_, None);
343        assert_eq!(tenancy.primary_role, None);
344    }
345
346    #[test]
347    fn test_tenancy_minimal() {
348        let tenancy = Tenancy::minimal("My Tenancy");
349
350        assert_eq!(tenancy.unwrap().name, "My Tenancy".to_string());
351    }
352
353    #[test]
354    fn test_tenancy_unique_name() {
355        let tenancy = Tenancy::builder()
356            .name("My Tenancy")
357            .primary_role(Role::default())
358            .build()
359            .unwrap();
360
361        assert_eq!(tenancy.unique_name(), "My Tenancy".to_string());
362    }
363
364    #[test]
365    fn test_tenancy_builder() {
366        let tenancy = Tenancy::builder()
367            .name("My Tenancy")
368            .description("My Tenancy description")
369            .primary_role(Role::minimal("My Role").unwrap())
370            .build()
371            .unwrap();
372
373        assert_eq!(tenancy.name, "My Tenancy".to_string());
374        assert_eq!(
375            tenancy.description.unwrap(),
376            "My Tenancy description".to_string()
377        );
378        assert_eq!(tenancy.id, None);
379        assert_eq!(tenancy.priority, None);
380        assert_eq!(tenancy.ref_, None);
381        assert_eq!(tenancy.primary_role.unwrap().name(), "My Role".to_string());
382    }
383
384    #[test]
385    fn test_is_valid_tenancy_name() {
386        // Test valid names
387        assert!(validate_and_trim_tenancy_name("ValidName123").is_ok());
388        assert!(validate_and_trim_tenancy_name("Valid_Name-With.Symbols!").is_ok());
389        assert!(validate_and_trim_tenancy_name("A").is_ok());
390        assert!(validate_and_trim_tenancy_name("1").is_ok());
391        assert!(validate_and_trim_tenancy_name("A name with spaces and symbols *&^%$#@!").is_ok());
392        assert!(validate_and_trim_tenancy_name(&"a".repeat(191)).is_ok()); // Max length
393
394        // Test invalid names
395        assert!(validate_and_trim_tenancy_name("").is_err()); // Empty name
396        assert!(validate_and_trim_tenancy_name(" ").is_err()); // Name with only space
397        assert!(validate_and_trim_tenancy_name(&"a".repeat(192)).is_err()); // Exceeds max length
398        assert!(validate_and_trim_tenancy_name("Invalid\nName").is_err()); // Contains newline
399        assert!(validate_and_trim_tenancy_name("Invalid\tName").is_err()); // Contains tab
400        assert!(validate_and_trim_tenancy_name("Invalid\rName").is_err()); // Contains carriage return
401    }
402}