1use std::{
2 fmt,
3 ops::{Deref, Index},
4};
5
6#[cfg(feature = "serde")]
7use serde::Serialize;
8
9use super::Attribute;
10
11#[derive(Debug, Clone)]
179#[cfg_attr(feature = "serde", derive(Serialize))]
180#[allow(clippy::len_without_is_empty)]
181pub struct Object<'a> {
182 attributes: Vec<Attribute<'a>>,
183 #[cfg_attr(feature = "serde", serde(skip))]
185 source: Option<&'a str>,
186}
187
188impl Object<'_> {
189 #[must_use]
208 pub fn new(attributes: Vec<Attribute<'static>>) -> Object<'static> {
209 Object {
210 attributes,
211 source: None,
212 }
213 }
214
215 pub(crate) fn from_parsed<'a>(source: &'a str, attributes: Vec<Attribute<'a>>) -> Object<'a> {
217 Object {
218 attributes,
219 source: Some(source),
220 }
221 }
222
223 #[must_use]
225 pub fn len(&self) -> usize {
226 self.attributes.len()
227 }
228
229 #[must_use]
231 pub fn get(&self, name: &str) -> Vec<&str> {
232 self.attributes
233 .iter()
234 .filter(|a| a.name == name)
235 .flat_map(|a| a.value.with_content())
236 .collect()
237 }
238
239 #[cfg(feature = "json")]
240 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
241 #[allow(clippy::missing_panics_doc)]
242 #[must_use]
243 pub fn json(&self) -> serde_json::Value {
245 serde_json::to_value(self).unwrap()
246 }
247
248 #[cfg(test)]
250 pub(crate) fn source(&self) -> Option<&str> {
251 self.source
252 }
253}
254
255impl<'a> Index<usize> for Object<'a> {
256 type Output = Attribute<'a>;
257
258 fn index(&self, index: usize) -> &Self::Output {
259 &self.attributes[index]
260 }
261}
262
263impl<'a> Deref for Object<'a> {
264 type Target = Vec<Attribute<'a>>;
265
266 fn deref(&self) -> &Self::Target {
267 &self.attributes
268 }
269}
270
271impl<'a> IntoIterator for Object<'a> {
272 type Item = Attribute<'a>;
273 type IntoIter = std::vec::IntoIter<Self::Item>;
274
275 fn into_iter(self) -> Self::IntoIter {
276 self.attributes.into_iter()
277 }
278}
279
280impl PartialEq for Object<'_> {
281 fn eq(&self, other: &Self) -> bool {
284 self.attributes == other.attributes
285 }
286}
287
288impl fmt::Display for Object<'_> {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 if let Some(source) = self.source {
292 write!(f, "{source}")
293 } else {
294 for attribute in &self.attributes {
295 write!(f, "{attribute}")?;
296 }
297 writeln!(f)
298 }
299 }
300}
301
302#[macro_export]
341macro_rules! object {
342 (
343 $(
344 $name:literal: $($value:literal),+
345 );+ $(;)?
346 ) => {
347 $crate::Object::new(vec![
348 $(
349 $crate::Attribute::new($name.parse().unwrap(), vec![$($value),+].try_into().unwrap()),
350 )*
351 ])
352 };
353}
354
355#[cfg(test)]
356mod tests {
357 use rstest::*;
358 #[cfg(feature = "json")]
359 use serde_json::json;
360
361 use super::*;
362
363 #[rstest]
364 #[case(
365 Object::new(vec![
366 Attribute::unchecked_single("role", "ACME Company"),
367 Attribute::unchecked_single("address", "Packet Street 6"),
368 Attribute::unchecked_single("address", "128 Series of Tubes"),
369 Attribute::unchecked_single("address", "Internet"),
370 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
371 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
372 Attribute::unchecked_single("source", "RIPE"),
373 ]),
374 Object::from_parsed(
375 concat!(
376 "role: ACME Company\n",
377 "address: Packet Street 6\n",
378 "address: 128 Series of Tubes\n",
379 "address: Internet\n",
380 "email: rpsl-rs@github.com\n",
381 "nic-hdl: RPSL1-RIPE\n",
382 "source: RIPE\n",
383 "\n"
384 ),
385 vec![
386 Attribute::unchecked_single("role", "ACME Company"),
387 Attribute::unchecked_single("address", "Packet Street 6"),
388 Attribute::unchecked_single("address", "128 Series of Tubes"),
389 Attribute::unchecked_single("address", "Internet"),
390 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
391 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
392 Attribute::unchecked_single("source", "RIPE"),
393 ]
394 ),
395 concat!(
396 "role: ACME Company\n",
397 "address: Packet Street 6\n",
398 "address: 128 Series of Tubes\n",
399 "address: Internet\n",
400 "email: rpsl-rs@github.com\n",
401 "nic-hdl: RPSL1-RIPE\n",
402 "source: RIPE\n",
403 "\n"
404 )
405 )]
406 #[case(
407 Object::new(vec![
408 Attribute::unchecked_single("role", "ACME Company"),
409 Attribute::unchecked_multi(
410 "address",
411 ["Packet Street 6", "128 Series of Tubes", "Internet"]
412 ),
413 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
414 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
415 Attribute::unchecked_single("source", "RIPE"),
416 ]),
417 Object::from_parsed(
418 concat!(
419 "role: ACME Company\n",
420 "address: Packet Street 6\n",
421 " 128 Series of Tubes\n",
422 " Internet\n",
423 "email: rpsl-rs@github.com\n",
424 "nic-hdl: RPSL1-RIPE\n",
425 "source: RIPE\n",
426 "\n"
427 ),
428 vec![
429 Attribute::unchecked_single("role", "ACME Company"),
430 Attribute::unchecked_multi(
431 "address",
432 ["Packet Street 6", "128 Series of Tubes", "Internet"]
433 ),
434 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
435 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
436 Attribute::unchecked_single("source", "RIPE"),
437 ]
438 ),
439 concat!(
440 "role: ACME Company\n",
441 "address: Packet Street 6\n",
442 " 128 Series of Tubes\n",
443 " Internet\n",
444 "email: rpsl-rs@github.com\n",
445 "nic-hdl: RPSL1-RIPE\n",
446 "source: RIPE\n",
447 "\n"
448 )
449 )]
450 fn object_display(
451 #[case] owned: Object<'static>,
452 #[case] borrowed: Object,
453 #[case] expected: &str,
454 ) {
455 assert_eq!(owned.to_string(), expected);
456 assert_eq!(borrowed.to_string(), expected);
457 }
458
459 #[rstest]
460 #[case(
461 Object::new(vec![
462 Attribute::unchecked_single("role", "ACME Company"),
463 ]),
464 1
465 )]
466 #[case(
467 Object::new(vec![
468 Attribute::unchecked_single("role", "ACME Company"),
469 Attribute::unchecked_single("address", "Packet Street 6"),
470 Attribute::unchecked_single("address", "128 Series of Tubes"),
471 Attribute::unchecked_single("address", "Internet"),
472 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
473 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
474 Attribute::unchecked_single("source", "RIPE"),
475 ]),
476 7
477 )]
478 fn object_len(#[case] object: Object, #[case] expected: usize) {
479 assert_eq!(object.len(), expected);
480 }
481
482 #[rstest]
483 #[case(
484 Object::new(vec![
485 Attribute::unchecked_single("role", "ACME Company"),
486 Attribute::unchecked_single("address", "Packet Street 6"),
487 Attribute::unchecked_single("address", "128 Series of Tubes"),
488 Attribute::unchecked_single("address", "Internet"),
489 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
490 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
491 Attribute::unchecked_single("source", "RIPE"),
492 ]),
493 2,
494 Attribute::unchecked_single("address", "128 Series of Tubes"),
495 )]
496 fn object_index(#[case] object: Object, #[case] index: usize, #[case] expected: Attribute) {
497 assert_eq!(object[index], expected);
498 }
499
500 #[rstest]
501 #[case(
502 vec![
503 Attribute::unchecked_single("role", "ACME Company"),
504 Attribute::unchecked_single("address", "Packet Street 6"),
505 Attribute::unchecked_single("address", "128 Series of Tubes"),
506 Attribute::unchecked_single("address", "Internet"),
507 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
508 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
509 Attribute::unchecked_single("source", "RIPE"),
510 ],
511 )]
512 fn object_deref(#[case] attributes: Vec<Attribute<'static>>) {
513 let object = Object::new(attributes.clone());
514 assert_eq!(*object, attributes);
515 }
516
517 #[rstest]
518 #[case(
519 vec![
520 Attribute::unchecked_single("role", "ACME Company"),
521 Attribute::unchecked_single("address", "Packet Street 6"),
522 Attribute::unchecked_single("address", "128 Series of Tubes"),
523 Attribute::unchecked_single("address", "Internet"),
524 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
525 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
526 Attribute::unchecked_single("source", "RIPE"),
527 ],
528 )]
529 fn object_into_iter(#[case] attributes: Vec<Attribute<'static>>) {
530 let object = Object::new(attributes.clone());
531
532 let attr_iter = attributes.into_iter();
533 let obj_iter = object.into_iter();
534
535 for (a, b) in attr_iter.zip(obj_iter) {
536 assert_eq!(a, b);
537 }
538 }
539
540 #[rstest]
541 #[case(
542 Object::new(vec![
543 Attribute::unchecked_single("role", "ACME Company"),
544 Attribute::unchecked_single("address", "Packet Street 6"),
545 Attribute::unchecked_single("address", "128 Series of Tubes"),
546 Attribute::unchecked_single("address", "Internet"),
547 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
548 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
549 Attribute::unchecked_single("source", "RIPE"),
550 ]),
551 json!({
552 "attributes": [
553 { "name": "role", "values": ["ACME Company"] },
554 { "name": "address", "values": ["Packet Street 6"] },
555 { "name": "address", "values": ["128 Series of Tubes"] },
556 { "name": "address", "values": ["Internet"] },
557 { "name": "email", "values": ["rpsl-rs@github.com"] },
558 { "name": "nic-hdl", "values": ["RPSL1-RIPE"] },
559 { "name": "source", "values": ["RIPE"] }
560 ]
561 })
562 )]
563 #[case(
564 Object::new(vec![
565 Attribute::unchecked_single("role", "ACME Company"),
566 Attribute::unchecked_multi(
567 "address",
568 ["Packet Street 6", "", "128 Series of Tubes", "Internet"]
569 ),
570 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
571 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
572 Attribute::unchecked_single("source", "RIPE"),
573 ]),
574 json!({
575 "attributes": [
576 { "name": "role", "values": ["ACME Company"] },
577 {
578 "name": "address",
579 "values": ["Packet Street 6", null, "128 Series of Tubes", "Internet"] },
580 { "name": "email", "values": ["rpsl-rs@github.com"] },
581 { "name": "nic-hdl", "values": ["RPSL1-RIPE"] },
582 { "name": "source", "values": ["RIPE"] }
583 ]
584 })
585 )]
586 #[cfg(feature = "json")]
587 fn object_json_repr(#[case] object: Object, #[case] expected: serde_json::Value) {
588 let json = object.json();
589 assert_eq!(json, expected);
590 }
591
592 #[rstest]
593 #[case(
594 Object::from_parsed(
595 concat!(
596 "role: ACME Company\n",
597 "address: Packet Street 6\n",
598 "address: 128 Series of Tubes\n",
599 "address: Internet\n",
600 "email: rpsl-rs@github.com\n",
601 "nic-hdl: RPSL1-RIPE\n",
602 "source: RIPE\n",
603 "\n", ),
605 vec![
606 Attribute::unchecked_single("role", "ACME Company"),
607 Attribute::unchecked_single("address", "Packet Street 6"),
608 Attribute::unchecked_single("address", "128 Series of Tubes"),
609 Attribute::unchecked_single("address", "Internet"),
610 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
611 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
612 Attribute::unchecked_single("source", "RIPE"),
613 ],
614 ),
615 concat!(
616 "role: ACME Company\n",
617 "address: Packet Street 6\n",
618 "address: 128 Series of Tubes\n",
619 "address: Internet\n",
620 "email: rpsl-rs@github.com\n",
621 "nic-hdl: RPSL1-RIPE\n",
622 "source: RIPE\n",
623 "\n" )
625 )]
626 #[case(
627 Object::from_parsed(
628 concat!(
629 "role: ACME Company\n",
630 "address: Packet Street 6\n",
631 "address: 128 Series of Tubes\n",
632 "address: Internet\n",
633 "email: rpsl-rs@github.com\n",
634 "nic-hdl: RPSL1-RIPE\n",
635 "source: RIPE\n",
636 ),
638 vec![
639 Attribute::unchecked_single("role", "ACME Company"),
640 Attribute::unchecked_single("address", "Packet Street 6"),
641 Attribute::unchecked_single("address", "128 Series of Tubes"),
642 Attribute::unchecked_single("address", "Internet"),
643 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
644 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
645 Attribute::unchecked_single("source", "RIPE"),
646 ],
647 ),
648 concat!(
649 "role: ACME Company\n",
650 "address: Packet Street 6\n",
651 "address: 128 Series of Tubes\n",
652 "address: Internet\n",
653 "email: rpsl-rs@github.com\n",
654 "nic-hdl: RPSL1-RIPE\n",
655 "source: RIPE\n",
656 )
658 )]
659 #[case(
660 Object::from_parsed(
661 concat!(
662 "role: ACME Company\n",
663 "address: Packet Street 6\n",
664 " 128 Series of Tubes\n",
666 " Internet\n",
667 "email: rpsl-rs@github.com\n",
668 "nic-hdl: RPSL1-RIPE\n",
669 "source: RIPE\n",
670 "\n"
671 ),
672 vec![
673 Attribute::unchecked_single("role", "ACME Company"),
674 Attribute::unchecked_multi(
675 "address",
676 ["Packet Street 6", "128 Series of Tubes", "Internet"]
677 ),
678 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
679 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
680 Attribute::unchecked_single("source", "RIPE"),
681 ],
682 ),
683 concat!(
684 "role: ACME Company\n",
685 "address: Packet Street 6\n",
686 " 128 Series of Tubes\n",
688 " Internet\n",
689 "email: rpsl-rs@github.com\n",
690 "nic-hdl: RPSL1-RIPE\n",
691 "source: RIPE\n",
692 "\n"
693 )
694 )]
695 #[case(
696 Object::from_parsed(
697 concat!(
698 "role: ACME Company\n",
699 "address: Packet Street 6\n",
700 "+ 128 Series of Tubes\n",
702 "+ Internet\n",
703 "email: rpsl-rs@github.com\n",
704 "nic-hdl: RPSL1-RIPE\n",
705 "source: RIPE\n",
706 "\n"
707 ),
708 vec![
709 Attribute::unchecked_single("role", "ACME Company"),
710 Attribute::unchecked_multi(
711 "address",
712 ["Packet Street 6", "128 Series of Tubes", "Internet"]
713 ),
714 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
715 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
716 Attribute::unchecked_single("source", "RIPE"),
717 ],
718 ),
719 concat!(
720 "role: ACME Company\n",
721 "address: Packet Street 6\n",
722 "+ 128 Series of Tubes\n",
724 "+ Internet\n",
725 "email: rpsl-rs@github.com\n",
726 "nic-hdl: RPSL1-RIPE\n",
727 "source: RIPE\n",
728 "\n"
729 )
730 )]
731 fn borrowed_objects_display_like_source(#[case] object: Object, #[case] expected: &str) {
733 assert_eq!(object.to_string(), expected);
734 }
735
736 #[rstest]
737 #[case(
738 object! {
739 "role": "ACME Company";
740 "address": "Packet Street 6", "128 Series of Tubes", "Internet";
741 "email": "rpsl-rs@github.com";
742 "nic-hdl": "RPSL1-RIPE";
743 "source": "RIPE";
744 },
745 Object::new(vec![
746 Attribute::unchecked_single("role", "ACME Company"),
747 Attribute::unchecked_multi(
748 "address",
749 ["Packet Street 6", "128 Series of Tubes", "Internet"],
750 ),
751 Attribute::unchecked_single("email", "rpsl-rs@github.com"),
752 Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
753 Attribute::unchecked_single("source", "RIPE"),
754 ])
755 )]
756 fn object_from_macro(#[case] from_macro: Object, #[case] expected: Object) {
757 assert_eq!(from_macro, expected);
758 }
759
760 #[rstest]
761 #[case(
762 Object::new(vec![
763 Attribute::unchecked_single("aut-num", "AS42"),
764 Attribute::unchecked_single(
765 "remarks",
766 "All imported prefixes will be tagged with geographic communities and",
767 ),
768 Attribute::unchecked_single(
769 "remarks",
770 "the type of peering relationship according to the table below, using the default",
771 ),
772 Attribute::unchecked_single(
773 "remarks",
774 "announce rule (x=0).",
775 ),
776 Attribute::unchecked_single("remarks", None),
777 Attribute::unchecked_single(
778 "remarks",
779 "The following communities can be used by peers and customers",
780 ),
781 Attribute::unchecked_multi(
782 "remarks",
783 [
784 "x = 0 - Announce (default rule)",
785 "x = 1 - Prepend x1",
786 "x = 2 - Prepend x2",
787 "x = 3 - Prepend x3",
788 "x = 9 - Do not announce",
789 ],
790 ),
791 ]),
792 vec![
793 ("aut-num", vec!["AS42"]),
794 (
795 "remarks",
796 vec![
797 "All imported prefixes will be tagged with geographic communities and",
798 "the type of peering relationship according to the table below, using the default",
799 "announce rule (x=0).",
800 "The following communities can be used by peers and customers",
801 "x = 0 - Announce (default rule)",
802 "x = 1 - Prepend x1",
803 "x = 2 - Prepend x2",
804 "x = 3 - Prepend x3",
805 "x = 9 - Do not announce",
806 ]
807 )
808 ]
809 )]
810 fn get_values_by_name(#[case] object: Object, #[case] name_expected: Vec<(&str, Vec<&str>)>) {
811 for (name, expected) in name_expected {
812 assert_eq!(object.get(name), expected);
813 }
814 }
815
816 #[rstest]
817 #[case(
818 Object::new(
819 vec![
820 Attribute::unchecked_single("role", "ACME Company"),
821 Attribute::unchecked_single("address", "Packet Street 6"),
822 Attribute::unchecked_single("address", "128 Series of Tubes"),
823 Attribute::unchecked_single("address", "Internet"),
824 ]),
825 Object::new(
826 vec![
827 Attribute::unchecked_single("role", "ACME Company"),
828 Attribute::unchecked_single("address", "Packet Street 6"),
829 Attribute::unchecked_single("address", "128 Series of Tubes"),
830 Attribute::unchecked_single("address", "Internet"),
831 ]),
832 )]
833 #[case(
834 Object::from_parsed(
835 concat!(
836 "role: ACME Company\n",
837 "address: Packet Street 6\n",
838 "address: 128 Series of Tubes\n",
839 "address: Internet\n",
840 "\n"
841 ),
842 vec![
843 Attribute::unchecked_single("role", "ACME Company"),
844 Attribute::unchecked_single("address", "Packet Street 6"),
845 Attribute::unchecked_single("address", "128 Series of Tubes"),
846 Attribute::unchecked_single("address", "Internet"),
847 ],
848 ),
849 Object::new(
850 vec![
851 Attribute::unchecked_single("role", "ACME Company"),
852 Attribute::unchecked_single("address", "Packet Street 6"),
853 Attribute::unchecked_single("address", "128 Series of Tubes"),
854 Attribute::unchecked_single("address", "Internet"),
855 ],
856 ),
857 )]
858 fn eq_objects_are_eq(#[case] object_1: Object, #[case] object_2: Object) {
860 assert_eq!(object_1, object_2);
861 }
862
863 #[rstest]
864 #[case(
865 Object::new(
866 vec![
867 Attribute::unchecked_single("role", "Umbrella Corporation"),
868 Attribute::unchecked_single("address", "Paraguas Street"),
869 Attribute::unchecked_single("address", "Raccoon City"),
870 Attribute::unchecked_single("address", "Colorado"),
871 ]),
872 Object::new(
873 vec![
874 Attribute::unchecked_single("role", "ACME Company"),
875 Attribute::unchecked_single("address", "Packet Street 6"),
876 Attribute::unchecked_single("address", "128 Series of Tubes"),
877 Attribute::unchecked_single("address", "Internet"),
878 ]),
879 )]
880 fn ne_objects_are_ne(#[case] object_1: Object, #[case] object_2: Object) {
882 assert_ne!(object_1, object_2);
883 }
884}