opsview/config/
bsmcomponent.rs

1//! Contains the [`BSMComponent`] struct and implementations.
2//!
3//! In Opsview, a [Business Service Management
4//! (BSM)](https://docs.itrsgroup.com/docs/opsview/current/monitoring/business-service-monitoring/business-service-monitoring/index.html)
5//! Component is a building block for creating [BSM
6//! Services](https://docs.itrsgroup.com/docs/opsview/current/monitoring/business-service-monitoring/create-business-service/index.html#create-a-business-service).
7
8use super::{
9    Host, HostRef, HostTemplate, HostTemplateRef, MonitoringCluster, MonitoringClusterRef,
10};
11use crate::{prelude::*, util::*};
12use lazy_static::lazy_static;
13use regex::Regex;
14use serde::{Deserialize, Serialize};
15use std::sync::Arc;
16
17/// Represents a [Business Service Management
18/// (BSM)](https://docs.itrsgroup.com/docs/opsview/current/monitoring/business-service-monitoring/business-service-monitoring/index.html)
19/// Component in Opsview.
20///
21/// This struct encapsulates the data structure for a [BSM
22/// Component](https://docs.itrsgroup.com/docs/opsview/current/monitoring/business-service-monitoring/create-business-service/index.html#create-a-component)
23/// as used in Opsview. BSM Components are building blocks for creating [BSM
24/// Services](https://docs.itrsgroup.com/docs/opsview/current/monitoring/business-service-monitoring/create-business-service/index.html#create-a-business-service).
25///
26/// # Example
27/// ```rust
28/// use opsview::config::{BSMComponent, Host, HostGroup, HostTemplate, MonitoringCluster};
29/// use opsview::prelude::*;
30///
31/// let host_template = HostTemplate::builder()
32///    .name("My Host Template")
33///    .build()
34///    .unwrap();
35///
36/// let mut host_templates = ConfigObjectMap::<HostTemplate>::new();
37/// host_templates.add(host_template.clone());
38///
39/// // Shadowing host_templates to avoid mutable objects after adding the host template to the map.
40/// let host_templates = host_templates;
41///
42/// let parent_group = HostGroup::minimal("Opsview")
43///     .expect("Failed to create a minimal HostGroup with name 'Opsview'");
44///
45/// let host_group = HostGroup::builder()
46///   .name("My Host Group")
47///   .parent(parent_group)
48///   .build()
49///   .unwrap();
50///
51/// let cluster_1 = MonitoringCluster::minimal("Cluster 1")
52///     .expect("Failed to create a minimal MonitoringCluster with name 'Cluster 1'");
53///
54/// let host = Host::builder()
55///   .name("my_host")
56///   .alias("My Host")
57///   .ip("127.0.0.1")
58///   .hostgroup(host_group)
59///   .hosttemplates(&host_templates)
60///   .monitored_by(cluster_1)
61///   .build()
62///   .unwrap();
63///
64/// let mut hosts = ConfigObjectMap::<Host>::new();
65/// hosts.add(host);;
66///
67/// // Shadowing hosts to avoid mutable objects after adding the host to the map.
68/// let hosts = hosts;
69///
70/// let bsm_component = BSMComponent::builder()
71///  .name("My BSM Component")
72///  .host_template(host_template)
73///  .hosts(&hosts)
74///  .quorum_pct("100.00")
75///  .build()
76///  .unwrap();
77///
78///  assert_eq!(bsm_component.name, "My BSM Component".to_string());
79///  assert_eq!(bsm_component.hosts.as_ref().unwrap().len(), 1);
80///  assert_eq!(bsm_component.hosts.unwrap().get("my_host").unwrap().name(), "my_host".to_string());
81/// ```
82#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
83pub struct BSMComponent {
84    // Required fields ---------------------------------------------------------------------------//
85    /// The name of the [`BSMComponent`].
86    pub name: String,
87
88    // Semi-optional fields ----------------------------------------------------------------------//
89    // Required when building a new object, but not always present from the API, so optional for
90    // serializing purposes.
91    /// The [`HostTemplate`] associated with the component.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub host_template: Option<HostTemplateRef>,
94
95    /// A unique identifier for the [`HostTemplate`].
96    #[serde(
97        skip_serializing_if = "Option::is_none",
98        deserialize_with = "deserialize_string_or_number_to_u64",
99        default
100    )]
101    pub host_template_id: Option<u64>,
102
103    /// A [`ConfigRefMap`] of [`HostRef`] objects associated with this component.
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub hosts: Option<ConfigRefMap<HostRef>>,
106
107    /// A string representing the quorum percentage for the component.
108    ///
109    /// The quorum percentage is a string representing a percentage with exactly 2 decimals.
110    /// The percentage must be a valid ratio for the number of hosts associated with the component.
111    ///
112    /// For example, if the component has 3 hosts, the quorum percentage must be one of the
113    /// following:
114    /// * 0.00
115    /// * 33.33
116    /// * 66.67
117    /// * 100.00
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub quorum_pct: Option<String>,
120
121    // Optional fields ---------------------------------------------------------------------------//
122    /// A reference to the [`MonitoringCluster`] which will handle the notifications for the
123    /// component.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub monitoring_cluster: Option<MonitoringClusterRef>,
126
127    // Read-only fields --------------------------------------------------------------------------//
128    /// Unix Timestamp indicating when the icon was last updated, or 0 if there is no icon.
129    #[serde(
130        skip_serializing_if = "Option::is_none",
131        deserialize_with = "deserialize_string_or_number_to_u64",
132        default
133    )]
134    pub has_icon: Option<u64>,
135
136    /// The unique identifier of the [`BSMComponent`].
137    #[serde(
138        skip_serializing_if = "Option::is_none",
139        deserialize_with = "deserialize_string_or_number_to_u64",
140        default
141    )]
142    pub id: Option<u64>,
143
144    /// A reference string unique to this [`BSMComponent`].
145    #[serde(
146        rename = "ref",
147        skip_serializing_if = "Option::is_none",
148        deserialize_with = "deserialize_readonly",
149        default
150    )]
151    pub ref_: Option<String>,
152
153    /// A boolean indicating whether the component is uncommitted.
154    #[serde(
155        skip_serializing_if = "Option::is_none",
156        deserialize_with = "deserialize_string_or_number_to_option_bool",
157        serialize_with = "serialize_option_bool_as_string",
158        default
159    )]
160    pub uncommitted: Option<bool>,
161}
162
163/// Implementation of the [`CreateFromJson`] trait for `BSMComponent`.
164///
165/// Enables the creation of a `BSMComponent` instance from a JSON representation,
166/// typically used when parsing JSON data from the Opsview API.
167impl CreateFromJson for BSMComponent {}
168
169/// Implementation of the [`ConfigObject`] trait for `BSMComponent`.
170///
171/// Provides specific behavior for [`BSMComponent`]s, such as determining the configuration path
172/// and unique name within the Opsview system.
173impl ConfigObject for BSMComponent {
174    type Builder = BSMComponentBuilder;
175
176    /// Returns a builder for constructing a `BSMComponent` object.
177    ///
178    /// # Returns
179    /// A `BSMComponentBuilder` object.
180    fn builder() -> Self::Builder {
181        BSMComponentBuilder::new()
182    }
183
184    /// Returns the API configuration path for [`BSMComponent`]s.
185    ///
186    /// # Returns
187    /// A string representing the API path where [`BSMComponent`]s are configured.
188    fn config_path() -> Option<String> {
189        Some("/config/bsmcomponent".to_string())
190    }
191
192    /// Returns a minimal `BSMComponent` object with only the name set.
193    ///
194    /// # Arguments
195    /// * `name` - Name of the [`BSMComponent`].
196    ///
197    /// # Returns
198    /// A minimal `BSMComponent` object with only the name set and all other fields in their default
199    /// state.
200    fn minimal(name: &str) -> Result<Self, OpsviewConfigError> {
201        Ok(Self {
202            name: validate_and_trim_bsmcomponent_name(name)?,
203            ..Default::default()
204        })
205    }
206
207    /// Returns the unique name of the [`BSMComponent`].
208    ///
209    /// This name is used to identify the `BSMComponent` when building the `HashMap` for a
210    /// [`ConfigObjectMap`].
211    ///
212    /// Since names are not required to be unique for BSMComponent objects in Opsview, we have to
213    /// add the id at the end of the string, if it's present.
214    ///
215    /// If the id is not present, but the ref_ is, use the ref_ instead as is.
216    ///
217    /// If neither is present, use the name and hope for the best. // TODO: Investigate a better approach.
218    ///
219    /// # Returns
220    /// A string representing the unique name of the [`BSMComponent`].
221    fn unique_name(&self) -> String {
222        let name = self.name.clone();
223        match (self.id.as_ref(), self.ref_.as_ref()) {
224            (Some(id), _) => format!("{}-{}", name, id),
225            (_, Some(ref_)) => ref_.clone(),
226            _ => name,
227        }
228    }
229}
230
231impl Persistent for BSMComponent {
232    /// Returns the unique identifier.
233    fn id(&self) -> Option<u64> {
234        self.id
235    }
236
237    /// Returns the reference string if it's not empty.
238    fn ref_(&self) -> Option<String> {
239        if self.ref_.as_ref().is_some_and(|x| !x.is_empty()) {
240            self.ref_.clone()
241        } else {
242            None
243        }
244    }
245    /// Returns the name if it's not empty.
246    fn name(&self) -> Option<String> {
247        if self.name.is_empty() {
248            None
249        } else {
250            Some(self.name.clone())
251        }
252    }
253
254    fn name_regex(&self) -> Option<String> {
255        Some(BSM_COMPONENT_NAME_REGEX_STR.to_string())
256    }
257
258    fn validated_name(&self, name: &str) -> Result<String, OpsviewConfigError> {
259        validate_and_trim_bsmcomponent_name(name)
260    }
261
262    fn set_name(&mut self, new_name: &str) -> Result<String, OpsviewConfigError> {
263        self.name = self.validated_name(new_name)?;
264        Ok(self.name.clone())
265    }
266
267    /// Clears the read-only fields.
268    fn clear_readonly(&mut self) {
269        self.has_icon = None;
270        self.id = None;
271        self.ref_ = None;
272        self.uncommitted = None;
273    }
274}
275
276impl PersistentMap for ConfigObjectMap<BSMComponent> {
277    fn config_path() -> Option<String> {
278        Some("/config/bsmcomponent".to_string())
279    }
280}
281
282/// Builder for creating instances of [`BSMComponent`].
283///
284/// This struct provides a fluent interface for constructing a `BSMComponent` object.
285#[derive(Clone, Debug, Default)]
286pub struct BSMComponentBuilder {
287    name: Option<String>,
288    host_template: Option<HostTemplateRef>,
289    host_template_id: Option<u64>,
290    hosts: Option<ConfigRefMap<HostRef>>,
291    quorum_pct: Option<String>,
292    monitoring_cluster: Option<MonitoringClusterRef>,
293}
294
295impl Builder for BSMComponentBuilder {
296    type ConfigObject = BSMComponent;
297
298    /// Creates a new instance of [`BSMComponentBuilder`] with default values.
299    ///
300    /// Initializes a new builder for creating a [`BSMComponent`] object with all fields in their
301    /// default state.
302    fn new() -> Self {
303        BSMComponentBuilder::default()
304    }
305
306    /// A fluent method that sets the `name` field and returns Self, allowing for method
307    /// chaining.
308    ///
309    /// # Arguments
310    /// * `name` - Name of the [`BSMComponent`].
311    fn name(mut self, name: &str) -> Self {
312        self.name = Some(name.to_string());
313        self
314    }
315
316    /// Builds the [`BSMComponent`] with the specified properties.
317    ///
318    /// Constructs a new `BSMComponent` based on the current state of the builder.
319    /// This method performs validations and returns an error if any required field
320    /// is not set.
321    ///
322    /// # Returns
323    /// A `Result` containing the constructed `BSMComponent` or an error if the component
324    /// could not be built due to missing required fields.
325    fn build(self) -> Result<Self::ConfigObject, OpsviewConfigError> {
326        let name = require_field(&self.name, "name")?;
327        let host_template = require_field(&self.host_template, "host_template")?;
328        let hosts = require_field(&self.hosts, "hosts")?;
329        let quorum_pct = require_field(&self.quorum_pct, "quorum_pct")?;
330
331        // TODO: Assert that self.host_template_id == host_template.id if both are present.
332
333        Ok(BSMComponent {
334            // Required fields
335            name: validate_and_trim_bsmcomponent_name(&name)?,
336            host_template: Some(host_template),
337            host_template_id: self.host_template_id,
338            monitoring_cluster: self.monitoring_cluster,
339            quorum_pct: Some(validated_pct_and_ratio(&quorum_pct, hosts.len())?),
340            hosts: Some(hosts),
341            // Read-only fields
342            has_icon: None,
343            id: None,
344            ref_: None,
345            uncommitted: None,
346        })
347    }
348}
349
350impl BSMComponentBuilder {
351    /// Clears the `host_template` field.
352    pub fn clear_host_template(mut self) -> Self {
353        self.host_template = None;
354        self
355    }
356
357    /// Clears the `host_template_id` field.
358    pub fn clear_host_template_id(mut self) -> Self {
359        self.host_template_id = None;
360        self
361    }
362
363    /// Clears the `hosts` field.
364    pub fn clear_hosts(mut self) -> Self {
365        self.hosts = None;
366        self
367    }
368
369    /// Clears the `monitoring_cluster` field.
370    pub fn clear_monitoring_cluster(mut self) -> Self {
371        self.monitoring_cluster = None;
372        self
373    }
374
375    /// Clears the `name` field.
376    pub fn clear_name(mut self) -> Self {
377        self.name = None;
378        self
379    }
380
381    /// Clears the `quorum_pct` field.
382    pub fn clear_quorum_pct(mut self) -> Self {
383        self.quorum_pct = None;
384        self
385    }
386
387    /// A fluent method that sets the `host_template` field and returns Self, allowing for method
388    /// chaining.
389    ///
390    /// # Arguments
391    /// * `host_template` - `HostTemplate` associated with the component.
392    pub fn host_template(mut self, host_template: HostTemplate) -> Self {
393        self.host_template = Some(HostTemplateRef::from(host_template));
394        self
395    }
396
397    /// A fluent method that sets the `host_template_id` field and returns Self, allowing for method
398    /// chaining.
399    ///
400    /// # Arguments
401    /// * `host_template_id` - Unique identifier for the `HostTemplate`.
402    pub fn host_template_id(mut self, host_template_id: u64) -> Self {
403        self.host_template_id = Some(host_template_id);
404        self
405    }
406
407    /// A fluent method that sets the `hosts` field and returns Self, allowing for method
408    /// chaining.
409    ///
410    /// # Arguments
411    /// * `hosts` - A reference to a [`ConfigObjectMap`] of [`Host`] objects associated with this component.
412    pub fn hosts(mut self, hosts: &ConfigObjectMap<Host>) -> Self {
413        if let Some(ref host_template) = self.host_template {
414            for host in hosts.values() {
415                if !host.has_template(host_template) {
416                    panic!(
417                        "Host '{}' does not have the template '{}'",
418                        host.name,
419                        host_template.name()
420                    );
421                }
422            }
423        } else {
424            panic!("host_template must be set before hosts");
425        }
426
427        self.hosts = Some(hosts.into());
428        self
429    }
430
431    /// A fluent method that sets the `monitoring_cluster` field and returns Self, allowing for
432    /// method chaining.
433    ///
434    /// # Arguments
435    /// * `monitoring_cluster` - [`MonitoringCluster`] which will handle the notifications for the component.
436    pub fn monitoring_cluster(mut self, monitoring_cluster: MonitoringCluster) -> Self {
437        self.monitoring_cluster = Some(MonitoringClusterRef::from(monitoring_cluster));
438        self
439    }
440
441    /// A fluent method that sets the `quorum_pct` field and returns Self, allowing for method
442    /// chaining.
443    ///
444    /// # Arguments
445    /// * `quorum_pct` - String representing the quorum percentage for the component.
446    pub fn quorum_pct(mut self, quorum_pct: &str) -> Self {
447        self.quorum_pct = Some(quorum_pct.to_string());
448        self
449    }
450}
451
452/// A reference version of [`BSMComponent`] that is used when passing or retrieving a [`BSMComponent`]
453/// object as part of another object.
454#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
455pub struct BSMComponentRef {
456    name: String,
457    #[serde(
458        rename = "ref",
459        skip_serializing_if = "Option::is_none",
460        deserialize_with = "deserialize_readonly",
461        default
462    )]
463    ref_: Option<String>,
464}
465
466/// Enables the creation of a [`BSMComponentRef`] instance from a JSON representation.
467/// Typically used when parsing JSON data from the Opsview API.
468impl CreateFromJson for BSMComponentRef {}
469
470impl ConfigRef for BSMComponentRef {
471    type FullObject = BSMComponent;
472
473    /// Returns the reference string of the [`BSMComponentRef`] object.
474    fn ref_(&self) -> Option<String> {
475        self.ref_.clone()
476    }
477
478    /// Returns the name of the [`BSMComponentRef`] object.
479    fn name(&self) -> String {
480        self.name.clone()
481    }
482
483    /// Returns the unique name of the [`BSMComponentRef`].
484    ///
485    /// This name is used to identify the `BSMComponentRef` when building the `HashMap` for a
486    /// [`ConfigRefMap`].
487    ///
488    /// If the name is not present, but the ref_ is, use the ref_ instead as is.
489    ///
490    /// If neither is present, use the name and hope for the best. // TODO: Investigate a better approach.
491    ///
492    /// # Returns
493    /// A string representing the unique name of the [`BSMComponentRef`].
494    fn unique_name(&self) -> String {
495        let name = self.name.clone();
496        match self.ref_.as_ref() {
497            Some(ref_) => ref_.clone(),
498            _ => name,
499        }
500    }
501}
502
503impl From<BSMComponent> for BSMComponentRef {
504    fn from(component: BSMComponent) -> Self {
505        Self {
506            name: component.name.clone(),
507            ref_: component.ref_.clone(),
508        }
509    }
510}
511
512impl From<Arc<BSMComponent>> for BSMComponentRef {
513    fn from(item: Arc<BSMComponent>) -> Self {
514        let component: BSMComponent = Arc::try_unwrap(item).unwrap_or_else(|arc| (*arc).clone());
515        BSMComponentRef::from(component)
516    }
517}
518
519impl From<&ConfigObjectMap<BSMComponent>> for ConfigRefMap<BSMComponentRef> {
520    fn from(components: &ConfigObjectMap<BSMComponent>) -> Self {
521        ref_map_from(components)
522    }
523}
524
525lazy_static! {
526    static ref QUORUM_PCT_REGEX: Regex = regex::Regex::new(r"^\d{1,3}\.\d{2}$").unwrap();
527}
528
529/// Validates the format of the quorum percentage.
530///
531/// # Arguments
532/// * `quorum_pct` - String representing the quorum percentage.
533///
534/// # Returns
535/// A Result indicating whether the quorum percentage is valid or not.
536fn validated_pct_and_ratio(
537    percentage: &str,
538    number_of_hosts: usize,
539) -> Result<String, OpsviewConfigError> {
540    if percentage == "0.00" {
541        return Ok(percentage.to_string());
542    }
543
544    if percentage == "100.00" {
545        return Ok(percentage.to_string());
546    }
547
548    if number_of_hosts == 0 {
549        return Err(OpsviewConfigError::InvalidQuorum(
550            "The number of hosts must be greater than 0".to_string(),
551        ));
552    }
553
554    // Validate the percentage format
555    if !QUORUM_PCT_REGEX.is_match(percentage) {
556        return Err(OpsviewConfigError::InvalidQuorum(
557            "Must be a number with exactly 2 decimals".to_string(),
558        ));
559    }
560
561    // Generate valid percentages as strings
562    let mut valid_percentages = Vec::new();
563    for host_count in 0..=number_of_hosts {
564        let pct = 100.0 * host_count as f64 / number_of_hosts as f64;
565        valid_percentages.push(format!("{:.2}", pct));
566    }
567
568    // Check if the provided percentage is in the list of valid percentages
569    if valid_percentages.contains(&percentage.to_string()) {
570        Ok(percentage.to_string())
571    } else {
572        Err(OpsviewConfigError::InvalidQuorum(format!(
573            "The percentage '{}' is not a valid ratio for '{}' hosts",
574            percentage, number_of_hosts
575        )))
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use crate::config::{HostGroup, MonitoringCluster};
583    use pretty_assertions::assert_eq;
584
585    #[test]
586    fn test_is_valid_pct_and_ratio() {
587        assert!(validated_pct_and_ratio("100.00", 1).is_ok());
588        assert!(validated_pct_and_ratio("99.99", 10000).is_ok());
589        assert!(validated_pct_and_ratio("0.00", 0).is_ok());
590        assert!(validated_pct_and_ratio("0.01", 10000).is_ok());
591        assert!(validated_pct_and_ratio("0.99", 10000).is_ok());
592        assert!(validated_pct_and_ratio("1.00", 100).is_ok());
593        assert!(validated_pct_and_ratio("1.01", 10000).is_ok());
594        assert!(validated_pct_and_ratio("99.99", 10000).is_ok());
595        assert!(validated_pct_and_ratio("100.00", 1).is_ok());
596        assert!(validated_pct_and_ratio("50.00", 0).is_err());
597        assert!(validated_pct_and_ratio("100.01", 10000).is_err());
598        assert!(validated_pct_and_ratio("999.99", 1000).is_err());
599        assert!(validated_pct_and_ratio("999.99", 10).is_err());
600        assert!(validated_pct_and_ratio("999.99", 100).is_err());
601        assert!(validated_pct_and_ratio("100", 1).is_err());
602        assert!(validated_pct_and_ratio("1", 100).is_err());
603        assert!(validated_pct_and_ratio("0.00", 3).is_ok()); // 0/3
604        assert!(validated_pct_and_ratio("100.00", 3).is_ok()); // 3/3
605        assert!(validated_pct_and_ratio("33.33", 3).is_ok()); // 1/3
606        assert!(validated_pct_and_ratio("66.67", 3).is_ok()); // 2/3
607        assert!(validated_pct_and_ratio("100", 3).is_err());
608        assert!(validated_pct_and_ratio("0.00", 2).is_ok());
609        assert!(validated_pct_and_ratio("50.00", 2).is_ok());
610        assert!(validated_pct_and_ratio("100.00", 2).is_ok());
611        assert!(validated_pct_and_ratio("90.00", 2).is_err());
612
613        let host_template = HostTemplate::builder()
614            .name("Host Template ")
615            .build()
616            .unwrap();
617
618        let mut host_templates = ConfigObjectMap::<HostTemplate>::new();
619        host_templates.add(host_template.clone());
620
621        let host_templates = host_templates;
622
623        let root_hostgroup = HostGroup::builder()
624            .name("Opsview")
625            .clear_parent()
626            .build()
627            .unwrap();
628
629        let cluster = MonitoringCluster::minimal("Cluster 1")
630            .expect("Failed to create cluster with name 'Cluster 1'");
631
632        let host = Host::builder()
633            .name("Host_1")
634            .alias("Host 1")
635            .ip("127.0.0.1")
636            .hostgroup(root_hostgroup)
637            .monitored_by(cluster)
638            .hosttemplates(&host_templates)
639            .build()
640            .unwrap();
641
642        let mut hosts = ConfigObjectMap::<Host>::new();
643        hosts.add(host);
644
645        let bsm_comp_1 = BSMComponent::builder()
646            .name("Comp 1")
647            .host_template(host_template.clone())
648            .hosts(&hosts)
649            .quorum_pct("100.00")
650            .build();
651
652        assert!(bsm_comp_1.is_ok());
653
654        let bsm_comp_2 = BSMComponent::builder()
655            .name("Comp 1")
656            .host_template(host_template.clone())
657            .hosts(&hosts)
658            .quorum_pct("100")
659            .build();
660
661        assert!(bsm_comp_2.is_err());
662        assert_eq!(
663            bsm_comp_2.err().unwrap().to_string(),
664            "Invalid quorum: Must be a number with exactly 2 decimals",
665        );
666        let bsm_comp_3 = BSMComponent::builder()
667            .name("Comp 1")
668            .host_template(host_template)
669            .hosts(&hosts)
670            .quorum_pct("90.00")
671            .build();
672
673        assert!(bsm_comp_3.is_err());
674        assert_eq!(
675            bsm_comp_3.err().unwrap().to_string(),
676            "Invalid quorum: The percentage '90.00' is not a valid ratio for '1' hosts",
677        );
678    }
679
680    #[test]
681    fn test_default() {
682        let bsm_component = BSMComponent::default();
683
684        assert!(bsm_component.name.is_empty());
685    }
686
687    #[test]
688    fn test_minimal() {
689        let bsm_component = BSMComponent::minimal("My BSM Component");
690
691        assert_eq!(bsm_component.unwrap().name, "My BSM Component".to_string());
692    }
693
694    #[test]
695    fn test_is_valid_bsmcomponent_name() {
696        // Test valid names
697        assert!(validate_and_trim_bsmcomponent_name("ValidComponent123").is_ok());
698        assert!(validate_and_trim_bsmcomponent_name("Valid_Component-With.Symbols!").is_ok());
699        assert!(validate_and_trim_bsmcomponent_name("A").is_ok());
700        assert!(validate_and_trim_bsmcomponent_name(
701            "A component name with spaces and symbols *&^%$#@!"
702        )
703        .is_ok());
704        assert!(validate_and_trim_bsmcomponent_name(&"a".repeat(255)).is_ok()); // Max length
705
706        // Test invalid names
707        assert!(validate_and_trim_bsmcomponent_name("").is_err()); // Empty name
708        assert!(validate_and_trim_bsmcomponent_name(" ").is_err()); // Name with only space
709        assert!(validate_and_trim_bsmcomponent_name(&"a".repeat(256)).is_err()); // Exceeds max length
710        assert!(validate_and_trim_bsmcomponent_name("Invalid\nComponent").is_err()); // Contains newline
711        assert!(validate_and_trim_bsmcomponent_name("Invalid\tComponent").is_err()); // Contains tab
712        assert!(validate_and_trim_bsmcomponent_name("Invalid\rComponent").is_err());
713        // Contains carriage return
714    }
715}