1#![allow(warnings)]
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::str::FromStr;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct AttrPath {
9 a: String,
11 s: Option<String>,
12}
13
14impl ToString for AttrPath {
15 fn to_string(&self) -> String {
16 match self {
17 Self {
18 a: attrname,
19 s: Some(subattr),
20 } => format!("{attrname}.{subattr}"),
21 Self {
22 a: attrname,
23 s: None,
24 } => attrname.to_owned(),
25 }
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum ScimFilter {
31 Or(Box<ScimFilter>, Box<ScimFilter>),
32 And(Box<ScimFilter>, Box<ScimFilter>),
33 Not(Box<ScimFilter>),
34
35 Present(AttrPath),
36 Equal(AttrPath, Value),
37 NotEqual(AttrPath, Value),
38 Contains(AttrPath, Value),
39 StartsWith(AttrPath, Value),
40 EndsWith(AttrPath, Value),
41 Greater(AttrPath, Value),
42 Less(AttrPath, Value),
43 GreaterOrEqual(AttrPath, Value),
44 LessOrEqual(AttrPath, Value),
45
46 Complex(String, Box<ScimComplexFilter>),
47}
48
49impl ToString for ScimFilter {
50 fn to_string(&self) -> String {
51 match self {
52 Self::And(this, that) => format!("({} and {})", this.to_string(), that.to_string()),
53 Self::Contains(attrpath, value) => format!("({} co {value})", attrpath.to_string()),
54 Self::EndsWith(attrpath, value) => format!("({} ew {value})", attrpath.to_string()),
55 Self::Equal(attrpath, value) => format!("({} eq {value})", attrpath.to_string()),
56 Self::Greater(attrpath, value) => format!("({} gt {value})", attrpath.to_string()),
57 Self::GreaterOrEqual(attrpath, value) => {
58 format!("({} ge {value})", attrpath.to_string())
59 }
60 Self::Less(attrpath, value) => format!("({} lt {value})", attrpath.to_string()),
61 Self::LessOrEqual(attrpath, value) => format!("({} le {value})", attrpath.to_string()),
62 Self::Not(expr) => format!("(not ({}))", expr.to_string()),
63 Self::NotEqual(attrpath, value) => format!("({} ne {value})", attrpath.to_string()),
64 Self::Or(this, that) => format!("({} or {})", this.to_string(), that.to_string()),
65 Self::Present(attrpath) => format!("({} pr)", attrpath.to_string()),
66 Self::StartsWith(attrpath, value) => format!("({} sw {value})", attrpath.to_string()),
67 Self::Complex(attrname, expr) => format!("{attrname}[{}]", expr.to_string()),
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub enum ScimComplexFilter {
74 Or(Box<ScimComplexFilter>, Box<ScimComplexFilter>),
75 And(Box<ScimComplexFilter>, Box<ScimComplexFilter>),
76 Not(Box<ScimComplexFilter>),
77
78 Present(String),
79 Equal(String, Value),
80 NotEqual(String, Value),
81 Contains(String, Value),
82 StartsWith(String, Value),
83 EndsWith(String, Value),
84 Greater(String, Value),
85 Less(String, Value),
86 GreaterOrEqual(String, Value),
87 LessOrEqual(String, Value),
88}
89
90impl ToString for ScimComplexFilter {
91 fn to_string(&self) -> String {
92 match self {
93 Self::And(this, that) => format!("({} and {})", this.to_string(), that.to_string()),
94 Self::Contains(attrname, value) => format!("({attrname} co {value})"),
95 Self::EndsWith(attrname, value) => format!("({attrname} ew {value})"),
96 Self::Equal(attrname, value) => format!("({attrname} eq {value})"),
97 Self::Greater(attrname, value) => format!("({attrname} gt {value})"),
98 Self::GreaterOrEqual(attrname, value) => format!("({attrname} ge {value})"),
99 Self::Less(attrname, value) => format!("({attrname} lt {value})"),
100 Self::LessOrEqual(attrname, value) => format!("({attrname} le {value})"),
101 Self::Not(expr) => format!("(not ({}))", expr.to_string()),
102 Self::NotEqual(attrname, value) => format!("({attrname} ne {value})"),
103 Self::Or(this, that) => format!("({} or {})", this.to_string(), that.to_string()),
104 Self::Present(attrname) => format!("({attrname} pr)"),
105 Self::StartsWith(attrname, value) => format!("({attrname} sw {value})"),
106 }
107 }
108}
109
110peg::parser! {
113 grammar scimfilter() for str {
114
115 pub rule parse() -> ScimFilter = precedence!{
116 a:(@) separator()+ "or" separator()+ b:@ {
117 ScimFilter::Or(
118 Box::new(a),
119 Box::new(b)
120 )
121 }
122 --
123 a:(@) separator()+ "and" separator()+ b:@ {
124 ScimFilter::And(
125 Box::new(a),
126 Box::new(b)
127 )
128 }
129 --
130 "not" separator()+ "(" e:parse() ")" {
131 ScimFilter::Not(Box::new(e))
132 }
133 --
134 a:attrname()"[" e:parse_complex() "]" {
135 ScimFilter::Complex(
136 a,
137 Box::new(e)
138 )
139 }
140 --
141 a:attrexp() { a }
142 "(" e:parse() ")" { e }
143 }
144
145 pub rule parse_complex() -> ScimComplexFilter = precedence!{
146 a:(@) separator()+ "or" separator()+ b:@ {
147 ScimComplexFilter::Or(
148 Box::new(a),
149 Box::new(b)
150 )
151 }
152 --
153 a:(@) separator()+ "and" separator()+ b:@ {
154 ScimComplexFilter::And(
155 Box::new(a),
156 Box::new(b)
157 )
158 }
159 --
160 "not" separator()+ "(" e:parse_complex() ")" {
161 ScimComplexFilter::Not(Box::new(e))
162 }
163 --
164 a:complex_attrexp() { a }
165 "(" e:parse_complex() ")" { e }
166 }
167
168 pub(crate) rule attrexp() -> ScimFilter =
169 pres()
170 / eq()
171 / ne()
172 / co()
173 / sw()
174 / ew()
175 / gt()
176 / lt()
177 / ge()
178 / le()
179
180 pub(crate) rule pres() -> ScimFilter =
181 a:attrpath() separator()+ "pr" { ScimFilter::Present(a) }
182
183 pub(crate) rule eq() -> ScimFilter =
184 a:attrpath() separator()+ "eq" separator()+ v:value() { ScimFilter::Equal(a, v) }
185
186 pub(crate) rule ne() -> ScimFilter =
187 a:attrpath() separator()+ "ne" separator()+ v:value() { ScimFilter::NotEqual(a, v) }
188
189 pub(crate) rule co() -> ScimFilter =
190 a:attrpath() separator()+ "co" separator()+ v:value() { ScimFilter::Contains(a, v) }
191
192 pub(crate) rule sw() -> ScimFilter =
193 a:attrpath() separator()+ "sw" separator()+ v:value() { ScimFilter::StartsWith(a, v) }
194
195 pub(crate) rule ew() -> ScimFilter =
196 a:attrpath() separator()+ "ew" separator()+ v:value() { ScimFilter::EndsWith(a, v) }
197
198 pub(crate) rule gt() -> ScimFilter =
199 a:attrpath() separator()+ "gt" separator()+ v:value() { ScimFilter::Greater(a, v) }
200
201 pub(crate) rule lt() -> ScimFilter =
202 a:attrpath() separator()+ "lt" separator()+ v:value() { ScimFilter::Less(a, v) }
203
204 pub(crate) rule ge() -> ScimFilter =
205 a:attrpath() separator()+ "ge" separator()+ v:value() { ScimFilter::GreaterOrEqual(a, v) }
206
207 pub(crate) rule le() -> ScimFilter =
208 a:attrpath() separator()+ "le" separator()+ v:value() { ScimFilter::LessOrEqual(a, v) }
209
210 pub(crate) rule complex_attrexp() -> ScimComplexFilter =
211 c_pres()
212 / c_eq()
213 / c_ne()
214 / c_co()
215 / c_sw()
216 / c_ew()
217 / c_gt()
218 / c_lt()
219 / c_ge()
220 / c_le()
221
222 pub(crate) rule c_pres() -> ScimComplexFilter =
223 a:attrname() separator()+ "pr" { ScimComplexFilter::Present(a) }
224
225 pub(crate) rule c_eq() -> ScimComplexFilter =
226 a:attrname() separator()+ "eq" separator()+ v:value() { ScimComplexFilter::Equal(a, v) }
227
228 pub(crate) rule c_ne() -> ScimComplexFilter =
229 a:attrname() separator()+ "ne" separator()+ v:value() { ScimComplexFilter::NotEqual(a, v) }
230
231 pub(crate) rule c_co() -> ScimComplexFilter =
232 a:attrname() separator()+ "co" separator()+ v:value() { ScimComplexFilter::Contains(a, v) }
233
234 pub(crate) rule c_sw() -> ScimComplexFilter =
235 a:attrname() separator()+ "sw" separator()+ v:value() { ScimComplexFilter::StartsWith(a, v) }
236
237 pub(crate) rule c_ew() -> ScimComplexFilter =
238 a:attrname() separator()+ "ew" separator()+ v:value() { ScimComplexFilter::EndsWith(a, v) }
239
240 pub(crate) rule c_gt() -> ScimComplexFilter =
241 a:attrname() separator()+ "gt" separator()+ v:value() { ScimComplexFilter::Greater(a, v) }
242
243 pub(crate) rule c_lt() -> ScimComplexFilter =
244 a:attrname() separator()+ "lt" separator()+ v:value() { ScimComplexFilter::Less(a, v) }
245
246 pub(crate) rule c_ge() -> ScimComplexFilter =
247 a:attrname() separator()+ "ge" separator()+ v:value() { ScimComplexFilter::GreaterOrEqual(a, v) }
248
249 pub(crate) rule c_le() -> ScimComplexFilter =
250 a:attrname() separator()+ "le" separator()+ v:value() { ScimComplexFilter::LessOrEqual(a, v) }
251
252 rule separator() =
253 ['\n' | ' ' | '\t' ]
254
255 rule operator() =
256 ['\n' | ' ' | '\t' | '(' | ')' | '[' | ']' ]
257
258 rule value() -> Value =
259 quotedvalue() / unquotedvalue()
260
261 rule quotedvalue() -> Value =
262 s:$(['"'] ((['\\'][_]) / (!['"'][_]))* ['"']) {? serde_json::from_str(s).map_err(|_| "invalid json value" ) }
263
264 rule unquotedvalue() -> Value =
265 s:$((!operator()[_])*) {? serde_json::from_str(s).map_err(|_| "invalid json value" ) }
266
267 pub(crate) rule attrpath() -> AttrPath =
268 a:attrname() s:subattr()? { AttrPath { a, s } }
269
270 rule subattr() -> String =
271 "." s:attrname() { s.to_string() }
272
273 pub(crate) rule attrname() -> String =
274 s:$([ 'a'..='z' | 'A'..='Z']['a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' ]*) { s.to_string() }
275 }
276}
277
278impl FromStr for AttrPath {
279 type Err = peg::error::ParseError<peg::str::LineCol>;
280 fn from_str(input: &str) -> Result<Self, Self::Err> {
281 scimfilter::attrpath(input)
282 }
283}
284
285impl FromStr for ScimFilter {
286 type Err = peg::error::ParseError<peg::str::LineCol>;
287 fn from_str(input: &str) -> Result<Self, Self::Err> {
288 scimfilter::parse(input)
289 }
290}
291
292impl FromStr for ScimComplexFilter {
293 type Err = peg::error::ParseError<peg::str::LineCol>;
294 fn from_str(input: &str) -> Result<Self, Self::Err> {
295 scimfilter::parse_complex(input)
296 }
297}
298
299#[cfg(test)]
300mod test {
301 use super::*;
302 use crate::filter::AttrPath;
303 use crate::filter::ScimFilter;
304 use serde_json::Value;
305
306 #[test]
307 fn test_scimfilter_attrname() {
308 assert_eq!(scimfilter::attrname("abcd-_"), Ok("abcd-_".to_string()));
309 assert_eq!(scimfilter::attrname("aB-_CD"), Ok("aB-_CD".to_string()));
310 assert_eq!(scimfilter::attrname("a1-_23"), Ok("a1-_23".to_string()));
311 assert!(scimfilter::attrname("-bcd").is_err());
312 assert!(scimfilter::attrname("_bcd").is_err());
313 assert!(scimfilter::attrname("0bcd").is_err());
314 }
315
316 #[test]
317 fn test_scimfilter_attrpath() {
318 assert_eq!(
319 scimfilter::attrpath("abcd"),
320 Ok(AttrPath {
321 a: "abcd".to_string(),
322 s: None
323 })
324 );
325
326 assert_eq!(
327 scimfilter::attrpath("abcd.abcd"),
328 Ok(AttrPath {
329 a: "abcd".to_string(),
330 s: Some("abcd".to_string())
331 })
332 );
333
334 assert!(scimfilter::attrname("abcd.0").is_err());
335 assert!(scimfilter::attrname("abcd._").is_err());
336 assert!(scimfilter::attrname("abcd,0").is_err());
337 assert!(scimfilter::attrname(".abcd").is_err());
338 }
339
340 #[test]
341 fn test_scimfilter_pres() {
342 assert!(
343 scimfilter::parse("abcd pr")
344 == Ok(ScimFilter::Present(AttrPath {
345 a: "abcd".to_string(),
346 s: None
347 }))
348 );
349 }
350
351 #[test]
352 fn test_scimfilter_eq() {
353 assert!(
354 scimfilter::parse("abcd eq \"dcba\"")
355 == Ok(ScimFilter::Equal(
356 AttrPath {
357 a: "abcd".to_string(),
358 s: None
359 },
360 Value::String("dcba".to_string())
361 ))
362 );
363 }
364
365 #[test]
366 fn test_scimfilter_ne() {
367 assert!(
368 scimfilter::parse("abcd ne \"dcba\"")
369 == Ok(ScimFilter::NotEqual(
370 AttrPath {
371 a: "abcd".to_string(),
372 s: None
373 },
374 Value::String("dcba".to_string())
375 ))
376 );
377 }
378
379 #[test]
380 fn test_scimfilter_co() {
381 assert!(
382 scimfilter::parse("abcd co \"dcba\"")
383 == Ok(ScimFilter::Contains(
384 AttrPath {
385 a: "abcd".to_string(),
386 s: None
387 },
388 Value::String("dcba".to_string())
389 ))
390 );
391 }
392
393 #[test]
394 fn test_scimfilter_sw() {
395 assert!(
396 scimfilter::parse("abcd sw \"dcba\"")
397 == Ok(ScimFilter::StartsWith(
398 AttrPath {
399 a: "abcd".to_string(),
400 s: None
401 },
402 Value::String("dcba".to_string())
403 ))
404 );
405 }
406
407 #[test]
408 fn test_scimfilter_ew() {
409 assert!(
410 scimfilter::parse("abcd ew \"dcba\"")
411 == Ok(ScimFilter::EndsWith(
412 AttrPath {
413 a: "abcd".to_string(),
414 s: None
415 },
416 Value::String("dcba".to_string())
417 ))
418 );
419 }
420
421 #[test]
422 fn test_scimfilter_gt() {
423 assert!(
424 scimfilter::parse("abcd gt \"dcba\"")
425 == Ok(ScimFilter::Greater(
426 AttrPath {
427 a: "abcd".to_string(),
428 s: None
429 },
430 Value::String("dcba".to_string())
431 ))
432 );
433 }
434
435 #[test]
436 fn test_scimfilter_lt() {
437 assert!(
438 scimfilter::parse("abcd lt \"dcba\"")
439 == Ok(ScimFilter::Less(
440 AttrPath {
441 a: "abcd".to_string(),
442 s: None
443 },
444 Value::String("dcba".to_string())
445 ))
446 );
447 }
448
449 #[test]
450 fn test_scimfilter_ge() {
451 assert!(
452 scimfilter::parse("abcd ge \"dcba\"")
453 == Ok(ScimFilter::GreaterOrEqual(
454 AttrPath {
455 a: "abcd".to_string(),
456 s: None
457 },
458 Value::String("dcba".to_string())
459 ))
460 );
461 }
462
463 #[test]
464 fn test_scimfilter_le() {
465 assert!(
466 scimfilter::parse("abcd le \"dcba\"")
467 == Ok(ScimFilter::LessOrEqual(
468 AttrPath {
469 a: "abcd".to_string(),
470 s: None
471 },
472 Value::String("dcba".to_string())
473 ))
474 );
475 }
476
477 #[test]
478 fn test_scimfilter_group() {
479 let f = scimfilter::parse("(abcd eq \"dcba\")");
480 eprintln!("{:?}", f);
481 assert!(
482 f == Ok(ScimFilter::Equal(
483 AttrPath {
484 a: "abcd".to_string(),
485 s: None
486 },
487 Value::String("dcba".to_string())
488 ))
489 );
490 }
491
492 #[test]
493 fn test_scimfilter_not() {
494 let f = scimfilter::parse("not (abcd eq \"dcba\")");
495 eprintln!("{:?}", f);
496
497 assert!(
498 f == Ok(ScimFilter::Not(Box::new(ScimFilter::Equal(
499 AttrPath {
500 a: "abcd".to_string(),
501 s: None
502 },
503 Value::String("dcba".to_string())
504 ))))
505 );
506 }
507
508 #[test]
509 fn test_scimfilter_and() {
510 let f = scimfilter::parse("abcd eq \"dcba\" and bcda ne \"1234\"");
511 eprintln!("{:?}", f);
512
513 assert!(
514 f == Ok(ScimFilter::And(
515 Box::new(ScimFilter::Equal(
516 AttrPath {
517 a: "abcd".to_string(),
518 s: None
519 },
520 Value::String("dcba".to_string())
521 )),
522 Box::new(ScimFilter::NotEqual(
523 AttrPath {
524 a: "bcda".to_string(),
525 s: None
526 },
527 Value::String("1234".to_string())
528 ))
529 ))
530 );
531 }
532
533 #[test]
534 fn test_scimfilter_or() {
535 let f = scimfilter::parse("abcd eq \"dcba\" or bcda ne \"1234\"");
536 eprintln!("{:?}", f);
537
538 assert!(
539 f == Ok(ScimFilter::Or(
540 Box::new(ScimFilter::Equal(
541 AttrPath {
542 a: "abcd".to_string(),
543 s: None
544 },
545 Value::String("dcba".to_string())
546 )),
547 Box::new(ScimFilter::NotEqual(
548 AttrPath {
549 a: "bcda".to_string(),
550 s: None
551 },
552 Value::String("1234".to_string())
553 ))
554 ))
555 );
556 }
557
558 #[test]
559 fn test_scimfilter_complex() {
560 let f = scimfilter::parse("emails[type eq \"work\"]");
561 eprintln!("-- {:?}", f);
562 assert!(f.is_ok());
563
564 let f = scimfilter::parse("emails[type eq \"work\" and value co \"@example.com\"] or ims[type eq \"xmpp\" and value co \"@foo.com\"]");
565 eprintln!("{:?}", f);
566
567 assert_eq!(
568 f,
569 Ok(ScimFilter::Or(
570 Box::new(ScimFilter::Complex(
571 "emails".to_string(),
572 Box::new(ScimComplexFilter::And(
573 Box::new(ScimComplexFilter::Equal(
574 "type".to_string(),
575 Value::String("work".to_string())
576 )),
577 Box::new(ScimComplexFilter::Contains(
578 "value".to_string(),
579 Value::String("@example.com".to_string())
580 ))
581 ))
582 )),
583 Box::new(ScimFilter::Complex(
584 "ims".to_string(),
585 Box::new(ScimComplexFilter::And(
586 Box::new(ScimComplexFilter::Equal(
587 "type".to_string(),
588 Value::String("xmpp".to_string())
589 )),
590 Box::new(ScimComplexFilter::Contains(
591 "value".to_string(),
592 Value::String("@foo.com".to_string())
593 ))
594 ))
595 ))
596 ))
597 );
598 }
599
600 #[test]
601 fn test_scimfilter_precedence_1() {
602 let f = scimfilter::parse("a pr or b pr and c pr or d pr");
603 eprintln!("{:?}", f);
604
605 assert!(
606 f == Ok(ScimFilter::Or(
607 Box::new(ScimFilter::Or(
608 Box::new(ScimFilter::Present(AttrPath {
609 a: "a".to_string(),
610 s: None
611 })),
612 Box::new(ScimFilter::And(
613 Box::new(ScimFilter::Present(AttrPath {
614 a: "b".to_string(),
615 s: None
616 })),
617 Box::new(ScimFilter::Present(AttrPath {
618 a: "c".to_string(),
619 s: None
620 })),
621 )),
622 )),
623 Box::new(ScimFilter::Present(AttrPath {
624 a: "d".to_string(),
625 s: None
626 }))
627 ))
628 );
629 }
630
631 #[test]
632 fn test_scimfilter_precedence_2() {
633 let f = scimfilter::parse("a pr and b pr or c pr and d pr");
634 eprintln!("{:?}", f);
635
636 assert!(
637 f == Ok(ScimFilter::Or(
638 Box::new(ScimFilter::And(
639 Box::new(ScimFilter::Present(AttrPath {
640 a: "a".to_string(),
641 s: None
642 })),
643 Box::new(ScimFilter::Present(AttrPath {
644 a: "b".to_string(),
645 s: None
646 })),
647 )),
648 Box::new(ScimFilter::And(
649 Box::new(ScimFilter::Present(AttrPath {
650 a: "c".to_string(),
651 s: None
652 })),
653 Box::new(ScimFilter::Present(AttrPath {
654 a: "d".to_string(),
655 s: None
656 })),
657 )),
658 ))
659 );
660 }
661
662 #[test]
663 fn test_scimfilter_precedence_3() {
664 let f = scimfilter::parse("a pr and (b pr or c pr) and d pr");
665 eprintln!("{:?}", f);
666
667 assert!(
668 f == Ok(ScimFilter::And(
669 Box::new(ScimFilter::And(
670 Box::new(ScimFilter::Present(AttrPath {
671 a: "a".to_string(),
672 s: None
673 })),
674 Box::new(ScimFilter::Or(
675 Box::new(ScimFilter::Present(AttrPath {
676 a: "b".to_string(),
677 s: None
678 })),
679 Box::new(ScimFilter::Present(AttrPath {
680 a: "c".to_string(),
681 s: None
682 })),
683 )),
684 )),
685 Box::new(ScimFilter::Present(AttrPath {
686 a: "d".to_string(),
687 s: None
688 })),
689 ))
690 );
691 }
692
693 #[test]
694 fn test_scimfilter_precedence_4() {
695 let f = scimfilter::parse("a pr and not (b pr or c pr) and d pr");
696 eprintln!("{:?}", f);
697
698 assert!(
699 f == Ok(ScimFilter::And(
700 Box::new(ScimFilter::And(
701 Box::new(ScimFilter::Present(AttrPath {
702 a: "a".to_string(),
703 s: None
704 })),
705 Box::new(ScimFilter::Not(Box::new(ScimFilter::Or(
706 Box::new(ScimFilter::Present(AttrPath {
707 a: "b".to_string(),
708 s: None
709 })),
710 Box::new(ScimFilter::Present(AttrPath {
711 a: "c".to_string(),
712 s: None
713 })),
714 )))),
715 )),
716 Box::new(ScimFilter::Present(AttrPath {
717 a: "d".to_string(),
718 s: None
719 })),
720 ))
721 );
722 }
723
724 #[test]
725 fn test_scimfilter_quoted_values() {
726 assert_eq!(
727 scimfilter::parse(r#"description eq "text ( ) [ ] 'single' \"escaped\" \\\\consecutive\\\\ \/slash\b\f\n\r\t\u0041 and or not eq ne co sw ew gt lt ge le pr true false""#),
728 Ok(ScimFilter::Equal(
729 AttrPath { a: "description".to_string(), s: None },
730 Value::String("text ( ) [ ] 'single' \"escaped\" \\\\consecutive\\\\ /slash\u{08}\u{0C}\n\r\tA and or not eq ne co sw ew gt lt ge le pr true false".to_string())
731 ))
732 );
733 }
734
735 #[test]
736 fn test_scimfilter_quoted_values_incomplete_escape() {
737 let result = scimfilter::parse(r#"name eq "test\""#);
738 assert!(result.is_err());
739 }
740
741 #[test]
742 fn test_scimfilter_quoted_values_empty() {
743 assert_eq!(
744 scimfilter::parse(r#"name eq """#),
745 Ok(ScimFilter::Equal(
746 AttrPath {
747 a: "name".to_string(),
748 s: None
749 },
750 Value::String("".to_string())
751 ))
752 );
753 }
754}