1use 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;
26pub const SPEC_CONV_VERB: &str = "Imported";
28
29#[derive(Clone, Debug, Default, Deserialize, Serialize, HasValidity)]
31#[serde(rename_all = "camelCase")]
32pub struct ProductSpecificationCharacteristic {
33 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 pub is_unique: bool,
41 max_cardinality: Cardinality,
42 min_cardinality: Cardinality,
43 pub name: String,
45 #[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 #[serde(skip_serializing_if = "Option::is_none")]
52 pub valid_for: Option<TimePeriod>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub product_spec_char_relationship: Option<Vec<ProductSpecificationCharacteristicRelationship>>,
56}
57
58impl ProductSpecificationCharacteristic {
59 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 pub fn configurable(mut self, configurable: bool) -> ProductSpecificationCharacteristic {
79 self.configurable = configurable;
80 self
81 }
82
83 pub fn description(mut self, description: String) -> ProductSpecificationCharacteristic {
85 self.description = Some(description.clone());
86 self
87 }
88
89 pub fn extensible(mut self, extensible: bool) -> ProductSpecificationCharacteristic {
91 self.extensible = Some(extensible);
92 self
93 }
94
95 pub fn validity(mut self, validity: Option<TimePeriod>) -> ProductSpecificationCharacteristic {
97 self.valid_for = validity;
98 self
99 }
100
101 pub fn cardinality(
110 mut self,
111 min: Cardinality,
112 max: Cardinality,
113 ) -> ProductSpecificationCharacteristic {
114 if min > max {
116 return self;
118 }
119 self.min_cardinality = min;
120 self.max_cardinality = max;
121 self
122 }
123}
124
125impl 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#[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#[derive(
153 Clone, Debug, Default, Deserialize, HasId, HasLastUpdate, HasDescription, HasName, Serialize,
154)]
155#[serde(rename_all = "camelCase")]
156pub struct ProductSpecification {
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub id: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub href: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub brand: Option<String>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub description: Option<String>,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub is_bundle: Option<bool>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub last_update: Option<String>,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub lifecycle_status: Option<String>,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub name: Option<String>,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 product_number: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub version: Option<String>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub product_spec_characteristic: Option<Vec<ProductSpecificationCharacteristic>>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub bundled_product_specification: Option<Vec<BundledProductSpecification>>,
193}
194
195impl ProductSpecification {
196 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 pub fn status(&mut self, status: &str) {
207 self.lifecycle_status = Some(status.to_owned());
208 }
209
210 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 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 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 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#[derive(Clone, Debug, Deserialize, Serialize)]
254pub struct ProductSpecificationRef {
255 pub id: String,
257 pub href: String,
259 pub name: Option<String>,
261 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 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
297impl From<&ServiceSpecification> for ProductSpecification {
300 fn from(value: &ServiceSpecification) -> Self {
301 let mut ps = ProductSpecification::new(value.get_name());
302 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 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 ps.version.clone_from(&value.version);
328 }
329 ps.lifecycle_status.clone_from(&value.lifecycle_status);
330 ps
331 }
332}
333
334#[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#[derive(Clone, Default, Debug)]
376pub enum ProductSpecificationEventType {
377 #[default]
379 ProductSpecificationCreateEvent,
380 ProductSpecificationDeleteEvent,
382}
383
384#[derive(Clone, Debug, Default, Deserialize, Serialize, HasValidity)]
390#[serde(rename_all = "camelCase")]
391pub struct ProductSpecificationCharacteristicValue {
392 pub is_default: bool,
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub range_interval: Option<String>,
397 #[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 #[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 pub fn new() -> ProductSpecificationCharacteristicValue {
426 ProductSpecificationCharacteristicValue {
427 is_default: false,
428 ..Default::default()
429 }
430 }
431
432 pub fn regex(
441 mut self,
442 regex: String,
443 ) -> Result<ProductSpecificationCharacteristicValue, TMFError> {
444 let _re = Regex::new(®ex)?;
446 self.regex = Some(regex);
447 Ok(self)
448 }
449
450 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 None => self.value = value,
478 }
479 Ok(self)
480 }
481
482 pub fn validate(
493 mut self,
494 value: serde_json::Value,
495 ) -> Result<ProductSpecificationCharacteristicValue, TMFError> {
496 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#[derive(Clone, Debug, Deserialize, Serialize, HasValidity)]
514#[serde(rename_all = "camelCase")]
515pub struct ProductSpecificationCharacteristicValueUse {
516 #[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 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 pub fn with_spec(&mut self, specification: ProductSpecification) {
548 self.product_specification = Some(ProductSpecificationRef::from(specification));
549 }
550}
551
552#[derive(Clone, Debug, Deserialize, Serialize, HasValidity)]
554pub struct ProductSpecificationCharacteristicRelationship {
555 pub id: String,
557 pub href: String,
559 pub char_spec_seq: u32,
561 pub name: String,
563 pub relationship_type: String,
565 #[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 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]
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}