Skip to main content

haste_fhir_client/
url.rs

1use derivative::Derivative;
2use haste_fhir_operation_error::derive::OperationOutcomeError;
3use haste_reflect::{MetaValue, derive::Reflect};
4use std::{borrow::Cow, collections::HashMap, fmt::Display};
5
6static RESULT_PARAMETERS: &[&str] = &[
7    "_count",
8    "_offset",
9    "_total",
10    "_sort",
11    "_include",
12    "_revinclude",
13    "_summary",
14    "_elements",
15    "_contained",
16    "_containedType",
17    "_since",
18    "_type",
19    "_format",
20    "_pretty",
21];
22
23/// One `name[:modifier]` segment of a chained parameter's name.
24#[derive(Debug, Clone, PartialEq, Eq, Reflect)]
25#[fhir_type = "BackboneElement"]
26pub struct ChainLink {
27    pub name: String,
28    pub modifier: Option<String>,
29}
30
31impl ChainLink {
32    /// Parses one segment, rejecting an empty name or modifier and a second
33    /// modifier (`:b`, `a:`, `a:b:c`).
34    fn parse(segment: &str) -> Result<Self, ParseError> {
35        let (name, modifier) = match segment.split_once(':') {
36            Some((name, modifier)) => (name, Some(modifier)),
37            None => (segment, None),
38        };
39
40        if name.is_empty() || modifier.is_some_and(|m| m.is_empty() || m.contains(':')) {
41            return Err(ParseError::InvalidParameter(segment.to_string()));
42        }
43
44        Ok(ChainLink {
45            name: name.to_string(),
46            modifier: modifier.map(str::to_string),
47        })
48    }
49}
50
51/// A search parameter as written in the URL.
52///
53/// `name` and `modifier` are the first segment and `chains` the rest, each
54/// with its own modifier: `subject:Patient.name:exact` is `subject` with
55/// modifier `Patient`, then `name` with modifier `exact`.
56#[derive(Derivative, Clone, Reflect)]
57#[fhir_type = "BackboneElement"]
58#[derivative(Debug)]
59pub struct Parameter {
60    pub name: String,
61    /// The values as written after `=`, one per unescaped comma, with their
62    /// escapes (`\,` `\|` `\$` `\\`) still in place. Read them through
63    /// [`Parameter::values`] or [`Parameter::segmented`], which resolve the
64    /// escapes once the parameter's type says what a `|` or `$` means; build
65    /// one from literal text with [`escape`].
66    #[derivative(Debug(format_with = "crate::redact"))]
67    pub value: Vec<String>,
68    pub modifier: Option<String>,
69    pub chains: Vec<ChainLink>,
70}
71
72/// Whether `value` holds an escape to resolve: one memchr, the common case
73/// being none.
74fn has_escape(value: &str) -> bool {
75    value.contains('\\')
76}
77
78/// `value` with every escape (`\,` `\|` `\$` `\\`) resolved to its literal
79/// character (<https://hl7.org/fhir/R4/search.html#escaping>); borrowed when
80/// it has none. A backslash before any other character stays as written.
81fn unescape(value: &str) -> Cow<'_, str> {
82    if !has_escape(value) {
83        return Cow::Borrowed(value);
84    }
85    let mut out = String::with_capacity(value.len());
86    let mut chars = value.chars();
87    while let Some(c) = chars.next() {
88        if c != '\\' {
89            out.push(c);
90        } else if let Some(next) = chars.next() {
91            if !matches!(next, ',' | '|' | '$' | '\\') {
92                out.push('\\');
93            }
94            out.push(next);
95        } else {
96            out.push(c);
97        }
98    }
99    Cow::Owned(out)
100}
101
102/// `literal` escaped so that it reads back as itself: the inverse of what
103/// [`Parameter::values`] resolves. For building a parameter from text that
104/// was never in a URL.
105#[must_use]
106pub fn escape(literal: &str) -> Cow<'_, str> {
107    if !literal.contains([',', '|', '$', '\\']) {
108        return Cow::Borrowed(literal);
109    }
110    let mut out = String::with_capacity(literal.len() + 2);
111    for c in literal.chars() {
112        if matches!(c, ',' | '|' | '$' | '\\') {
113            out.push('\\');
114        }
115        out.push(c);
116    }
117    Cow::Owned(out)
118}
119
120/// One value's segments between unescaped `separator`s, each with its escapes
121/// resolved: `system|code` of a token, `value|system|code` of a quantity.
122///
123/// A segment borrows from the value unless it held an escape. An empty value
124/// is one empty segment, as with `str::split`.
125pub struct Segments<'a> {
126    source: &'a str,
127    rest: Option<&'a str>,
128    separator: char,
129    escaped: bool,
130}
131
132impl<'a> Segments<'a> {
133    /// The segments of `value` as written in a URL.
134    #[must_use]
135    pub fn of(value: &'a str, separator: char) -> Self {
136        Segments {
137            source: value,
138            rest: Some(value),
139            separator,
140            escaped: has_escape(value),
141        }
142    }
143
144    /// The value as written, for an error that names it.
145    #[must_use]
146    pub fn source(&self) -> &'a str {
147        self.source
148    }
149
150    /// The pieces as written, their escapes still in place.
151    fn raw(mut self) -> impl Iterator<Item = &'a str> {
152        std::iter::from_fn(move || self.next_raw())
153    }
154
155    /// The next raw piece, its escapes still in place. A value without a
156    /// backslash splits with `str::find`.
157    fn next_raw(&mut self) -> Option<&'a str> {
158        let current = self.rest?;
159        let split = if self.escaped {
160            let mut escaped = false;
161            current.char_indices().find_map(|(i, c)| {
162                if escaped {
163                    escaped = false;
164                    None
165                } else if c == '\\' {
166                    escaped = true;
167                    None
168                } else {
169                    (c == self.separator).then_some(i)
170                }
171            })
172        } else {
173            current.find(self.separator)
174        };
175        if let Some(i) = split {
176            self.rest = Some(&current[i + self.separator.len_utf8()..]);
177            Some(&current[..i])
178        } else {
179            self.rest = None;
180            Some(current)
181        }
182    }
183}
184
185impl<'a> Iterator for Segments<'a> {
186    type Item = Cow<'a, str>;
187
188    fn next(&mut self) -> Option<Self::Item> {
189        let raw = self.next_raw()?;
190        Some(if self.escaped {
191            unescape(raw)
192        } else {
193            Cow::Borrowed(raw)
194        })
195    }
196}
197
198impl Parameter {
199    /// Whether this parameter follows a reference (`subject.name`).
200    #[must_use]
201    pub fn is_chained(&self) -> bool {
202        !self.chains.is_empty()
203    }
204
205    /// The parameter's name as written in a URL: `subject:Patient.name:exact`.
206    #[must_use]
207    pub fn key(&self) -> String {
208        let segments = std::iter::once((&self.name, &self.modifier))
209            .chain(self.chains.iter().map(|link| (&link.name, &link.modifier)));
210
211        let mut key = String::new();
212        for (i, (name, modifier)) in segments.enumerate() {
213            if i > 0 {
214                key.push('.');
215            }
216            key.push_str(name);
217            if let Some(modifier) = modifier {
218                key.push(':');
219                key.push_str(modifier);
220            }
221        }
222        key
223    }
224
225    /// The values as written after `=` in a URL: the pieces joined by commas,
226    /// their escapes intact, so a parsed query serialises to what was read.
227    #[must_use]
228    pub fn value_string(&self) -> String {
229        self.value.join(",")
230    }
231
232    /// Every value with its escapes resolved. Borrowed from the parameter
233    /// when none has an escape, the common case.
234    #[must_use]
235    pub fn values(&self) -> Cow<'_, [String]> {
236        if self.value.iter().any(|value| has_escape(value)) {
237            Cow::Owned(
238                self.value
239                    .iter()
240                    .map(|value| unescape(value).into_owned())
241                    .collect(),
242            )
243        } else {
244            Cow::Borrowed(&self.value)
245        }
246    }
247
248    /// Each value with its escapes resolved, borrowed when it has none. For
249    /// a parser that keeps a slice of the value.
250    pub fn each_value(&self) -> impl Iterator<Item = Cow<'_, str>> {
251        self.value.iter().map(|value| unescape(value))
252    }
253
254    /// Each value split on unescaped `separator`s, every segment with its
255    /// escapes resolved: `'|'` for a token's `system|code`.
256    pub fn segmented(&self, separator: char) -> impl Iterator<Item = Segments<'_>> {
257        self.value
258            .iter()
259            .map(move |value| Segments::of(value, separator))
260    }
261
262    fn parse(key: &str, value: &str) -> Result<Self, ParseError> {
263        let mut segments = key.split('.').map(ChainLink::parse);
264        // `split` always yields at least one segment.
265        let first = segments
266            .next()
267            .ok_or_else(|| ParseError::InvalidParameter(key.to_string()))??;
268
269        Ok(Parameter {
270            name: first.name,
271            modifier: first.modifier,
272            // The pieces keep their escapes: which of `|` and `$` separate is
273            // only known with the parameter's type.
274            value: Segments::of(value, ',').raw().map(str::to_string).collect(),
275            chains: segments.collect::<Result<_, _>>()?,
276        })
277    }
278}
279
280impl From<(String, Vec<String>)> for Parameter {
281    fn from(tuple: (String, Vec<String>)) -> Self {
282        Parameter {
283            name: tuple.0,
284            value: tuple.1,
285            modifier: None,
286            chains: Vec::new(),
287        }
288    }
289}
290
291impl From<(String, Vec<String>, String)> for Parameter {
292    fn from(tuple: (String, Vec<String>, String)) -> Self {
293        Parameter {
294            name: tuple.0,
295            value: tuple.1,
296            modifier: Some(tuple.2),
297            chains: Vec::new(),
298        }
299    }
300}
301
302/// Represent both resource parameters IE Patient.name and
303/// result parameters IE _count
304#[derive(Derivative, Clone)]
305#[derivative(Debug = "transparent")]
306pub enum ParsedParameter {
307    #[derivative(Debug = "transparent")]
308    Result(Parameter),
309    #[derivative(Debug = "transparent")]
310    Resource(Parameter),
311}
312
313impl<T: Into<Parameter>> From<T> for ParsedParameter {
314    fn from(param: T) -> Self {
315        let param = param.into();
316        if RESULT_PARAMETERS.contains(&param.name.as_str()) {
317            ParsedParameter::Result(param)
318        } else {
319            ParsedParameter::Resource(param)
320        }
321    }
322}
323
324impl ParsedParameter {
325    #[must_use]
326    pub fn name(&self) -> &str {
327        match self {
328            ParsedParameter::Result(p) | ParsedParameter::Resource(p) => &p.name,
329        }
330    }
331}
332
333impl MetaValue for ParsedParameter {
334    fn fields(&self) -> Vec<&'static str> {
335        match self {
336            ParsedParameter::Result(p) | ParsedParameter::Resource(p) => p.fields(),
337        }
338    }
339
340    fn get_field<'a>(&'a self, field: &str) -> Option<&'a dyn MetaValue> {
341        match self {
342            ParsedParameter::Result(p) | ParsedParameter::Resource(p) => p.get_field(field),
343        }
344    }
345
346    fn get_field_mut<'a>(&'a mut self, field: &str) -> Option<&'a mut dyn MetaValue> {
347        match self {
348            ParsedParameter::Result(p) | ParsedParameter::Resource(p) => p.get_field_mut(field),
349        }
350    }
351
352    fn get_index(&self, index: usize) -> Option<&dyn MetaValue> {
353        match self {
354            ParsedParameter::Result(p) | ParsedParameter::Resource(p) => p.get_index(index),
355        }
356    }
357
358    fn get_index_mut(&mut self, index: usize) -> Option<&mut dyn MetaValue> {
359        match self {
360            ParsedParameter::Result(p) | ParsedParameter::Resource(p) => p.get_index_mut(index),
361        }
362    }
363
364    fn flatten(&self) -> Vec<&dyn MetaValue> {
365        match self {
366            ParsedParameter::Result(p) | ParsedParameter::Resource(p) => p.flatten(),
367        }
368    }
369
370    fn as_any(&self) -> &dyn std::any::Any {
371        match self {
372            ParsedParameter::Result(p) | ParsedParameter::Resource(p) => p.as_any(),
373        }
374    }
375
376    fn fhir_type(&self) -> &'static str {
377        match self {
378            ParsedParameter::Result(p) | ParsedParameter::Resource(p) => p.fhir_type(),
379        }
380    }
381
382    fn is_many(&self) -> bool {
383        false
384    }
385}
386
387#[derive(Debug, OperationOutcomeError)]
388pub enum ParseError {
389    #[fatal(
390        code = "invalid",
391        diagnostic = "Error parsing query parameters '{arg0}'"
392    )]
393    InvalidParameter(String),
394}
395
396impl Display for ParseError {
397    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398        match self {
399            ParseError::InvalidParameter(param) => {
400                write!(f, "Invalid query parameter: {param}")
401            }
402        }
403    }
404}
405
406impl std::error::Error for ParseError {}
407
408#[derive(Derivative, Clone)]
409#[derivative(Debug = "transparent")]
410pub struct ParsedParameters(Vec<ParsedParameter>);
411
412impl<T: Into<ParsedParameter>> From<Vec<T>> for ParsedParameters {
413    fn from(params: Vec<T>) -> Self {
414        let parsed_params = params.into_iter().map(std::convert::Into::into).collect();
415        ParsedParameters(parsed_params)
416    }
417}
418
419impl ParsedParameters {
420    #[must_use]
421    pub fn new(params: Vec<ParsedParameter>) -> Self {
422        Self(params)
423    }
424    #[must_use]
425    pub fn parameters(&self) -> &Vec<ParsedParameter> {
426        &self.0
427    }
428    #[must_use]
429    pub fn owned_parameters(self) -> Vec<ParsedParameter> {
430        self.0
431    }
432    #[must_use]
433    pub fn get(&self, name: &str) -> Option<&ParsedParameter> {
434        self.0.iter().find(|p| match p {
435            ParsedParameter::Resource(param) | ParsedParameter::Result(param) => param.name == name,
436        })
437    }
438}
439
440impl MetaValue for ParsedParameters {
441    fn fields(&self) -> Vec<&'static str> {
442        todo!()
443    }
444
445    fn get_field<'a>(&'a self, field: &str) -> Option<&'a dyn MetaValue> {
446        if let Some(p) = self.get(field) {
447            Some(p)
448        } else {
449            None
450        }
451    }
452
453    fn get_field_mut<'a>(&'a mut self, _field: &str) -> Option<&'a mut dyn MetaValue> {
454        None
455    }
456
457    fn get_index(&self, _index: usize) -> Option<&dyn MetaValue> {
458        None
459    }
460
461    fn get_index_mut(&mut self, _index: usize) -> Option<&mut dyn MetaValue> {
462        None
463    }
464
465    fn flatten(&self) -> Vec<&dyn MetaValue> {
466        vec![]
467    }
468
469    fn as_any(&self) -> &dyn std::any::Any {
470        self
471    }
472
473    fn fhir_type(&self) -> &'static str {
474        "BackboneElement"
475    }
476
477    fn is_many(&self) -> bool {
478        true
479    }
480}
481
482impl TryFrom<&str> for ParsedParameters {
483    type Error = ParseError;
484    fn try_from(query_string: &str) -> Result<Self, ParseError> {
485        let mut query_string = query_string;
486        if query_string.is_empty() {
487            return Ok(Self(vec![]));
488        }
489
490        if query_string.starts_with('?') {
491            query_string = &query_string[1..];
492        }
493
494        let pairs = query_string
495            .split('&')
496            .map(|pair| {
497                pair.split_once('=')
498                    .ok_or_else(|| ParseError::InvalidParameter(pair.to_string()))
499            })
500            .collect::<Result<Vec<(&str, &str)>, ParseError>>()?;
501
502        Self::try_from(pairs.as_slice())
503    }
504}
505
506impl TryFrom<&HashMap<String, String>> for ParsedParameters {
507    type Error = ParseError;
508    fn try_from(query_params: &HashMap<String, String>) -> Result<Self, ParseError> {
509        let pairs: Vec<(&String, &String)> = query_params.iter().collect();
510
511        Self::try_from(pairs.as_slice())
512    }
513}
514
515/// Query pairs in order. A name that repeats stays one parameter per pair,
516/// which a search reads as every one having to match; a map cannot carry a
517/// repeat, a slice can.
518impl<Name: AsRef<str>, Value: AsRef<str>> TryFrom<&[(Name, Value)]> for ParsedParameters {
519    type Error = ParseError;
520    fn try_from(pairs: &[(Name, Value)]) -> Result<Self, ParseError> {
521        let params = pairs
522            .iter()
523            .map(|(name, value)| {
524                Parameter::parse(name.as_ref(), value.as_ref()).map(ParsedParameter::from)
525            })
526            .collect::<Result<Vec<ParsedParameter>, ParseError>>()?;
527
528        Ok(Self(params))
529    }
530}
531
532/// The comparison prefix of a number, date or quantity value. A value
533/// without one compares as `eq`.
534#[derive(Debug, Clone, Copy, PartialEq, Eq)]
535pub enum Prefix {
536    Eq,
537    Ne,
538    Gt,
539    Lt,
540    Ge,
541    Le,
542    Sa,
543    Eb,
544    Ap,
545}
546
547impl Prefix {
548    /// The prefix `code` names, if any.
549    #[must_use]
550    pub fn parse(code: &str) -> Option<Self> {
551        Some(match code {
552            "eq" => Prefix::Eq,
553            "ne" => Prefix::Ne,
554            "gt" => Prefix::Gt,
555            "lt" => Prefix::Lt,
556            "ge" => Prefix::Ge,
557            "le" => Prefix::Le,
558            "sa" => Prefix::Sa,
559            "eb" => Prefix::Eb,
560            "ap" => Prefix::Ap,
561            _ => return None,
562        })
563    }
564}
565
566/// Splits the comparison prefix off a number, date or quantity value. A
567/// value without one compares as `eq`; a bare prefix is a value.
568#[must_use]
569pub fn parse_prefix(value: &str) -> (Prefix, &str) {
570    match (value.get(..2).and_then(Prefix::parse), value.get(2..)) {
571        (Some(prefix), Some(rest)) if !rest.is_empty() => (prefix, rest),
572        _ => (Prefix::Eq, value),
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    #[test]
581    fn test_parse_prefix() {
582        let cases = vec![
583            ("lt5.0", (Prefix::Lt, "5.0")),
584            ("le10", (Prefix::Le, "10")),
585            ("gt3.14", (Prefix::Gt, "3.14")),
586            ("ge2.71", (Prefix::Ge, "2.71")),
587            ("eq42", (Prefix::Eq, "42")),
588            ("ne0", (Prefix::Ne, "0")),
589            ("sa2020", (Prefix::Sa, "2020")),
590            ("eb2020-01", (Prefix::Eb, "2020-01")),
591            ("ap70", (Prefix::Ap, "70")),
592            ("ne", (Prefix::Eq, "ne")),
593            ("é5", (Prefix::Eq, "é5")),
594            ("5.0", (Prefix::Eq, "5.0")),
595            ("10", (Prefix::Eq, "10")),
596        ];
597
598        for (input, expected) in cases {
599            let result = parse_prefix(input);
600            assert_eq!(result, expected, "Failed for input: {input}");
601        }
602    }
603
604    #[test]
605    fn test_split_unescaped() {
606        let split = |v| Segments::of(v, ',').raw().collect::<Vec<_>>();
607        assert_eq!(split("a,b"), ["a", "b"]);
608        assert_eq!(split(r"a\,b"), [r"a\,b"]);
609        assert_eq!(split(r"a\\,b"), [r"a\\", "b"]);
610        assert_eq!(split(""), [""]);
611        assert_eq!(split("é,ü"), ["é", "ü"]);
612    }
613
614    #[test]
615    fn test_unescape() {
616        assert_eq!(unescape(r"a\,b\|c\$d\\e"), r"a,b|c$d\e");
617        assert_eq!(unescape(r"\x"), r"\x");
618        assert_eq!(unescape("plain"), "plain");
619        assert_eq!(unescape("trailing\\"), "trailing\\");
620        assert!(matches!(unescape("plain"), Cow::Borrowed(_)));
621    }
622
623    #[test]
624    fn test_escape_round_trips() {
625        for literal in ["plain", "a,b|c$d\\e", r"\x", "é|ü"] {
626            assert_eq!(unescape(&escape(literal)), literal);
627        }
628        assert!(matches!(escape("plain"), Cow::Borrowed(_)));
629        assert_eq!(escape(r"a\,b"), r"a\\\,b");
630    }
631
632    #[test]
633    fn test_segments() {
634        let segments = |v| Segments::of(v, '|').collect::<Vec<_>>();
635        assert_eq!(segments("a|b"), ["a", "b"]);
636        assert_eq!(segments(r"a\|b|c"), ["a|b", "c"]);
637        assert_eq!(segments(r"x\\|y\|z"), [r"x\", "y|z"]);
638        assert_eq!(segments("|"), ["", ""]);
639        assert_eq!(segments(""), [""]);
640        assert_eq!(segments("é|ü"), ["é", "ü"]);
641        // Without an escape every segment borrows.
642        assert!(Segments::of("a|b", '|').all(|segment| matches!(segment, Cow::Borrowed(_))));
643        assert_eq!(Segments::of("a|b", '|').source(), "a|b");
644    }
645
646    #[test]
647    fn test_parse_escaped_values() {
648        let parameter = |q| {
649            let params = ParsedParameters::try_from(q).unwrap();
650            let ParsedParameter::Resource(p) = params.get("p").unwrap() else {
651                panic!("expected a resource parameter")
652            };
653            p.clone()
654        };
655        // The pieces keep their escapes.
656        assert_eq!(parameter(r"p=Smith\,Jones").value, [r"Smith\,Jones"]);
657        assert_eq!(parameter("p=Smith,Jones").value, ["Smith", "Jones"]);
658        assert_eq!(
659            parameter(r"p=http://a\|b|1,x").value,
660            [r"http://a\|b|1", "x"]
661        );
662        assert_eq!(parameter(r"p=a\\,b").value, [r"a\\", "b"]);
663
664        // The accessors resolve them.
665        assert_eq!(&*parameter(r"p=Smith\,Jones").values(), ["Smith,Jones"]);
666        assert_eq!(&*parameter(r"p=a\\,b").values(), [r"a\", "b"]);
667        assert!(matches!(
668            parameter("p=Smith,Jones").values(),
669            Cow::Borrowed(_)
670        ));
671        assert_eq!(
672            parameter(r"p=a\,b,c").each_value().collect::<Vec<_>>(),
673            ["a,b", "c"]
674        );
675        assert_eq!(
676            parameter(r"p=http://a\|b|1,x")
677                .segmented('|')
678                .map(Iterator::collect::<Vec<_>>)
679                .collect::<Vec<_>>(),
680            [vec!["http://a|b", "1"], vec!["x"]]
681        );
682    }
683
684    #[test]
685    fn test_value_string_round_trips() {
686        for query in [
687            r"p=Smith\,Jones",
688            "p=Smith,Jones",
689            r"p=a\|b|1,x\,y",
690            r"p=a\\,b",
691        ] {
692            let params = ParsedParameters::try_from(query).unwrap();
693            let ParsedParameter::Resource(p) = params.get("p").unwrap() else {
694                panic!("expected a resource parameter")
695            };
696            assert_eq!(format!("p={}", p.value_string()), query);
697        }
698    }
699
700    #[test]
701    fn test_parse_parameters() {
702        let query_string = "?name=John,Doe&_count=10&address.city=NewYork&status:exact=active";
703        let parsed_params = ParsedParameters::try_from(query_string).unwrap();
704
705        assert_eq!(parsed_params.parameters().len(), 4);
706
707        match parsed_params.get("name") {
708            Some(ParsedParameter::Resource(param)) => {
709                assert_eq!(param.name, "name");
710                assert_eq!(param.value, vec!["John", "Doe"]);
711                assert!(param.modifier.is_none());
712                assert!(param.chains.is_empty());
713            }
714            _ => panic!("Expected Resource parameter"),
715        }
716
717        match parsed_params.get("_count") {
718            Some(ParsedParameter::Result(param)) => {
719                assert_eq!(param.name, "_count");
720                assert_eq!(param.value, vec!["10"]);
721                assert!(param.modifier.is_none());
722                assert!(param.chains.is_empty());
723            }
724            _ => panic!("Expected Result parameter"),
725        }
726
727        match parsed_params.get("address") {
728            Some(ParsedParameter::Resource(param)) => {
729                assert_eq!(param.name, "address");
730                assert_eq!(param.value, vec!["NewYork"]);
731                assert!(param.modifier.is_none());
732                assert_eq!(
733                    param.chains,
734                    vec![ChainLink {
735                        name: "city".to_string(),
736                        modifier: None
737                    }]
738                );
739            }
740            _ => panic!("Expected Resource parameter"),
741        }
742
743        match parsed_params.get("status") {
744            Some(ParsedParameter::Resource(param)) => {
745                assert_eq!(param.name, "status");
746                assert_eq!(param.value, vec!["active"]);
747                assert_eq!(param.modifier, Some("exact".to_string()));
748                assert!(param.chains.is_empty());
749            }
750            _ => panic!("Expected Resource parameter"),
751        }
752    }
753}