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