Skip to main content

ldap_types/
schema.rs

1//! Contains all the code related to representing and parsing LDAP schemas
2//!
3//! LDAP Schema is defined in RFC2252 <https://www.rfc-editor.org/rfc/rfc2252.txt>
4
5use std::collections::HashSet;
6
7#[cfg(feature = "chumsky")]
8use chumsky::{prelude::*, text::digits};
9use educe::Educe;
10use enum_as_inner::EnumAsInner;
11use oid::ObjectIdentifier;
12
13use itertools::Itertools as _;
14
15#[cfg(feature = "chumsky")]
16use std::sync::LazyLock;
17
18use crate::basic::{KeyString, KeyStringOrOID, OIDWithLength};
19
20#[cfg(feature = "chumsky")]
21use crate::basic::{
22    keystring_or_oid_parser, keystring_parser, oid_parser, quoted_keystring_parser,
23};
24
25#[cfg(feature = "serde")]
26use serde::{Deserialize, Serialize};
27
28/// possible tags in the LDAP schema syntax line
29#[cfg(feature = "chumsky")]
30static LDAP_SYNTAX_TAGS: LazyLock<Vec<LDAPSchemaTagDescriptor>> = LazyLock::new(|| {
31    vec![
32        LDAPSchemaTagDescriptor {
33            tag_name: "DESC".to_string(),
34            tag_type: LDAPSchemaTagType::String,
35        },
36        LDAPSchemaTagDescriptor {
37            tag_name: "X-BINARY-TRANSFER-REQUIRED".to_string(),
38            tag_type: LDAPSchemaTagType::Boolean,
39        },
40        LDAPSchemaTagDescriptor {
41            tag_name: "X-NOT-HUMAN-READABLE".to_string(),
42            tag_type: LDAPSchemaTagType::Boolean,
43        },
44    ]
45});
46
47/// possible tags in the LDAP schema matching rule line
48#[cfg(feature = "chumsky")]
49static MATCHING_RULE_TAGS: LazyLock<Vec<LDAPSchemaTagDescriptor>> = LazyLock::new(|| {
50    vec![
51        LDAPSchemaTagDescriptor {
52            tag_name: "NAME".to_string(),
53            tag_type: LDAPSchemaTagType::QuotedKeyStringList,
54        },
55        LDAPSchemaTagDescriptor {
56            tag_name: "SYNTAX".to_string(),
57            tag_type: LDAPSchemaTagType::OIDWithLength,
58        },
59    ]
60});
61
62/// possible tags in the LDAP schema matching rule use line
63#[cfg(feature = "chumsky")]
64static MATCHING_RULE_USE_TAGS: LazyLock<Vec<LDAPSchemaTagDescriptor>> = LazyLock::new(|| {
65    vec![
66        LDAPSchemaTagDescriptor {
67            tag_name: "NAME".to_string(),
68            tag_type: LDAPSchemaTagType::QuotedKeyStringList,
69        },
70        LDAPSchemaTagDescriptor {
71            tag_name: "APPLIES".to_string(),
72            tag_type: LDAPSchemaTagType::KeyStringOrOIDList,
73        },
74    ]
75});
76
77/// possible tags in the LDAP schema attribute type line
78#[cfg(feature = "chumsky")]
79static ATTRIBUTE_TYPE_TAGS: LazyLock<Vec<LDAPSchemaTagDescriptor>> = LazyLock::new(|| {
80    vec![
81        LDAPSchemaTagDescriptor {
82            tag_name: "NAME".to_string(),
83            tag_type: LDAPSchemaTagType::QuotedKeyStringList,
84        },
85        LDAPSchemaTagDescriptor {
86            tag_name: "SUP".to_string(),
87            tag_type: LDAPSchemaTagType::KeyString,
88        },
89        LDAPSchemaTagDescriptor {
90            tag_name: "DESC".to_string(),
91            tag_type: LDAPSchemaTagType::String,
92        },
93        LDAPSchemaTagDescriptor {
94            tag_name: "SYNTAX".to_string(),
95            tag_type: LDAPSchemaTagType::OIDWithLength,
96        },
97        LDAPSchemaTagDescriptor {
98            tag_name: "EQUALITY".to_string(),
99            tag_type: LDAPSchemaTagType::KeyString,
100        },
101        LDAPSchemaTagDescriptor {
102            tag_name: "SUBSTR".to_string(),
103            tag_type: LDAPSchemaTagType::KeyString,
104        },
105        LDAPSchemaTagDescriptor {
106            tag_name: "ORDERING".to_string(),
107            tag_type: LDAPSchemaTagType::KeyString,
108        },
109        LDAPSchemaTagDescriptor {
110            tag_name: "SINGLE-VALUE".to_string(),
111            tag_type: LDAPSchemaTagType::Standalone,
112        },
113        LDAPSchemaTagDescriptor {
114            tag_name: "NO-USER-MODIFICATION".to_string(),
115            tag_type: LDAPSchemaTagType::Standalone,
116        },
117        LDAPSchemaTagDescriptor {
118            tag_name: "USAGE".to_string(),
119            tag_type: LDAPSchemaTagType::KeyString,
120        },
121        LDAPSchemaTagDescriptor {
122            tag_name: "COLLECTIVE".to_string(),
123            tag_type: LDAPSchemaTagType::Standalone,
124        },
125        LDAPSchemaTagDescriptor {
126            tag_name: "OBSOLETE".to_string(),
127            tag_type: LDAPSchemaTagType::Standalone,
128        },
129        LDAPSchemaTagDescriptor {
130            tag_name: "X-ORDERED".to_string(),
131            tag_type: LDAPSchemaTagType::QuotedKeyString,
132        },
133    ]
134});
135
136/// possible tags in the LDAP schema object class line
137#[cfg(feature = "chumsky")]
138static OBJECT_CLASS_TAGS: LazyLock<Vec<LDAPSchemaTagDescriptor>> = LazyLock::new(|| {
139    vec![
140        LDAPSchemaTagDescriptor {
141            tag_name: "NAME".to_string(),
142            tag_type: LDAPSchemaTagType::QuotedKeyStringList,
143        },
144        LDAPSchemaTagDescriptor {
145            tag_name: "SUP".to_string(),
146            tag_type: LDAPSchemaTagType::KeyStringOrOIDList,
147        },
148        LDAPSchemaTagDescriptor {
149            tag_name: "DESC".to_string(),
150            tag_type: LDAPSchemaTagType::String,
151        },
152        LDAPSchemaTagDescriptor {
153            tag_name: "ABSTRACT".to_string(),
154            tag_type: LDAPSchemaTagType::Standalone,
155        },
156        LDAPSchemaTagDescriptor {
157            tag_name: "STRUCTURAL".to_string(),
158            tag_type: LDAPSchemaTagType::Standalone,
159        },
160        LDAPSchemaTagDescriptor {
161            tag_name: "AUXILIARY".to_string(),
162            tag_type: LDAPSchemaTagType::Standalone,
163        },
164        LDAPSchemaTagDescriptor {
165            tag_name: "MUST".to_string(),
166            tag_type: LDAPSchemaTagType::KeyStringOrOIDList,
167        },
168        LDAPSchemaTagDescriptor {
169            tag_name: "MAY".to_string(),
170            tag_type: LDAPSchemaTagType::KeyStringOrOIDList,
171        },
172        LDAPSchemaTagDescriptor {
173            tag_name: "OBSOLETE".to_string(),
174            tag_type: LDAPSchemaTagType::Standalone,
175        },
176    ]
177});
178
179/// all possible tag names in the LDAP schema
180#[cfg(feature = "chumsky")]
181static ALL_SCHEMA_TAG_NAMES: LazyLock<HashSet<String>> = LazyLock::new(|| {
182    let mut tags = HashSet::new();
183    for tag in ATTRIBUTE_TYPE_TAGS.iter() {
184        tags.insert(tag.tag_name.to_owned());
185    }
186    for tag in OBJECT_CLASS_TAGS.iter() {
187        tags.insert(tag.tag_name.to_owned());
188    }
189    for tag in LDAP_SYNTAX_TAGS.iter() {
190        tags.insert(tag.tag_name.to_owned());
191    }
192    for tag in MATCHING_RULE_TAGS.iter() {
193        tags.insert(tag.tag_name.to_owned());
194    }
195    for tag in MATCHING_RULE_USE_TAGS.iter() {
196        tags.insert(tag.tag_name.to_owned());
197    }
198    tags
199});
200
201/// stores the parameter values that can appear behind a tag in an LDAP schema entry
202#[derive(Clone, Debug, EnumAsInner, Educe)]
203#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
204#[educe(PartialEq, Eq, Hash)]
205pub enum LDAPSchemaTagValue {
206    /// the tag has no value
207    Standalone,
208    /// the tag has an OID value
209    OID(#[educe(Hash(method = "crate::basic::hash_oid"))] ObjectIdentifier),
210    /// the tag has an OID value with an optional length
211    OIDWithLength(OIDWithLength),
212    /// the tag has a string value
213    String(String),
214    /// the tag has a key string value
215    KeyString(KeyString),
216    /// the tag has a quoted key string value
217    QuotedKeyString(KeyString),
218    /// the tag has a keystring or an OID value
219    KeyStringOrOID(KeyStringOrOID),
220    /// the tag has a boolean value
221    Boolean(bool),
222    /// the tag has a value that is a list of quoted key strings
223    QuotedKeyStringList(Vec<KeyString>),
224    /// the tag has a value that is a list of key strings or OIDs
225    KeyStringOrOIDList(Vec<KeyStringOrOID>),
226}
227
228/// a single tag in an LDAP schema entry
229#[derive(PartialEq, Eq, Debug, Hash)]
230#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
231pub struct LDAPSchemaTag {
232    /// the name of the tag
233    tag_name: String,
234    /// the value of the tag, if any
235    tag_value: LDAPSchemaTagValue,
236}
237
238/// encodes the expected value type for a schema tag
239/// this allows code reuse in the parser
240#[cfg(feature = "chumsky")]
241#[derive(PartialEq, Eq, Debug, Hash)]
242#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
243pub enum LDAPSchemaTagType {
244    /// the tag is expected to not have a value
245    Standalone,
246    /// the tag is expected to have an OID value
247    OID,
248    /// the tag is expected to have an OID value with an optional length
249    OIDWithLength,
250    /// the tag is expected to have a string value
251    String,
252    /// the tag is expected to have a key string value
253    KeyString,
254    /// the tag is expected to have a quoted key string value
255    QuotedKeyString,
256    /// the tag is expected to have a key string or an OID value
257    KeyStringOrOID,
258    /// the tag is expected to have a boolean value
259    Boolean,
260    /// the tag is expected to have a value that is a list of quoted key strings
261    QuotedKeyStringList,
262    /// the tag is expected to have a value that is a list of keystrings or OIDs
263    KeyStringOrOIDList,
264}
265
266/// describes an expected tag in an LDAP schema entry
267#[cfg(feature = "chumsky")]
268#[derive(PartialEq, Eq, Debug, Hash)]
269#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
270pub struct LDAPSchemaTagDescriptor {
271    /// the tag name of the expected tag
272    pub tag_name: String,
273    /// the type of parameter we expect the tag to have
274    pub tag_type: LDAPSchemaTagType,
275}
276
277/// this parses the LDAP schema tag value that is described by its parameter
278#[cfg(feature = "chumsky")]
279pub fn ldap_schema_tag_value_parser<'src>(
280    tag_type: &LDAPSchemaTagType,
281) -> impl Parser<'src, &'src str, LDAPSchemaTagValue, extra::Err<Rich<'src, char>>> {
282    match tag_type {
283        LDAPSchemaTagType::Standalone => empty()
284            .map(|()| LDAPSchemaTagValue::Standalone)
285            .labelled("no value")
286            .boxed(),
287        LDAPSchemaTagType::OID => oid_parser()
288            .map(LDAPSchemaTagValue::OID)
289            .labelled("OID")
290            .boxed(),
291        LDAPSchemaTagType::OIDWithLength => oid_parser()
292            .then(
293                digits(10)
294                    .collect::<String>()
295                    .delimited_by(just('{'), just('}'))
296                    .try_map(|x, span| {
297                        x.parse().map_err(|e| {
298                            Rich::custom(
299                                span,
300                                format!("Failed to convert parsed digits to integer: {e}"),
301                            )
302                        })
303                    })
304                    .or_not(),
305            )
306            .map(|(oid, len)| LDAPSchemaTagValue::OIDWithLength(OIDWithLength { oid, length: len }))
307            .labelled("OID with optional length")
308            .boxed(),
309        LDAPSchemaTagType::String => none_of("'")
310            .repeated()
311            .collect::<String>()
312            .delimited_by(just('\''), just('\''))
313            .map(LDAPSchemaTagValue::String)
314            .labelled("single-quoted string")
315            .boxed(),
316        LDAPSchemaTagType::KeyString => keystring_parser()
317            .try_map(|ks, span| {
318                if ALL_SCHEMA_TAG_NAMES.contains(&ks.0) {
319                    return Err(Rich::custom(
320                        span,
321                        format!("'{}' is a reserved tag name and cannot be used as a KeyString value here", ks.0),
322                    ));
323                }
324                Ok(ks)
325            })
326            .map(LDAPSchemaTagValue::KeyString)
327            .labelled("keystring")
328            .boxed(),
329        LDAPSchemaTagType::QuotedKeyString => quoted_keystring_parser()
330            .map(LDAPSchemaTagValue::QuotedKeyString)
331            .labelled("quoted keystring")
332            .boxed(),
333        LDAPSchemaTagType::KeyStringOrOID => keystring_or_oid_parser()
334            .try_map(|ksoid, span| {
335                if let KeyStringOrOID::KeyString(ks) = &ksoid {
336                    if ALL_SCHEMA_TAG_NAMES.contains(&ks.0) {
337                        return Err(Rich::custom(
338                            span,
339                            format!("'{}' is a reserved tag name and cannot be used as a KeyStringOrOID value here", ks.0),
340                        ));
341                    }
342                }
343                Ok(ksoid)
344            })
345            .map(LDAPSchemaTagValue::KeyStringOrOID)
346            .labelled("keystring or OID")
347            .boxed(),
348        LDAPSchemaTagType::Boolean => just("TRUE")
349            .to(true)
350            .or(just("FALSE").to(false))
351            .delimited_by(just('\''), just('\''))
352            .map(LDAPSchemaTagValue::Boolean)
353            .labelled("single-quoted uppercase boolean")
354            .boxed(),
355        LDAPSchemaTagType::KeyStringOrOIDList => keystring_or_oid_parser()
356            .padded()
357            .separated_by(just('$'))
358            .collect()
359            .delimited_by(just('('), just(')'))
360            .or(keystring_or_oid_parser().map(|x| vec![x]))
361            .map(LDAPSchemaTagValue::KeyStringOrOIDList)
362            .labelled("list of keystrings or OIDs separated by $")
363            .boxed(),
364        LDAPSchemaTagType::QuotedKeyStringList => quoted_keystring_parser()
365            .padded()
366            .repeated()
367            .collect()
368            .delimited_by(just('('), just(')'))
369            .or(quoted_keystring_parser().map(|x| vec![x]))
370            .map(LDAPSchemaTagValue::QuotedKeyStringList)
371            .labelled("list of quoted keystrings separated by spaces")
372            .boxed(),
373    }
374}
375
376/// this parses an LDAP schema tag described by its parameter
377#[cfg(feature = "chumsky")]
378#[must_use]
379pub fn ldap_schema_tag_parser<'src>(
380    tag_descriptor: &'src LDAPSchemaTagDescriptor,
381) -> impl Parser<'src, &'src str, LDAPSchemaTag, extra::Err<Rich<'src, char>>> + 'src {
382    just(tag_descriptor.tag_name.to_owned())
383        .padded()
384        .ignore_then(ldap_schema_tag_value_parser(&tag_descriptor.tag_type).padded())
385        .map(move |tag_value| LDAPSchemaTag {
386            tag_name: tag_descriptor.tag_name.to_string(),
387            tag_value,
388        })
389}
390
391/// this parses an LDAP schema entry described by its parameter
392///
393/// the tags can be in any order
394///
395/// this function only parses the tags, it does not check if required tags
396/// exist in the output
397///
398/// # Panics
399///
400/// This panics when the tag_descriptors parameter is empty
401#[cfg(feature = "chumsky")]
402#[must_use]
403pub fn ldap_schema_parser<'src>(
404    tag_descriptors: &'src [LDAPSchemaTagDescriptor],
405) -> impl Parser<'src, &'src str, (ObjectIdentifier, Vec<LDAPSchemaTag>), extra::Err<Rich<'src, char>>>
406       + 'src {
407    #[expect(
408        clippy::expect_used,
409        reason = "this fails essentially based on the contents of the tag_descriptors parameter only and chumsky offers no good way to return this type of error"
410    )]
411    let (first, rest) = tag_descriptors
412        .split_first()
413        .expect("tag descriptors must have at least one element");
414    oid_parser()
415        .then(
416            rest.iter()
417                .fold(ldap_schema_tag_parser(first).boxed(), |p, td| {
418                    p.or(ldap_schema_tag_parser(td)).boxed()
419                })
420                .padded()
421                .repeated()
422                .collect(),
423        )
424        .padded()
425        .delimited_by(just('('), just(')'))
426}
427
428/// this is used to extract a required tag's value from the result of [ldap_schema_parser]
429///
430/// # Errors
431///
432/// returns an error if the required tag was not found in schema tag list
433#[cfg(feature = "chumsky")]
434pub fn required_tag<'src>(
435    tag_name: &str,
436    span: &SimpleSpan,
437    tags: &[LDAPSchemaTag],
438) -> Result<LDAPSchemaTagValue, Rich<'src, char>> {
439    tags.iter()
440        .find(|x| x.tag_name == tag_name)
441        .ok_or_else(|| {
442            Rich::custom(
443                *span,
444                format!("No {tag_name} tag in parsed LDAP schema tag list"),
445            )
446        })
447        .map(|x| x.tag_value.to_owned())
448}
449
450/// this is used to extract an optional tag's value from the result of [ldap_schema_parser]
451#[cfg(feature = "chumsky")]
452#[must_use]
453pub fn optional_tag(tag_name: &str, tags: &[LDAPSchemaTag]) -> Option<LDAPSchemaTagValue> {
454    tags.iter()
455        .find(|x| x.tag_name == tag_name)
456        .map(|x| x.tag_value.to_owned())
457}
458
459/// this describes an LDAP syntax schema entry
460#[derive(Clone, Educe)]
461#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
462#[educe(PartialEq, Eq, Hash)]
463pub struct LDAPSyntax {
464    /// the OID of the syntax
465    #[educe(Hash(method = "crate::basic::hash_oid"))]
466    pub oid: ObjectIdentifier,
467    /// the human-readable description of the syntax
468    pub desc: String,
469    /// does this syntax require binary transfer
470    pub x_binary_transfer_required: bool,
471    /// is this syntax human-readable
472    pub x_not_human_readable: bool,
473}
474
475impl std::fmt::Debug for LDAPSyntax {
476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
477        let string_oid: String = self.oid.clone().into();
478        f.debug_struct("LDAPSyntax")
479            .field("oid", &string_oid)
480            .field("desc", &self.desc)
481            .field(
482                "x_binary_transfer_required",
483                &self.x_binary_transfer_required,
484            )
485            .field("x_not_human_readable", &self.x_not_human_readable)
486            .finish()
487    }
488}
489
490/// parse an LDAP syntax schema entry
491///
492/// <https://ldapwiki.com/wiki/LDAPSyntaxes>
493#[cfg(feature = "chumsky")]
494#[must_use]
495pub fn ldap_syntax_parser<'src>(
496) -> impl Parser<'src, &'src str, LDAPSyntax, extra::Err<Rich<'src, char>>> {
497    ldap_schema_parser(&LDAP_SYNTAX_TAGS).try_map(|(oid, tags), span| {
498        Ok(LDAPSyntax {
499            oid,
500            desc: required_tag("DESC", &span, &tags)?
501                .as_string()
502                .ok_or_else(|| Rich::custom(span, "DESC parameter should be a string"))?
503                .to_string(),
504            x_binary_transfer_required: *optional_tag("X-BINARY-TRANSFER-REQUIRED", &tags)
505                .unwrap_or(LDAPSchemaTagValue::Boolean(false))
506                .as_boolean()
507                .ok_or_else(|| {
508                    Rich::custom(
509                        span,
510                        "X-BINARY-TRANSFER_REQUIRED parameter should be a boolean",
511                    )
512                })?,
513            x_not_human_readable: *optional_tag("X-NOT-HUMAN-READABLE", &tags)
514                .unwrap_or(LDAPSchemaTagValue::Boolean(false))
515                .as_boolean()
516                .ok_or_else(|| {
517                    Rich::custom(span, "X-NOT_HUMAN_READABLE parameter should be a boolean")
518                })?,
519        })
520    })
521}
522
523/// a matching rule LDAP schema entry
524///
525/// <https://ldapwiki.com/wiki/MatchingRule>
526#[derive(Clone, Educe)]
527#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
528#[educe(PartialEq, Eq, Hash)]
529pub struct MatchingRule {
530    /// the matching rule's OID
531    #[educe(Hash(method = "crate::basic::hash_oid"))]
532    pub oid: ObjectIdentifier,
533    /// the matching rule's name
534    pub name: Vec<KeyString>,
535    /// the syntax this matching rule can be used with
536    pub syntax: OIDWithLength,
537}
538
539impl std::fmt::Debug for MatchingRule {
540    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
541        let string_oid: String = self.oid.clone().into();
542        f.debug_struct("MatchingRule")
543            .field("oid", &string_oid)
544            .field("name", &self.name)
545            .field("syntax", &self.syntax)
546            .finish()
547    }
548}
549
550/// parse a matching rule LDAP schema entry
551#[cfg(feature = "chumsky")]
552#[must_use]
553pub fn matching_rule_parser<'src>(
554) -> impl Parser<'src, &'src str, MatchingRule, extra::Err<Rich<'src, char>>> {
555    ldap_schema_parser(&MATCHING_RULE_TAGS).try_map(|(oid, tags), span| {
556        Ok(MatchingRule {
557            oid,
558            name: required_tag("NAME", &span, &tags)?
559                .as_quoted_key_string_list()
560                .ok_or_else(|| {
561                    Rich::custom(span, "NAME parameter should be a quoted keystring list")
562                })?
563                .to_vec(),
564            syntax: required_tag("SYNTAX", &span, &tags)?
565                .as_oid_with_length()
566                .ok_or_else(|| {
567                    Rich::custom(
568                        span,
569                        "SYNTAX parameter should be an OID with an optional length",
570                    )
571                })?
572                .to_owned(),
573        })
574    })
575}
576
577/// parse a matching rule use LDAP schema entry
578///
579/// <https://ldapwiki.com/wiki/MatchingRuleUse>
580#[derive(Clone, Educe)]
581#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
582#[educe(PartialEq, Eq, Hash)]
583pub struct MatchingRuleUse {
584    /// the OID of the matching rule this applies to
585    #[educe(Hash(method = "crate::basic::hash_oid"))]
586    pub oid: ObjectIdentifier,
587    /// the name of the matching rule
588    pub name: Vec<KeyString>,
589    /// the attributes this matching rule can be used with
590    pub applies: Vec<KeyStringOrOID>,
591}
592
593impl std::fmt::Debug for MatchingRuleUse {
594    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
595        let string_oid: String = self.oid.clone().into();
596        f.debug_struct("MatchingRuleUse")
597            .field("oid", &string_oid)
598            .field("name", &self.name)
599            .field("applies", &self.applies)
600            .finish()
601    }
602}
603
604/// parse a matching rule use LDAP schema entry
605#[cfg(feature = "chumsky")]
606#[must_use]
607pub fn matching_rule_use_parser<'src>(
608) -> impl Parser<'src, &'src str, MatchingRuleUse, extra::Err<Rich<'src, char>>> {
609    ldap_schema_parser(&MATCHING_RULE_USE_TAGS).try_map(|(oid, tags), span| {
610        Ok(MatchingRuleUse {
611            oid,
612            name: required_tag("NAME", &span, &tags)?
613                .as_quoted_key_string_list()
614                .ok_or_else(|| {
615                    Rich::custom(span, "NAME parameter should be a quoted keystring list")
616                })?
617                .to_vec(),
618            applies: required_tag("APPLIES", &span, &tags)?
619                .as_key_string_or_oid_list()
620                .ok_or_else(|| {
621                    Rich::custom(span, "APPLIES parameter should be a keystring or OID list")
622                })?
623                .to_vec(),
624        })
625    })
626}
627
628/// an attribute type LDAP schema entry
629///
630/// <https://ldapwiki.com/wiki/AttributeTypes>
631#[derive(Clone, Educe)]
632#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
633#[educe(PartialEq, Eq, Hash)]
634#[expect(
635    clippy::struct_excessive_bools,
636    reason = "this is the LDAP schema, we can not refactor this easily"
637)]
638pub struct AttributeType {
639    /// the OID of the attribute type
640    #[educe(Hash(method = "crate::basic::hash_oid"))]
641    pub oid: ObjectIdentifier,
642    /// the name of the attribute type
643    pub name: Vec<KeyString>,
644    /// the parent in the inheritance tree
645    pub sup: Option<KeyString>,
646    /// a human-readable description
647    pub desc: Option<String>,
648    /// the LDAP syntax of the attribute type
649    pub syntax: Option<OIDWithLength>,
650    /// is this a single or multi-valued attribute
651    pub single_value: bool,
652    /// the equality match to use with this attribute type
653    pub equality: Option<KeyString>,
654    /// the substring match to use with this attribute type
655    pub substr: Option<KeyString>,
656    /// the ordering to use with this attribute type
657    pub ordering: Option<KeyString>,
658    /// is user modification of this attribute type allowed
659    /// (e.g. often operational attributes are not user modifiable)
660    pub no_user_modification: bool,
661    /// if this attribute is a
662    ///
663    /// * user attribute (userApplications)
664    /// * an operational attribute (directoryOperation)
665    /// * an operational attribute that needs to be replicated (distributedOperation)
666    /// * an operational attribute that should not be replicated (dSAOperation)
667    pub usage: Option<KeyString>,
668    /// is this a collective attribute
669    ///
670    /// <https://ldapwiki.com/wiki/Collective%20Attribute>
671    pub collective: bool,
672    /// is this attribute obsolete
673    pub obsolete: bool,
674    /// is this attribute ordered and if so how
675    ///
676    /// * values (order among multiple attribute values is preserved)
677    /// * siblings (order among entries using this attribute as RDN is preserved)
678    ///
679    /// <https://tools.ietf.org/html/draft-chu-ldap-xordered-00>
680    pub x_ordered: Option<KeyString>,
681}
682
683impl std::fmt::Debug for AttributeType {
684    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
685        let string_oid: String = self.oid.clone().into();
686        f.debug_struct("AttributeType")
687            .field("oid", &string_oid)
688            .field("name", &self.name)
689            .field("sup", &self.sup)
690            .field("desc", &self.desc)
691            .field("syntax", &self.syntax)
692            .field("single_value", &self.single_value)
693            .field("equality", &self.equality)
694            .field("substr", &self.substr)
695            .field("ordering", &self.ordering)
696            .field("no_user_modification", &self.no_user_modification)
697            .field("usage", &self.usage)
698            .field("collective", &self.collective)
699            .field("obsolete", &self.obsolete)
700            .field("x_ordered", &self.x_ordered)
701            .finish()
702    }
703}
704
705/// parser for attribute type LDAP schema entries
706#[cfg(feature = "chumsky")]
707#[must_use]
708pub fn attribute_type_parser<'src>(
709) -> impl Parser<'src, &'src str, AttributeType, extra::Err<Rich<'src, char>>> {
710    ldap_schema_parser(&ATTRIBUTE_TYPE_TAGS).try_map(|(oid, tags), span| {
711        Ok(AttributeType {
712            oid,
713            name: required_tag("NAME", &span, &tags)?
714                .as_quoted_key_string_list()
715                .ok_or_else(|| {
716                    Rich::custom(span, "NAME parameter should be a quoted keystring list")
717                })?
718                .to_vec(),
719            sup: optional_tag("SUP", &tags)
720                .map(|tag_value| {
721                    tag_value
722                        .as_key_string()
723                        .map(|val| val.to_owned())
724                        .ok_or_else(|| Rich::custom(span, "SUP parameter should be a key string"))
725                })
726                .transpose()?,
727            desc: optional_tag("DESC", &tags)
728                .map(|tag_value| {
729                    tag_value
730                        .as_string()
731                        .map(|val| val.to_string())
732                        .ok_or_else(|| Rich::custom(span, "DESC parameter should be a string"))
733                })
734                .transpose()?,
735            syntax: optional_tag("SYNTAX", &tags)
736                .map(|tag_value| {
737                    tag_value
738                        .as_oid_with_length()
739                        .map(|val| val.to_owned())
740                        .ok_or_else(|| {
741                            Rich::custom(
742                                span,
743                                "SYNTAX parameter should be an OID with an optional length",
744                            )
745                        })
746                })
747                .transpose()?,
748            single_value: optional_tag("SINGLE-VALUE", &tags).is_some(),
749            equality: optional_tag("EQUALITY", &tags)
750                .map(|tag_value| {
751                    tag_value
752                        .as_key_string()
753                        .map(|val| val.to_owned())
754                        .ok_or_else(|| {
755                            Rich::custom(span, "EQUALITY parameter should be a key string")
756                        })
757                })
758                .transpose()?,
759            substr: optional_tag("SUBSTR", &tags)
760                .map(|tag_value| {
761                    tag_value
762                        .as_key_string()
763                        .map(|val| val.to_owned())
764                        .ok_or_else(|| {
765                            Rich::custom(span, "SUBSTR parameter should be a key string")
766                        })
767                })
768                .transpose()?,
769            ordering: optional_tag("ORDERING", &tags)
770                .map(|tag_value| {
771                    tag_value
772                        .as_key_string()
773                        .map(|val| val.to_owned())
774                        .ok_or_else(|| {
775                            Rich::custom(span, "ORDERING parameter should be a key string")
776                        })
777                })
778                .transpose()?,
779            no_user_modification: optional_tag("NO-USER-MODIFICATION", &tags).is_some(),
780            usage: optional_tag("USAGE", &tags)
781                .map(|tag_value| {
782                    tag_value
783                        .as_key_string()
784                        .map(|val| val.to_owned())
785                        .ok_or_else(|| Rich::custom(span, "USAGE parameter should be a key string"))
786                })
787                .transpose()?,
788            collective: optional_tag("COLLECTIVE", &tags).is_some(),
789            obsolete: optional_tag("OBSOLETE", &tags).is_some(),
790            x_ordered: optional_tag("X-ORDERED", &tags)
791                .map(|tag_value| {
792                    tag_value
793                        .as_quoted_key_string()
794                        .map(|val| val.to_owned())
795                        .ok_or_else(|| {
796                            Rich::custom(span, "X-ORDERED parameter should be a quoted key string")
797                        })
798                })
799                .transpose()?,
800        })
801    })
802}
803
804/// type of LDAP object class
805#[derive(PartialEq, Eq, Clone, Debug, EnumAsInner, Hash)]
806#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
807pub enum ObjectClassType {
808    /// this can not be used as an actual object class and is purely used
809    /// as a parent for the other types
810    Abstract,
811    /// this is the main objectclass of an object, other than structural classes
812    /// that are ancestors in the inheritance hierarchy only one of these can be used
813    /// on any given LDAP object
814    Structural,
815    /// these are objectclasses that are added on to the main structural object class
816    /// of an entry
817    Auxiliary,
818}
819
820/// an LDAP schema objectclass entry
821#[derive(Clone, Educe)]
822#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
823#[educe(PartialEq, Eq, Hash)]
824pub struct ObjectClass {
825    /// the OID of the object class
826    #[educe(Hash(method = "crate::basic::hash_oid"))]
827    pub oid: ObjectIdentifier,
828    /// the name of the object class
829    pub name: Vec<KeyString>,
830    /// the parent of the object class
831    pub sup: Vec<KeyStringOrOID>,
832    /// the human-readable description
833    pub desc: Option<String>,
834    /// the type of object class
835    pub object_class_type: ObjectClassType,
836    /// the attributes that must be present on an object with this object class
837    pub must: Vec<KeyStringOrOID>,
838    /// the attributes that may optionally also be present on an object with this
839    /// object class
840    pub may: Vec<KeyStringOrOID>,
841    /// is this object class obsolete
842    pub obsolete: bool,
843}
844
845impl std::fmt::Debug for ObjectClass {
846    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
847        let string_oid: String = self.oid.clone().into();
848        f.debug_struct("ObjectClass")
849            .field("oid", &string_oid)
850            .field("name", &self.name)
851            .field("sup", &self.sup)
852            .field("desc", &self.desc)
853            .field("object_class_type", &self.object_class_type)
854            .field("must", &self.must)
855            .field("may", &self.may)
856            .field("obsolete", &self.obsolete)
857            .finish()
858    }
859}
860
861/// parses an LDAP schema object class entry
862#[cfg(feature = "chumsky")]
863#[must_use]
864pub fn object_class_parser<'src>(
865) -> impl Parser<'src, &'src str, ObjectClass, extra::Err<Rich<'src, char>>> {
866    ldap_schema_parser(&OBJECT_CLASS_TAGS).try_map(|(oid, tags), span| {
867        Ok(ObjectClass {
868            oid,
869            name: required_tag("NAME", &span, &tags)?
870                .as_quoted_key_string_list()
871                .ok_or_else(|| {
872                    Rich::custom(span, "NAME parameter should be a quoted keystring list")
873                })?
874                .to_vec(),
875            sup: optional_tag("SUP", &tags)
876                .map(|tag_value| {
877                    tag_value
878                        .as_key_string_or_oid_list()
879                        .map(|val| val.to_owned())
880                        .ok_or_else(|| {
881                            Rich::custom(span, "SUP parameter should be a key string or OID list")
882                        })
883                })
884                .transpose()?
885                .unwrap_or_default(),
886            desc: optional_tag("DESC", &tags)
887                .map(|tag_value| {
888                    tag_value
889                        .as_string()
890                        .map(|val| val.to_string())
891                        .ok_or_else(|| Rich::custom(span, "DESC parameter should be a string"))
892                })
893                .transpose()?,
894            object_class_type: optional_tag("ABSTRACT", &tags)
895                .map(|_| ObjectClassType::Abstract)
896                .or_else(|| optional_tag("STRUCTURAL", &tags).map(|_| ObjectClassType::Structural))
897                .or_else(|| optional_tag("AUXILIARY", &tags).map(|_| ObjectClassType::Auxiliary))
898                .unwrap_or(ObjectClassType::Structural),
899            must: optional_tag("MUST", &tags)
900                .map(|tag_value| {
901                    tag_value
902                        .as_key_string_or_oid_list()
903                        .map(|val| val.to_owned())
904                        .ok_or_else(|| {
905                            Rich::custom(span, "MUST parameter should be a key string or OID list")
906                        })
907                })
908                .transpose()?
909                .unwrap_or_default(),
910            may: optional_tag("MAY", &tags)
911                .map(|tag_value| {
912                    tag_value
913                        .as_key_string_or_oid_list()
914                        .map(|val| val.to_owned())
915                        .ok_or_else(|| {
916                            Rich::custom(span, "MAY parameter should be a key string or OID list")
917                        })
918                })
919                .transpose()?
920                .unwrap_or_default(),
921            obsolete: optional_tag("OBSOLETE", &tags).is_some(),
922        })
923    })
924}
925
926/// an entire LDAP schema for an LDAP server
927#[derive(Debug, Clone, Hash)]
928#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
929#[expect(
930    clippy::module_name_repetitions,
931    reason = "without schema it would just be LDAP which is probably not a good name to use anywhere when working with LDAP"
932)]
933pub struct LDAPSchema {
934    /// the supported LDAP syntaxes
935    pub ldap_syntaxes: Vec<LDAPSyntax>,
936    /// the supported LDAP matching rules
937    pub matching_rules: Vec<MatchingRule>,
938    /// the allowed uses (attributes) for the LDAP matching rules
939    pub matching_rule_use: Vec<MatchingRuleUse>,
940    /// the supported LDAP attribute types
941    pub attribute_types: Vec<AttributeType>,
942    /// the supported LDAP object classes
943    pub object_classes: Vec<ObjectClass>,
944    // these are not implemented by OpenLDAP to the best of my knowledge
945    // pub name_forms: Vec<String>,
946    // pub dit_content_rules: Vec<String>,
947    // pub dit_structure_rules: Vec<String>,
948}
949
950impl LDAPSchema {
951    /// returns the set of allowed attributes (either must or may) for an ObjectClass and all of its super-classes
952    pub fn allowed_attributes(
953        &self,
954        id: impl TryInto<KeyStringOrOID>,
955    ) -> Option<HashSet<&AttributeType>> {
956        if let Some(object_class) = self.find_object_class(id) {
957            let mut result = HashSet::new();
958            for attribute_name in object_class.must.iter().chain(object_class.may.iter()) {
959                if let Some(attribute) = self.find_attribute_type(attribute_name) {
960                    result.insert(attribute);
961                }
962            }
963            for sup in &object_class.sup {
964                if let Some(allowed_attributes) = self.allowed_attributes(sup) {
965                    result.extend(allowed_attributes);
966                }
967            }
968            Some(result)
969        } else {
970            None
971        }
972    }
973
974    /// returns the set of required attributes (must) for an ObjectClass and all of its super-classes
975    pub fn required_attributes(
976        &self,
977        id: impl TryInto<KeyStringOrOID>,
978    ) -> Option<HashSet<&AttributeType>> {
979        if let Some(object_class) = self.find_object_class(id) {
980            let mut result = HashSet::new();
981            for attribute_name in &object_class.must {
982                if let Some(attribute) = self.find_attribute_type(attribute_name) {
983                    result.insert(attribute);
984                }
985            }
986            for sup in &object_class.sup {
987                if let Some(required_attributes) = self.required_attributes(sup) {
988                    result.extend(required_attributes);
989                }
990            }
991            Some(result)
992        } else {
993            None
994        }
995    }
996
997    /// return the object class if it is present in the schema
998    pub fn find_object_class(&self, id: impl TryInto<KeyStringOrOID>) -> Option<&ObjectClass> {
999        let id: Result<KeyStringOrOID, _> = id.try_into();
1000        match id {
1001            Ok(id) => {
1002                let match_fn: Box<dyn FnMut(&&ObjectClass) -> bool> = match id {
1003                    KeyStringOrOID::OID(oid) => Box::new(move |at: &&ObjectClass| at.oid == oid),
1004                    KeyStringOrOID::KeyString(s) => Box::new(move |at: &&ObjectClass| {
1005                        at.name
1006                            .iter()
1007                            .map(|n| n.to_lowercase())
1008                            .contains(&s.to_lowercase())
1009                    }),
1010                };
1011                self.object_classes.iter().find(match_fn)
1012            }
1013            Err(_) => None,
1014        }
1015    }
1016
1017    /// apply the given function to the named object class
1018    /// and all its ancestors in the LDAP schema until one
1019    /// returns Some
1020    pub fn find_object_class_property<'a, R>(
1021        &'a self,
1022        id: impl TryInto<KeyStringOrOID>,
1023        f: fn(&'a ObjectClass) -> Option<&'a R>,
1024    ) -> Option<&'a R> {
1025        let object_class = self.find_object_class(id);
1026        if let Some(object_class) = object_class {
1027            if let Some(r) = f(object_class) {
1028                Some(r)
1029            } else {
1030                let ks_or_oids = &object_class.sup;
1031                for ks_or_oid in ks_or_oids {
1032                    if let Some(r) = self.find_object_class_property(ks_or_oid, f) {
1033                        return Some(r);
1034                    }
1035                }
1036                None
1037            }
1038        } else {
1039            None
1040        }
1041    }
1042
1043    /// return the attribute type if it is present in the schema
1044    pub fn find_attribute_type(&self, id: impl TryInto<KeyStringOrOID>) -> Option<&AttributeType> {
1045        let id: Result<KeyStringOrOID, _> = id.try_into();
1046        match id {
1047            Ok(id) => {
1048                let match_fn: Box<dyn FnMut(&&AttributeType) -> bool> = match id {
1049                    KeyStringOrOID::OID(oid) => Box::new(move |at: &&AttributeType| at.oid == oid),
1050                    KeyStringOrOID::KeyString(s) => Box::new(move |at: &&AttributeType| {
1051                        at.name
1052                            .iter()
1053                            .map(|n| n.to_lowercase())
1054                            .contains(&s.to_lowercase())
1055                    }),
1056                };
1057                self.attribute_types.iter().find(match_fn)
1058            }
1059            Err(_) => None,
1060        }
1061    }
1062
1063    /// apply the given function to the named attribute type
1064    /// and all its ancestors in the LDAP schema until one
1065    /// returns Some
1066    pub fn find_attribute_type_property<'a, R>(
1067        &'a self,
1068        id: impl TryInto<KeyStringOrOID>,
1069        f: fn(&'a AttributeType) -> Option<&'a R>,
1070    ) -> Option<&'a R> {
1071        let attribute_type = self.find_attribute_type(id);
1072        if let Some(attribute_type) = attribute_type {
1073            if let Some(r) = f(attribute_type) {
1074                Some(r)
1075            } else if let Some(sup @ KeyString(_)) = &attribute_type.sup {
1076                self.find_attribute_type_property(KeyStringOrOID::KeyString(sup.to_owned()), f)
1077            } else {
1078                None
1079            }
1080        } else {
1081            None
1082        }
1083    }
1084
1085    /// return the ldap syntax if it is present in the schema
1086    #[cfg(feature = "chumsky")]
1087    pub fn find_ldap_syntax(&self, id: impl TryInto<ObjectIdentifier>) -> Option<&LDAPSyntax> {
1088        let id: Result<ObjectIdentifier, _> = id.try_into();
1089        match id {
1090            Ok(id) => self
1091                .ldap_syntaxes
1092                .iter()
1093                .find(move |ls: &&LDAPSyntax| ls.oid == id),
1094            Err(_) => None,
1095        }
1096    }
1097
1098    /// return the matching rule if it is present in the schema
1099    #[cfg(feature = "chumsky")]
1100    pub fn find_matching_rule(&self, id: impl TryInto<ObjectIdentifier>) -> Option<&MatchingRule> {
1101        let id: Result<ObjectIdentifier, _> = id.try_into();
1102        match id {
1103            Ok(id) => self
1104                .matching_rules
1105                .iter()
1106                .find(move |ls: &&MatchingRule| ls.oid == id),
1107            Err(_) => None,
1108        }
1109    }
1110
1111    /// return the matching rule use if it is present in the schema
1112    #[cfg(feature = "chumsky")]
1113    pub fn find_matching_rule_use(
1114        &self,
1115        id: impl TryInto<ObjectIdentifier>,
1116    ) -> Option<&MatchingRuleUse> {
1117        let id: Result<ObjectIdentifier, _> = id.try_into();
1118        match id {
1119            Ok(id) => self
1120                .matching_rule_use
1121                .iter()
1122                .find(move |ls: &&MatchingRuleUse| ls.oid == id),
1123            Err(_) => None,
1124        }
1125    }
1126}
1127
1128#[cfg(test)]
1129#[expect(
1130    clippy::expect_used,
1131    reason = "In tests it is okay to fail using expect"
1132)]
1133mod test {
1134    #[cfg(feature = "chumsky")]
1135    use super::*;
1136    #[cfg(feature = "chumsky")]
1137    use crate::basic::ChumskyError;
1138
1139    #[cfg(feature = "chumsky")]
1140    #[test]
1141    fn test_parse_ldap_syntax() {
1142        #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1143        ldap_syntax_parser().parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )").into_result().unwrap();
1144    }
1145
1146    #[cfg(feature = "chumsky")]
1147    #[test]
1148    fn test_parse_ldap_syntax_value1() {
1149        assert_eq!(ldap_syntax_parser().parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )").into_result(),
1150            Ok(LDAPSyntax {
1151                #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1152                oid: "1.3.6.1.4.1.1466.115.121.1.8".to_string().try_into().unwrap(),
1153                         desc: "Certificate".to_string(),
1154                         x_binary_transfer_required: true,
1155                         x_not_human_readable: true,
1156                       }
1157            ));
1158    }
1159
1160    #[cfg(feature = "chumsky")]
1161    #[test]
1162    fn test_parse_ldap_syntax_value2() {
1163        assert_eq!(ldap_syntax_parser().parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-NOT-HUMAN-READABLE 'TRUE' X-BINARY-TRANSFER-REQUIRED 'TRUE' )").into_result(),
1164            Ok(LDAPSyntax {
1165                #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1166                oid: "1.3.6.1.4.1.1466.115.121.1.8".to_string().try_into().unwrap(),
1167                         desc: "Certificate".to_string(),
1168                         x_binary_transfer_required: true,
1169                         x_not_human_readable: true,
1170                       }
1171            ));
1172    }
1173
1174    #[cfg(feature = "chumsky")]
1175    #[test]
1176    fn test_parse_ldap_syntax_value3() {
1177        assert_eq!(ldap_syntax_parser().parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-BINARY-TRANSFER-REQUIRED 'TRUE' )").into_result(),
1178            Ok(LDAPSyntax {
1179                #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1180                oid: "1.3.6.1.4.1.1466.115.121.1.8".to_string().try_into().unwrap(),
1181                         desc: "Certificate".to_string(),
1182                         x_binary_transfer_required: true,
1183                         x_not_human_readable: false,
1184                       }
1185            ));
1186    }
1187
1188    #[cfg(feature = "chumsky")]
1189    #[test]
1190    fn test_parse_ldap_syntax_value4() {
1191        assert_eq!(
1192            ldap_syntax_parser().parse(
1193                "( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-NOT-HUMAN-READABLE 'TRUE' )"
1194            ).into_result(),
1195            Ok(LDAPSyntax {
1196                #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1197                oid: "1.3.6.1.4.1.1466.115.121.1.8"
1198                    .to_string()
1199                    .try_into()
1200                    .unwrap(),
1201                desc: "Certificate".to_string(),
1202                x_binary_transfer_required: false,
1203                x_not_human_readable: true,
1204            })
1205        );
1206    }
1207
1208    #[cfg(feature = "chumsky")]
1209    #[test]
1210    fn test_parse_ldap_syntax_value5() {
1211        assert_eq!(
1212            ldap_syntax_parser()
1213                .parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' )")
1214                .into_result(),
1215            Ok(LDAPSyntax {
1216                #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1217                oid: "1.3.6.1.4.1.1466.115.121.1.8"
1218                    .to_string()
1219                    .try_into()
1220                    .unwrap(),
1221                desc: "Certificate".to_string(),
1222                x_binary_transfer_required: false,
1223                x_not_human_readable: false,
1224            })
1225        );
1226    }
1227
1228    #[cfg(feature = "chumsky")]
1229    #[test]
1230    fn test_parse_ldap_syntax_desc_required() {
1231        #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1232        ldap_syntax_parser()
1233            .parse("( 1.3.6.1.4.1.1466.115.121.1.8 )")
1234            .into_result()
1235            .unwrap_err();
1236    }
1237
1238    #[cfg(feature = "chumsky")]
1239    #[test]
1240    fn test_parse_matching_rule() {
1241        #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1242        matching_rule_parser()
1243            .parse("( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )")
1244            .into_result()
1245            .unwrap();
1246    }
1247
1248    #[cfg(feature = "chumsky")]
1249    #[test]
1250    fn test_parse_matching_rule_value() {
1251        assert_eq!(
1252            matching_rule_parser()
1253                .parse("( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )")
1254                .into_result(),
1255            Ok(MatchingRule {
1256                #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1257                oid: "1.3.6.1.1.16.3".to_string().try_into().unwrap(),
1258                name: vec![KeyString("UUIDOrderingMatch".to_string())],
1259                syntax: OIDWithLength {
1260                    #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1261                    oid: "1.3.6.1.1.16.1".to_string().try_into().unwrap(),
1262                    length: None
1263                },
1264            })
1265        );
1266    }
1267
1268    #[cfg(feature = "chumsky")]
1269    #[test]
1270    fn test_parse_matching_rule_uses() {
1271        #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1272        matching_rule_use_parser().parse("( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES ( postalAddress $ registeredAddress $ homePostalAddress ) )").into_result().unwrap();
1273    }
1274
1275    #[cfg(feature = "chumsky")]
1276    #[test]
1277    fn test_parse_matching_rule_uses_value() {
1278        assert_eq!(matching_rule_use_parser().parse("( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES ( postalAddress $ registeredAddress $ homePostalAddress ) )").into_result(),
1279            Ok(MatchingRuleUse {
1280                #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1281                oid: "2.5.13.11".to_string().try_into().unwrap(),
1282                                 name: vec![KeyString("caseIgnoreListMatch".to_string())],
1283                                 applies: vec![KeyStringOrOID::KeyString(KeyString("postalAddress".to_string())),
1284                                               KeyStringOrOID::KeyString(KeyString("registeredAddress".to_string())),
1285                                               KeyStringOrOID::KeyString(KeyString("homePostalAddress".to_string()))
1286                                              ],
1287            })
1288        );
1289    }
1290
1291    #[cfg(feature = "chumsky")]
1292    #[test]
1293    fn test_parse_matching_rule_uses_single_applies_value() {
1294        assert_eq!(
1295            matching_rule_use_parser()
1296                .parse("( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES postalAddress )")
1297                .into_result(),
1298            Ok(MatchingRuleUse {
1299                #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1300                oid: "2.5.13.11".to_string().try_into().unwrap(),
1301                name: vec![KeyString("caseIgnoreListMatch".to_string())],
1302                applies: vec![KeyStringOrOID::KeyString(KeyString(
1303                    "postalAddress".to_string()
1304                ))],
1305            })
1306        );
1307    }
1308
1309    mod attribute_type_parser_tests {
1310        use super::*;
1311
1312        #[cfg(feature = "chumsky")]
1313        #[test]
1314        fn test_attribute_type_sup_missing() {
1315            let schema_str =
1316                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1317            let attr_type = attribute_type_parser()
1318                .parse(schema_str)
1319                .into_result()
1320                .expect("Parsing failed");
1321            assert!(attr_type.sup.is_none());
1322        }
1323
1324        #[cfg(feature = "chumsky")]
1325        #[test]
1326        fn test_attribute_type_sup_wrong_type() {
1327            let schema_str = "( 1.2.3 NAME 'test' SUP 'invalid value with spaces' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1328            let result = attribute_type_parser().parse(schema_str).into_result();
1329            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1330            let err = result.unwrap_err();
1331            let err_string = format!(
1332                "{}",
1333                ChumskyError {
1334                    description: "attribute type".to_string(),
1335                    source: schema_str.to_string(),
1336                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1337                }
1338            );
1339            assert!(
1340                err_string.contains("Unexpected token while parsing [], expected keystring"),
1341                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
1342            );
1343        }
1344
1345        #[cfg(feature = "chumsky")]
1346        #[test]
1347        fn test_attribute_type_sup_missing_param() {
1348            let schema_str = "( 1.2.3 NAME 'test' SUP DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1349            let result = attribute_type_parser().parse(schema_str).into_result();
1350            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1351            let err = result.unwrap_err();
1352            let err_string = format!(
1353                "{}",
1354                ChumskyError {
1355                    description: "attribute type".to_string(),
1356                    source: schema_str.to_string(),
1357                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1358                }
1359            );
1360            assert!(
1361                err_string.contains("Unexpected end of input while parsing [], expected keystring"),
1362                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
1363            );
1364        }
1365
1366        #[cfg(feature = "chumsky")]
1367        #[test]
1368        fn test_attribute_type_sup_correct() {
1369            let schema_str = "( 1.2.3 NAME 'test' SUP someKeyString DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1370            let attr_type = attribute_type_parser()
1371                .parse(schema_str)
1372                .into_result()
1373                .expect("Parsing failed");
1374            assert_eq!(attr_type.sup, Some(KeyString("someKeyString".to_string())));
1375        }
1376
1377        // Test cases for 'DESC'
1378        #[cfg(feature = "chumsky")]
1379        #[test]
1380        fn test_attribute_type_desc_missing() {
1381            let schema_str = "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1382            let attr_type = attribute_type_parser()
1383                .parse(schema_str)
1384                .into_result()
1385                .expect("Parsing failed");
1386            assert!(attr_type.desc.is_none());
1387        }
1388
1389        #[cfg(feature = "chumsky")]
1390        #[test]
1391        fn test_attribute_type_desc_wrong_type() {
1392            let schema_str =
1393                "( 1.2.3 NAME 'test' DESC unquoted String SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1394            let result = attribute_type_parser().parse(schema_str).into_result();
1395            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1396            let err = result.unwrap_err();
1397            let err_string = format!(
1398                "{}",
1399                ChumskyError {
1400                    description: "attribute type".to_string(),
1401                    source: schema_str.to_string(),
1402                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1403                }
1404            );
1405            assert!(
1406                err_string.contains("Unexpected token while parsing [], expected single-quoted string"),
1407                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
1408            );
1409        }
1410
1411        #[cfg(feature = "chumsky")]
1412        #[test]
1413        fn test_attribute_type_desc_missing_param() {
1414            let schema_str = "( 1.2.3 NAME 'test' DESC SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1415            let result = attribute_type_parser().parse(schema_str).into_result();
1416            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1417            let err = result.unwrap_err();
1418            let err_string = format!(
1419                "{}",
1420                ChumskyError {
1421                    description: "attribute type".to_string(),
1422                    source: schema_str.to_string(),
1423                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1424                }
1425            );
1426            assert!(
1427                err_string.contains("Unexpected token while parsing [], expected single-quoted string"),
1428                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
1429            );
1430        }
1431
1432        #[cfg(feature = "chumsky")]
1433        #[test]
1434        fn test_attribute_type_desc_correct() {
1435            let schema_str = "( 1.2.3 NAME 'test' DESC 'Some description' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1436            let attr_type = attribute_type_parser()
1437                .parse(schema_str)
1438                .into_result()
1439                .expect("Parsing failed");
1440            assert_eq!(attr_type.desc, Some("Some description".to_string()));
1441        }
1442
1443        // Test cases for 'SYNTAX'
1444        #[cfg(feature = "chumsky")]
1445        #[test]
1446        fn test_attribute_type_syntax_missing() {
1447            let schema_str = "( 1.2.3 NAME 'test' DESC 'Test Attribute' )";
1448            let attr_type = attribute_type_parser()
1449                .parse(schema_str)
1450                .into_result()
1451                .expect("Parsing failed");
1452            assert!(attr_type.syntax.is_none());
1453        }
1454
1455        #[cfg(feature = "chumsky")]
1456        #[test]
1457        fn test_attribute_type_syntax_wrong_type() {
1458            let schema_str = "( 1.2.3 NAME 'test' SYNTAX 'not an OID' DESC 'Test Attribute' )";
1459            let result = attribute_type_parser().parse(schema_str).into_result();
1460            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1461            let err = result.unwrap_err();
1462            let err_string = format!(
1463                "{}",
1464                ChumskyError {
1465                    description: "attribute type".to_string(),
1466                    source: schema_str.to_string(),
1467                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1468                }
1469            );
1470            assert!(
1471                err_string.contains("Unexpected end of input while parsing [], expected OID with optional length"),
1472                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected OID with optional length'",
1473            );
1474        }
1475
1476        #[cfg(feature = "chumsky")]
1477        #[test]
1478        fn test_attribute_type_syntax_missing_param() {
1479            let schema_str = "( 1.2.3 NAME 'test' SYNTAX DESC 'Test Attribute' )";
1480            let result = attribute_type_parser().parse(schema_str).into_result();
1481            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1482            let err = result.unwrap_err();
1483            let err_string = format!(
1484                "{}",
1485                ChumskyError {
1486                    description: "attribute type".to_string(),
1487                    source: schema_str.to_string(),
1488                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1489                }
1490            );
1491            assert!(
1492                err_string.contains("Unexpected end of input while parsing [], expected OID with optional length"),
1493                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected OID with optional length'",
1494            );
1495        }
1496
1497        #[cfg(feature = "chumsky")]
1498        #[test]
1499        fn test_attribute_type_syntax_correct_with_length() {
1500            let schema_str = "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} DESC 'Test Attribute' )";
1501            let attr_type = attribute_type_parser()
1502                .parse(schema_str)
1503                .into_result()
1504                .expect("Parsing failed");
1505            assert_eq!(
1506                attr_type.syntax,
1507                Some(OIDWithLength {
1508                    #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1509                    oid: "1.3.6.1.4.1.1466.115.121.1.15"
1510                        .to_string()
1511                        .try_into()
1512                        .unwrap(),
1513                    length: Some(255)
1514                })
1515            );
1516        }
1517
1518        #[cfg(feature = "chumsky")]
1519        #[test]
1520        fn test_attribute_type_syntax_correct_without_length() {
1521            let schema_str =
1522                "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Test Attribute' )";
1523            let attr_type = attribute_type_parser()
1524                .parse(schema_str)
1525                .into_result()
1526                .expect("Parsing failed");
1527            assert_eq!(
1528                attr_type.syntax,
1529                Some(OIDWithLength {
1530                    #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1531                    oid: "1.3.6.1.4.1.1466.115.121.1.15"
1532                        .to_string()
1533                        .try_into()
1534                        .unwrap(),
1535                    length: None
1536                })
1537            );
1538        }
1539
1540        // Test cases for 'SINGLE-VALUE'
1541        #[cfg(feature = "chumsky")]
1542        #[test]
1543        fn test_attribute_type_single_value_missing() {
1544            let schema_str =
1545                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1546            let attr_type = attribute_type_parser()
1547                .parse(schema_str)
1548                .into_result()
1549                .expect("Parsing failed");
1550            assert!(!attr_type.single_value);
1551        }
1552
1553        #[cfg(feature = "chumsky")]
1554        #[test]
1555        fn test_attribute_type_single_value_present() {
1556            let schema_str = "( 1.2.3 NAME 'test' SINGLE-VALUE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1557            let attr_type = attribute_type_parser()
1558                .parse(schema_str)
1559                .into_result()
1560                .expect("Parsing failed");
1561            assert!(attr_type.single_value);
1562        }
1563
1564        // Test cases for 'EQUALITY'
1565        #[cfg(feature = "chumsky")]
1566        #[test]
1567        fn test_attribute_type_equality_missing() {
1568            let schema_str =
1569                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1570            let attr_type = attribute_type_parser()
1571                .parse(schema_str)
1572                .into_result()
1573                .expect("Parsing failed");
1574            assert!(attr_type.equality.is_none());
1575        }
1576
1577        #[cfg(feature = "chumsky")]
1578        #[test]
1579        fn test_attribute_type_equality_wrong_type() {
1580            let schema_str = "( 1.2.3 NAME 'test' EQUALITY 'invalid equality with spaces' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1581            let result = attribute_type_parser().parse(schema_str).into_result();
1582            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1583            let err = result.unwrap_err();
1584            let err_string = format!(
1585                "{}",
1586                ChumskyError {
1587                    description: "attribute type".to_string(),
1588                    source: schema_str.to_string(),
1589                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1590                }
1591            );
1592            assert!(
1593                err_string.contains("Unexpected token while parsing [], expected keystring"),
1594                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
1595            );
1596        }
1597
1598        #[cfg(feature = "chumsky")]
1599        #[test]
1600        fn test_attribute_type_equality_missing_param() {
1601            let schema_str = "( 1.2.3 NAME 'test' EQUALITY DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1602            let result = attribute_type_parser().parse(schema_str).into_result();
1603            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1604            let err = result.unwrap_err();
1605            let err_string = format!(
1606                "{}",
1607                ChumskyError {
1608                    description: "attribute type".to_string(),
1609                    source: schema_str.to_string(),
1610                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1611                }
1612            );
1613            assert!(
1614                err_string.contains("Unexpected end of input while parsing [], expected keystring"),
1615                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
1616            );
1617        }
1618
1619        #[cfg(feature = "chumsky")]
1620        #[test]
1621        fn test_attribute_type_equality_correct() {
1622            let schema_str = "( 1.2.3 NAME 'test' EQUALITY caseIgnoreMatch DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1623            let attr_type = attribute_type_parser()
1624                .parse(schema_str)
1625                .into_result()
1626                .expect("Parsing failed");
1627            assert_eq!(
1628                attr_type.equality,
1629                Some(KeyString("caseIgnoreMatch".to_string()))
1630            );
1631        }
1632
1633        // Test cases for 'SUBSTR'
1634        #[cfg(feature = "chumsky")]
1635        #[test]
1636        fn test_attribute_type_substr_missing() {
1637            let schema_str =
1638                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1639            let attr_type = attribute_type_parser()
1640                .parse(schema_str)
1641                .into_result()
1642                .expect("Parsing failed");
1643            assert!(attr_type.substr.is_none());
1644        }
1645
1646        #[cfg(feature = "chumsky")]
1647        #[test]
1648        fn test_attribute_type_substr_wrong_type() {
1649            let schema_str = "( 1.2.3 NAME 'test' SUBSTR 'invalid substr with spaces' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1650            let result = attribute_type_parser().parse(schema_str).into_result();
1651            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1652            let err = result.unwrap_err();
1653            let err_string = format!(
1654                "{}",
1655                ChumskyError {
1656                    description: "attribute type".to_string(),
1657                    source: schema_str.to_string(),
1658                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1659                }
1660            );
1661            assert!(
1662                err_string.contains("Unexpected token while parsing [], expected keystring"),
1663                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
1664            );
1665        }
1666
1667        #[cfg(feature = "chumsky")]
1668        #[test]
1669        fn test_attribute_type_substr_missing_param() {
1670            let schema_str = "( 1.2.3 NAME 'test' SUBSTR DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1671            let result = attribute_type_parser().parse(schema_str).into_result();
1672            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1673            let err = result.unwrap_err();
1674            let err_string = format!(
1675                "{}",
1676                ChumskyError {
1677                    description: "attribute type".to_string(),
1678                    source: schema_str.to_string(),
1679                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1680                }
1681            );
1682            assert!(
1683                err_string.contains("Unexpected end of input while parsing [], expected keystring"),
1684                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
1685            );
1686        }
1687
1688        #[cfg(feature = "chumsky")]
1689        #[test]
1690        fn test_attribute_type_substr_correct() {
1691            let schema_str = "( 1.2.3 NAME 'test' SUBSTR caseIgnoreSubstringsMatch DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1692            let attr_type = attribute_type_parser()
1693                .parse(schema_str)
1694                .into_result()
1695                .expect("Parsing failed");
1696            assert_eq!(
1697                attr_type.substr,
1698                Some(KeyString("caseIgnoreSubstringsMatch".to_string()))
1699            );
1700        }
1701
1702        // Test cases for 'ORDERING'
1703        #[cfg(feature = "chumsky")]
1704        #[test]
1705        fn test_attribute_type_ordering_missing() {
1706            let schema_str =
1707                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1708            let attr_type = attribute_type_parser()
1709                .parse(schema_str)
1710                .into_result()
1711                .expect("Parsing failed");
1712            assert!(attr_type.ordering.is_none());
1713        }
1714
1715        #[cfg(feature = "chumsky")]
1716        #[test]
1717        fn test_attribute_type_ordering_wrong_type() {
1718            let schema_str = "( 1.2.3 NAME 'test' ORDERING 'invalid ordering with spaces' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1719            let result = attribute_type_parser().parse(schema_str).into_result();
1720            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1721            let err = result.unwrap_err();
1722            let err_string = format!(
1723                "{}",
1724                ChumskyError {
1725                    description: "attribute type".to_string(),
1726                    source: schema_str.to_string(),
1727                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1728                }
1729            );
1730            assert!(
1731                err_string.contains("Unexpected token while parsing [], expected keystring"),
1732                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
1733            );
1734        }
1735
1736        #[cfg(feature = "chumsky")]
1737        #[test]
1738        fn test_attribute_type_ordering_missing_param() {
1739            let schema_str = "( 1.2.3 NAME 'test' ORDERING DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1740            let result = attribute_type_parser().parse(schema_str).into_result();
1741            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1742            let err = result.unwrap_err();
1743            let err_string = format!(
1744                "{}",
1745                ChumskyError {
1746                    description: "attribute type".to_string(),
1747                    source: schema_str.to_string(),
1748                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1749                }
1750            );
1751            assert!(
1752                err_string.contains("Unexpected end of input while parsing [], expected keystring"),
1753                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
1754            );
1755        }
1756
1757        #[cfg(feature = "chumsky")]
1758        #[test]
1759        fn test_attribute_type_ordering_correct() {
1760            let schema_str = "( 1.2.3 NAME 'test' ORDERING caseIgnoreOrderingMatch DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1761            let attr_type = attribute_type_parser()
1762                .parse(schema_str)
1763                .into_result()
1764                .expect("Parsing failed");
1765            assert_eq!(
1766                attr_type.ordering,
1767                Some(KeyString("caseIgnoreOrderingMatch".to_string()))
1768            );
1769        }
1770
1771        // Test cases for 'NO-USER-MODIFICATION'
1772        #[cfg(feature = "chumsky")]
1773        #[test]
1774        fn test_attribute_type_no_user_modification_missing() {
1775            let schema_str =
1776                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1777            let attr_type = attribute_type_parser()
1778                .parse(schema_str)
1779                .into_result()
1780                .expect("Parsing failed");
1781            assert!(!attr_type.no_user_modification);
1782        }
1783
1784        #[cfg(feature = "chumsky")]
1785        #[test]
1786        fn test_attribute_type_no_user_modification_present() {
1787            let schema_str = "( 1.2.3 NAME 'test' NO-USER-MODIFICATION DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1788            let attr_type = attribute_type_parser()
1789                .parse(schema_str)
1790                .into_result()
1791                .expect("Parsing failed");
1792            assert!(attr_type.no_user_modification);
1793        }
1794
1795        // Test cases for 'USAGE'
1796        #[cfg(feature = "chumsky")]
1797        #[test]
1798        fn test_attribute_type_usage_missing() {
1799            let schema_str =
1800                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1801            let attr_type = attribute_type_parser()
1802                .parse(schema_str)
1803                .into_result()
1804                .expect("Parsing failed");
1805            assert!(attr_type.usage.is_none());
1806        }
1807
1808        #[cfg(feature = "chumsky")]
1809        #[test]
1810        fn test_attribute_type_usage_wrong_type() {
1811            let schema_str = "( 1.2.3 NAME 'test' USAGE 'invalid usage with spaces' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1812            let result = attribute_type_parser().parse(schema_str).into_result();
1813            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1814            let err = result.unwrap_err();
1815            let err_string = format!(
1816                "{}",
1817                ChumskyError {
1818                    description: "attribute type".to_string(),
1819                    source: schema_str.to_string(),
1820                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1821                }
1822            );
1823            assert!(
1824                err_string.contains("Unexpected token while parsing [], expected keystring"),
1825                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
1826            );
1827        }
1828
1829        #[cfg(feature = "chumsky")]
1830        #[test]
1831        fn test_attribute_type_usage_missing_param() {
1832            let schema_str = "( 1.2.3 NAME 'test' USAGE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1833            let result = attribute_type_parser().parse(schema_str).into_result();
1834            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1835            let err = result.unwrap_err();
1836            let err_string = format!(
1837                "{}",
1838                ChumskyError {
1839                    description: "attribute type".to_string(),
1840                    source: schema_str.to_string(),
1841                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1842                }
1843            );
1844            assert!(
1845                err_string.contains("Unexpected end of input while parsing [], expected keystring"),
1846                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
1847            );
1848        }
1849
1850        #[cfg(feature = "chumsky")]
1851        #[test]
1852        fn test_attribute_type_usage_correct() {
1853            let schema_str = "( 1.2.3 NAME 'test' USAGE userApplications DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1854            let attr_type = attribute_type_parser()
1855                .parse(schema_str)
1856                .into_result()
1857                .expect("Parsing failed");
1858            assert_eq!(
1859                attr_type.usage,
1860                Some(KeyString("userApplications".to_string()))
1861            );
1862        }
1863
1864        // Test cases for 'COLLECTIVE'
1865        #[cfg(feature = "chumsky")]
1866        #[test]
1867        fn test_attribute_type_collective_missing() {
1868            let schema_str =
1869                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1870            let attr_type = attribute_type_parser()
1871                .parse(schema_str)
1872                .into_result()
1873                .expect("Parsing failed");
1874            assert!(!attr_type.collective);
1875        }
1876
1877        #[cfg(feature = "chumsky")]
1878        #[test]
1879        fn test_attribute_type_collective_present() {
1880            let schema_str = "( 1.2.3 NAME 'test' COLLECTIVE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1881            let attr_type = attribute_type_parser()
1882                .parse(schema_str)
1883                .into_result()
1884                .expect("Parsing failed");
1885            assert!(attr_type.collective);
1886        }
1887
1888        // Test cases for 'OBSOLETE'
1889        #[cfg(feature = "chumsky")]
1890        #[test]
1891        fn test_attribute_type_obsolete_missing() {
1892            let schema_str =
1893                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1894            let attr_type = attribute_type_parser()
1895                .parse(schema_str)
1896                .into_result()
1897                .expect("Parsing failed");
1898            assert!(!attr_type.obsolete);
1899        }
1900
1901        #[cfg(feature = "chumsky")]
1902        #[test]
1903        fn test_attribute_type_obsolete_present() {
1904            let schema_str = "( 1.2.3 NAME 'test' OBSOLETE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1905            let attr_type = attribute_type_parser()
1906                .parse(schema_str)
1907                .into_result()
1908                .expect("Parsing failed");
1909            assert!(attr_type.obsolete);
1910        }
1911
1912        // Test cases for 'X-ORDERED'
1913        #[cfg(feature = "chumsky")]
1914        #[test]
1915        fn test_attribute_type_x_ordered_missing() {
1916            let schema_str =
1917                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1918            let attr_type = attribute_type_parser()
1919                .parse(schema_str)
1920                .into_result()
1921                .expect("Parsing failed");
1922            assert!(attr_type.x_ordered.is_none());
1923        }
1924
1925        #[cfg(feature = "chumsky")]
1926        #[test]
1927        fn test_attribute_type_x_ordered_wrong_type() {
1928            let schema_str = "( 1.2.3 NAME 'test' X-ORDERED unquotedString DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1929            let result = attribute_type_parser().parse(schema_str).into_result();
1930            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1931            let err = result.unwrap_err();
1932            let err_string = format!(
1933                "{}",
1934                ChumskyError {
1935                    description: "attribute type".to_string(),
1936                    source: schema_str.to_string(),
1937                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1938                }
1939            );
1940            assert!(
1941                err_string.contains("Unexpected token while parsing [], expected quoted keystring"),
1942                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected quoted keystring'",
1943            );
1944        }
1945
1946        #[cfg(feature = "chumsky")]
1947        #[test]
1948        fn test_attribute_type_x_ordered_missing_param() {
1949            let schema_str = "( 1.2.3 NAME 'test' X-ORDERED DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1950            let result = attribute_type_parser().parse(schema_str).into_result();
1951            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1952            let err = result.unwrap_err();
1953            let err_string = format!(
1954                "{}",
1955                ChumskyError {
1956                    description: "attribute type".to_string(),
1957                    source: schema_str.to_string(),
1958                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
1959                }
1960            );
1961            assert!(
1962                err_string.contains("Unexpected token while parsing [], expected quoted keystring"),
1963                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected quoted keystring'",
1964            );
1965        }
1966
1967        #[cfg(feature = "chumsky")]
1968        #[test]
1969        fn test_attribute_type_x_ordered_correct() {
1970            let schema_str = "( 1.2.3 NAME 'test' X-ORDERED 'values' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1971            let attr_type = attribute_type_parser()
1972                .parse(schema_str)
1973                .into_result()
1974                .expect("Parsing failed");
1975            assert_eq!(attr_type.x_ordered, Some(KeyString("values".to_string())));
1976        }
1977    }
1978
1979    mod object_class_parser_tests {
1980        use super::*;
1981
1982        // Test cases for 'SUP'
1983        #[cfg(feature = "chumsky")]
1984        #[test]
1985        fn test_object_class_sup_missing() {
1986            let schema_str = "( 1.2.3 NAME 'testOC' MUST attr1 )";
1987            let object_class = object_class_parser()
1988                .parse(schema_str)
1989                .into_result()
1990                .expect("Parsing failed");
1991            assert!(object_class.sup.is_empty());
1992        }
1993
1994        #[cfg(feature = "chumsky")]
1995        #[test]
1996        fn test_object_class_sup_wrong_type() {
1997            let schema_str = "( 1.2.3 NAME 'testOC' SUP 'invalid value with spaces' MUST attr1 )";
1998            let result = object_class_parser().parse(schema_str).into_result();
1999            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2000            let err = result.unwrap_err();
2001            let err_string = format!(
2002                "{}",
2003                ChumskyError {
2004                    description: "object class".to_string(),
2005                    source: schema_str.to_string(),
2006                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2007                }
2008            );
2009            assert!(
2010                err_string.contains("Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $"),
2011                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $'",
2012            );
2013        }
2014
2015        #[cfg(feature = "chumsky")]
2016        #[test]
2017        fn test_object_class_sup_missing_param() {
2018            let schema_str = "( 1.2.3 NAME 'testOC' SUP MUST attr1 )";
2019            let result = object_class_parser().parse(schema_str).into_result();
2020            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2021            let err = result.unwrap_err();
2022            let err_string = format!(
2023                "{}",
2024                ChumskyError {
2025                    description: "object class".to_string(),
2026                    source: schema_str.to_string(),
2027                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2028                }
2029            );
2030            assert!(
2031                err_string.contains("Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')'"),
2032                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')''",
2033            );
2034        }
2035
2036        #[cfg(feature = "chumsky")]
2037        #[test]
2038        fn test_object_class_sup_correct_single() {
2039            let schema_str = "( 1.2.3 NAME 'testOC' SUP top MUST attr1 )";
2040            let object_class = object_class_parser()
2041                .parse(schema_str)
2042                .into_result()
2043                .expect("Parsing failed");
2044            assert_eq!(
2045                object_class.sup,
2046                vec![KeyStringOrOID::KeyString(KeyString("top".to_string()))]
2047            );
2048        }
2049
2050        #[cfg(feature = "chumsky")]
2051        #[test]
2052        fn test_object_class_sup_correct_list() {
2053            let schema_str = "( 1.2.3 NAME 'testOC' SUP ( top $ person ) MUST attr1 )";
2054            let object_class = object_class_parser()
2055                .parse(schema_str)
2056                .into_result()
2057                .expect("Parsing failed");
2058            assert_eq!(
2059                object_class.sup,
2060                vec![
2061                    KeyStringOrOID::KeyString(KeyString("top".to_string())),
2062                    KeyStringOrOID::KeyString(KeyString("person".to_string()))
2063                ]
2064            );
2065        }
2066
2067        // Test cases for 'DESC'
2068        #[cfg(feature = "chumsky")]
2069        #[test]
2070        fn test_object_class_desc_missing() {
2071            let schema_str = "( 1.2.3 NAME 'testOC' MUST attr1 )";
2072            let object_class = object_class_parser()
2073                .parse(schema_str)
2074                .into_result()
2075                .expect("Parsing failed");
2076            assert!(object_class.desc.is_none());
2077        }
2078
2079        #[cfg(feature = "chumsky")]
2080        #[test]
2081        fn test_object_class_desc_wrong_type() {
2082            let schema_str = "( 1.2.3 NAME 'testOC' DESC unquoted String MUST attr1 )";
2083            let result = object_class_parser().parse(schema_str).into_result();
2084            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2085            let err = result.unwrap_err();
2086            let err_string = format!(
2087                "{}",
2088                ChumskyError {
2089                    description: "object class".to_string(),
2090                    source: schema_str.to_string(),
2091                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2092                }
2093            );
2094            assert!(
2095                err_string.contains("Unexpected token while parsing [], expected single-quoted string"),
2096                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
2097            );
2098        }
2099
2100        #[cfg(feature = "chumsky")]
2101        #[test]
2102        fn test_object_class_desc_missing_param() {
2103            let schema_str = "( 1.2.3 NAME 'testOC' DESC MUST attr1 )";
2104            let result = object_class_parser().parse(schema_str).into_result();
2105            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2106            let err = result.unwrap_err();
2107            let err_string = format!(
2108                "{}",
2109                ChumskyError {
2110                    description: "object class".to_string(),
2111                    source: schema_str.to_string(),
2112                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2113                }
2114            );
2115            assert!(
2116                err_string.contains("Unexpected token while parsing [], expected single-quoted string"),
2117                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
2118            );
2119        }
2120
2121        #[cfg(feature = "chumsky")]
2122        #[test]
2123        fn test_object_class_desc_correct() {
2124            let schema_str = "( 1.2.3 NAME 'testOC' DESC 'Some description' MUST attr1 )";
2125            let object_class = object_class_parser()
2126                .parse(schema_str)
2127                .into_result()
2128                .expect("Parsing failed");
2129            assert_eq!(object_class.desc, Some("Some description".to_string()));
2130        }
2131
2132        // Test cases for 'object_class_type' (ABSTRACT, STRUCTURAL, AUXILIARY)
2133        #[cfg(feature = "chumsky")]
2134        #[test]
2135        fn test_object_class_type_abstract_present() {
2136            let schema_str = "( 1.2.3 NAME 'testOC' ABSTRACT MUST attr1 )";
2137            let object_class = object_class_parser()
2138                .parse(schema_str)
2139                .into_result()
2140                .expect("Parsing failed");
2141            assert_eq!(object_class.object_class_type, ObjectClassType::Abstract);
2142        }
2143
2144        #[cfg(feature = "chumsky")]
2145        #[test]
2146        fn test_object_class_type_structural_present() {
2147            let schema_str = "( 1.2.3 NAME 'testOC' STRUCTURAL MUST attr1 )";
2148            let object_class = object_class_parser()
2149                .parse(schema_str)
2150                .into_result()
2151                .expect("Parsing failed");
2152            assert_eq!(object_class.object_class_type, ObjectClassType::Structural);
2153        }
2154
2155        #[cfg(feature = "chumsky")]
2156        #[test]
2157        fn test_object_class_type_auxiliary_present() {
2158            let schema_str = "( 1.2.3 NAME 'testOC' AUXILIARY MUST attr1 )";
2159            let object_class = object_class_parser()
2160                .parse(schema_str)
2161                .into_result()
2162                .expect("Parsing failed");
2163            assert_eq!(object_class.object_class_type, ObjectClassType::Auxiliary);
2164        }
2165
2166        #[cfg(feature = "chumsky")]
2167        #[test]
2168        fn test_object_class_type_default_structural() {
2169            let schema_str = "( 1.2.3 NAME 'testOC' MUST attr1 )";
2170            let object_class = object_class_parser()
2171                .parse(schema_str)
2172                .into_result()
2173                .expect("Parsing failed");
2174            assert_eq!(object_class.object_class_type, ObjectClassType::Structural);
2175        }
2176
2177        // Test for multiple type tags - parser should pick the first encountered.
2178        // In OBJECT_CLASS_TAGS, ABSTRACT is before STRUCTURAL and AUXILIARY.
2179        #[cfg(feature = "chumsky")]
2180        #[test]
2181        fn test_object_class_type_multiple_tags_abstract_first() {
2182            let schema_str = "( 1.2.3 NAME 'testOC' ABSTRACT STRUCTURAL MUST attr1 )";
2183            let object_class = object_class_parser()
2184                .parse(schema_str)
2185                .into_result()
2186                .expect("Parsing failed");
2187            assert_eq!(object_class.object_class_type, ObjectClassType::Abstract);
2188        }
2189
2190        // Test for multiple type tags - if STRUCTURAL is encountered first.
2191        // This scenario might not be easily parsable with current ldap_schema_parser due to fixed order in OBJECT_CLASS_TAGS.
2192        // But if it were possible to define `STRUCTURAL` before `ABSTRACT`, this test would be relevant.
2193        // For now, let's assume the parser handles input as defined in OBJECT_CLASS_TAGS.
2194        // This test case is more about understanding parsing behavior rather than "error" of multiple tags.
2195        #[cfg(feature = "chumsky")]
2196        #[test]
2197        fn test_object_class_type_multiple_tags_structural_first_if_possible() {
2198            // Note: Due to fixed order of OBJECT_CLASS_TAGS, ABSTRACT is always parsed before STRUCTURAL.
2199            // This test is conceptual unless OBJECT_CLASS_TAGS order can be dynamic or input order matters for tag parsing in ldap_schema_parser.
2200            // Based on `object_class_parser`'s `or_else` chain, the first one found wins.
2201            // The actual input string for parsing doesn't necessarily enforce order, but the `ldap_schema_parser` picks based on its internal `fold` order.
2202            // The `fold` processes tags in the order they appear in `tag_descriptors`.
2203            // So if STRUCTURAL appears before ABSTRACT in the input string, the ldap_schema_parser should still pick ABSTRACT if it's listed earlier in OBJECT_CLASS_TAGS.
2204            // Let's create an input where STRUCTURAL comes first to see if the `or_else` chain correctly picks based on tag definition order.
2205            // However, the `ldap_schema_parser` gets a list of tags and can find them in any order in the input. The `or_else` in the `object_class_parser` logic will then apply a preference.
2206
2207            // The OBJECT_CLASS_TAGS is defined as:
2208            // ABSTRACT, STRUCTURAL, AUXILIARY
2209            // So, ABSTRACT will always be checked first by `object_class_type` logic if present.
2210            // If the input string has STRUCTURAL before ABSTRACT, but ABSTRACT is still found by `ldap_schema_parser` as one of the `tags`,
2211            // then `optional_tag("ABSTRACT", &tags)` will return Some, and the `or_else` chain will stop there.
2212
2213            // So a test where STRUCTURAL is *picked* over ABSTRACT when both are present in the *input* string,
2214            // would require STRUCTURAL to be defined earlier in OBJECT_CLASS_TAGS or a different parsing strategy.
2215            // For now, we'll confirm that if STRUCTURAL is the only one, it's picked.
2216            // The previous `test_object_class_type_structural_present` already covers this.
2217
2218            // Re-evaluating `test_object_class_type_multiple_tags_abstract_first`, if `STRUCTURAL` were to appear before `ABSTRACT` in `OBJECT_CLASS_TAGS`,
2219            // this test would be crucial. For now, given the fixed tag order, I will test the documented behavior.
2220
2221            let schema_str = "( 1.2.3 NAME 'testOC' STRUCTURAL ABSTRACT MUST attr1 )"; // STRUCTURAL appears first in input
2222            let object_class = object_class_parser()
2223                .parse(schema_str)
2224                .into_result()
2225                .expect("Parsing failed");
2226            // Due to `OBJECT_CLASS_TAGS` definition order and `or_else` chain, ABSTRACT takes precedence if both are present in the `tags` list.
2227            assert_eq!(object_class.object_class_type, ObjectClassType::Abstract);
2228        }
2229
2230        // Test cases for 'MUST'
2231        #[cfg(feature = "chumsky")]
2232        #[test]
2233        fn test_object_class_must_missing() {
2234            let schema_str = "( 1.2.3 NAME 'testOC' MAY attr1 )";
2235            let object_class = object_class_parser()
2236                .parse(schema_str)
2237                .into_result()
2238                .expect("Parsing failed");
2239            assert!(object_class.must.is_empty());
2240        }
2241
2242        #[cfg(feature = "chumsky")]
2243        #[test]
2244        fn test_object_class_must_wrong_type() {
2245            let schema_str = "( 1.2.3 NAME 'testOC' MUST 'invalid value with spaces' MAY attr1 )";
2246            let result = object_class_parser().parse(schema_str).into_result();
2247            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2248            let err = result.unwrap_err();
2249            let err_string = format!(
2250                "{}",
2251                ChumskyError {
2252                    description: "object class".to_string(),
2253                    source: schema_str.to_string(),
2254                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2255                }
2256            );
2257            assert!(
2258                err_string.contains("Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $"),
2259                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $'",
2260            );
2261        }
2262
2263        #[cfg(feature = "chumsky")]
2264        #[test]
2265        fn test_object_class_must_missing_param() {
2266            let schema_str = "( 1.2.3 NAME 'testOC' MUST MAY attr1 )";
2267            let result = object_class_parser().parse(schema_str).into_result();
2268            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2269            let err = result.unwrap_err();
2270            let err_string = format!(
2271                "{}",
2272                ChumskyError {
2273                    description: "object class".to_string(),
2274                    source: schema_str.to_string(),
2275                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2276                }
2277            );
2278            assert!(
2279                err_string.contains("Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')'"),
2280                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')''",
2281            );
2282        }
2283
2284        #[cfg(feature = "chumsky")]
2285        #[test]
2286        fn test_object_class_must_correct_single() {
2287            let schema_str = "( 1.2.3 NAME 'testOC' MUST cn MAY attr1 )";
2288            let object_class = object_class_parser()
2289                .parse(schema_str)
2290                .into_result()
2291                .expect("Parsing failed");
2292            assert_eq!(
2293                object_class.must,
2294                vec![KeyStringOrOID::KeyString(KeyString("cn".to_string()))]
2295            );
2296        }
2297
2298        #[cfg(feature = "chumsky")]
2299        #[test]
2300        fn test_object_class_must_correct_list() {
2301            let schema_str = "( 1.2.3 NAME 'testOC' MUST ( cn $ sn ) MAY attr1 )";
2302            let object_class = object_class_parser()
2303                .parse(schema_str)
2304                .into_result()
2305                .expect("Parsing failed");
2306            assert_eq!(
2307                object_class.must,
2308                vec![
2309                    KeyStringOrOID::KeyString(KeyString("cn".to_string())),
2310                    KeyStringOrOID::KeyString(KeyString("sn".to_string()))
2311                ]
2312            );
2313        }
2314
2315        // Test cases for 'MAY'
2316        #[cfg(feature = "chumsky")]
2317        #[test]
2318        fn test_object_class_may_missing() {
2319            let schema_str = "( 1.2.3 NAME 'testOC' MUST attr1 )";
2320            let object_class = object_class_parser()
2321                .parse(schema_str)
2322                .into_result()
2323                .expect("Parsing failed");
2324            assert!(object_class.may.is_empty());
2325        }
2326
2327        #[cfg(feature = "chumsky")]
2328        #[test]
2329        fn test_object_class_may_wrong_type() {
2330            let schema_str = "( 1.2.3 NAME 'testOC' MAY 'invalid value with spaces' MUST attr1 )";
2331            let result = object_class_parser().parse(schema_str).into_result();
2332            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2333            let err = result.unwrap_err();
2334            let err_string = format!(
2335                "{}",
2336                ChumskyError {
2337                    description: "object class".to_string(),
2338                    source: schema_str.to_string(),
2339                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2340                }
2341            );
2342            assert!(
2343                err_string.contains("Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $"),
2344                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $'",
2345            );
2346        }
2347
2348        #[cfg(feature = "chumsky")]
2349        #[test]
2350        fn test_object_class_may_missing_param() {
2351            let schema_str = "( 1.2.3 NAME 'testOC' MAY MUST attr1 )";
2352            let result = object_class_parser().parse(schema_str).into_result();
2353            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2354            let err = result.unwrap_err();
2355            let err_string = format!(
2356                "{}",
2357                ChumskyError {
2358                    description: "object class".to_string(),
2359                    source: schema_str.to_string(),
2360                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2361                }
2362            );
2363            assert!(
2364                err_string.contains("Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')'"),
2365                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')''",
2366            );
2367        }
2368
2369        #[cfg(feature = "chumsky")]
2370        #[test]
2371        fn test_object_class_may_correct_single() {
2372            let schema_str = "( 1.2.3 NAME 'testOC' MAY description MUST attr1 )";
2373            let object_class = object_class_parser()
2374                .parse(schema_str)
2375                .into_result()
2376                .expect("Parsing failed");
2377            assert_eq!(
2378                object_class.may,
2379                vec![KeyStringOrOID::KeyString(KeyString(
2380                    "description".to_string()
2381                ))]
2382            );
2383        }
2384
2385        #[cfg(feature = "chumsky")]
2386        #[test]
2387        fn test_object_class_may_correct_list() {
2388            let schema_str = "( 1.2.3 NAME 'testOC' MAY ( description $ seeAlso ) MUST attr1 )";
2389            let object_class = object_class_parser()
2390                .parse(schema_str)
2391                .into_result()
2392                .expect("Parsing failed");
2393            assert_eq!(
2394                object_class.may,
2395                vec![
2396                    KeyStringOrOID::KeyString(KeyString("description".to_string())),
2397                    KeyStringOrOID::KeyString(KeyString("seeAlso".to_string()))
2398                ]
2399            );
2400        }
2401
2402        // Test cases for 'OBSOLETE'
2403        #[cfg(feature = "chumsky")]
2404        #[test]
2405        fn test_object_class_obsolete_missing() {
2406            let schema_str = "( 1.2.3 NAME 'testOC' MUST attr1 )";
2407            let object_class = object_class_parser()
2408                .parse(schema_str)
2409                .into_result()
2410                .expect("Parsing failed");
2411            assert!(!object_class.obsolete);
2412        }
2413
2414        #[cfg(feature = "chumsky")]
2415        #[test]
2416        fn test_object_class_obsolete_present() {
2417            let schema_str = "( 1.2.3 NAME 'testOC' OBSOLETE MUST attr1 )";
2418            let object_class = object_class_parser()
2419                .parse(schema_str)
2420                .into_result()
2421                .expect("Parsing failed");
2422            assert!(object_class.obsolete);
2423        }
2424
2425        // Test cases for 'DESC'
2426        #[cfg(feature = "chumsky")]
2427        #[test]
2428        fn test_attribute_type_desc_missing() {
2429            let schema_str = "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2430            let attr_type = attribute_type_parser()
2431                .parse(schema_str)
2432                .into_result()
2433                .expect("Parsing failed");
2434            assert!(attr_type.desc.is_none());
2435        }
2436
2437        #[cfg(feature = "chumsky")]
2438        #[test]
2439        fn test_attribute_type_desc_wrong_type() {
2440            let schema_str =
2441                "( 1.2.3 NAME 'test' DESC unquoted String SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2442            let result = attribute_type_parser().parse(schema_str).into_result();
2443            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2444            let err = result.unwrap_err();
2445            let err_string = format!(
2446                "{}",
2447                ChumskyError {
2448                    description: "attribute type".to_string(),
2449                    source: schema_str.to_string(),
2450                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2451                }
2452            );
2453            assert!(
2454                err_string.contains("Unexpected token while parsing [], expected single-quoted string"),
2455                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
2456            );
2457        }
2458
2459        #[cfg(feature = "chumsky")]
2460        #[test]
2461        fn test_attribute_type_desc_missing_param() {
2462            let schema_str = "( 1.2.3 NAME 'test' DESC SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2463            let result = attribute_type_parser().parse(schema_str).into_result();
2464            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2465            let err = result.unwrap_err();
2466            let err_string = format!(
2467                "{}",
2468                ChumskyError {
2469                    description: "attribute type".to_string(),
2470                    source: schema_str.to_string(),
2471                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2472                }
2473            );
2474            assert!(
2475                err_string.contains("Unexpected token while parsing [], expected single-quoted string"),
2476                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
2477            );
2478        }
2479
2480        #[cfg(feature = "chumsky")]
2481        #[test]
2482        fn test_attribute_type_desc_correct() {
2483            let schema_str = "( 1.2.3 NAME 'test' DESC 'Some description' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2484            let attr_type = attribute_type_parser()
2485                .parse(schema_str)
2486                .into_result()
2487                .expect("Parsing failed");
2488            assert_eq!(attr_type.desc, Some("Some description".to_string()));
2489        }
2490
2491        // Test cases for 'SYNTAX'
2492        #[cfg(feature = "chumsky")]
2493        #[test]
2494        fn test_attribute_type_syntax_missing() {
2495            let schema_str = "( 1.2.3 NAME 'test' DESC 'Test Attribute' )";
2496            let attr_type = attribute_type_parser()
2497                .parse(schema_str)
2498                .into_result()
2499                .expect("Parsing failed");
2500            assert!(attr_type.syntax.is_none());
2501        }
2502
2503        #[cfg(feature = "chumsky")]
2504        #[test]
2505        fn test_attribute_type_syntax_wrong_type() {
2506            let schema_str = "( 1.2.3 NAME 'test' SYNTAX 'not an OID' DESC 'Test Attribute' )";
2507            let result = attribute_type_parser().parse(schema_str).into_result();
2508            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2509            let err = result.unwrap_err();
2510            let err_string = format!(
2511                "{}",
2512                ChumskyError {
2513                    description: "attribute type".to_string(),
2514                    source: schema_str.to_string(),
2515                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2516                }
2517            );
2518            assert!(
2519                err_string.contains("Unexpected end of input while parsing [], expected OID with optional length"),
2520                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected OID with optional length'",
2521            );
2522        }
2523
2524        #[cfg(feature = "chumsky")]
2525        #[test]
2526        fn test_attribute_type_syntax_missing_param() {
2527            let schema_str = "( 1.2.3 NAME 'test' SYNTAX DESC 'Test Attribute' )";
2528            let result = attribute_type_parser().parse(schema_str).into_result();
2529            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2530            let err = result.unwrap_err();
2531            let err_string = format!(
2532                "{}",
2533                ChumskyError {
2534                    description: "attribute type".to_string(),
2535                    source: schema_str.to_string(),
2536                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2537                }
2538            );
2539            assert!(
2540                err_string.contains("Unexpected end of input while parsing [], expected OID with optional length"),
2541                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected OID with optional length'",
2542            );
2543        }
2544
2545        #[cfg(feature = "chumsky")]
2546        #[test]
2547        fn test_attribute_type_syntax_correct_with_length() {
2548            let schema_str = "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} DESC 'Test Attribute' )";
2549            let attr_type = attribute_type_parser()
2550                .parse(schema_str)
2551                .into_result()
2552                .expect("Parsing failed");
2553            assert_eq!(
2554                attr_type.syntax,
2555                Some(OIDWithLength {
2556                    #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
2557                    oid: "1.3.6.1.4.1.1466.115.121.1.15"
2558                        .to_string()
2559                        .try_into()
2560                        .unwrap(),
2561                    length: Some(255)
2562                })
2563            );
2564        }
2565
2566        #[cfg(feature = "chumsky")]
2567        #[test]
2568        fn test_attribute_type_syntax_correct_without_length() {
2569            let schema_str =
2570                "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Test Attribute' )";
2571            let attr_type = attribute_type_parser()
2572                .parse(schema_str)
2573                .into_result()
2574                .expect("Parsing failed");
2575            assert_eq!(
2576                attr_type.syntax,
2577                Some(OIDWithLength {
2578                    #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
2579                    oid: "1.3.6.1.4.1.1466.115.121.1.15"
2580                        .to_string()
2581                        .try_into()
2582                        .unwrap(),
2583                    length: None
2584                })
2585            );
2586        }
2587
2588        // Test cases for 'SINGLE-VALUE'
2589        #[cfg(feature = "chumsky")]
2590        #[test]
2591        fn test_attribute_type_single_value_missing() {
2592            let schema_str =
2593                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2594            let attr_type = attribute_type_parser()
2595                .parse(schema_str)
2596                .into_result()
2597                .expect("Parsing failed");
2598            assert!(!attr_type.single_value);
2599        }
2600
2601        #[cfg(feature = "chumsky")]
2602        #[test]
2603        fn test_attribute_type_single_value_present() {
2604            let schema_str = "( 1.2.3 NAME 'test' SINGLE-VALUE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2605            let attr_type = attribute_type_parser()
2606                .parse(schema_str)
2607                .into_result()
2608                .expect("Parsing failed");
2609            assert!(attr_type.single_value);
2610        }
2611
2612        // Test cases for 'EQUALITY'
2613        #[cfg(feature = "chumsky")]
2614        #[test]
2615        fn test_attribute_type_equality_missing() {
2616            let schema_str =
2617                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2618            let attr_type = attribute_type_parser()
2619                .parse(schema_str)
2620                .into_result()
2621                .expect("Parsing failed");
2622            assert!(attr_type.equality.is_none());
2623        }
2624
2625        #[cfg(feature = "chumsky")]
2626        #[test]
2627        fn test_attribute_type_equality_wrong_type() {
2628            let schema_str = "( 1.2.3 NAME 'test' EQUALITY 'invalid equality with spaces' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2629            let result = attribute_type_parser().parse(schema_str).into_result();
2630            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2631            let err = result.unwrap_err();
2632            let err_string = format!(
2633                "{}",
2634                ChumskyError {
2635                    description: "attribute type".to_string(),
2636                    source: schema_str.to_string(),
2637                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2638                }
2639            );
2640            assert!(
2641                err_string.contains("Unexpected token while parsing [], expected keystring"),
2642                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
2643            );
2644        }
2645
2646        #[cfg(feature = "chumsky")]
2647        #[test]
2648        fn test_attribute_type_equality_missing_param() {
2649            let schema_str = "( 1.2.3 NAME 'test' EQUALITY DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2650            let result = attribute_type_parser().parse(schema_str).into_result();
2651            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2652            let err = result.unwrap_err();
2653            let err_string = format!(
2654                "{}",
2655                ChumskyError {
2656                    description: "attribute type".to_string(),
2657                    source: schema_str.to_string(),
2658                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2659                }
2660            );
2661            assert!(
2662                err_string.contains("Unexpected end of input while parsing [], expected keystring"),
2663                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
2664            );
2665        }
2666
2667        #[cfg(feature = "chumsky")]
2668        #[test]
2669        fn test_attribute_type_equality_correct() {
2670            let schema_str = "( 1.2.3 NAME 'test' EQUALITY caseIgnoreMatch DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2671            let attr_type = attribute_type_parser()
2672                .parse(schema_str)
2673                .into_result()
2674                .expect("Parsing failed");
2675            assert_eq!(
2676                attr_type.equality,
2677                Some(KeyString("caseIgnoreMatch".to_string()))
2678            );
2679        }
2680
2681        // Test cases for 'SUBSTR'
2682        #[cfg(feature = "chumsky")]
2683        #[test]
2684        fn test_attribute_type_substr_missing() {
2685            let schema_str =
2686                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2687            let attr_type = attribute_type_parser()
2688                .parse(schema_str)
2689                .into_result()
2690                .expect("Parsing failed");
2691            assert!(attr_type.substr.is_none());
2692        }
2693
2694        #[cfg(feature = "chumsky")]
2695        #[test]
2696        fn test_attribute_type_substr_wrong_type() {
2697            let schema_str = "( 1.2.3 NAME 'test' SUBSTR 'invalid substr with spaces' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2698            let result = attribute_type_parser().parse(schema_str).into_result();
2699            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2700            let err = result.unwrap_err();
2701            let err_string = format!(
2702                "{}",
2703                ChumskyError {
2704                    description: "attribute type".to_string(),
2705                    source: schema_str.to_string(),
2706                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2707                }
2708            );
2709            assert!(
2710                err_string.contains("Unexpected token while parsing [], expected keystring"),
2711                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
2712            );
2713        }
2714
2715        #[cfg(feature = "chumsky")]
2716        #[test]
2717        fn test_attribute_type_substr_missing_param() {
2718            let schema_str = "( 1.2.3 NAME 'test' SUBSTR DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2719            let result = attribute_type_parser().parse(schema_str).into_result();
2720            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2721            let err = result.unwrap_err();
2722            let err_string = format!(
2723                "{}",
2724                ChumskyError {
2725                    description: "attribute type".to_string(),
2726                    source: schema_str.to_string(),
2727                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2728                }
2729            );
2730            assert!(
2731                err_string.contains("Unexpected end of input while parsing [], expected keystring"),
2732                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
2733            );
2734        }
2735
2736        #[cfg(feature = "chumsky")]
2737        #[test]
2738        fn test_attribute_type_substr_correct() {
2739            let schema_str = "( 1.2.3 NAME 'test' SUBSTR caseIgnoreSubstringsMatch DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2740            let attr_type = attribute_type_parser()
2741                .parse(schema_str)
2742                .into_result()
2743                .expect("Parsing failed");
2744            assert_eq!(
2745                attr_type.substr,
2746                Some(KeyString("caseIgnoreSubstringsMatch".to_string()))
2747            );
2748        }
2749
2750        // Test cases for 'ORDERING'
2751        #[cfg(feature = "chumsky")]
2752        #[test]
2753        fn test_attribute_type_ordering_missing() {
2754            let schema_str =
2755                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2756            let attr_type = attribute_type_parser()
2757                .parse(schema_str)
2758                .into_result()
2759                .expect("Parsing failed");
2760            assert!(attr_type.ordering.is_none());
2761        }
2762
2763        #[cfg(feature = "chumsky")]
2764        #[test]
2765        fn test_attribute_type_ordering_wrong_type() {
2766            let schema_str = "( 1.2.3 NAME 'test' ORDERING 'invalid ordering with spaces' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2767            let result = attribute_type_parser().parse(schema_str).into_result();
2768            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2769            let err = result.unwrap_err();
2770            let err_string = format!(
2771                "{}",
2772                ChumskyError {
2773                    description: "attribute type".to_string(),
2774                    source: schema_str.to_string(),
2775                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2776                }
2777            );
2778            assert!(
2779                err_string.contains("Unexpected token while parsing [], expected keystring"),
2780                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
2781            );
2782        }
2783
2784        #[cfg(feature = "chumsky")]
2785        #[test]
2786        fn test_attribute_type_ordering_missing_param() {
2787            let schema_str = "( 1.2.3 NAME 'test' ORDERING DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2788            let result = attribute_type_parser().parse(schema_str).into_result();
2789            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2790            let err = result.unwrap_err();
2791            let err_string = format!(
2792                "{}",
2793                ChumskyError {
2794                    description: "attribute type".to_string(),
2795                    source: schema_str.to_string(),
2796                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2797                }
2798            );
2799            assert!(
2800                err_string.contains("Unexpected end of input while parsing [], expected keystring"),
2801                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
2802            );
2803        }
2804
2805        #[cfg(feature = "chumsky")]
2806        #[test]
2807        fn test_attribute_type_ordering_correct() {
2808            let schema_str = "( 1.2.3 NAME 'test' ORDERING caseIgnoreOrderingMatch DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2809            let attr_type = attribute_type_parser()
2810                .parse(schema_str)
2811                .into_result()
2812                .expect("Parsing failed");
2813            assert_eq!(
2814                attr_type.ordering,
2815                Some(KeyString("caseIgnoreOrderingMatch".to_string()))
2816            );
2817        }
2818
2819        // Test cases for 'NO-USER-MODIFICATION'
2820        #[cfg(feature = "chumsky")]
2821        #[test]
2822        fn test_attribute_type_no_user_modification_missing() {
2823            let schema_str =
2824                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2825            let attr_type = attribute_type_parser()
2826                .parse(schema_str)
2827                .into_result()
2828                .expect("Parsing failed");
2829            assert!(!attr_type.no_user_modification);
2830        }
2831
2832        #[cfg(feature = "chumsky")]
2833        #[test]
2834        fn test_attribute_type_no_user_modification_present() {
2835            let schema_str = "( 1.2.3 NAME 'test' NO-USER-MODIFICATION DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2836            let attr_type = attribute_type_parser()
2837                .parse(schema_str)
2838                .into_result()
2839                .expect("Parsing failed");
2840            assert!(attr_type.no_user_modification);
2841        }
2842
2843        // Test cases for 'USAGE'
2844        #[cfg(feature = "chumsky")]
2845        #[test]
2846        fn test_attribute_type_usage_missing() {
2847            let schema_str =
2848                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2849            let attr_type = attribute_type_parser()
2850                .parse(schema_str)
2851                .into_result()
2852                .expect("Parsing failed");
2853            assert!(attr_type.usage.is_none());
2854        }
2855
2856        #[cfg(feature = "chumsky")]
2857        #[test]
2858        fn test_attribute_type_usage_wrong_type() {
2859            let schema_str = "( 1.2.3 NAME 'test' USAGE 'invalid usage with spaces' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2860            let result = attribute_type_parser().parse(schema_str).into_result();
2861            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2862            let err = result.unwrap_err();
2863            let err_string = format!(
2864                "{}",
2865                ChumskyError {
2866                    description: "attribute type".to_string(),
2867                    source: schema_str.to_string(),
2868                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2869                }
2870            );
2871            assert!(
2872                err_string.contains("Unexpected token while parsing [], expected keystring"),
2873                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
2874            );
2875        }
2876
2877        #[cfg(feature = "chumsky")]
2878        #[test]
2879        fn test_attribute_type_usage_missing_param() {
2880            let schema_str = "( 1.2.3 NAME 'test' USAGE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2881            let result = attribute_type_parser().parse(schema_str).into_result();
2882            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2883            let err = result.unwrap_err();
2884            let err_string = format!(
2885                "{}",
2886                ChumskyError {
2887                    description: "attribute type".to_string(),
2888                    source: schema_str.to_string(),
2889                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2890                }
2891            );
2892            assert!(
2893                err_string.contains("Unexpected end of input while parsing [], expected keystring"),
2894                "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
2895            );
2896        }
2897
2898        #[cfg(feature = "chumsky")]
2899        #[test]
2900        fn test_attribute_type_usage_correct() {
2901            let schema_str = "( 1.2.3 NAME 'test' USAGE userApplications DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2902            let attr_type = attribute_type_parser()
2903                .parse(schema_str)
2904                .into_result()
2905                .expect("Parsing failed");
2906            assert_eq!(
2907                attr_type.usage,
2908                Some(KeyString("userApplications".to_string()))
2909            );
2910        }
2911
2912        // Test cases for 'COLLECTIVE'
2913        #[cfg(feature = "chumsky")]
2914        #[test]
2915        fn test_attribute_type_collective_missing() {
2916            let schema_str =
2917                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2918            let attr_type = attribute_type_parser()
2919                .parse(schema_str)
2920                .into_result()
2921                .expect("Parsing failed");
2922            assert!(!attr_type.collective);
2923        }
2924
2925        #[cfg(feature = "chumsky")]
2926        #[test]
2927        fn test_attribute_type_collective_present() {
2928            let schema_str = "( 1.2.3 NAME 'test' COLLECTIVE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2929            let attr_type = attribute_type_parser()
2930                .parse(schema_str)
2931                .into_result()
2932                .expect("Parsing failed");
2933            assert!(attr_type.collective);
2934        }
2935
2936        // Test cases for 'OBSOLETE'
2937        #[cfg(feature = "chumsky")]
2938        #[test]
2939        fn test_attribute_type_obsolete_missing() {
2940            let schema_str =
2941                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2942            let attr_type = attribute_type_parser()
2943                .parse(schema_str)
2944                .into_result()
2945                .expect("Parsing failed");
2946            assert!(!attr_type.obsolete);
2947        }
2948
2949        #[cfg(feature = "chumsky")]
2950        #[test]
2951        fn test_attribute_type_obsolete_present() {
2952            let schema_str = "( 1.2.3 NAME 'test' OBSOLETE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2953            let attr_type = attribute_type_parser()
2954                .parse(schema_str)
2955                .into_result()
2956                .expect("Parsing failed");
2957            assert!(attr_type.obsolete);
2958        }
2959
2960        // Test cases for 'X-ORDERED'
2961        #[cfg(feature = "chumsky")]
2962        #[test]
2963        fn test_attribute_type_x_ordered_missing() {
2964            let schema_str =
2965                "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2966            let attr_type = attribute_type_parser()
2967                .parse(schema_str)
2968                .into_result()
2969                .expect("Parsing failed");
2970            assert!(attr_type.x_ordered.is_none());
2971        }
2972
2973        #[cfg(feature = "chumsky")]
2974        #[test]
2975        fn test_attribute_type_x_ordered_wrong_type() {
2976            let schema_str = "( 1.2.3 NAME 'test' X-ORDERED unquotedString DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2977            let result = attribute_type_parser().parse(schema_str).into_result();
2978            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2979            let err = result.unwrap_err();
2980            let err_string = format!(
2981                "{}",
2982                ChumskyError {
2983                    description: "attribute type".to_string(),
2984                    source: schema_str.to_string(),
2985                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
2986                }
2987            );
2988            assert!(
2989                err_string.contains("Unexpected token while parsing [], expected quoted keystring"),
2990                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected quoted keystring'",
2991            );
2992        }
2993
2994        #[cfg(feature = "chumsky")]
2995        #[test]
2996        fn test_attribute_type_x_ordered_missing_param() {
2997            let schema_str = "( 1.2.3 NAME 'test' X-ORDERED DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2998            let result = attribute_type_parser().parse(schema_str).into_result();
2999            #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
3000            let err = result.unwrap_err();
3001            let err_string = format!(
3002                "{}",
3003                ChumskyError {
3004                    description: "attribute type".to_string(),
3005                    source: schema_str.to_string(),
3006                    errors: err.into_iter().map(|e| e.into_owned()).collect(),
3007                }
3008            );
3009            assert!(
3010                err_string.contains("Unexpected token while parsing [], expected quoted keystring"),
3011                "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected quoted keystring'",
3012            );
3013        }
3014
3015        #[cfg(feature = "chumsky")]
3016        #[test]
3017        fn test_attribute_type_x_ordered_correct() {
3018            let schema_str = "( 1.2.3 NAME 'test' X-ORDERED 'values' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
3019            let attr_type = attribute_type_parser()
3020                .parse(schema_str)
3021                .into_result()
3022                .expect("Parsing failed");
3023            assert_eq!(attr_type.x_ordered, Some(KeyString("values".to_string())));
3024        }
3025    }
3026}