rpsl/
object.rs

1use std::{
2    fmt,
3    ops::{Deref, Index},
4};
5
6#[cfg(feature = "serde")]
7use serde::Serialize;
8
9use super::Attribute;
10
11/// A RPSL object.
12///
13/// ```text
14/// ┌───────────────────────────────────────────────┐
15/// │  Object                                       │
16/// ├───────────────────────────────────────────────┤
17/// │  [role]    ──── ACME Company                  │
18/// │  [address] ──┬─ Packet Street 6               │
19/// │              ├─ 128 Series of Tubes           │
20/// │              └─ Internet                      │
21/// │  [email]   ──── rpsl-rs@github.com            │
22/// │  [nic-hdl] ──── RPSL1-RIPE                    │
23/// │  [source]  ──── RIPE                          │
24/// └───────────────────────────────────────────────┘
25/// ```
26///
27/// # Examples
28///
29/// A role object for the ACME corporation.
30/// ```
31/// # use rpsl::{Attribute, Object};
32/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
33/// let role_acme = Object::new(vec![
34///     Attribute::new("role".parse()?, "ACME Company".parse()?),
35///     Attribute::new("address".parse()?, "Packet Street 6".parse()?),
36///     Attribute::new("address".parse()?, "128 Series of Tubes".parse()?),
37///     Attribute::new("address".parse()?, "Internet".parse()?),
38///     Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?),
39///     Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?),
40///     Attribute::new("source".parse()?, "RIPE".parse()?),
41/// ]);
42/// # Ok(())
43/// # }
44/// ```
45///
46/// Although creating an [`Object`] from a vector of [`Attribute`]s works, the more idiomatic way
47/// to do it is by using the [`object!`](crate::object!) macro.
48/// ```
49/// # use rpsl::{Attribute, Object, object};
50/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
51/// # let role_acme = Object::new(vec![
52/// #     Attribute::new("role".parse()?, "ACME Company".parse()?),
53/// #     Attribute::new("address".parse()?, "Packet Street 6".parse()?),
54/// #     Attribute::new("address".parse()?, "128 Series of Tubes".parse()?),
55/// #     Attribute::new("address".parse()?, "Internet".parse()?),
56/// #     Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?),
57/// #     Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?),
58/// #     Attribute::new("source".parse()?, "RIPE".parse()?),
59/// # ]);
60/// assert_eq!(
61///     role_acme,
62///     object! {
63///         "role": "ACME Company";
64///         "address": "Packet Street 6";
65///         "address": "128 Series of Tubes";
66///         "address": "Internet";
67///         "email": "rpsl-rs@github.com";
68///         "nic-hdl": "RPSL1-RIPE";
69///         "source": "RIPE";
70///     },
71/// );
72/// # Ok(())
73/// # }
74/// ```
75///
76/// Each attribute can be accessed by index.
77/// ```
78/// # use rpsl::{Attribute, Object};
79/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
80/// # let role_acme = Object::new(vec![
81/// #     Attribute::new("role".parse()?, "ACME Company".parse()?),
82/// #     Attribute::new("address".parse()?, "Packet Street 6".parse()?),
83/// #     Attribute::new("address".parse()?, "128 Series of Tubes".parse()?),
84/// #     Attribute::new("address".parse()?, "Internet".parse()?),
85/// #     Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?),
86/// #     Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?),
87/// #     Attribute::new("source".parse()?, "RIPE".parse()?),
88/// # ]);
89/// assert_eq!(role_acme[0], Attribute::new("role".parse()?, "ACME Company".parse()?));
90/// assert_eq!(role_acme[6], Attribute::new("source".parse()?, "RIPE".parse()?));
91/// # Ok(())
92/// # }
93/// ```
94///
95/// While specific attribute values can be accessed by name.
96/// ```
97/// # use rpsl::{Attribute, Object};
98/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
99/// # let role_acme = Object::new(vec![
100/// #     Attribute::new("role".parse()?, "ACME Company".parse()?),
101/// #     Attribute::new("address".parse()?, "Packet Street 6".parse()?),
102/// #     Attribute::new("address".parse()?, "128 Series of Tubes".parse()?),
103/// #     Attribute::new("address".parse()?, "Internet".parse()?),
104/// #     Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?),
105/// #     Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?),
106/// #     Attribute::new("source".parse()?, "RIPE".parse()?),
107/// # ]);
108/// assert_eq!(role_acme.get("role"), vec!["ACME Company"]);
109/// assert_eq!(role_acme.get("address"), vec!["Packet Street 6", "128 Series of Tubes", "Internet"]);
110/// assert_eq!(role_acme.get("email"), vec!["rpsl-rs@github.com"]);
111/// assert_eq!(role_acme.get("nic-hdl"), vec!["RPSL1-RIPE"]);
112/// assert_eq!(role_acme.get("source"), vec!["RIPE"]);
113/// # Ok(())
114/// # }
115/// ```
116///
117/// The entire object can also be represented as RPSL.
118/// ```
119/// # use rpsl::{Attribute, Object};
120/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
121/// # let role_acme = Object::new(vec![
122/// #     Attribute::new("role".parse()?, "ACME Company".parse()?),
123/// #     Attribute::new("address".parse()?, "Packet Street 6".parse()?),
124/// #     Attribute::new("address".parse()?, "128 Series of Tubes".parse()?),
125/// #     Attribute::new("address".parse()?, "Internet".parse()?),
126/// #     Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?),
127/// #     Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?),
128/// #     Attribute::new("source".parse()?, "RIPE".parse()?),
129/// # ]);
130/// assert_eq!(
131///    role_acme.to_string(),
132///    concat!(
133///        "role:           ACME Company\n",
134///        "address:        Packet Street 6\n",
135///        "address:        128 Series of Tubes\n",
136///        "address:        Internet\n",
137///        "email:          rpsl-rs@github.com\n",
138///        "nic-hdl:        RPSL1-RIPE\n",
139///        "source:         RIPE\n",
140///        "\n"
141///    )
142/// );
143/// # Ok(())
144/// # }
145/// ```
146///
147/// Or serialized to JSON if the corresponding feature is enabled.
148/// ```
149/// # use rpsl::{Attribute, Object};
150/// # #[cfg(feature = "json")]
151/// # use serde_json::json;
152/// # let role_acme = Object::new(vec![
153/// #     Attribute::new("role".parse()?, "ACME Company".parse()?),
154/// #     Attribute::new("address".parse()?, "Packet Street 6".parse()?),
155/// #     Attribute::new("address".parse()?, "128 Series of Tubes".parse()?),
156/// #     Attribute::new("address".parse()?, "Internet".parse()?),
157/// #     Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?),
158/// #     Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?),
159/// #     Attribute::new("source".parse()?, "RIPE".parse()?),
160/// # ]);
161/// # #[cfg(feature = "json")]
162/// assert_eq!(
163///    role_acme.json(),
164///    json!({
165///        "attributes": [
166///            { "name": "role", "values": ["ACME Company"] },
167///            { "name": "address", "values": ["Packet Street 6"] },
168///            { "name": "address", "values": ["128 Series of Tubes"] },
169///            { "name": "address", "values": ["Internet"] },
170///            { "name": "email", "values": ["rpsl-rs@github.com"] },
171///            { "name": "nic-hdl", "values": ["RPSL1-RIPE"] },
172///            { "name": "source", "values": ["RIPE"] }
173///        ]
174///    })
175/// );
176/// # Ok::<(), Box<dyn std::error::Error>>(())
177/// ```
178#[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    /// Contains the source if the object was created by parsing RPSL.
184    #[cfg_attr(feature = "serde", serde(skip))]
185    source: Option<&'a str>,
186}
187
188impl Object<'_> {
189    /// Create a new RPSL object from a vector of attributes.
190    ///
191    /// # Example
192    /// ```
193    /// # use rpsl::{Attribute, Object};
194    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
195    /// let role_acme = Object::new(vec![
196    ///     Attribute::new("role".parse()?, "ACME Company".parse()?),
197    ///     Attribute::new("address".parse()?, "Packet Street 6".parse()?),
198    ///     Attribute::new("address".parse()?, "128 Series of Tubes".parse()?),
199    ///     Attribute::new("address".parse()?, "Internet".parse()?),
200    ///     Attribute::new("email".parse()?, "rpsl-rs@github.com".parse()?),
201    ///     Attribute::new("nic-hdl".parse()?, "RPSL1-RIPE".parse()?),
202    ///     Attribute::new("source".parse()?, "RIPE".parse()?),
203    /// ]);
204    /// # Ok(())
205    /// # }
206    /// ```
207    #[must_use]
208    pub fn new(attributes: Vec<Attribute<'static>>) -> Object<'static> {
209        Object {
210            attributes,
211            source: None,
212        }
213    }
214
215    /// Create a new RPSL object from a text source and it's corresponding parsed attributes.
216    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    /// The number of attributes in the object.
224    #[must_use]
225    pub fn len(&self) -> usize {
226        self.attributes.len()
227    }
228
229    /// Get the value(s) of specific attribute(s).
230    #[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    /// Serialize the object into a JSON value.
244    pub fn json(&self) -> serde_json::Value {
245        serde_json::to_value(self).unwrap()
246    }
247
248    /// Access the source field for use in tests.
249    #[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    /// Compare two objects.
282    /// Since objects that are semantically equal may display differently, only `PartialEq` is implemented.
283    fn eq(&self, other: &Self) -> bool {
284        self.attributes == other.attributes
285    }
286}
287
288impl fmt::Display for Object<'_> {
289    /// Display the object as RPSL.
290    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/// Creates an [`Object`] containing the given attributes.
303///
304/// - Create an [`Object`] containing only single value attributes:
305/// ```
306/// # use rpsl::object;
307/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
308/// let obj = object! {
309///     "role": "ACME Company";
310///     "address": "Packet Street 6";
311///     "address": "128 Series of Tubes";
312///     "address": "Internet";
313/// };
314/// assert_eq!(obj[0].name, "role");
315/// assert_eq!(obj[0].value, "ACME Company");
316/// assert_eq!(obj[1].name, "address");
317/// assert_eq!(obj[1].value, "Packet Street 6");
318/// assert_eq!(obj[2].name, "address");
319/// assert_eq!(obj[2].value, "128 Series of Tubes");
320/// assert_eq!(obj[3].name, "address");
321/// assert_eq!(obj[3].value, "Internet");
322/// # Ok(())
323/// # }
324/// ```
325///
326/// - Create an `Object` containing multi value attributes:
327/// ```
328/// # use rpsl::object;
329/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
330/// let obj = object! {
331///    "role": "ACME Company";
332///    "address": "Packet Street 6", "128 Series of Tubes", "Internet";
333/// };
334/// assert_eq!(obj[0].name, "role");
335/// assert_eq!(obj[0].value, "ACME Company");
336/// assert_eq!(obj[1].name, "address");
337/// assert_eq!(obj[1].value, vec!["Packet Street 6", "128 Series of Tubes", "Internet"]);
338/// # Ok(())
339/// # }
340#[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", // Terminated by a trailing newline.
604            ),
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" // Contains a trailing newline.
624        )
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                // Not terminated by a trailing newline.
637            ),
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            // Does not contain a trailing newline.
657        )
658    )]
659    #[case(
660        Object::from_parsed(
661            concat!(
662                "role:           ACME Company\n",
663                "address:        Packet Street 6\n",
664                // Using space as a continuation char.
665                "                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            // Using space as a continuation char.
687            "                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                // Using + as a continuation char.
701                "+               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            // Using + as a continuation char.
723            "+               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    /// Borrowed objects display as the original RPSL they were created from.
732    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    /// Objects with equal attributes evaluate as equal, without taking the source field into consideration.
859    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    /// Objects that have different attributes do not evaluate as equal.
881    fn ne_objects_are_ne(#[case] object_1: Object, #[case] object_2: Object) {
882        assert_ne!(object_1, object_2);
883    }
884}