1use 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#[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#[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#[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#[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#[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#[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#[derive(Clone, Debug, EnumAsInner, Educe)]
203#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
204#[educe(PartialEq, Eq, Hash)]
205pub enum LDAPSchemaTagValue {
206 Standalone,
208 OID(#[educe(Hash(method = "crate::basic::hash_oid"))] ObjectIdentifier),
210 OIDWithLength(OIDWithLength),
212 String(String),
214 KeyString(KeyString),
216 QuotedKeyString(KeyString),
218 KeyStringOrOID(KeyStringOrOID),
220 Boolean(bool),
222 QuotedKeyStringList(Vec<KeyString>),
224 KeyStringOrOIDList(Vec<KeyStringOrOID>),
226}
227
228#[derive(PartialEq, Eq, Debug, Hash)]
230#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
231pub struct LDAPSchemaTag {
232 tag_name: String,
234 tag_value: LDAPSchemaTagValue,
236}
237
238#[cfg(feature = "chumsky")]
241#[derive(PartialEq, Eq, Debug, Hash)]
242#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
243pub enum LDAPSchemaTagType {
244 Standalone,
246 OID,
248 OIDWithLength,
250 String,
252 KeyString,
254 QuotedKeyString,
256 KeyStringOrOID,
258 Boolean,
260 QuotedKeyStringList,
262 KeyStringOrOIDList,
264}
265
266#[cfg(feature = "chumsky")]
268#[derive(PartialEq, Eq, Debug, Hash)]
269#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
270pub struct LDAPSchemaTagDescriptor {
271 pub tag_name: String,
273 pub tag_type: LDAPSchemaTagType,
275}
276
277#[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 && ALL_SCHEMA_TAG_NAMES.contains(&ks.0)
337 {
338 return Err(Rich::custom(
339 span,
340 format!("'{}' is a reserved tag name and cannot be used as a KeyStringOrOID value here", ks.0),
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#[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#[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#[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#[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#[derive(Clone, Educe)]
461#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
462#[educe(PartialEq, Eq, Hash)]
463pub struct LDAPSyntax {
464 #[educe(Hash(method = "crate::basic::hash_oid"))]
466 pub oid: ObjectIdentifier,
467 pub desc: String,
469 pub x_binary_transfer_required: bool,
471 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#[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#[derive(Clone, Educe)]
527#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
528#[educe(PartialEq, Eq, Hash)]
529pub struct MatchingRule {
530 #[educe(Hash(method = "crate::basic::hash_oid"))]
532 pub oid: ObjectIdentifier,
533 pub name: Vec<KeyString>,
535 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#[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#[derive(Clone, Educe)]
581#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
582#[educe(PartialEq, Eq, Hash)]
583pub struct MatchingRuleUse {
584 #[educe(Hash(method = "crate::basic::hash_oid"))]
586 pub oid: ObjectIdentifier,
587 pub name: Vec<KeyString>,
589 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#[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#[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 #[educe(Hash(method = "crate::basic::hash_oid"))]
641 pub oid: ObjectIdentifier,
642 pub name: Vec<KeyString>,
644 pub sup: Option<KeyString>,
646 pub desc: Option<String>,
648 pub syntax: Option<OIDWithLength>,
650 pub single_value: bool,
652 pub equality: Option<KeyString>,
654 pub substr: Option<KeyString>,
656 pub ordering: Option<KeyString>,
658 pub no_user_modification: bool,
661 pub usage: Option<KeyString>,
668 pub collective: bool,
672 pub obsolete: bool,
674 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#[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#[derive(PartialEq, Eq, Clone, Debug, EnumAsInner, Hash)]
806#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
807pub enum ObjectClassType {
808 Abstract,
811 Structural,
815 Auxiliary,
818}
819
820#[derive(Clone, Educe)]
822#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
823#[educe(PartialEq, Eq, Hash)]
824pub struct ObjectClass {
825 #[educe(Hash(method = "crate::basic::hash_oid"))]
827 pub oid: ObjectIdentifier,
828 pub name: Vec<KeyString>,
830 pub sup: Vec<KeyStringOrOID>,
832 pub desc: Option<String>,
834 pub object_class_type: ObjectClassType,
836 pub must: Vec<KeyStringOrOID>,
838 pub may: Vec<KeyStringOrOID>,
841 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#[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#[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 pub ldap_syntaxes: Vec<LDAPSyntax>,
936 pub matching_rules: Vec<MatchingRule>,
938 pub matching_rule_use: Vec<MatchingRuleUse>,
940 pub attribute_types: Vec<AttributeType>,
942 pub object_classes: Vec<ObjectClass>,
944 }
949
950impl LDAPSchema {
951 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 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 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 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 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 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 #[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 #[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 #[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 #[cfg(feature = "chumsky")]
1139 use pretty_assertions::assert_eq;
1140
1141 #[cfg(feature = "chumsky")]
1142 #[test]
1143 fn test_parse_ldap_syntax() {
1144 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1145 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();
1146 }
1147
1148 #[cfg(feature = "chumsky")]
1149 #[test]
1150 fn test_parse_ldap_syntax_value1() {
1151 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(),
1152 Ok(LDAPSyntax {
1153 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1154 oid: "1.3.6.1.4.1.1466.115.121.1.8".to_string().try_into().unwrap(),
1155 desc: "Certificate".to_string(),
1156 x_binary_transfer_required: true,
1157 x_not_human_readable: true,
1158 }
1159 ));
1160 }
1161
1162 #[cfg(feature = "chumsky")]
1163 #[test]
1164 fn test_parse_ldap_syntax_value2() {
1165 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(),
1166 Ok(LDAPSyntax {
1167 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1168 oid: "1.3.6.1.4.1.1466.115.121.1.8".to_string().try_into().unwrap(),
1169 desc: "Certificate".to_string(),
1170 x_binary_transfer_required: true,
1171 x_not_human_readable: true,
1172 }
1173 ));
1174 }
1175
1176 #[cfg(feature = "chumsky")]
1177 #[test]
1178 fn test_parse_ldap_syntax_value3() {
1179 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(),
1180 Ok(LDAPSyntax {
1181 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1182 oid: "1.3.6.1.4.1.1466.115.121.1.8".to_string().try_into().unwrap(),
1183 desc: "Certificate".to_string(),
1184 x_binary_transfer_required: true,
1185 x_not_human_readable: false,
1186 }
1187 ));
1188 }
1189
1190 #[cfg(feature = "chumsky")]
1191 #[test]
1192 fn test_parse_ldap_syntax_value4() {
1193 assert_eq!(
1194 ldap_syntax_parser().parse(
1195 "( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-NOT-HUMAN-READABLE 'TRUE' )"
1196 ).into_result(),
1197 Ok(LDAPSyntax {
1198 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1199 oid: "1.3.6.1.4.1.1466.115.121.1.8"
1200 .to_string()
1201 .try_into()
1202 .unwrap(),
1203 desc: "Certificate".to_string(),
1204 x_binary_transfer_required: false,
1205 x_not_human_readable: true,
1206 })
1207 );
1208 }
1209
1210 #[cfg(feature = "chumsky")]
1211 #[test]
1212 fn test_parse_ldap_syntax_value5() {
1213 assert_eq!(
1214 ldap_syntax_parser()
1215 .parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' )")
1216 .into_result(),
1217 Ok(LDAPSyntax {
1218 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1219 oid: "1.3.6.1.4.1.1466.115.121.1.8"
1220 .to_string()
1221 .try_into()
1222 .unwrap(),
1223 desc: "Certificate".to_string(),
1224 x_binary_transfer_required: false,
1225 x_not_human_readable: false,
1226 })
1227 );
1228 }
1229
1230 #[cfg(feature = "chumsky")]
1231 #[test]
1232 fn test_parse_ldap_syntax_desc_required() {
1233 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1234 ldap_syntax_parser()
1235 .parse("( 1.3.6.1.4.1.1466.115.121.1.8 )")
1236 .into_result()
1237 .unwrap_err();
1238 }
1239
1240 #[cfg(feature = "chumsky")]
1241 #[test]
1242 fn test_parse_matching_rule() {
1243 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1244 matching_rule_parser()
1245 .parse("( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )")
1246 .into_result()
1247 .unwrap();
1248 }
1249
1250 #[cfg(feature = "chumsky")]
1251 #[test]
1252 fn test_parse_matching_rule_value() {
1253 assert_eq!(
1254 matching_rule_parser()
1255 .parse("( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )")
1256 .into_result(),
1257 Ok(MatchingRule {
1258 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1259 oid: "1.3.6.1.1.16.3".to_string().try_into().unwrap(),
1260 name: vec![KeyString("UUIDOrderingMatch".to_string())],
1261 syntax: OIDWithLength {
1262 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1263 oid: "1.3.6.1.1.16.1".to_string().try_into().unwrap(),
1264 length: None
1265 },
1266 })
1267 );
1268 }
1269
1270 #[cfg(feature = "chumsky")]
1271 #[test]
1272 fn test_parse_matching_rule_uses() {
1273 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1274 matching_rule_use_parser().parse("( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES ( postalAddress $ registeredAddress $ homePostalAddress ) )").into_result().unwrap();
1275 }
1276
1277 #[cfg(feature = "chumsky")]
1278 #[test]
1279 fn test_parse_matching_rule_uses_value() {
1280 assert_eq!(matching_rule_use_parser().parse("( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES ( postalAddress $ registeredAddress $ homePostalAddress ) )").into_result(),
1281 Ok(MatchingRuleUse {
1282 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1283 oid: "2.5.13.11".to_string().try_into().unwrap(),
1284 name: vec![KeyString("caseIgnoreListMatch".to_string())],
1285 applies: vec![KeyStringOrOID::KeyString(KeyString("postalAddress".to_string())),
1286 KeyStringOrOID::KeyString(KeyString("registeredAddress".to_string())),
1287 KeyStringOrOID::KeyString(KeyString("homePostalAddress".to_string()))
1288 ],
1289 })
1290 );
1291 }
1292
1293 #[cfg(feature = "chumsky")]
1294 #[test]
1295 fn test_parse_matching_rule_uses_single_applies_value() {
1296 assert_eq!(
1297 matching_rule_use_parser()
1298 .parse("( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES postalAddress )")
1299 .into_result(),
1300 Ok(MatchingRuleUse {
1301 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1302 oid: "2.5.13.11".to_string().try_into().unwrap(),
1303 name: vec![KeyString("caseIgnoreListMatch".to_string())],
1304 applies: vec![KeyStringOrOID::KeyString(KeyString(
1305 "postalAddress".to_string()
1306 ))],
1307 })
1308 );
1309 }
1310
1311 mod attribute_type_parser_tests {
1312 use super::*;
1313 #[cfg(feature = "chumsky")]
1314 use pretty_assertions::assert_eq;
1315
1316 #[cfg(feature = "chumsky")]
1317 #[test]
1318 fn test_attribute_type_sup_missing() {
1319 let schema_str =
1320 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1321 let attr_type = attribute_type_parser()
1322 .parse(schema_str)
1323 .into_result()
1324 .expect("Parsing failed");
1325 assert!(attr_type.sup.is_none());
1326 }
1327
1328 #[cfg(feature = "chumsky")]
1329 #[test]
1330 fn test_attribute_type_sup_wrong_type() {
1331 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 )";
1332 let result = attribute_type_parser().parse(schema_str).into_result();
1333 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1334 let err = result.unwrap_err();
1335 let err_string = format!(
1336 "{}",
1337 ChumskyError {
1338 description: "attribute type".to_string(),
1339 source: schema_str.to_string(),
1340 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1341 }
1342 );
1343 assert!(
1344 err_string.contains("Unexpected token while parsing [], expected keystring"),
1345 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
1346 );
1347 }
1348
1349 #[cfg(feature = "chumsky")]
1350 #[test]
1351 fn test_attribute_type_sup_missing_param() {
1352 let schema_str = "( 1.2.3 NAME 'test' SUP DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1353 let result = attribute_type_parser().parse(schema_str).into_result();
1354 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1355 let err = result.unwrap_err();
1356 let err_string = format!(
1357 "{}",
1358 ChumskyError {
1359 description: "attribute type".to_string(),
1360 source: schema_str.to_string(),
1361 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1362 }
1363 );
1364 assert!(
1365 err_string.contains("Unexpected end of input while parsing [], expected keystring"),
1366 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
1367 );
1368 }
1369
1370 #[cfg(feature = "chumsky")]
1371 #[test]
1372 fn test_attribute_type_sup_correct() {
1373 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 )";
1374 let attr_type = attribute_type_parser()
1375 .parse(schema_str)
1376 .into_result()
1377 .expect("Parsing failed");
1378 assert_eq!(attr_type.sup, Some(KeyString("someKeyString".to_string())));
1379 }
1380
1381 #[cfg(feature = "chumsky")]
1383 #[test]
1384 fn test_attribute_type_desc_missing() {
1385 let schema_str = "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1386 let attr_type = attribute_type_parser()
1387 .parse(schema_str)
1388 .into_result()
1389 .expect("Parsing failed");
1390 assert!(attr_type.desc.is_none());
1391 }
1392
1393 #[cfg(feature = "chumsky")]
1394 #[test]
1395 fn test_attribute_type_desc_wrong_type() {
1396 let schema_str =
1397 "( 1.2.3 NAME 'test' DESC unquoted String SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1398 let result = attribute_type_parser().parse(schema_str).into_result();
1399 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1400 let err = result.unwrap_err();
1401 let err_string = format!(
1402 "{}",
1403 ChumskyError {
1404 description: "attribute type".to_string(),
1405 source: schema_str.to_string(),
1406 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1407 }
1408 );
1409 assert!(
1410 err_string
1411 .contains("Unexpected token while parsing [], expected single-quoted string"),
1412 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
1413 );
1414 }
1415
1416 #[cfg(feature = "chumsky")]
1417 #[test]
1418 fn test_attribute_type_desc_missing_param() {
1419 let schema_str = "( 1.2.3 NAME 'test' DESC SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1420 let result = attribute_type_parser().parse(schema_str).into_result();
1421 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1422 let err = result.unwrap_err();
1423 let err_string = format!(
1424 "{}",
1425 ChumskyError {
1426 description: "attribute type".to_string(),
1427 source: schema_str.to_string(),
1428 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1429 }
1430 );
1431 assert!(
1432 err_string
1433 .contains("Unexpected token while parsing [], expected single-quoted string"),
1434 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
1435 );
1436 }
1437
1438 #[cfg(feature = "chumsky")]
1439 #[test]
1440 fn test_attribute_type_desc_correct() {
1441 let schema_str = "( 1.2.3 NAME 'test' DESC 'Some description' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1442 let attr_type = attribute_type_parser()
1443 .parse(schema_str)
1444 .into_result()
1445 .expect("Parsing failed");
1446 assert_eq!(attr_type.desc, Some("Some description".to_string()));
1447 }
1448
1449 #[cfg(feature = "chumsky")]
1451 #[test]
1452 fn test_attribute_type_syntax_missing() {
1453 let schema_str = "( 1.2.3 NAME 'test' DESC 'Test Attribute' )";
1454 let attr_type = attribute_type_parser()
1455 .parse(schema_str)
1456 .into_result()
1457 .expect("Parsing failed");
1458 assert!(attr_type.syntax.is_none());
1459 }
1460
1461 #[cfg(feature = "chumsky")]
1462 #[test]
1463 fn test_attribute_type_syntax_wrong_type() {
1464 let schema_str = "( 1.2.3 NAME 'test' SYNTAX 'not an OID' DESC 'Test Attribute' )";
1465 let result = attribute_type_parser().parse(schema_str).into_result();
1466 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1467 let err = result.unwrap_err();
1468 let err_string = format!(
1469 "{}",
1470 ChumskyError {
1471 description: "attribute type".to_string(),
1472 source: schema_str.to_string(),
1473 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1474 }
1475 );
1476 assert!(
1477 err_string.contains(
1478 "Unexpected end of input while parsing [], expected OID with optional length"
1479 ),
1480 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected OID with optional length'",
1481 );
1482 }
1483
1484 #[cfg(feature = "chumsky")]
1485 #[test]
1486 fn test_attribute_type_syntax_missing_param() {
1487 let schema_str = "( 1.2.3 NAME 'test' SYNTAX DESC 'Test Attribute' )";
1488 let result = attribute_type_parser().parse(schema_str).into_result();
1489 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1490 let err = result.unwrap_err();
1491 let err_string = format!(
1492 "{}",
1493 ChumskyError {
1494 description: "attribute type".to_string(),
1495 source: schema_str.to_string(),
1496 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1497 }
1498 );
1499 assert!(
1500 err_string.contains(
1501 "Unexpected end of input while parsing [], expected OID with optional length"
1502 ),
1503 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected OID with optional length'",
1504 );
1505 }
1506
1507 #[cfg(feature = "chumsky")]
1508 #[test]
1509 fn test_attribute_type_syntax_correct_with_length() {
1510 let schema_str = "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} DESC 'Test Attribute' )";
1511 let attr_type = attribute_type_parser()
1512 .parse(schema_str)
1513 .into_result()
1514 .expect("Parsing failed");
1515 assert_eq!(
1516 attr_type.syntax,
1517 Some(OIDWithLength {
1518 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1519 oid: "1.3.6.1.4.1.1466.115.121.1.15"
1520 .to_string()
1521 .try_into()
1522 .unwrap(),
1523 length: Some(255)
1524 })
1525 );
1526 }
1527
1528 #[cfg(feature = "chumsky")]
1529 #[test]
1530 fn test_attribute_type_syntax_correct_without_length() {
1531 let schema_str =
1532 "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Test Attribute' )";
1533 let attr_type = attribute_type_parser()
1534 .parse(schema_str)
1535 .into_result()
1536 .expect("Parsing failed");
1537 assert_eq!(
1538 attr_type.syntax,
1539 Some(OIDWithLength {
1540 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1541 oid: "1.3.6.1.4.1.1466.115.121.1.15"
1542 .to_string()
1543 .try_into()
1544 .unwrap(),
1545 length: None
1546 })
1547 );
1548 }
1549
1550 #[cfg(feature = "chumsky")]
1552 #[test]
1553 fn test_attribute_type_single_value_missing() {
1554 let schema_str =
1555 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1556 let attr_type = attribute_type_parser()
1557 .parse(schema_str)
1558 .into_result()
1559 .expect("Parsing failed");
1560 assert!(!attr_type.single_value);
1561 }
1562
1563 #[cfg(feature = "chumsky")]
1564 #[test]
1565 fn test_attribute_type_single_value_present() {
1566 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 )";
1567 let attr_type = attribute_type_parser()
1568 .parse(schema_str)
1569 .into_result()
1570 .expect("Parsing failed");
1571 assert!(attr_type.single_value);
1572 }
1573
1574 #[cfg(feature = "chumsky")]
1576 #[test]
1577 fn test_attribute_type_equality_missing() {
1578 let schema_str =
1579 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1580 let attr_type = attribute_type_parser()
1581 .parse(schema_str)
1582 .into_result()
1583 .expect("Parsing failed");
1584 assert!(attr_type.equality.is_none());
1585 }
1586
1587 #[cfg(feature = "chumsky")]
1588 #[test]
1589 fn test_attribute_type_equality_wrong_type() {
1590 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 )";
1591 let result = attribute_type_parser().parse(schema_str).into_result();
1592 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1593 let err = result.unwrap_err();
1594 let err_string = format!(
1595 "{}",
1596 ChumskyError {
1597 description: "attribute type".to_string(),
1598 source: schema_str.to_string(),
1599 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1600 }
1601 );
1602 assert!(
1603 err_string.contains("Unexpected token while parsing [], expected keystring"),
1604 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
1605 );
1606 }
1607
1608 #[cfg(feature = "chumsky")]
1609 #[test]
1610 fn test_attribute_type_equality_missing_param() {
1611 let schema_str = "( 1.2.3 NAME 'test' EQUALITY DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1612 let result = attribute_type_parser().parse(schema_str).into_result();
1613 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1614 let err = result.unwrap_err();
1615 let err_string = format!(
1616 "{}",
1617 ChumskyError {
1618 description: "attribute type".to_string(),
1619 source: schema_str.to_string(),
1620 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1621 }
1622 );
1623 assert!(
1624 err_string.contains("Unexpected end of input while parsing [], expected keystring"),
1625 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
1626 );
1627 }
1628
1629 #[cfg(feature = "chumsky")]
1630 #[test]
1631 fn test_attribute_type_equality_correct() {
1632 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 )";
1633 let attr_type = attribute_type_parser()
1634 .parse(schema_str)
1635 .into_result()
1636 .expect("Parsing failed");
1637 assert_eq!(
1638 attr_type.equality,
1639 Some(KeyString("caseIgnoreMatch".to_string()))
1640 );
1641 }
1642
1643 #[cfg(feature = "chumsky")]
1645 #[test]
1646 fn test_attribute_type_substr_missing() {
1647 let schema_str =
1648 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1649 let attr_type = attribute_type_parser()
1650 .parse(schema_str)
1651 .into_result()
1652 .expect("Parsing failed");
1653 assert!(attr_type.substr.is_none());
1654 }
1655
1656 #[cfg(feature = "chumsky")]
1657 #[test]
1658 fn test_attribute_type_substr_wrong_type() {
1659 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 )";
1660 let result = attribute_type_parser().parse(schema_str).into_result();
1661 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1662 let err = result.unwrap_err();
1663 let err_string = format!(
1664 "{}",
1665 ChumskyError {
1666 description: "attribute type".to_string(),
1667 source: schema_str.to_string(),
1668 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1669 }
1670 );
1671 assert!(
1672 err_string.contains("Unexpected token while parsing [], expected keystring"),
1673 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
1674 );
1675 }
1676
1677 #[cfg(feature = "chumsky")]
1678 #[test]
1679 fn test_attribute_type_substr_missing_param() {
1680 let schema_str = "( 1.2.3 NAME 'test' SUBSTR DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1681 let result = attribute_type_parser().parse(schema_str).into_result();
1682 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1683 let err = result.unwrap_err();
1684 let err_string = format!(
1685 "{}",
1686 ChumskyError {
1687 description: "attribute type".to_string(),
1688 source: schema_str.to_string(),
1689 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1690 }
1691 );
1692 assert!(
1693 err_string.contains("Unexpected end of input while parsing [], expected keystring"),
1694 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
1695 );
1696 }
1697
1698 #[cfg(feature = "chumsky")]
1699 #[test]
1700 fn test_attribute_type_substr_correct() {
1701 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 )";
1702 let attr_type = attribute_type_parser()
1703 .parse(schema_str)
1704 .into_result()
1705 .expect("Parsing failed");
1706 assert_eq!(
1707 attr_type.substr,
1708 Some(KeyString("caseIgnoreSubstringsMatch".to_string()))
1709 );
1710 }
1711
1712 #[cfg(feature = "chumsky")]
1714 #[test]
1715 fn test_attribute_type_ordering_missing() {
1716 let schema_str =
1717 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1718 let attr_type = attribute_type_parser()
1719 .parse(schema_str)
1720 .into_result()
1721 .expect("Parsing failed");
1722 assert!(attr_type.ordering.is_none());
1723 }
1724
1725 #[cfg(feature = "chumsky")]
1726 #[test]
1727 fn test_attribute_type_ordering_wrong_type() {
1728 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 )";
1729 let result = attribute_type_parser().parse(schema_str).into_result();
1730 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1731 let err = result.unwrap_err();
1732 let err_string = format!(
1733 "{}",
1734 ChumskyError {
1735 description: "attribute type".to_string(),
1736 source: schema_str.to_string(),
1737 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1738 }
1739 );
1740 assert!(
1741 err_string.contains("Unexpected token while parsing [], expected keystring"),
1742 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
1743 );
1744 }
1745
1746 #[cfg(feature = "chumsky")]
1747 #[test]
1748 fn test_attribute_type_ordering_missing_param() {
1749 let schema_str = "( 1.2.3 NAME 'test' ORDERING DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1750 let result = attribute_type_parser().parse(schema_str).into_result();
1751 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1752 let err = result.unwrap_err();
1753 let err_string = format!(
1754 "{}",
1755 ChumskyError {
1756 description: "attribute type".to_string(),
1757 source: schema_str.to_string(),
1758 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1759 }
1760 );
1761 assert!(
1762 err_string.contains("Unexpected end of input while parsing [], expected keystring"),
1763 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
1764 );
1765 }
1766
1767 #[cfg(feature = "chumsky")]
1768 #[test]
1769 fn test_attribute_type_ordering_correct() {
1770 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 )";
1771 let attr_type = attribute_type_parser()
1772 .parse(schema_str)
1773 .into_result()
1774 .expect("Parsing failed");
1775 assert_eq!(
1776 attr_type.ordering,
1777 Some(KeyString("caseIgnoreOrderingMatch".to_string()))
1778 );
1779 }
1780
1781 #[cfg(feature = "chumsky")]
1783 #[test]
1784 fn test_attribute_type_no_user_modification_missing() {
1785 let schema_str =
1786 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1787 let attr_type = attribute_type_parser()
1788 .parse(schema_str)
1789 .into_result()
1790 .expect("Parsing failed");
1791 assert!(!attr_type.no_user_modification);
1792 }
1793
1794 #[cfg(feature = "chumsky")]
1795 #[test]
1796 fn test_attribute_type_no_user_modification_present() {
1797 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 )";
1798 let attr_type = attribute_type_parser()
1799 .parse(schema_str)
1800 .into_result()
1801 .expect("Parsing failed");
1802 assert!(attr_type.no_user_modification);
1803 }
1804
1805 #[cfg(feature = "chumsky")]
1807 #[test]
1808 fn test_attribute_type_usage_missing() {
1809 let schema_str =
1810 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1811 let attr_type = attribute_type_parser()
1812 .parse(schema_str)
1813 .into_result()
1814 .expect("Parsing failed");
1815 assert!(attr_type.usage.is_none());
1816 }
1817
1818 #[cfg(feature = "chumsky")]
1819 #[test]
1820 fn test_attribute_type_usage_wrong_type() {
1821 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 )";
1822 let result = attribute_type_parser().parse(schema_str).into_result();
1823 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1824 let err = result.unwrap_err();
1825 let err_string = format!(
1826 "{}",
1827 ChumskyError {
1828 description: "attribute type".to_string(),
1829 source: schema_str.to_string(),
1830 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1831 }
1832 );
1833 assert!(
1834 err_string.contains("Unexpected token while parsing [], expected keystring"),
1835 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
1836 );
1837 }
1838
1839 #[cfg(feature = "chumsky")]
1840 #[test]
1841 fn test_attribute_type_usage_missing_param() {
1842 let schema_str = "( 1.2.3 NAME 'test' USAGE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1843 let result = attribute_type_parser().parse(schema_str).into_result();
1844 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1845 let err = result.unwrap_err();
1846 let err_string = format!(
1847 "{}",
1848 ChumskyError {
1849 description: "attribute type".to_string(),
1850 source: schema_str.to_string(),
1851 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1852 }
1853 );
1854 assert!(
1855 err_string.contains("Unexpected end of input while parsing [], expected keystring"),
1856 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
1857 );
1858 }
1859
1860 #[cfg(feature = "chumsky")]
1861 #[test]
1862 fn test_attribute_type_usage_correct() {
1863 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 )";
1864 let attr_type = attribute_type_parser()
1865 .parse(schema_str)
1866 .into_result()
1867 .expect("Parsing failed");
1868 assert_eq!(
1869 attr_type.usage,
1870 Some(KeyString("userApplications".to_string()))
1871 );
1872 }
1873
1874 #[cfg(feature = "chumsky")]
1876 #[test]
1877 fn test_attribute_type_collective_missing() {
1878 let schema_str =
1879 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1880 let attr_type = attribute_type_parser()
1881 .parse(schema_str)
1882 .into_result()
1883 .expect("Parsing failed");
1884 assert!(!attr_type.collective);
1885 }
1886
1887 #[cfg(feature = "chumsky")]
1888 #[test]
1889 fn test_attribute_type_collective_present() {
1890 let schema_str = "( 1.2.3 NAME 'test' COLLECTIVE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1891 let attr_type = attribute_type_parser()
1892 .parse(schema_str)
1893 .into_result()
1894 .expect("Parsing failed");
1895 assert!(attr_type.collective);
1896 }
1897
1898 #[cfg(feature = "chumsky")]
1900 #[test]
1901 fn test_attribute_type_obsolete_missing() {
1902 let schema_str =
1903 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1904 let attr_type = attribute_type_parser()
1905 .parse(schema_str)
1906 .into_result()
1907 .expect("Parsing failed");
1908 assert!(!attr_type.obsolete);
1909 }
1910
1911 #[cfg(feature = "chumsky")]
1912 #[test]
1913 fn test_attribute_type_obsolete_present() {
1914 let schema_str = "( 1.2.3 NAME 'test' OBSOLETE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1915 let attr_type = attribute_type_parser()
1916 .parse(schema_str)
1917 .into_result()
1918 .expect("Parsing failed");
1919 assert!(attr_type.obsolete);
1920 }
1921
1922 #[cfg(feature = "chumsky")]
1924 #[test]
1925 fn test_attribute_type_x_ordered_missing() {
1926 let schema_str =
1927 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
1928 let attr_type = attribute_type_parser()
1929 .parse(schema_str)
1930 .into_result()
1931 .expect("Parsing failed");
1932 assert!(attr_type.x_ordered.is_none());
1933 }
1934
1935 #[cfg(feature = "chumsky")]
1936 #[test]
1937 fn test_attribute_type_x_ordered_wrong_type() {
1938 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 )";
1939 let result = attribute_type_parser().parse(schema_str).into_result();
1940 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1941 let err = result.unwrap_err();
1942 let err_string = format!(
1943 "{}",
1944 ChumskyError {
1945 description: "attribute type".to_string(),
1946 source: schema_str.to_string(),
1947 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1948 }
1949 );
1950 assert!(
1951 err_string.contains("Unexpected token while parsing [], expected quoted keystring"),
1952 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected quoted keystring'",
1953 );
1954 }
1955
1956 #[cfg(feature = "chumsky")]
1957 #[test]
1958 fn test_attribute_type_x_ordered_missing_param() {
1959 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 )";
1960 let result = attribute_type_parser().parse(schema_str).into_result();
1961 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1962 let err = result.unwrap_err();
1963 let err_string = format!(
1964 "{}",
1965 ChumskyError {
1966 description: "attribute type".to_string(),
1967 source: schema_str.to_string(),
1968 errors: err.into_iter().map(|e| e.into_owned()).collect(),
1969 }
1970 );
1971 assert!(
1972 err_string.contains("Unexpected token while parsing [], expected quoted keystring"),
1973 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected quoted keystring'",
1974 );
1975 }
1976
1977 #[cfg(feature = "chumsky")]
1978 #[test]
1979 fn test_attribute_type_x_ordered_correct() {
1980 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 )";
1981 let attr_type = attribute_type_parser()
1982 .parse(schema_str)
1983 .into_result()
1984 .expect("Parsing failed");
1985 assert_eq!(attr_type.x_ordered, Some(KeyString("values".to_string())));
1986 }
1987 }
1988
1989 mod object_class_parser_tests {
1990 use super::*;
1991 #[cfg(feature = "chumsky")]
1992 use pretty_assertions::assert_eq;
1993
1994 #[cfg(feature = "chumsky")]
1996 #[test]
1997 fn test_object_class_sup_missing() {
1998 let schema_str = "( 1.2.3 NAME 'testOC' MUST attr1 )";
1999 let object_class = object_class_parser()
2000 .parse(schema_str)
2001 .into_result()
2002 .expect("Parsing failed");
2003 assert!(object_class.sup.is_empty());
2004 }
2005
2006 #[cfg(feature = "chumsky")]
2007 #[test]
2008 fn test_object_class_sup_wrong_type() {
2009 let schema_str = "( 1.2.3 NAME 'testOC' SUP 'invalid value with spaces' MUST attr1 )";
2010 let result = object_class_parser().parse(schema_str).into_result();
2011 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2012 let err = result.unwrap_err();
2013 let err_string = format!(
2014 "{}",
2015 ChumskyError {
2016 description: "object class".to_string(),
2017 source: schema_str.to_string(),
2018 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2019 }
2020 );
2021 assert!(
2022 err_string.contains("Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $"),
2023 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $'",
2024 );
2025 }
2026
2027 #[cfg(feature = "chumsky")]
2028 #[test]
2029 fn test_object_class_sup_missing_param() {
2030 let schema_str = "( 1.2.3 NAME 'testOC' SUP MUST attr1 )";
2031 let result = object_class_parser().parse(schema_str).into_result();
2032 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2033 let err = result.unwrap_err();
2034 let err_string = format!(
2035 "{}",
2036 ChumskyError {
2037 description: "object class".to_string(),
2038 source: schema_str.to_string(),
2039 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2040 }
2041 );
2042 assert!(
2043 err_string.contains(
2044 "Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')'"
2045 ),
2046 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')''",
2047 );
2048 }
2049
2050 #[cfg(feature = "chumsky")]
2051 #[test]
2052 fn test_object_class_sup_correct_single() {
2053 let schema_str = "( 1.2.3 NAME 'testOC' SUP top 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![KeyStringOrOID::KeyString(KeyString("top".to_string()))]
2061 );
2062 }
2063
2064 #[cfg(feature = "chumsky")]
2065 #[test]
2066 fn test_object_class_sup_correct_list() {
2067 let schema_str = "( 1.2.3 NAME 'testOC' SUP ( top $ person ) MUST attr1 )";
2068 let object_class = object_class_parser()
2069 .parse(schema_str)
2070 .into_result()
2071 .expect("Parsing failed");
2072 assert_eq!(
2073 object_class.sup,
2074 vec![
2075 KeyStringOrOID::KeyString(KeyString("top".to_string())),
2076 KeyStringOrOID::KeyString(KeyString("person".to_string()))
2077 ]
2078 );
2079 }
2080
2081 #[cfg(feature = "chumsky")]
2083 #[test]
2084 fn test_object_class_desc_missing() {
2085 let schema_str = "( 1.2.3 NAME 'testOC' MUST attr1 )";
2086 let object_class = object_class_parser()
2087 .parse(schema_str)
2088 .into_result()
2089 .expect("Parsing failed");
2090 assert!(object_class.desc.is_none());
2091 }
2092
2093 #[cfg(feature = "chumsky")]
2094 #[test]
2095 fn test_object_class_desc_wrong_type() {
2096 let schema_str = "( 1.2.3 NAME 'testOC' DESC unquoted String MUST attr1 )";
2097 let result = object_class_parser().parse(schema_str).into_result();
2098 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2099 let err = result.unwrap_err();
2100 let err_string = format!(
2101 "{}",
2102 ChumskyError {
2103 description: "object class".to_string(),
2104 source: schema_str.to_string(),
2105 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2106 }
2107 );
2108 assert!(
2109 err_string
2110 .contains("Unexpected token while parsing [], expected single-quoted string"),
2111 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
2112 );
2113 }
2114
2115 #[cfg(feature = "chumsky")]
2116 #[test]
2117 fn test_object_class_desc_missing_param() {
2118 let schema_str = "( 1.2.3 NAME 'testOC' DESC MUST attr1 )";
2119 let result = object_class_parser().parse(schema_str).into_result();
2120 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2121 let err = result.unwrap_err();
2122 let err_string = format!(
2123 "{}",
2124 ChumskyError {
2125 description: "object class".to_string(),
2126 source: schema_str.to_string(),
2127 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2128 }
2129 );
2130 assert!(
2131 err_string
2132 .contains("Unexpected token while parsing [], expected single-quoted string"),
2133 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
2134 );
2135 }
2136
2137 #[cfg(feature = "chumsky")]
2138 #[test]
2139 fn test_object_class_desc_correct() {
2140 let schema_str = "( 1.2.3 NAME 'testOC' DESC 'Some description' MUST attr1 )";
2141 let object_class = object_class_parser()
2142 .parse(schema_str)
2143 .into_result()
2144 .expect("Parsing failed");
2145 assert_eq!(object_class.desc, Some("Some description".to_string()));
2146 }
2147
2148 #[cfg(feature = "chumsky")]
2150 #[test]
2151 fn test_object_class_type_abstract_present() {
2152 let schema_str = "( 1.2.3 NAME 'testOC' ABSTRACT MUST attr1 )";
2153 let object_class = object_class_parser()
2154 .parse(schema_str)
2155 .into_result()
2156 .expect("Parsing failed");
2157 assert_eq!(object_class.object_class_type, ObjectClassType::Abstract);
2158 }
2159
2160 #[cfg(feature = "chumsky")]
2161 #[test]
2162 fn test_object_class_type_structural_present() {
2163 let schema_str = "( 1.2.3 NAME 'testOC' STRUCTURAL MUST attr1 )";
2164 let object_class = object_class_parser()
2165 .parse(schema_str)
2166 .into_result()
2167 .expect("Parsing failed");
2168 assert_eq!(object_class.object_class_type, ObjectClassType::Structural);
2169 }
2170
2171 #[cfg(feature = "chumsky")]
2172 #[test]
2173 fn test_object_class_type_auxiliary_present() {
2174 let schema_str = "( 1.2.3 NAME 'testOC' AUXILIARY MUST attr1 )";
2175 let object_class = object_class_parser()
2176 .parse(schema_str)
2177 .into_result()
2178 .expect("Parsing failed");
2179 assert_eq!(object_class.object_class_type, ObjectClassType::Auxiliary);
2180 }
2181
2182 #[cfg(feature = "chumsky")]
2183 #[test]
2184 fn test_object_class_type_default_structural() {
2185 let schema_str = "( 1.2.3 NAME 'testOC' MUST attr1 )";
2186 let object_class = object_class_parser()
2187 .parse(schema_str)
2188 .into_result()
2189 .expect("Parsing failed");
2190 assert_eq!(object_class.object_class_type, ObjectClassType::Structural);
2191 }
2192
2193 #[cfg(feature = "chumsky")]
2196 #[test]
2197 fn test_object_class_type_multiple_tags_abstract_first() {
2198 let schema_str = "( 1.2.3 NAME 'testOC' ABSTRACT STRUCTURAL MUST attr1 )";
2199 let object_class = object_class_parser()
2200 .parse(schema_str)
2201 .into_result()
2202 .expect("Parsing failed");
2203 assert_eq!(object_class.object_class_type, ObjectClassType::Abstract);
2204 }
2205
2206 #[cfg(feature = "chumsky")]
2212 #[test]
2213 fn test_object_class_type_multiple_tags_structural_first_if_possible() {
2214 let schema_str = "( 1.2.3 NAME 'testOC' STRUCTURAL ABSTRACT MUST attr1 )"; let object_class = object_class_parser()
2239 .parse(schema_str)
2240 .into_result()
2241 .expect("Parsing failed");
2242 assert_eq!(object_class.object_class_type, ObjectClassType::Abstract);
2244 }
2245
2246 #[cfg(feature = "chumsky")]
2248 #[test]
2249 fn test_object_class_must_missing() {
2250 let schema_str = "( 1.2.3 NAME 'testOC' MAY attr1 )";
2251 let object_class = object_class_parser()
2252 .parse(schema_str)
2253 .into_result()
2254 .expect("Parsing failed");
2255 assert!(object_class.must.is_empty());
2256 }
2257
2258 #[cfg(feature = "chumsky")]
2259 #[test]
2260 fn test_object_class_must_wrong_type() {
2261 let schema_str = "( 1.2.3 NAME 'testOC' MUST 'invalid value with spaces' MAY attr1 )";
2262 let result = object_class_parser().parse(schema_str).into_result();
2263 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2264 let err = result.unwrap_err();
2265 let err_string = format!(
2266 "{}",
2267 ChumskyError {
2268 description: "object class".to_string(),
2269 source: schema_str.to_string(),
2270 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2271 }
2272 );
2273 assert!(
2274 err_string.contains("Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $"),
2275 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $'",
2276 );
2277 }
2278
2279 #[cfg(feature = "chumsky")]
2280 #[test]
2281 fn test_object_class_must_missing_param() {
2282 let schema_str = "( 1.2.3 NAME 'testOC' MUST MAY attr1 )";
2283 let result = object_class_parser().parse(schema_str).into_result();
2284 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2285 let err = result.unwrap_err();
2286 let err_string = format!(
2287 "{}",
2288 ChumskyError {
2289 description: "object class".to_string(),
2290 source: schema_str.to_string(),
2291 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2292 }
2293 );
2294 assert!(
2295 err_string.contains(
2296 "Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')'"
2297 ),
2298 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')''",
2299 );
2300 }
2301
2302 #[cfg(feature = "chumsky")]
2303 #[test]
2304 fn test_object_class_must_correct_single() {
2305 let schema_str = "( 1.2.3 NAME 'testOC' MUST cn MAY attr1 )";
2306 let object_class = object_class_parser()
2307 .parse(schema_str)
2308 .into_result()
2309 .expect("Parsing failed");
2310 assert_eq!(
2311 object_class.must,
2312 vec![KeyStringOrOID::KeyString(KeyString("cn".to_string()))]
2313 );
2314 }
2315
2316 #[cfg(feature = "chumsky")]
2317 #[test]
2318 fn test_object_class_must_correct_list() {
2319 let schema_str = "( 1.2.3 NAME 'testOC' MUST ( cn $ sn ) MAY attr1 )";
2320 let object_class = object_class_parser()
2321 .parse(schema_str)
2322 .into_result()
2323 .expect("Parsing failed");
2324 assert_eq!(
2325 object_class.must,
2326 vec![
2327 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
2328 KeyStringOrOID::KeyString(KeyString("sn".to_string()))
2329 ]
2330 );
2331 }
2332
2333 #[cfg(feature = "chumsky")]
2335 #[test]
2336 fn test_object_class_may_missing() {
2337 let schema_str = "( 1.2.3 NAME 'testOC' MUST attr1 )";
2338 let object_class = object_class_parser()
2339 .parse(schema_str)
2340 .into_result()
2341 .expect("Parsing failed");
2342 assert!(object_class.may.is_empty());
2343 }
2344
2345 #[cfg(feature = "chumsky")]
2346 #[test]
2347 fn test_object_class_may_wrong_type() {
2348 let schema_str = "( 1.2.3 NAME 'testOC' MAY 'invalid value with spaces' MUST attr1 )";
2349 let result = object_class_parser().parse(schema_str).into_result();
2350 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2351 let err = result.unwrap_err();
2352 let err_string = format!(
2353 "{}",
2354 ChumskyError {
2355 description: "object class".to_string(),
2356 source: schema_str.to_string(),
2357 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2358 }
2359 );
2360 assert!(
2361 err_string.contains("Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $"),
2362 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected list of keystrings or OIDs separated by $'",
2363 );
2364 }
2365
2366 #[cfg(feature = "chumsky")]
2367 #[test]
2368 fn test_object_class_may_missing_param() {
2369 let schema_str = "( 1.2.3 NAME 'testOC' MAY MUST attr1 )";
2370 let result = object_class_parser().parse(schema_str).into_result();
2371 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2372 let err = result.unwrap_err();
2373 let err_string = format!(
2374 "{}",
2375 ChumskyError {
2376 description: "object class".to_string(),
2377 source: schema_str.to_string(),
2378 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2379 }
2380 );
2381 assert!(
2382 err_string.contains(
2383 "Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')'"
2384 ),
2385 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected 'N', 'S', 'D', 'A', 'M', 'O', ')''",
2386 );
2387 }
2388
2389 #[cfg(feature = "chumsky")]
2390 #[test]
2391 fn test_object_class_may_correct_single() {
2392 let schema_str = "( 1.2.3 NAME 'testOC' MAY description MUST attr1 )";
2393 let object_class = object_class_parser()
2394 .parse(schema_str)
2395 .into_result()
2396 .expect("Parsing failed");
2397 assert_eq!(
2398 object_class.may,
2399 vec![KeyStringOrOID::KeyString(KeyString(
2400 "description".to_string()
2401 ))]
2402 );
2403 }
2404
2405 #[cfg(feature = "chumsky")]
2406 #[test]
2407 fn test_object_class_may_correct_list() {
2408 let schema_str = "( 1.2.3 NAME 'testOC' MAY ( description $ seeAlso ) MUST attr1 )";
2409 let object_class = object_class_parser()
2410 .parse(schema_str)
2411 .into_result()
2412 .expect("Parsing failed");
2413 assert_eq!(
2414 object_class.may,
2415 vec![
2416 KeyStringOrOID::KeyString(KeyString("description".to_string())),
2417 KeyStringOrOID::KeyString(KeyString("seeAlso".to_string()))
2418 ]
2419 );
2420 }
2421
2422 #[cfg(feature = "chumsky")]
2424 #[test]
2425 fn test_object_class_obsolete_missing() {
2426 let schema_str = "( 1.2.3 NAME 'testOC' MUST attr1 )";
2427 let object_class = object_class_parser()
2428 .parse(schema_str)
2429 .into_result()
2430 .expect("Parsing failed");
2431 assert!(!object_class.obsolete);
2432 }
2433
2434 #[cfg(feature = "chumsky")]
2435 #[test]
2436 fn test_object_class_obsolete_present() {
2437 let schema_str = "( 1.2.3 NAME 'testOC' OBSOLETE MUST attr1 )";
2438 let object_class = object_class_parser()
2439 .parse(schema_str)
2440 .into_result()
2441 .expect("Parsing failed");
2442 assert!(object_class.obsolete);
2443 }
2444
2445 #[cfg(feature = "chumsky")]
2447 #[test]
2448 fn test_attribute_type_desc_missing() {
2449 let schema_str = "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2450 let attr_type = attribute_type_parser()
2451 .parse(schema_str)
2452 .into_result()
2453 .expect("Parsing failed");
2454 assert!(attr_type.desc.is_none());
2455 }
2456
2457 #[cfg(feature = "chumsky")]
2458 #[test]
2459 fn test_attribute_type_desc_wrong_type() {
2460 let schema_str =
2461 "( 1.2.3 NAME 'test' DESC unquoted String SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2462 let result = attribute_type_parser().parse(schema_str).into_result();
2463 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2464 let err = result.unwrap_err();
2465 let err_string = format!(
2466 "{}",
2467 ChumskyError {
2468 description: "attribute type".to_string(),
2469 source: schema_str.to_string(),
2470 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2471 }
2472 );
2473 assert!(
2474 err_string
2475 .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_missing_param() {
2483 let schema_str = "( 1.2.3 NAME 'test' DESC SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2484 let result = attribute_type_parser().parse(schema_str).into_result();
2485 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2486 let err = result.unwrap_err();
2487 let err_string = format!(
2488 "{}",
2489 ChumskyError {
2490 description: "attribute type".to_string(),
2491 source: schema_str.to_string(),
2492 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2493 }
2494 );
2495 assert!(
2496 err_string
2497 .contains("Unexpected token while parsing [], expected single-quoted string"),
2498 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected single-quoted string'",
2499 );
2500 }
2501
2502 #[cfg(feature = "chumsky")]
2503 #[test]
2504 fn test_attribute_type_desc_correct() {
2505 let schema_str = "( 1.2.3 NAME 'test' DESC 'Some description' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2506 let attr_type = attribute_type_parser()
2507 .parse(schema_str)
2508 .into_result()
2509 .expect("Parsing failed");
2510 assert_eq!(attr_type.desc, Some("Some description".to_string()));
2511 }
2512
2513 #[cfg(feature = "chumsky")]
2515 #[test]
2516 fn test_attribute_type_syntax_missing() {
2517 let schema_str = "( 1.2.3 NAME 'test' DESC 'Test Attribute' )";
2518 let attr_type = attribute_type_parser()
2519 .parse(schema_str)
2520 .into_result()
2521 .expect("Parsing failed");
2522 assert!(attr_type.syntax.is_none());
2523 }
2524
2525 #[cfg(feature = "chumsky")]
2526 #[test]
2527 fn test_attribute_type_syntax_wrong_type() {
2528 let schema_str = "( 1.2.3 NAME 'test' SYNTAX 'not an OID' DESC 'Test Attribute' )";
2529 let result = attribute_type_parser().parse(schema_str).into_result();
2530 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2531 let err = result.unwrap_err();
2532 let err_string = format!(
2533 "{}",
2534 ChumskyError {
2535 description: "attribute type".to_string(),
2536 source: schema_str.to_string(),
2537 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2538 }
2539 );
2540 assert!(
2541 err_string.contains(
2542 "Unexpected end of input while parsing [], expected OID with optional length"
2543 ),
2544 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected OID with optional length'",
2545 );
2546 }
2547
2548 #[cfg(feature = "chumsky")]
2549 #[test]
2550 fn test_attribute_type_syntax_missing_param() {
2551 let schema_str = "( 1.2.3 NAME 'test' SYNTAX DESC 'Test Attribute' )";
2552 let result = attribute_type_parser().parse(schema_str).into_result();
2553 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2554 let err = result.unwrap_err();
2555 let err_string = format!(
2556 "{}",
2557 ChumskyError {
2558 description: "attribute type".to_string(),
2559 source: schema_str.to_string(),
2560 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2561 }
2562 );
2563 assert!(
2564 err_string.contains(
2565 "Unexpected end of input while parsing [], expected OID with optional length"
2566 ),
2567 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected OID with optional length'",
2568 );
2569 }
2570
2571 #[cfg(feature = "chumsky")]
2572 #[test]
2573 fn test_attribute_type_syntax_correct_with_length() {
2574 let schema_str = "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} DESC 'Test Attribute' )";
2575 let attr_type = attribute_type_parser()
2576 .parse(schema_str)
2577 .into_result()
2578 .expect("Parsing failed");
2579 assert_eq!(
2580 attr_type.syntax,
2581 Some(OIDWithLength {
2582 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
2583 oid: "1.3.6.1.4.1.1466.115.121.1.15"
2584 .to_string()
2585 .try_into()
2586 .unwrap(),
2587 length: Some(255)
2588 })
2589 );
2590 }
2591
2592 #[cfg(feature = "chumsky")]
2593 #[test]
2594 fn test_attribute_type_syntax_correct_without_length() {
2595 let schema_str =
2596 "( 1.2.3 NAME 'test' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Test Attribute' )";
2597 let attr_type = attribute_type_parser()
2598 .parse(schema_str)
2599 .into_result()
2600 .expect("Parsing failed");
2601 assert_eq!(
2602 attr_type.syntax,
2603 Some(OIDWithLength {
2604 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
2605 oid: "1.3.6.1.4.1.1466.115.121.1.15"
2606 .to_string()
2607 .try_into()
2608 .unwrap(),
2609 length: None
2610 })
2611 );
2612 }
2613
2614 #[cfg(feature = "chumsky")]
2616 #[test]
2617 fn test_attribute_type_single_value_missing() {
2618 let schema_str =
2619 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2620 let attr_type = attribute_type_parser()
2621 .parse(schema_str)
2622 .into_result()
2623 .expect("Parsing failed");
2624 assert!(!attr_type.single_value);
2625 }
2626
2627 #[cfg(feature = "chumsky")]
2628 #[test]
2629 fn test_attribute_type_single_value_present() {
2630 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 )";
2631 let attr_type = attribute_type_parser()
2632 .parse(schema_str)
2633 .into_result()
2634 .expect("Parsing failed");
2635 assert!(attr_type.single_value);
2636 }
2637
2638 #[cfg(feature = "chumsky")]
2640 #[test]
2641 fn test_attribute_type_equality_missing() {
2642 let schema_str =
2643 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2644 let attr_type = attribute_type_parser()
2645 .parse(schema_str)
2646 .into_result()
2647 .expect("Parsing failed");
2648 assert!(attr_type.equality.is_none());
2649 }
2650
2651 #[cfg(feature = "chumsky")]
2652 #[test]
2653 fn test_attribute_type_equality_wrong_type() {
2654 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 )";
2655 let result = attribute_type_parser().parse(schema_str).into_result();
2656 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2657 let err = result.unwrap_err();
2658 let err_string = format!(
2659 "{}",
2660 ChumskyError {
2661 description: "attribute type".to_string(),
2662 source: schema_str.to_string(),
2663 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2664 }
2665 );
2666 assert!(
2667 err_string.contains("Unexpected token while parsing [], expected keystring"),
2668 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
2669 );
2670 }
2671
2672 #[cfg(feature = "chumsky")]
2673 #[test]
2674 fn test_attribute_type_equality_missing_param() {
2675 let schema_str = "( 1.2.3 NAME 'test' EQUALITY DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2676 let result = attribute_type_parser().parse(schema_str).into_result();
2677 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2678 let err = result.unwrap_err();
2679 let err_string = format!(
2680 "{}",
2681 ChumskyError {
2682 description: "attribute type".to_string(),
2683 source: schema_str.to_string(),
2684 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2685 }
2686 );
2687 assert!(
2688 err_string.contains("Unexpected end of input while parsing [], expected keystring"),
2689 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
2690 );
2691 }
2692
2693 #[cfg(feature = "chumsky")]
2694 #[test]
2695 fn test_attribute_type_equality_correct() {
2696 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 )";
2697 let attr_type = attribute_type_parser()
2698 .parse(schema_str)
2699 .into_result()
2700 .expect("Parsing failed");
2701 assert_eq!(
2702 attr_type.equality,
2703 Some(KeyString("caseIgnoreMatch".to_string()))
2704 );
2705 }
2706
2707 #[cfg(feature = "chumsky")]
2709 #[test]
2710 fn test_attribute_type_substr_missing() {
2711 let schema_str =
2712 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2713 let attr_type = attribute_type_parser()
2714 .parse(schema_str)
2715 .into_result()
2716 .expect("Parsing failed");
2717 assert!(attr_type.substr.is_none());
2718 }
2719
2720 #[cfg(feature = "chumsky")]
2721 #[test]
2722 fn test_attribute_type_substr_wrong_type() {
2723 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 )";
2724 let result = attribute_type_parser().parse(schema_str).into_result();
2725 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2726 let err = result.unwrap_err();
2727 let err_string = format!(
2728 "{}",
2729 ChumskyError {
2730 description: "attribute type".to_string(),
2731 source: schema_str.to_string(),
2732 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2733 }
2734 );
2735 assert!(
2736 err_string.contains("Unexpected token while parsing [], expected keystring"),
2737 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
2738 );
2739 }
2740
2741 #[cfg(feature = "chumsky")]
2742 #[test]
2743 fn test_attribute_type_substr_missing_param() {
2744 let schema_str = "( 1.2.3 NAME 'test' SUBSTR DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2745 let result = attribute_type_parser().parse(schema_str).into_result();
2746 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2747 let err = result.unwrap_err();
2748 let err_string = format!(
2749 "{}",
2750 ChumskyError {
2751 description: "attribute type".to_string(),
2752 source: schema_str.to_string(),
2753 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2754 }
2755 );
2756 assert!(
2757 err_string.contains("Unexpected end of input while parsing [], expected keystring"),
2758 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
2759 );
2760 }
2761
2762 #[cfg(feature = "chumsky")]
2763 #[test]
2764 fn test_attribute_type_substr_correct() {
2765 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 )";
2766 let attr_type = attribute_type_parser()
2767 .parse(schema_str)
2768 .into_result()
2769 .expect("Parsing failed");
2770 assert_eq!(
2771 attr_type.substr,
2772 Some(KeyString("caseIgnoreSubstringsMatch".to_string()))
2773 );
2774 }
2775
2776 #[cfg(feature = "chumsky")]
2778 #[test]
2779 fn test_attribute_type_ordering_missing() {
2780 let schema_str =
2781 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2782 let attr_type = attribute_type_parser()
2783 .parse(schema_str)
2784 .into_result()
2785 .expect("Parsing failed");
2786 assert!(attr_type.ordering.is_none());
2787 }
2788
2789 #[cfg(feature = "chumsky")]
2790 #[test]
2791 fn test_attribute_type_ordering_wrong_type() {
2792 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 )";
2793 let result = attribute_type_parser().parse(schema_str).into_result();
2794 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2795 let err = result.unwrap_err();
2796 let err_string = format!(
2797 "{}",
2798 ChumskyError {
2799 description: "attribute type".to_string(),
2800 source: schema_str.to_string(),
2801 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2802 }
2803 );
2804 assert!(
2805 err_string.contains("Unexpected token while parsing [], expected keystring"),
2806 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
2807 );
2808 }
2809
2810 #[cfg(feature = "chumsky")]
2811 #[test]
2812 fn test_attribute_type_ordering_missing_param() {
2813 let schema_str = "( 1.2.3 NAME 'test' ORDERING DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2814 let result = attribute_type_parser().parse(schema_str).into_result();
2815 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2816 let err = result.unwrap_err();
2817 let err_string = format!(
2818 "{}",
2819 ChumskyError {
2820 description: "attribute type".to_string(),
2821 source: schema_str.to_string(),
2822 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2823 }
2824 );
2825 assert!(
2826 err_string.contains("Unexpected end of input while parsing [], expected keystring"),
2827 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
2828 );
2829 }
2830
2831 #[cfg(feature = "chumsky")]
2832 #[test]
2833 fn test_attribute_type_ordering_correct() {
2834 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 )";
2835 let attr_type = attribute_type_parser()
2836 .parse(schema_str)
2837 .into_result()
2838 .expect("Parsing failed");
2839 assert_eq!(
2840 attr_type.ordering,
2841 Some(KeyString("caseIgnoreOrderingMatch".to_string()))
2842 );
2843 }
2844
2845 #[cfg(feature = "chumsky")]
2847 #[test]
2848 fn test_attribute_type_no_user_modification_missing() {
2849 let schema_str =
2850 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2851 let attr_type = attribute_type_parser()
2852 .parse(schema_str)
2853 .into_result()
2854 .expect("Parsing failed");
2855 assert!(!attr_type.no_user_modification);
2856 }
2857
2858 #[cfg(feature = "chumsky")]
2859 #[test]
2860 fn test_attribute_type_no_user_modification_present() {
2861 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 )";
2862 let attr_type = attribute_type_parser()
2863 .parse(schema_str)
2864 .into_result()
2865 .expect("Parsing failed");
2866 assert!(attr_type.no_user_modification);
2867 }
2868
2869 #[cfg(feature = "chumsky")]
2871 #[test]
2872 fn test_attribute_type_usage_missing() {
2873 let schema_str =
2874 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2875 let attr_type = attribute_type_parser()
2876 .parse(schema_str)
2877 .into_result()
2878 .expect("Parsing failed");
2879 assert!(attr_type.usage.is_none());
2880 }
2881
2882 #[cfg(feature = "chumsky")]
2883 #[test]
2884 fn test_attribute_type_usage_wrong_type() {
2885 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 )";
2886 let result = attribute_type_parser().parse(schema_str).into_result();
2887 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2888 let err = result.unwrap_err();
2889 let err_string = format!(
2890 "{}",
2891 ChumskyError {
2892 description: "attribute type".to_string(),
2893 source: schema_str.to_string(),
2894 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2895 }
2896 );
2897 assert!(
2898 err_string.contains("Unexpected token while parsing [], expected keystring"),
2899 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected keystring'",
2900 );
2901 }
2902
2903 #[cfg(feature = "chumsky")]
2904 #[test]
2905 fn test_attribute_type_usage_missing_param() {
2906 let schema_str = "( 1.2.3 NAME 'test' USAGE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2907 let result = attribute_type_parser().parse(schema_str).into_result();
2908 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
2909 let err = result.unwrap_err();
2910 let err_string = format!(
2911 "{}",
2912 ChumskyError {
2913 description: "attribute type".to_string(),
2914 source: schema_str.to_string(),
2915 errors: err.into_iter().map(|e| e.into_owned()).collect(),
2916 }
2917 );
2918 assert!(
2919 err_string.contains("Unexpected end of input while parsing [], expected keystring"),
2920 "Error string '{err_string}' does not contain 'Unexpected end of input while parsing [], expected keystring'",
2921 );
2922 }
2923
2924 #[cfg(feature = "chumsky")]
2925 #[test]
2926 fn test_attribute_type_usage_correct() {
2927 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 )";
2928 let attr_type = attribute_type_parser()
2929 .parse(schema_str)
2930 .into_result()
2931 .expect("Parsing failed");
2932 assert_eq!(
2933 attr_type.usage,
2934 Some(KeyString("userApplications".to_string()))
2935 );
2936 }
2937
2938 #[cfg(feature = "chumsky")]
2940 #[test]
2941 fn test_attribute_type_collective_missing() {
2942 let schema_str =
2943 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2944 let attr_type = attribute_type_parser()
2945 .parse(schema_str)
2946 .into_result()
2947 .expect("Parsing failed");
2948 assert!(!attr_type.collective);
2949 }
2950
2951 #[cfg(feature = "chumsky")]
2952 #[test]
2953 fn test_attribute_type_collective_present() {
2954 let schema_str = "( 1.2.3 NAME 'test' COLLECTIVE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2955 let attr_type = attribute_type_parser()
2956 .parse(schema_str)
2957 .into_result()
2958 .expect("Parsing failed");
2959 assert!(attr_type.collective);
2960 }
2961
2962 #[cfg(feature = "chumsky")]
2964 #[test]
2965 fn test_attribute_type_obsolete_missing() {
2966 let schema_str =
2967 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2968 let attr_type = attribute_type_parser()
2969 .parse(schema_str)
2970 .into_result()
2971 .expect("Parsing failed");
2972 assert!(!attr_type.obsolete);
2973 }
2974
2975 #[cfg(feature = "chumsky")]
2976 #[test]
2977 fn test_attribute_type_obsolete_present() {
2978 let schema_str = "( 1.2.3 NAME 'test' OBSOLETE DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2979 let attr_type = attribute_type_parser()
2980 .parse(schema_str)
2981 .into_result()
2982 .expect("Parsing failed");
2983 assert!(attr_type.obsolete);
2984 }
2985
2986 #[cfg(feature = "chumsky")]
2988 #[test]
2989 fn test_attribute_type_x_ordered_missing() {
2990 let schema_str =
2991 "( 1.2.3 NAME 'test' DESC 'Test Attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
2992 let attr_type = attribute_type_parser()
2993 .parse(schema_str)
2994 .into_result()
2995 .expect("Parsing failed");
2996 assert!(attr_type.x_ordered.is_none());
2997 }
2998
2999 #[cfg(feature = "chumsky")]
3000 #[test]
3001 fn test_attribute_type_x_ordered_wrong_type() {
3002 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 )";
3003 let result = attribute_type_parser().parse(schema_str).into_result();
3004 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
3005 let err = result.unwrap_err();
3006 let err_string = format!(
3007 "{}",
3008 ChumskyError {
3009 description: "attribute type".to_string(),
3010 source: schema_str.to_string(),
3011 errors: err.into_iter().map(|e| e.into_owned()).collect(),
3012 }
3013 );
3014 assert!(
3015 err_string.contains("Unexpected token while parsing [], expected quoted keystring"),
3016 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected quoted keystring'",
3017 );
3018 }
3019
3020 #[cfg(feature = "chumsky")]
3021 #[test]
3022 fn test_attribute_type_x_ordered_missing_param() {
3023 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 )";
3024 let result = attribute_type_parser().parse(schema_str).into_result();
3025 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
3026 let err = result.unwrap_err();
3027 let err_string = format!(
3028 "{}",
3029 ChumskyError {
3030 description: "attribute type".to_string(),
3031 source: schema_str.to_string(),
3032 errors: err.into_iter().map(|e| e.into_owned()).collect(),
3033 }
3034 );
3035 assert!(
3036 err_string.contains("Unexpected token while parsing [], expected quoted keystring"),
3037 "Error string '{err_string}' does not contain 'Unexpected token while parsing [], expected quoted keystring'",
3038 );
3039 }
3040
3041 #[cfg(feature = "chumsky")]
3042 #[test]
3043 fn test_attribute_type_x_ordered_correct() {
3044 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 )";
3045 let attr_type = attribute_type_parser()
3046 .parse(schema_str)
3047 .into_result()
3048 .expect("Parsing failed");
3049 assert_eq!(attr_type.x_ordered, Some(KeyString("values".to_string())));
3050 }
3051 }
3052}