Skip to main content

rpsl/
object.rs

1use std::{
2    borrow::Cow,
3    fmt,
4    ops::{Deref, Index},
5};
6
7#[cfg(feature = "serde")]
8use serde::Serialize;
9
10use super::Attribute;
11use crate::spec::{AttributeError, Raw, Specification};
12
13/// A RPSL object.
14///
15/// ```text
16/// ┌───────────────────────────────────────────────┐
17/// │  Object                                       │
18/// ├───────────────────────────────────────────────┤
19/// │  [role]    ──── ACME Company                  │
20/// │  [address] ──┬─ Packet Street 6               │
21/// │              ├─ 128 Series of Tubes           │
22/// │              └─ Internet                      │
23/// │  [email]   ──── rpsl-rs@github.com            │
24/// │  [nic-hdl] ──── RPSL1-RIPE                    │
25/// │  [source]  ──── RIPE                          │
26/// └───────────────────────────────────────────────┘
27/// ```
28///
29/// # Examples
30///
31/// A role object for the ACME corporation.
32/// ```
33/// # use rpsl::{Attribute, Object};
34/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
35/// let role_acme = Object::new(vec![
36///     Attribute::new("role", "ACME Company"),
37///     Attribute::new("address", "Packet Street 6"),
38///     Attribute::new("address", "128 Series of Tubes"),
39///     Attribute::new("address", "Internet"),
40///     Attribute::new("email", "rpsl-rs@github.com"),
41///     Attribute::new("nic-hdl", "RPSL1-RIPE"),
42///     Attribute::new("source", "RIPE"),
43/// ]);
44/// # Ok(())
45/// # }
46/// ```
47///
48/// Although creating an [`Object`] from a vector of [`Attribute`]s works, the easier way
49/// to do it is by using the [`object!`](crate::object!) macro.
50/// ```
51/// # use rpsl::{Attribute, Object, object};
52/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
53/// # let role_acme = Object::new(vec![
54/// #     Attribute::new("role", "ACME Company"),
55/// #     Attribute::new("address", "Packet Street 6"),
56/// #     Attribute::new("address", "128 Series of Tubes"),
57/// #     Attribute::new("address", "Internet"),
58/// #     Attribute::new("email", "rpsl-rs@github.com"),
59/// #     Attribute::new("nic-hdl", "RPSL1-RIPE"),
60/// #     Attribute::new("source", "RIPE"),
61/// # ]);
62/// assert_eq!(
63///     role_acme,
64///     object! {
65///         "role": "ACME Company";
66///         "address": "Packet Street 6";
67///         "address": "128 Series of Tubes";
68///         "address": "Internet";
69///         "email": "rpsl-rs@github.com";
70///         "nic-hdl": "RPSL1-RIPE";
71///         "source": "RIPE";
72///     },
73/// );
74/// # Ok(())
75/// # }
76/// ```
77///
78/// Each attribute can be accessed by index.
79/// ```
80/// # use rpsl::{Attribute, Object};
81/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
82/// # let role_acme = Object::new(vec![
83/// #     Attribute::new("role", "ACME Company"),
84/// #     Attribute::new("address", "Packet Street 6"),
85/// #     Attribute::new("address", "128 Series of Tubes"),
86/// #     Attribute::new("address", "Internet"),
87/// #     Attribute::new("email", "rpsl-rs@github.com"),
88/// #     Attribute::new("nic-hdl", "RPSL1-RIPE"),
89/// #     Attribute::new("source", "RIPE"),
90/// # ]);
91/// assert_eq!(role_acme[0], Attribute::new("role", "ACME Company"));
92/// assert_eq!(role_acme[6], Attribute::new("source", "RIPE"));
93/// # Ok(())
94/// # }
95/// ```
96///
97/// While specific attribute values can be accessed by name.
98/// ```
99/// # use rpsl::{Attribute, Object};
100/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
101/// # let role_acme = Object::new(vec![
102/// #     Attribute::new("role", "ACME Company"),
103/// #     Attribute::new("address", "Packet Street 6"),
104/// #     Attribute::new("address", "128 Series of Tubes"),
105/// #     Attribute::new("address", "Internet"),
106/// #     Attribute::new("email", "rpsl-rs@github.com"),
107/// #     Attribute::new("nic-hdl", "RPSL1-RIPE"),
108/// #     Attribute::new("source", "RIPE"),
109/// # ]);
110/// assert_eq!(role_acme.get("role"), vec!["ACME Company"]);
111/// assert_eq!(role_acme.get("address"), vec!["Packet Street 6", "128 Series of Tubes", "Internet"]);
112/// assert_eq!(role_acme.get("email"), vec!["rpsl-rs@github.com"]);
113/// assert_eq!(role_acme.get("nic-hdl"), vec!["RPSL1-RIPE"]);
114/// assert_eq!(role_acme.get("source"), vec!["RIPE"]);
115/// # Ok(())
116/// # }
117/// ```
118///
119/// The entire object can also be represented as RPSL.
120/// ```
121/// # use rpsl::{Attribute, Object};
122/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
123/// # let role_acme = Object::new(vec![
124/// #     Attribute::new("role", "ACME Company"),
125/// #     Attribute::new("address", "Packet Street 6"),
126/// #     Attribute::new("address", "128 Series of Tubes"),
127/// #     Attribute::new("address", "Internet"),
128/// #     Attribute::new("email", "rpsl-rs@github.com"),
129/// #     Attribute::new("nic-hdl", "RPSL1-RIPE"),
130/// #     Attribute::new("source", "RIPE"),
131/// # ]);
132/// assert_eq!(
133///    role_acme.to_string(),
134///    concat!(
135///        "role:           ACME Company\n",
136///        "address:        Packet Street 6\n",
137///        "address:        128 Series of Tubes\n",
138///        "address:        Internet\n",
139///        "email:          rpsl-rs@github.com\n",
140///        "nic-hdl:        RPSL1-RIPE\n",
141///        "source:         RIPE\n",
142///        "\n"
143///    )
144/// );
145/// # Ok(())
146/// # }
147/// ```
148///
149/// Or serialized to JSON if the corresponding feature is enabled.
150/// ```
151/// # use rpsl::{Attribute, Object};
152/// # #[cfg(feature = "json")]
153/// # use serde_json::json;
154/// # let role_acme = Object::new(vec![
155/// #     Attribute::new("role", "ACME Company"),
156/// #     Attribute::new("address", "Packet Street 6"),
157/// #     Attribute::new("address", "128 Series of Tubes"),
158/// #     Attribute::new("address", "Internet"),
159/// #     Attribute::new("email", "rpsl-rs@github.com"),
160/// #     Attribute::new("nic-hdl", "RPSL1-RIPE"),
161/// #     Attribute::new("source", "RIPE"),
162/// # ]);
163/// # #[cfg(feature = "json")]
164/// assert_eq!(
165///    role_acme.json(),
166///    json!({
167///        "attributes": [
168///            { "name": "role", "values": ["ACME Company"] },
169///            { "name": "address", "values": ["Packet Street 6"] },
170///            { "name": "address", "values": ["128 Series of Tubes"] },
171///            { "name": "address", "values": ["Internet"] },
172///            { "name": "email", "values": ["rpsl-rs@github.com"] },
173///            { "name": "nic-hdl", "values": ["RPSL1-RIPE"] },
174///            { "name": "source", "values": ["RIPE"] }
175///        ]
176///    })
177/// );
178/// # Ok::<(), Box<dyn std::error::Error>>(())
179/// ```
180#[derive(Debug, Clone)]
181#[cfg_attr(feature = "serde", derive(Serialize), serde(bound = ""))]
182#[allow(clippy::len_without_is_empty)]
183pub struct Object<'a, Spec: Specification = Raw> {
184    attributes: Vec<Attribute<'a, Spec>>,
185    /// Contains the source if the object was created by parsing RPSL.
186    #[cfg_attr(feature = "serde", serde(skip))]
187    source: Option<Cow<'a, str>>,
188}
189
190impl<'a, Spec: Specification> Object<'a, Spec> {
191    /// Validate that all attributes of this object conform to a target specification.
192    ///
193    /// # Errors
194    /// Returns an [`ObjectValidationError`] if any attribute fails to satisfy the target specification.
195    ///
196    /// # Examples
197    /// ```
198    /// # use rpsl::{object, spec::Rfc2622};
199    /// let obj = object! {
200    ///     "role": "ACME Company";
201    ///     "address": "Packet Street 6";
202    /// };
203    /// obj.validate::<Rfc2622>()?;
204    /// # Ok::<(), Box<dyn std::error::Error>>(())
205    /// ```
206    pub fn validate<TargetSpec: Specification>(&self) -> Result<(), ObjectValidationError> {
207        let mut errors = Vec::new();
208        for (index, attribute) in self.attributes.iter().enumerate() {
209            if let Err(error) = attribute.validate::<TargetSpec>() {
210                errors.push((index, error));
211            }
212        }
213
214        if errors.is_empty() {
215            Ok(())
216        } else {
217            Err(ObjectValidationError::new(errors))
218        }
219    }
220
221    /// Convert every attribute in this object into a target specification.
222    ///
223    /// # Errors
224    /// Returns the first [`AttributeError`] encountered. Use [`Object::validate`] to collect
225    /// all attribute errors before converting.
226    ///
227    /// # Examples
228    /// ```
229    /// # use rpsl::{object, Object, spec::Rfc2622};
230    /// let obj = object! {
231    ///     "role": "ACME Company";
232    /// };
233    /// let validated: Object<Rfc2622> = obj.into_spec()?;
234    /// # Ok::<(), Box<dyn std::error::Error>>(())
235    /// ```
236    pub fn into_spec<TargetSpec: Specification>(
237        self,
238    ) -> Result<Object<'a, TargetSpec>, AttributeError> {
239        let Object { attributes, source } = self;
240
241        let mut converted = Vec::with_capacity(attributes.len());
242        for attribute in attributes {
243            converted.push(attribute.into_spec::<TargetSpec>()?);
244        }
245
246        Ok(Object {
247            attributes: converted,
248            source,
249        })
250    }
251
252    /// Create a new RPSL object from a vector of attributes.
253    ///
254    /// # Example
255    /// ```
256    /// # use rpsl::{Attribute, Object};
257    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
258    /// let role_acme = Object::new(vec![
259    ///     Attribute::new("role", "ACME Company"),
260    ///     Attribute::new("address", "Packet Street 6"),
261    ///     Attribute::new("address", "128 Series of Tubes"),
262    ///     Attribute::new("address", "Internet"),
263    ///     Attribute::new("email", "rpsl-rs@github.com"),
264    ///     Attribute::new("nic-hdl", "RPSL1-RIPE"),
265    ///     Attribute::new("source", "RIPE"),
266    /// ]);
267    /// # Ok(())
268    /// # }
269    /// ```
270    #[must_use]
271    pub fn new(attributes: Vec<Attribute<'static, Spec>>) -> Object<'static, Spec> {
272        Object {
273            attributes,
274            source: None,
275        }
276    }
277
278    /// Create a new RPSL object from a text source and it's corresponding parsed attributes.
279    pub(crate) fn new_parsed(
280        source: &'a str,
281        attributes: Vec<Attribute<'a, Spec>>,
282    ) -> Object<'a, Spec> {
283        Object {
284            attributes,
285            source: Some(Cow::Borrowed(source)),
286        }
287    }
288
289    /// The number of attributes in the object.
290    #[must_use]
291    pub fn len(&self) -> usize {
292        self.attributes.len()
293    }
294
295    /// Get the value(s) of specific attribute(s).
296    #[must_use]
297    pub fn get(&self, name: &str) -> Vec<&str> {
298        self.attributes
299            .iter()
300            .filter(|a| a.name == name)
301            .flat_map(|a| a.value.with_content())
302            .collect()
303    }
304
305    #[cfg(feature = "json")]
306    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
307    #[allow(clippy::missing_panics_doc)]
308    #[must_use]
309    /// Serialize the object into a JSON value.
310    pub fn json(&self) -> serde_json::Value {
311        serde_json::to_value(self).unwrap()
312    }
313
314    /// Access the source field for use in tests.
315    #[cfg(test)]
316    pub(crate) fn source(&self) -> Option<&str> {
317        self.source.as_deref()
318    }
319
320    /// Convert this object into an owned (`'static`) variant.
321    pub fn into_owned(self) -> Object<'static, Spec> {
322        Object {
323            attributes: self
324                .attributes
325                .into_iter()
326                .map(Attribute::into_owned)
327                .collect(),
328            source: self.source.map(|s| Cow::Owned(s.into_owned())),
329        }
330    }
331}
332
333impl<'a, Spec: Specification> Index<usize> for Object<'a, Spec> {
334    type Output = Attribute<'a, Spec>;
335
336    fn index(&self, index: usize) -> &Self::Output {
337        &self.attributes[index]
338    }
339}
340
341impl<'a, Spec: Specification> Deref for Object<'a, Spec> {
342    type Target = Vec<Attribute<'a, Spec>>;
343
344    fn deref(&self) -> &Self::Target {
345        &self.attributes
346    }
347}
348
349impl<'a, Spec: Specification> IntoIterator for Object<'a, Spec> {
350    type Item = Attribute<'a, Spec>;
351    type IntoIter = std::vec::IntoIter<Self::Item>;
352
353    fn into_iter(self) -> Self::IntoIter {
354        self.attributes.into_iter()
355    }
356}
357
358impl PartialEq for Object<'_> {
359    /// Compare two objects.
360    /// Since objects that are semantically equal may display differently, only `PartialEq` is implemented.
361    fn eq(&self, other: &Self) -> bool {
362        self.attributes == other.attributes
363    }
364}
365
366impl fmt::Display for Object<'_> {
367    /// Display the object as RPSL.
368    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
369        if let Some(source) = &self.source {
370            write!(f, "{source}")
371        } else {
372            for attribute in &self.attributes {
373                write!(f, "{attribute}")?;
374            }
375            writeln!(f)
376        }
377    }
378}
379
380/// Creates an [`Object`] containing the given attributes.
381///
382/// - Create an [`Object`] containing only single value attributes:
383/// ```
384/// # use rpsl::object;
385/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
386/// let obj = object! {
387///     "role": "ACME Company";
388///     "address": "Packet Street 6";
389///     "address": "128 Series of Tubes";
390///     "address": "Internet";
391/// };
392/// assert_eq!(obj[0].name, "role");
393/// assert_eq!(obj[0].value, "ACME Company");
394/// assert_eq!(obj[1].name, "address");
395/// assert_eq!(obj[1].value, "Packet Street 6");
396/// assert_eq!(obj[2].name, "address");
397/// assert_eq!(obj[2].value, "128 Series of Tubes");
398/// assert_eq!(obj[3].name, "address");
399/// assert_eq!(obj[3].value, "Internet");
400/// # Ok(())
401/// # }
402/// ```
403///
404/// - Create an `Object` containing multi value attributes:
405/// ```
406/// # use rpsl::object;
407/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
408/// let obj = object! {
409///    "role": "ACME Company";
410///    "address": "Packet Street 6", "128 Series of Tubes", "Internet";
411/// };
412/// assert_eq!(obj[0].name, "role");
413/// assert_eq!(obj[0].value, "ACME Company");
414/// assert_eq!(obj[1].name, "address");
415/// assert_eq!(obj[1].value, vec!["Packet Street 6", "128 Series of Tubes", "Internet"]);
416/// # Ok(())
417/// # }
418#[macro_export]
419macro_rules! object {
420    (
421        $(
422            $name:literal: $($value:literal),+
423        );+ $(;)?
424    ) => {
425        $crate::Object::new(vec![
426            $(
427                {
428                    let name = $crate::Name::new($name);
429                    let value: $crate::Value = vec![$($value),+].into();
430                    $crate::Attribute::new(name, value)
431                },
432            )*
433        ])
434    };
435}
436
437/// Contains all attribute validation errors for an [`Object`].
438#[derive(Debug, thiserror::Error)]
439#[error("{num} attribute(s) failed validation", num = .errors.len())]
440pub struct ObjectValidationError {
441    /// Validation errors paired with the index of the offending attribute.
442    errors: Vec<(usize, AttributeError)>,
443}
444
445impl ObjectValidationError {
446    fn new(errors: Vec<(usize, AttributeError)>) -> Self {
447        Self { errors }
448    }
449
450    /// The number of attributes that failed validation.
451    #[must_use]
452    pub fn len(&self) -> usize {
453        self.errors.len()
454    }
455
456    /// Returns `true` if no attribute validation errors are contained.
457    #[must_use]
458    pub fn is_empty(&self) -> bool {
459        self.errors.is_empty()
460    }
461
462    /// Iterate over the attribute errors.
463    pub fn iter_errors(&self) -> impl Iterator<Item = &AttributeError> {
464        self.errors.iter().map(|(_, error)| error)
465    }
466
467    /// Iterate over attribute errors together with the index of the offending attribute.
468    pub fn iter_indexed(&self) -> impl Iterator<Item = (usize, &AttributeError)> {
469        self.errors.iter().map(|(index, error)| (*index, error))
470    }
471
472    /// Return attribute errors together with the index of the offending attribute.
473    #[must_use]
474    pub fn into_errors(self) -> Vec<(usize, AttributeError)> {
475        self.errors
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use rstest::*;
482    #[cfg(feature = "json")]
483    use serde_json::json;
484
485    use super::*;
486    use crate::{
487        spec::{InvalidNameError, Rfc2622},
488        Name,
489    };
490
491    #[rstest]
492    #[case(
493        Object::new(vec![
494            Attribute::unchecked_single("role", "ACME Company"),
495            Attribute::unchecked_single("address", "Packet Street 6"),
496            Attribute::unchecked_single("address", "128 Series of Tubes"),
497            Attribute::unchecked_single("address", "Internet"),
498            Attribute::unchecked_single("email", "rpsl-rs@github.com"),
499            Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
500            Attribute::unchecked_single("source", "RIPE"),
501        ]),
502        Object::new_parsed(
503            concat!(
504                "role:           ACME Company\n",
505                "address:        Packet Street 6\n",
506                "address:        128 Series of Tubes\n",
507                "address:        Internet\n",
508                "email:          rpsl-rs@github.com\n",
509                "nic-hdl:        RPSL1-RIPE\n",
510                "source:         RIPE\n",
511                "\n"
512            ),
513            vec![
514                Attribute::unchecked_single("role", "ACME Company"),
515                Attribute::unchecked_single("address", "Packet Street 6"),
516                Attribute::unchecked_single("address", "128 Series of Tubes"),
517                Attribute::unchecked_single("address", "Internet"),
518                Attribute::unchecked_single("email", "rpsl-rs@github.com"),
519                Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
520                Attribute::unchecked_single("source", "RIPE"),
521            ]
522        ),
523        concat!(
524            "role:           ACME Company\n",
525            "address:        Packet Street 6\n",
526            "address:        128 Series of Tubes\n",
527            "address:        Internet\n",
528            "email:          rpsl-rs@github.com\n",
529            "nic-hdl:        RPSL1-RIPE\n",
530            "source:         RIPE\n",
531            "\n"
532        )
533    )]
534    #[case(
535        Object::new(vec![
536            Attribute::unchecked_single("role", "ACME Company"),
537            Attribute::unchecked_multi(
538                "address",
539                ["Packet Street 6", "128 Series of Tubes", "Internet"]
540            ),
541            Attribute::unchecked_single("email", "rpsl-rs@github.com"),
542            Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
543            Attribute::unchecked_single("source", "RIPE"),
544        ]),
545        Object::new_parsed(
546            concat!(
547                "role:           ACME Company\n",
548                "address:        Packet Street 6\n",
549                "                128 Series of Tubes\n",
550                "                Internet\n",
551                "email:          rpsl-rs@github.com\n",
552                "nic-hdl:        RPSL1-RIPE\n",
553                "source:         RIPE\n",
554                "\n"
555            ),
556            vec![
557                Attribute::unchecked_single("role", "ACME Company"),
558                Attribute::unchecked_multi(
559                    "address",
560                    ["Packet Street 6", "128 Series of Tubes", "Internet"]
561                ),
562                Attribute::unchecked_single("email", "rpsl-rs@github.com"),
563                Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
564                Attribute::unchecked_single("source", "RIPE"),
565            ]
566        ),
567        concat!(
568            "role:           ACME Company\n",
569            "address:        Packet Street 6\n",
570            "                128 Series of Tubes\n",
571            "                Internet\n",
572            "email:          rpsl-rs@github.com\n",
573            "nic-hdl:        RPSL1-RIPE\n",
574            "source:         RIPE\n",
575            "\n"
576        )
577    )]
578    fn object_display(
579        #[case] owned: Object<'static>,
580        #[case] borrowed: Object,
581        #[case] expected: &str,
582    ) {
583        assert_eq!(owned.to_string(), expected);
584        assert_eq!(borrowed.to_string(), expected);
585    }
586
587    #[rstest]
588    #[case(
589        Object::new(vec![
590            Attribute::unchecked_single("role", "ACME Company"),
591        ]),
592        1
593    )]
594    #[case(
595        Object::new(vec![
596            Attribute::unchecked_single("role", "ACME Company"),
597            Attribute::unchecked_single("address", "Packet Street 6"),
598            Attribute::unchecked_single("address", "128 Series of Tubes"),
599            Attribute::unchecked_single("address", "Internet"),
600            Attribute::unchecked_single("email", "rpsl-rs@github.com"),
601            Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
602            Attribute::unchecked_single("source", "RIPE"),
603        ]),
604        7
605    )]
606    fn object_len(#[case] object: Object, #[case] expected: usize) {
607        assert_eq!(object.len(), expected);
608    }
609
610    #[rstest]
611    #[case(
612        Object::new(vec![
613            Attribute::unchecked_single("role", "ACME Company"),
614            Attribute::unchecked_single("address", "Packet Street 6"),
615            Attribute::unchecked_single("address", "128 Series of Tubes"),
616            Attribute::unchecked_single("address", "Internet"),
617            Attribute::unchecked_single("email", "rpsl-rs@github.com"),
618            Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
619            Attribute::unchecked_single("source", "RIPE"),
620        ]),
621        2,
622        Attribute::unchecked_single("address", "128 Series of Tubes"),
623    )]
624    fn object_index(#[case] object: Object, #[case] index: usize, #[case] expected: Attribute) {
625        assert_eq!(object[index], expected);
626    }
627
628    #[rstest]
629    #[case(
630        vec![
631            Attribute::unchecked_single("role", "ACME Company"),
632            Attribute::unchecked_single("address", "Packet Street 6"),
633            Attribute::unchecked_single("address", "128 Series of Tubes"),
634            Attribute::unchecked_single("address", "Internet"),
635            Attribute::unchecked_single("email", "rpsl-rs@github.com"),
636            Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
637            Attribute::unchecked_single("source", "RIPE"),
638        ],
639    )]
640    fn object_deref(#[case] attributes: Vec<Attribute<'static>>) {
641        let object = Object::new(attributes.clone());
642        assert_eq!(*object, attributes);
643    }
644
645    #[rstest]
646    #[case(
647        vec![
648            Attribute::unchecked_single("role", "ACME Company"),
649            Attribute::unchecked_single("address", "Packet Street 6"),
650            Attribute::unchecked_single("address", "128 Series of Tubes"),
651            Attribute::unchecked_single("address", "Internet"),
652            Attribute::unchecked_single("email", "rpsl-rs@github.com"),
653            Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
654            Attribute::unchecked_single("source", "RIPE"),
655        ],
656    )]
657    fn object_into_iter(#[case] attributes: Vec<Attribute<'static>>) {
658        let object = Object::new(attributes.clone());
659
660        let attr_iter = attributes.into_iter();
661        let obj_iter = object.into_iter();
662
663        for (a, b) in attr_iter.zip(obj_iter) {
664            assert_eq!(a, b);
665        }
666    }
667
668    #[rstest]
669    #[case(
670        Object::new(vec![
671            Attribute::unchecked_single("role", "ACME Company"),
672            Attribute::unchecked_single("address", "Packet Street 6"),
673            Attribute::unchecked_single("address", "128 Series of Tubes"),
674            Attribute::unchecked_single("address", "Internet"),
675            Attribute::unchecked_single("email", "rpsl-rs@github.com"),
676            Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
677            Attribute::unchecked_single("source", "RIPE"),
678        ]),
679        json!({
680            "attributes": [
681                { "name": "role", "values": ["ACME Company"] },
682                { "name": "address", "values": ["Packet Street 6"] },
683                { "name": "address", "values": ["128 Series of Tubes"] },
684                { "name": "address", "values": ["Internet"] },
685                { "name": "email", "values": ["rpsl-rs@github.com"] },
686                { "name": "nic-hdl", "values": ["RPSL1-RIPE"] },
687                { "name": "source", "values": ["RIPE"] }
688            ]
689        })
690    )]
691    #[case(
692        Object::new(vec![
693            Attribute::unchecked_single("role", "ACME Company"),
694            Attribute::unchecked_multi(
695                "address",
696                ["Packet Street 6", "", "128 Series of Tubes", "Internet"]
697            ),
698            Attribute::unchecked_single("email", "rpsl-rs@github.com"),
699            Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
700            Attribute::unchecked_single("source", "RIPE"),
701        ]),
702        json!({
703            "attributes": [
704                { "name": "role", "values": ["ACME Company"] },
705                {
706                    "name": "address",
707                    "values": ["Packet Street 6", null, "128 Series of Tubes", "Internet"] },
708                { "name": "email", "values": ["rpsl-rs@github.com"] },
709                { "name": "nic-hdl", "values": ["RPSL1-RIPE"] },
710                { "name": "source", "values": ["RIPE"] }
711            ]
712        })
713    )]
714    #[cfg(feature = "json")]
715    fn object_json_repr(#[case] object: Object, #[case] expected: serde_json::Value) {
716        let json = object.json();
717        assert_eq!(json, expected);
718    }
719
720    #[rstest]
721    #[case(
722        Object::new_parsed(
723            concat!(
724                "role:           ACME Company\n",
725                "address:        Packet Street 6\n",
726                "address:        128 Series of Tubes\n",
727                "address:        Internet\n",
728                "email:          rpsl-rs@github.com\n",
729                "nic-hdl:        RPSL1-RIPE\n",
730                "source:         RIPE\n",
731                "\n", // Terminated by a trailing newline.
732            ),
733            vec![
734                Attribute::unchecked_single("role", "ACME Company"),
735                Attribute::unchecked_single("address", "Packet Street 6"),
736                Attribute::unchecked_single("address", "128 Series of Tubes"),
737                Attribute::unchecked_single("address", "Internet"),
738                Attribute::unchecked_single("email", "rpsl-rs@github.com"),
739                Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
740                Attribute::unchecked_single("source", "RIPE"),
741            ],
742        ),
743        concat!(
744            "role:           ACME Company\n",
745            "address:        Packet Street 6\n",
746            "address:        128 Series of Tubes\n",
747            "address:        Internet\n",
748            "email:          rpsl-rs@github.com\n",
749            "nic-hdl:        RPSL1-RIPE\n",
750            "source:         RIPE\n",
751            "\n" // Contains a trailing newline.
752        )
753    )]
754    #[case(
755        Object::new_parsed(
756            concat!(
757                "role:           ACME Company\n",
758                "address:        Packet Street 6\n",
759                "address:        128 Series of Tubes\n",
760                "address:        Internet\n",
761                "email:          rpsl-rs@github.com\n",
762                "nic-hdl:        RPSL1-RIPE\n",
763                "source:         RIPE\n",
764                // Not terminated by a trailing newline.
765            ),
766            vec![
767                Attribute::unchecked_single("role", "ACME Company"),
768                Attribute::unchecked_single("address", "Packet Street 6"),
769                Attribute::unchecked_single("address", "128 Series of Tubes"),
770                Attribute::unchecked_single("address", "Internet"),
771                Attribute::unchecked_single("email", "rpsl-rs@github.com"),
772                Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
773                Attribute::unchecked_single("source", "RIPE"),
774            ],
775        ),
776        concat!(
777            "role:           ACME Company\n",
778            "address:        Packet Street 6\n",
779            "address:        128 Series of Tubes\n",
780            "address:        Internet\n",
781            "email:          rpsl-rs@github.com\n",
782            "nic-hdl:        RPSL1-RIPE\n",
783            "source:         RIPE\n",
784            // Does not contain a trailing newline.
785        )
786    )]
787    #[case(
788        Object::new_parsed(
789            concat!(
790                "role:           ACME Company\n",
791                "address:        Packet Street 6\n",
792                // Using space as a continuation char.
793                "                128 Series of Tubes\n",
794                "                Internet\n",
795                "email:          rpsl-rs@github.com\n",
796                "nic-hdl:        RPSL1-RIPE\n",
797                "source:         RIPE\n",
798                "\n"
799            ),
800            vec![
801                Attribute::unchecked_single("role", "ACME Company"),
802                Attribute::unchecked_multi(
803                    "address",
804                    ["Packet Street 6", "128 Series of Tubes", "Internet"]
805                ),
806                Attribute::unchecked_single("email", "rpsl-rs@github.com"),
807                Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
808                Attribute::unchecked_single("source", "RIPE"),
809            ],
810        ),
811        concat!(
812            "role:           ACME Company\n",
813            "address:        Packet Street 6\n",
814            // Using space as a continuation char.
815            "                128 Series of Tubes\n",
816            "                Internet\n",
817            "email:          rpsl-rs@github.com\n",
818            "nic-hdl:        RPSL1-RIPE\n",
819            "source:         RIPE\n",
820            "\n"
821        )
822    )]
823    #[case(
824        Object::new_parsed(
825            concat!(
826                "role:           ACME Company\n",
827                "address:        Packet Street 6\n",
828                // Using + as a continuation char.
829                "+               128 Series of Tubes\n",
830                "+               Internet\n",
831                "email:          rpsl-rs@github.com\n",
832                "nic-hdl:        RPSL1-RIPE\n",
833                "source:         RIPE\n",
834                "\n"
835            ),
836            vec![
837                Attribute::unchecked_single("role", "ACME Company"),
838                Attribute::unchecked_multi(
839                    "address",
840                    ["Packet Street 6", "128 Series of Tubes", "Internet"]
841                ),
842                Attribute::unchecked_single("email", "rpsl-rs@github.com"),
843                Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
844                Attribute::unchecked_single("source", "RIPE"),
845            ],
846        ),
847        concat!(
848            "role:           ACME Company\n",
849            "address:        Packet Street 6\n",
850            // Using + as a continuation char.
851            "+               128 Series of Tubes\n",
852            "+               Internet\n",
853            "email:          rpsl-rs@github.com\n",
854            "nic-hdl:        RPSL1-RIPE\n",
855            "source:         RIPE\n",
856            "\n"
857        )
858    )]
859    /// Borrowed objects display as the original RPSL they were created from.
860    fn borrowed_objects_display_like_source(#[case] object: Object, #[case] expected: &str) {
861        assert_eq!(object.to_string(), expected);
862    }
863
864    #[rstest]
865    #[case(
866        object! {
867            "role": "ACME Company";
868            "address": "Packet Street 6", "128 Series of Tubes", "Internet";
869            "email": "rpsl-rs@github.com";
870            "nic-hdl": "RPSL1-RIPE";
871            "source": "RIPE";
872        },
873        Object::new(vec![
874            Attribute::unchecked_single("role", "ACME Company"),
875            Attribute::unchecked_multi(
876                "address",
877                ["Packet Street 6", "128 Series of Tubes", "Internet"],
878            ),
879            Attribute::unchecked_single("email", "rpsl-rs@github.com"),
880            Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE"),
881            Attribute::unchecked_single("source", "RIPE"),
882        ])
883    )]
884    fn object_from_macro(#[case] from_macro: Object, #[case] expected: Object) {
885        assert_eq!(from_macro, expected);
886    }
887
888    #[rstest]
889    #[allow(clippy::used_underscore_binding)]
890    #[case(
891        Object::new(vec![
892            Attribute::unchecked_single("role", "ACME Company"),
893            Attribute::unchecked_single("address", "Packet Street 6"),
894        ]),
895        Rfc2622
896    )]
897    fn object_validate_conformant_object_validates<TargetSpec: Specification>(
898        #[case] object: Object,
899        #[case] _target: TargetSpec,
900    ) {
901        object.validate::<TargetSpec>().unwrap();
902    }
903
904    #[rstest]
905    #[case(
906        Object::new(vec![
907            Attribute::unchecked_single("role", "ACME Company"),
908            Attribute::unchecked_single("a", "Packet Street 6"),
909            Attribute::unchecked_single("1mail", "rpsl-rs@github.com"),
910        ]),
911        Rfc2622,
912        vec![
913            (
914                1,
915                AttributeError::from(
916                    InvalidNameError::new(&Name::new("a"), "must be at least two characters long")
917                ),
918            ),
919            (
920                2,
921                AttributeError::from(
922                    InvalidNameError::new(&Name::new("1mail"), "must start with an ASCII alphabetic character")
923                ),
924            )
925        ]
926    )]
927    #[allow(clippy::used_underscore_binding)]
928    fn object_validate_invalid_object_returns_expected_errors<
929        TargetSpec: Specification + PartialEq,
930    >(
931        #[case] object: Object,
932        #[case] _target: TargetSpec,
933        #[case] expected: Vec<(usize, AttributeError)>,
934    ) {
935        let errors = object.validate::<TargetSpec>().unwrap_err().into_errors();
936        assert_eq!(errors, expected);
937    }
938
939    #[test]
940    fn object_into_spec_preserves_source() {
941        let source = concat!(
942            "role:           ACME Company\n",
943            "source:         RIPE\n",
944            "\n"
945        );
946        let object = Object::new_parsed(
947            source,
948            vec![
949                Attribute::unchecked_single("role", "ACME Company"),
950                Attribute::unchecked_single("source", "RIPE"),
951            ],
952        );
953
954        let converted = object.into_spec::<Rfc2622>().unwrap();
955        assert_eq!(converted.source(), Some(source));
956    }
957
958    #[rstest]
959    #[case(
960        Object::new(vec![
961            Attribute::unchecked_single("role", "ACME Company"),
962            Attribute::unchecked_single("a", "Packet Street 6"),
963            Attribute::unchecked_single("mail", "rpsl-rs@github.com"),
964        ]),
965        Rfc2622,
966        AttributeError::from(
967            InvalidNameError::new(&Name::new("a"), "must be at least two characters long")
968        ),
969    )]
970    #[allow(clippy::used_underscore_binding)]
971    fn object_into_spec_returns_first_validation_error<TargetSpec: Specification + PartialEq>(
972        #[case] object: Object,
973        #[case] _target: TargetSpec,
974        #[case] expected: AttributeError,
975    ) {
976        let error = object.into_spec::<TargetSpec>().unwrap_err();
977        assert_eq!(error, expected);
978    }
979
980    #[rstest]
981    #[case(
982        Object::new(vec![
983            Attribute::unchecked_single("aut-num", "AS42"),
984            Attribute::unchecked_single(
985                "remarks",
986                "All imported prefixes will be tagged with geographic communities and",
987            ),
988            Attribute::unchecked_single(
989                "remarks",
990                "the type of peering relationship according to the table below, using the default",
991            ),
992            Attribute::unchecked_single(
993                "remarks",
994                "announce rule (x=0).",
995            ),
996            Attribute::unchecked_single("remarks", None),
997            Attribute::unchecked_single(
998                "remarks",
999                "The following communities can be used by peers and customers",
1000            ),
1001            Attribute::unchecked_multi(
1002                "remarks",
1003                [
1004                    "x = 0 - Announce (default rule)",
1005                    "x = 1 - Prepend x1",
1006                    "x = 2 - Prepend x2",
1007                    "x = 3 - Prepend x3",
1008                    "x = 9 - Do not announce",
1009                ],
1010            ),
1011        ]),
1012        vec![
1013            ("aut-num", vec!["AS42"]),
1014            (
1015                "remarks",
1016                vec![
1017                    "All imported prefixes will be tagged with geographic communities and",
1018                    "the type of peering relationship according to the table below, using the default",
1019                    "announce rule (x=0).",
1020                    "The following communities can be used by peers and customers",
1021                    "x = 0 - Announce (default rule)",
1022                    "x = 1 - Prepend x1",
1023                    "x = 2 - Prepend x2",
1024                    "x = 3 - Prepend x3",
1025                    "x = 9 - Do not announce",
1026                ]
1027            )
1028        ]
1029    )]
1030    fn get_values_by_name(#[case] object: Object, #[case] name_expected: Vec<(&str, Vec<&str>)>) {
1031        for (name, expected) in name_expected {
1032            assert_eq!(object.get(name), expected);
1033        }
1034    }
1035
1036    #[rstest]
1037    #[case(
1038        Object::new(
1039            vec![
1040                Attribute::unchecked_single("role", "ACME Company"),
1041                Attribute::unchecked_single("address", "Packet Street 6"),
1042                Attribute::unchecked_single("address", "128 Series of Tubes"),
1043                Attribute::unchecked_single("address", "Internet"),
1044            ]),
1045        Object::new(
1046            vec![
1047                Attribute::unchecked_single("role", "ACME Company"),
1048                Attribute::unchecked_single("address", "Packet Street 6"),
1049                Attribute::unchecked_single("address", "128 Series of Tubes"),
1050                Attribute::unchecked_single("address", "Internet"),
1051            ]),
1052    )]
1053    #[case(
1054        Object::new_parsed(
1055            concat!(
1056                "role:           ACME Company\n",
1057                "address:        Packet Street 6\n",
1058                "address:        128 Series of Tubes\n",
1059                "address:        Internet\n",
1060                "\n"
1061            ),
1062            vec![
1063                Attribute::unchecked_single("role", "ACME Company"),
1064                Attribute::unchecked_single("address", "Packet Street 6"),
1065                Attribute::unchecked_single("address", "128 Series of Tubes"),
1066                Attribute::unchecked_single("address", "Internet"),
1067            ],
1068        ),
1069        Object::new(
1070            vec![
1071                Attribute::unchecked_single("role", "ACME Company"),
1072                Attribute::unchecked_single("address", "Packet Street 6"),
1073                Attribute::unchecked_single("address", "128 Series of Tubes"),
1074                Attribute::unchecked_single("address", "Internet"),
1075            ],
1076        ),
1077    )]
1078    /// Objects with equal attributes evaluate as equal, without taking the source field into consideration.
1079    fn eq_objects_are_eq(#[case] object_1: Object, #[case] object_2: Object) {
1080        assert_eq!(object_1, object_2);
1081    }
1082
1083    #[rstest]
1084    #[case(
1085        Object::new(
1086            vec![
1087                Attribute::unchecked_single("role", "Umbrella Corporation"),
1088                Attribute::unchecked_single("address", "Paraguas Street"),
1089                Attribute::unchecked_single("address", "Raccoon City"),
1090                Attribute::unchecked_single("address", "Colorado"),
1091            ]),
1092        Object::new(
1093            vec![
1094                Attribute::unchecked_single("role", "ACME Company"),
1095                Attribute::unchecked_single("address", "Packet Street 6"),
1096                Attribute::unchecked_single("address", "128 Series of Tubes"),
1097                Attribute::unchecked_single("address", "Internet"),
1098            ]),
1099    )]
1100    /// Objects that have different attributes do not evaluate as equal.
1101    fn ne_objects_are_ne(#[case] object_1: Object, #[case] object_2: Object) {
1102        assert_ne!(object_1, object_2);
1103    }
1104}