tmflib/tmf620/
product_specification.rs

1//! Product Specification Module
2//!
3
4use chrono::Utc;
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use super::MOD_PATH;
10
11use crate::common::event::{Event, EventPayload};
12use crate::common::tmf_error::TMFError;
13use crate::{
14    serde_value_to_type, vec_insert, Cardinality, HasDescription, HasId, HasLastUpdate, HasName,
15    HasReference, HasValidity, TMFEvent, TimePeriod,
16};
17use tmflib_derive::{HasDescription, HasId, HasLastUpdate, HasName, HasValidity};
18
19use crate::tmf633::characteristic_specification::CharacteristicSpecification;
20use crate::tmf633::service_specification::{ServiceSpecification, ServiceSpecificationRef};
21
22const CLASS_PATH: &str = "productSpecification";
23const SPEC_VERS: &str = "1.0";
24const CHAR_VALUE_MIN_CARD: Cardinality = 0;
25const CHAR_VALUE_MAX_CARD: Cardinality = 1;
26/// Verb to tag converted ServiceSpecifications with.
27pub const SPEC_CONV_VERB: &str = "Imported";
28
29/// Product Specification Characteristic
30#[derive(Clone, Debug, Default, Deserialize, Serialize, HasValidity)]
31#[serde(rename_all = "camelCase")]
32pub struct ProductSpecificationCharacteristic {
33    /// Is this characteristic configurable
34    pub configurable: bool,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    description: Option<String>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    extensible: Option<bool>,
39    /// Is this characteristic unique
40    pub is_unique: bool,
41    max_cardinality: Cardinality,
42    min_cardinality: Cardinality,
43    /// Characteristic Name
44    pub name: String,
45    /// Regular expression for value
46    #[serde(skip_serializing_if = "Option::is_none")]
47    regex: Option<String>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    value_type: Option<String>,
50    /// Validity period for this characteristic
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub valid_for: Option<TimePeriod>,
53    /// Set of characteristic relationships
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub product_spec_char_relationship: Option<Vec<ProductSpecificationCharacteristicRelationship>>,
56}
57
58impl ProductSpecificationCharacteristic {
59    /// Create a new characteristic
60    /// By default, new characteristics are optional (cardinality: min=0 max=1)
61    /// # Examples
62    /// ```
63    /// # use tmflib::tmf620::product_specification::ProductSpecificationCharacteristic;
64    /// let ps_char = ProductSpecificationCharacteristic::new(String::from("My Characteristic"));
65    /// ```
66    pub fn new(name: impl Into<String>) -> ProductSpecificationCharacteristic {
67        ProductSpecificationCharacteristic {
68            configurable: true,
69            max_cardinality: CHAR_VALUE_MAX_CARD,
70            min_cardinality: CHAR_VALUE_MIN_CARD,
71            name: name.into(),
72            valid_for: Some(TimePeriod::default()),
73            ..Default::default()
74        }
75    }
76
77    /// Set configuraable flag
78    pub fn configurable(mut self, configurable: bool) -> ProductSpecificationCharacteristic {
79        self.configurable = configurable;
80        self
81    }
82
83    /// Set description of characteristic
84    pub fn description(mut self, description: String) -> ProductSpecificationCharacteristic {
85        self.description = Some(description.clone());
86        self
87    }
88
89    /// Set extensible flag
90    pub fn extensible(mut self, extensible: bool) -> ProductSpecificationCharacteristic {
91        self.extensible = Some(extensible);
92        self
93    }
94
95    /// Set validity period
96    pub fn validity(mut self, validity: Option<TimePeriod>) -> ProductSpecificationCharacteristic {
97        self.valid_for = validity;
98        self
99    }
100
101    /// Set MIN / MAX cardindiality
102    /// Will ignore change if min > max.
103    /// # Examples
104    /// ```
105    /// # use tmflib::tmf620::product_specification::ProductSpecificationCharacteristic;
106    /// let ps_char = ProductSpecificationCharacteristic::new(String::from("My Characteristic"))
107    ///     .cardinality(0,1);
108    /// ```
109    pub fn cardinality(
110        mut self,
111        min: Cardinality,
112        max: Cardinality,
113    ) -> ProductSpecificationCharacteristic {
114        // Quick check to make sure min < max
115        if min > max {
116            // Not sure if we should just ignore this ?
117            return self;
118        }
119        self.min_cardinality = min;
120        self.max_cardinality = max;
121        self
122    }
123}
124
125// Conversion from Service CharacteristicSpecification into Product Spec.
126impl From<CharacteristicSpecification> for ProductSpecificationCharacteristic {
127    fn from(value: CharacteristicSpecification) -> Self {
128        ProductSpecificationCharacteristic {
129            name: value.name.unwrap_or_default(),
130            min_cardinality: value.min_cardinality.unwrap_or_default(),
131            max_cardinality: value.max_cardinality.unwrap_or_default(),
132            configurable: value.configurable.unwrap_or_default(),
133            is_unique: value.is_unique.unwrap_or_default(),
134            description: value.description.clone(),
135            valid_for: value.valid_for.clone(),
136            ..Default::default()
137        }
138    }
139}
140
141/// Bundled Products
142#[derive(Clone, Debug, Default, Deserialize, Serialize)]
143#[serde(rename_all = "camelCase")]
144pub struct BundledProductSpecification {
145    id: String,
146    href: String,
147    lifecycle_status: Option<String>,
148    name: String,
149}
150
151/// Product Specification
152#[derive(
153    Clone, Debug, Default, Deserialize, HasId, HasLastUpdate, HasDescription, HasName, Serialize,
154)]
155#[serde(rename_all = "camelCase")]
156pub struct ProductSpecification {
157    /// Id
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub id: Option<String>,
160    /// HREF where object is located
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub href: Option<String>,
163    /// Brand
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub brand: Option<String>,
166    /// Description
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub description: Option<String>,
169    /// Is a bundle?
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub is_bundle: Option<bool>,
172    /// Timestamp of last change to this payload
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub last_update: Option<String>,
175    /// Status of this specification
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub lifecycle_status: Option<String>,
178    /// Name
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub name: Option<String>,
181    /// Product Number / Code
182    #[serde(skip_serializing_if = "Option::is_none")]
183    product_number: Option<String>,
184    /// Version of this record
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub version: Option<String>,
187    /// Set of characteristics for this specification
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub product_spec_characteristic: Option<Vec<ProductSpecificationCharacteristic>>,
190    /// Bundled specifications
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub bundled_product_specification: Option<Vec<BundledProductSpecification>>,
193}
194
195impl ProductSpecification {
196    /// Create new instance of Product Specification
197    pub fn new(name: impl Into<String>) -> ProductSpecification {
198        let mut prod_spec = ProductSpecification::create_with_time();
199        prod_spec.name = Some(name.into());
200        prod_spec.version = Some(SPEC_VERS.to_string());
201
202        prod_spec
203    }
204
205    /// Set lifecycle status
206    pub fn status(&mut self, status: &str) {
207        self.lifecycle_status = Some(status.to_owned());
208    }
209
210    /// Add a new Characteristic into the specification
211    pub fn with_charateristic(
212        mut self,
213        characteristic: ProductSpecificationCharacteristic,
214    ) -> ProductSpecification {
215        vec_insert(&mut self.product_spec_characteristic, characteristic);
216        self
217    }
218
219    /// Get the class of this object
220    pub fn characteristic_by_name(
221        &self,
222        name: &str,
223    ) -> Option<&ProductSpecificationCharacteristic> {
224        match self.product_spec_characteristic.as_ref() {
225            Some(chars) => chars.iter().find(|c| c.name == name),
226            None => None,
227        }
228    }
229
230    /// Link remote characteristic specification
231    pub fn link_characteristic(&mut self, remote: &ProductSpecification, name: impl Into<String>) {
232        let name: String = name.into();
233        let char_opt = remote.characteristic_by_name(&name);
234
235        if let Some(char_spec) = char_opt {
236            let mut new_char = char_spec.clone();
237            let char_rel = ProductSpecificationCharacteristicRelationship {
238                id: remote.get_id(),
239                href: remote.get_href(),
240                char_spec_seq: 0,
241                name: name.clone(),
242                relationship_type: String::from("dependsOn"),
243                valid_for: new_char.valid_for.clone(),
244            };
245            // Insert relationship into placeholder characteristic
246            vec_insert(&mut new_char.product_spec_char_relationship, char_rel);
247            vec_insert(&mut self.product_spec_characteristic, new_char);
248        }
249    }
250}
251
252/// Product Specification Reference
253#[derive(Clone, Debug, Deserialize, Serialize)]
254pub struct ProductSpecificationRef {
255    /// Id
256    pub id: String,
257    /// HREF where object is located
258    pub href: String,
259    /// Description
260    pub name: Option<String>,
261    /// Version of this record
262    pub version: Option<String>,
263}
264
265impl From<ProductSpecification> for ProductSpecificationRef {
266    fn from(ps: ProductSpecification) -> ProductSpecificationRef {
267        ProductSpecificationRef {
268            id: ps.get_id(),
269            href: ps.get_href(),
270            name: ps.name.clone(),
271            version: ps.version.clone(),
272        }
273    }
274}
275
276impl HasReference for ProductSpecification {
277    type RefType = ProductSpecificationRef;
278    fn as_ref(&self) -> Option<Self::RefType> {
279        Some(ProductSpecificationRef::from(self.clone()))
280    }
281}
282
283impl From<&ServiceSpecificationRef> for ProductSpecificationRef {
284    fn from(value: &ServiceSpecificationRef) -> Self {
285        // we cannot simply copy across the href but we can reuse the id
286        let mut ps = ProductSpecification {
287            id: Some(value.id.clone()),
288            name: Some(value.name.clone()),
289            ..Default::default()
290        };
291        ps.generate_href();
292
293        ProductSpecificationRef::from(ps)
294    }
295}
296
297// Convert a service specification into a peroduct specification
298// used as part of the import process.
299impl From<&ServiceSpecification> for ProductSpecification {
300    fn from(value: &ServiceSpecification) -> Self {
301        let mut ps = ProductSpecification::new(value.get_name());
302        // get_description() is a method on the ServiceSpecification that always returns a string
303        ps.description = Some(format!("{} [{}]", value.get_description(), SPEC_CONV_VERB));
304        if value.description.is_some() {
305            ps.description = Some(format!("{} [{}]", value.get_description(), SPEC_CONV_VERB));
306        }
307        ps.is_bundle = value.is_bundle;
308        if value.last_update.is_some() {
309            ps.set_last_update(value.last_update.as_ref().unwrap());
310        }
311        if value.spec_characteristics.is_some() {
312            // We have characteristics that require conversion
313            let mut out: Vec<ProductSpecificationCharacteristic> = Vec::new();
314            value
315                .spec_characteristics
316                .as_ref()
317                .unwrap()
318                .iter()
319                .for_each(|cs| {
320                    let psc = ProductSpecificationCharacteristic::from(cs.clone());
321                    out.push(psc);
322                });
323            ps.product_spec_characteristic = Some(out);
324        }
325        if value.version.is_some() {
326            // If source has a version defined take that
327            ps.version.clone_from(&value.version);
328        }
329        ps.lifecycle_status.clone_from(&value.lifecycle_status);
330        ps
331    }
332}
333
334// Events
335/// Product Specification Event Container
336#[derive(Clone, Debug, Default, Deserialize, Serialize)]
337pub struct ProductSpecificationEvent {
338    product_specification: ProductSpecification,
339}
340
341impl TMFEvent<ProductSpecificationEvent> for ProductSpecification {
342    fn event(&self) -> ProductSpecificationEvent {
343        ProductSpecificationEvent {
344            product_specification: self.clone(),
345        }
346    }
347}
348
349impl EventPayload<ProductSpecificationEvent> for ProductSpecification {
350    type Subject = ProductSpecification;
351    type EventType = ProductSpecificationEventType;
352    fn to_event(
353        &self,
354        event_type: Self::EventType,
355    ) -> Event<ProductSpecificationEvent, Self::EventType> {
356        let now = Utc::now();
357        let event_time = chrono::DateTime::from_timestamp(now.timestamp(), 0).unwrap();
358        let desc = format!("{:?} for {}", event_type, self.get_name());
359        Event {
360            description: Some(desc),
361            domain: Some(ProductSpecification::get_class()),
362            event_id: Uuid::new_v4().to_string(),
363            href: self.href.clone(),
364            id: self.id.clone(),
365            title: self.name.clone(),
366            event_time: event_time.to_string(),
367            event_type,
368            event: self.event(),
369            ..Event::default()
370        }
371    }
372}
373
374/// Product Specification Event Type
375#[derive(Clone, Default, Debug)]
376pub enum ProductSpecificationEventType {
377    /// Product Specification Created
378    #[default]
379    ProductSpecificationCreateEvent,
380    /// Product Specification Deleted
381    ProductSpecificationDeleteEvent,
382}
383
384/// Product Specification Characteristic Value
385/// # Detalis
386/// This object contains values used by a specification characteristic.
387/// # Example
388/// If the Product Offering is "Internet", then the Specification might be "Bandwidht" and the Value might be "100Mb"
389#[derive(Clone, Debug, Default, Deserialize, Serialize, HasValidity)]
390#[serde(rename_all = "camelCase")]
391pub struct ProductSpecificationCharacteristicValue {
392    /// Description of Characteristic Value
393    pub is_default: bool,
394    /// Value Range Interval
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub range_interval: Option<String>,
397    /// Characteristic Value Regular Expression
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub regex: Option<String>,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    unit_of_measure: Option<String>,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    value_from: Option<String>,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    value_to: Option<String>,
406    ///
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub value_type: Option<String>,
409    #[serde(skip_serializing_if = "Option::is_none")]
410    valid_for: Option<TimePeriod>,
411    value: serde_json::Value,
412}
413
414impl ProductSpecificationCharacteristicValue {
415    /// Create a new Product Specification Characteristic Value with a value
416    /// # Description
417    /// Creates a new Characterisitic Value with the provided value.
418    /// Other fields can be set by directly updating the structure.
419    /// This bypasses regular experssion checks
420    /// # Example
421    /// ```
422    /// # use tmflib::tmf620::product_specification::ProductSpecificationCharacteristicValue;
423    /// let pscv = ProductSpecificationCharacteristicValue::new();
424    /// ```
425    pub fn new() -> ProductSpecificationCharacteristicValue {
426        ProductSpecificationCharacteristicValue {
427            is_default: false,
428            ..Default::default()
429        }
430    }
431
432    /// Set the regular expression for this characteristic value
433    /// # Example
434    /// ```
435    /// # use tmflib::tmf620::product_specification::ProductSpecificationCharacteristicValue;
436    ///
437    /// let pscv = ProductSpecificationCharacteristicValue::new()
438    ///     .regex(String::from("[0-9]+(Mb|Gb)"));
439    /// ```
440    pub fn regex(
441        mut self,
442        regex: String,
443    ) -> Result<ProductSpecificationCharacteristicValue, TMFError> {
444        // For now we only wish to test if we can parse the regex string
445        let _re = Regex::new(&regex)?;
446        self.regex = Some(regex);
447        Ok(self)
448    }
449
450    /// Set the value for this characteristic value
451    /// # Example
452    /// ```
453    /// # use tmflib::tmf620::product_specification::ProductSpecificationCharacteristicValue;
454    /// # use serde_json::json;
455    /// let pscv = ProductSpecificationCharacteristicValue::new()
456    ///     .regex(String::from("[0-9]+(Mb|Gb)")).unwrap()
457    ///     .value("100Mb".into()).unwrap();
458    /// ```
459    pub fn value(
460        mut self,
461        value: serde_json::Value,
462    ) -> Result<ProductSpecificationCharacteristicValue, TMFError> {
463        self.value_type = Some(serde_value_to_type(&value).to_string());
464        match self.regex {
465            Some(ref re_str) => {
466                let re = Regex::new(re_str)?;
467                let val_str = value.to_string();
468                if !re.is_match(&val_str) {
469                    return Err(TMFError::GenericError(format!(
470                        "Value {} does not match regex {}",
471                        val_str, re_str
472                    )));
473                }
474                self.value = value;
475            }
476            // If no regex, then just set the value
477            None => self.value = value,
478        }
479        Ok(self)
480    }
481
482    /// Validate a value against the regex (if set) and return an updated
483    /// ProductSpecificationCharacteristicValue with the value set.
484    /// # Example
485    /// ```
486    /// # use tmflib::tmf620::product_specification::ProductSpecificationCharacteristicValue;
487    /// # use serde_json::json;
488    /// let pscv = ProductSpecificationCharacteristicValue::new()
489    ///     .regex(String::from("[0-9]+(Mb|Gb)")).unwrap()
490    ///    .validate("200Mb".into()).unwrap();
491    /// ```
492    pub fn validate(
493        mut self,
494        value: serde_json::Value,
495    ) -> Result<ProductSpecificationCharacteristicValue, TMFError> {
496        // If we have a regex, then validate the value against it.
497        if let Some(re_str) = &self.regex {
498            let re = Regex::new(re_str)?;
499            let val_str = value.to_string();
500            if !re.is_match(&val_str) {
501                return Err(TMFError::GenericError(format!(
502                    "Value {} does not match regex {}",
503                    val_str, re_str
504                )));
505            }
506        }
507        self.value = value;
508        Ok(self)
509    }
510}
511
512/// Product Specification Characteristic Value Use
513#[derive(Clone, Debug, Deserialize, Serialize, HasValidity)]
514#[serde(rename_all = "camelCase")]
515pub struct ProductSpecificationCharacteristicValueUse {
516    /// Description of Characteristic Value Use
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub description: Option<String>,
519    max_cardinality: u16,
520    min_cardinality: u16,
521    name: String,
522    value_type: String,
523    #[serde(skip_serializing_if = "Option::is_none")]
524    valid_for: Option<TimePeriod>,
525    #[serde(skip_serializing_if = "Option::is_none")]
526    product_spec_characteristic_value: Option<Vec<ProductSpecificationCharacteristicValue>>,
527    #[serde(skip_serializing_if = "Option::is_none")]
528    product_specification: Option<ProductSpecificationRef>,
529}
530
531impl ProductSpecificationCharacteristicValueUse {
532    /// Create a new instance of ProductSpecificationCharacteristicValueUse
533    pub fn new(name: impl Into<String>) -> ProductSpecificationCharacteristicValueUse {
534        ProductSpecificationCharacteristicValueUse {
535            description: None,
536            max_cardinality: CHAR_VALUE_MAX_CARD,
537            min_cardinality: CHAR_VALUE_MIN_CARD,
538            name: name.into(),
539            value_type: String::from("String"),
540            valid_for: None,
541            product_spec_characteristic_value: None,
542            product_specification: None,
543        }
544    }
545
546    /// Add a specificatoin into the ProductSpecificationCharacteristicValueUse
547    pub fn with_spec(&mut self, specification: ProductSpecification) {
548        self.product_specification = Some(ProductSpecificationRef::from(specification));
549    }
550}
551
552/// Product Specification Characteristic Relationship
553#[derive(Clone, Debug, Deserialize, Serialize, HasValidity)]
554pub struct ProductSpecificationCharacteristicRelationship {
555    /// Id
556    pub id: String,
557    /// HREF where object is located
558    pub href: String,
559    /// Sequence number of the related characteristic
560    pub char_spec_seq: u32,
561    /// Name of the related characteristic
562    pub name: String,
563    /// Type of relationship
564    pub relationship_type: String,
565    /// Validity period for this relationship
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub valid_for: Option<TimePeriod>,
568}
569
570#[cfg(test)]
571mod test {
572
573    use crate::tmf633::characteristic_specification::CharacteristicSpecification;
574
575    use super::*;
576    const SPEC_NAME: &str = "MySpecification";
577    const VALUE_NAME: &str = "MyCharValueUse";
578    const DESC: &str = "A Description";
579    const SERVICE_SPEC: &str = "ServiceSpecification";
580    const SPEC_STATUS: &str = "SpecificationStatus";
581
582    #[test]
583    fn test_char_value_use_new() {
584        let value_use = ProductSpecificationCharacteristicValueUse::new(VALUE_NAME);
585
586        assert_eq!(value_use.name, VALUE_NAME.to_string());
587    }
588
589    #[test]
590    fn test_char_value_use_new_card() {
591        let value_use = ProductSpecificationCharacteristicValueUse::new(VALUE_NAME);
592
593        assert_eq!(value_use.min_cardinality, CHAR_VALUE_MIN_CARD);
594        assert_eq!(value_use.max_cardinality, CHAR_VALUE_MAX_CARD);
595    }
596
597    #[test]
598    fn test_char_value_use_new_value() {
599        let value_use = ProductSpecificationCharacteristicValueUse::new(VALUE_NAME);
600
601        assert_eq!(value_use.value_type, String::from("String"));
602    }
603
604    #[test]
605    fn test_spec_new() {
606        let spec = ProductSpecification::new(SPEC_NAME);
607
608        assert_eq!(spec.name, Some(SPEC_NAME.to_string()));
609    }
610    #[test]
611    fn test_spec_new_vers() {
612        let spec = ProductSpecification::new(SPEC_NAME);
613
614        assert_eq!(spec.version, Some(SPEC_VERS.to_string()));
615    }
616
617    #[test]
618    fn test_spec_char_configurable() {
619        let spec_char = ProductSpecificationCharacteristic::new(SPEC_NAME).configurable(true);
620
621        assert_eq!(spec_char.configurable, true);
622    }
623
624    #[test]
625    fn test_spec_char_description() {
626        let spec_char =
627            ProductSpecificationCharacteristic::new(SPEC_NAME).description(DESC.to_string());
628
629        assert_eq!(spec_char.description.unwrap(), DESC.to_string());
630    }
631
632    #[test]
633    fn test_spec_extensible() {
634        let spec_char = ProductSpecificationCharacteristic::new(SPEC_NAME).extensible(true);
635
636        assert_eq!(spec_char.extensible, Some(true));
637    }
638
639    #[test]
640    fn test_spec_cardinality() {
641        let spec_char = ProductSpecificationCharacteristic::new(SPEC_NAME).cardinality(1, 2);
642
643        assert_eq!(spec_char.min_cardinality, 1);
644        assert_eq!(spec_char.max_cardinality, 2);
645    }
646
647    #[test]
648    fn test_spec_cardinality_invalid() {
649        let spec_char = ProductSpecificationCharacteristic::new(SPEC_NAME).cardinality(10, 2);
650
651        // Show in valid setting wont' update anything.
652        assert_eq!(spec_char.min_cardinality, CHAR_VALUE_MIN_CARD);
653        assert_eq!(spec_char.max_cardinality, CHAR_VALUE_MAX_CARD);
654    }
655
656    #[test]
657    fn test_spec_from_service_spec() {
658        let service_spec = CharacteristicSpecification::new(SERVICE_SPEC);
659
660        let spec = ProductSpecificationCharacteristic::from(service_spec.clone());
661
662        assert_eq!(service_spec.name.unwrap(), spec.name);
663        assert_eq!(
664            service_spec.min_cardinality.unwrap_or_default(),
665            spec.min_cardinality
666        );
667        assert_eq!(
668            service_spec.max_cardinality.unwrap_or_default(),
669            spec.max_cardinality
670        );
671        assert_eq!(service_spec.description, spec.description);
672    }
673
674    #[test]
675    fn test_spec_status() {
676        let mut spec = ProductSpecification::new(SPEC_NAME);
677
678        spec.status(SPEC_STATUS);
679
680        assert_eq!(spec.lifecycle_status.is_some(), true);
681        assert_eq!(spec.lifecycle_status.unwrap(), SPEC_STATUS.to_string());
682    }
683
684    #[test]
685    fn test_spec_with_char() {
686        let spec_char1 = ProductSpecificationCharacteristic::new(SPEC_NAME).cardinality(1, 2);
687        let spec_char2 = ProductSpecificationCharacteristic::new(SPEC_NAME).cardinality(3, 4);
688        let spec = ProductSpecification::new(SPEC_NAME)
689            .with_charateristic(spec_char1)
690            .with_charateristic(spec_char2);
691
692        assert_eq!(spec.product_spec_characteristic.is_some(), true);
693        assert_eq!(spec.product_spec_characteristic.unwrap().len(), 2);
694    }
695
696    // #[test]
697    // fn test_prodspeccharval_new() {
698    //     let pscv = ProductSpecificationCharacteristicValue::new("Value".into());
699
700    //     assert_eq!(pscv.value,"Value".into());
701    // }
702
703    #[test]
704    fn test_prodspec_asref() {
705        let ps = ProductSpecification::new(SPEC_NAME);
706        let ps_ref = ps.as_ref().unwrap();
707
708        assert_eq!(ps_ref.id, ps.get_id());
709        assert_eq!(ps_ref.href, ps.get_href());
710        assert_eq!(ps_ref.name, ps.name);
711        assert_eq!(ps_ref.version, ps.version);
712    }
713
714    #[test]
715    fn test_prodspecvalue_regex() {
716        let pscv = ProductSpecificationCharacteristicValue::new()
717            .regex(String::from("[0-9]+(Mb|Gb)"))
718            .unwrap()
719            .value("100Mb".into())
720            .unwrap();
721
722        assert_eq!(pscv.regex.is_some(), true);
723        assert_eq!(pscv.regex.unwrap(), String::from("[0-9]+(Mb|Gb)"));
724        assert_eq!(pscv.value, serde_json::Value::String("100Mb".to_string()));
725    }
726
727    #[test]
728    fn test_prodspecvalue_regex_invalid() {
729        let pscv = ProductSpecificationCharacteristicValue::new()
730            .regex(String::from("[0-9]+(Mb|Gb)"))
731            .unwrap()
732            .value("Invalid".into());
733        assert!(pscv.is_err());
734    }
735
736    #[test]
737    fn test_with_charspec() {
738        let spec_char1 = ProductSpecificationCharacteristic::new(SPEC_NAME).cardinality(1, 2);
739        let spec_char2 = ProductSpecificationCharacteristic::new(SPEC_NAME).cardinality(3, 4);
740        let spec = ProductSpecification::new(SPEC_NAME)
741            .with_charateristic(spec_char1)
742            .with_charateristic(spec_char2);
743
744        assert_eq!(spec.product_spec_characteristic.is_some(), true);
745        assert_eq!(spec.product_spec_characteristic.unwrap().len(), 2);
746    }
747
748    #[test]
749    fn test_charspec_by_name() {
750        let spec_char1 = ProductSpecificationCharacteristic::new("Char1").cardinality(1, 2);
751        let spec_char2 = ProductSpecificationCharacteristic::new("Char2").cardinality(3, 4);
752        let spec = ProductSpecification::new(SPEC_NAME)
753            .with_charateristic(spec_char1)
754            .with_charateristic(spec_char2);
755
756        let c1 = spec.characteristic_by_name("Char1");
757        assert!(c1.is_some());
758        assert_eq!(c1.unwrap().name, "Char1".to_string());
759
760        let c2 = spec.characteristic_by_name("Char2");
761        assert!(c2.is_some());
762        assert_eq!(c2.unwrap().name, "Char2".to_string());
763
764        let c3 = spec.characteristic_by_name("Char3");
765        assert!(c3.is_none());
766    }
767
768    #[test]
769    fn test_link_characteristic() {
770        let spec_char1 = ProductSpecificationCharacteristic::new("Char1").cardinality(1, 2);
771        let mut spec1 = ProductSpecification::new("Spec1").with_charateristic(spec_char1);
772        let spec_char2 = ProductSpecificationCharacteristic::new("Char2").cardinality(3, 4);
773        let spec2 = ProductSpecification::new("Spec2").with_charateristic(spec_char2);
774
775        spec1.link_characteristic(&spec2, "Char2");
776
777        assert!(spec1.product_spec_characteristic.is_some());
778        let chars = spec1.product_spec_characteristic.unwrap();
779        assert_eq!(chars.len(), 2);
780        let linked_char = chars.iter().find(|c| c.name == "Char2".to_string());
781        assert!(linked_char.is_some());
782        let rels = &linked_char.unwrap().product_spec_char_relationship;
783        assert!(rels.is_some());
784        let rels = rels.as_ref().unwrap();
785        assert_eq!(rels.len(), 1);
786        let rel = &rels[0];
787        assert_eq!(rel.name, "Char2".to_string());
788        assert_eq!(rel.relationship_type, "dependsOn".to_string());
789        assert_eq!(rel.id, spec2.get_id());
790        assert_eq!(rel.href, spec2.get_href());
791    }
792}