scim_proto/
filter.rs

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    // Uri: Option<String>,
10    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
110// separator()* "(" e:term() ")" separator()* { e }
111
112peg::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}