urlpattern/
lib.rs

1// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
2//! rust-urlpattern is an implementation of the
3//! [URLPattern standard](https://wicg.github.io/urlpattern) for the Rust
4//! programming language.
5//!
6//! For a usage example, see the [UrlPattern] documentation.
7
8mod canonicalize_and_process;
9mod component;
10mod constructor_parser;
11mod error;
12mod matcher;
13mod parser;
14pub mod quirks;
15mod regexp;
16mod tokenizer;
17
18pub use error::Error;
19use serde::Deserialize;
20use serde::Serialize;
21use url::Url;
22
23use crate::canonicalize_and_process::is_special_scheme;
24use crate::canonicalize_and_process::process_base_url;
25use crate::canonicalize_and_process::special_scheme_default_port;
26use crate::canonicalize_and_process::ProcessType;
27use crate::component::Component;
28use crate::regexp::RegExp;
29
30/// Options to create a URL pattern.
31#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct UrlPatternOptions {
34  pub ignore_case: bool,
35}
36
37/// The structured input used to create a URL pattern.
38#[derive(Debug, Default, Clone, Eq, PartialEq)]
39pub struct UrlPatternInit {
40  pub protocol: Option<String>,
41  pub username: Option<String>,
42  pub password: Option<String>,
43  pub hostname: Option<String>,
44  pub port: Option<String>,
45  pub pathname: Option<String>,
46  pub search: Option<String>,
47  pub hash: Option<String>,
48  pub base_url: Option<Url>,
49}
50
51impl UrlPatternInit {
52  pub fn parse_constructor_string<R: RegExp>(
53    pattern: &str,
54    base_url: Option<Url>,
55  ) -> Result<UrlPatternInit, Error> {
56    let mut init = constructor_parser::parse_constructor_string::<R>(pattern)?;
57    if base_url.is_none() && init.protocol.is_none() {
58      return Err(Error::BaseUrlRequired);
59    }
60    init.base_url = base_url;
61    Ok(init)
62  }
63
64  // Ref: https://wicg.github.io/urlpattern/#process-a-urlpatterninit
65  // TODO: use UrlPatternInit for arguments?
66  #[allow(clippy::too_many_arguments)]
67  fn process(
68    &self,
69    kind: ProcessType,
70    protocol: Option<String>,
71    username: Option<String>,
72    password: Option<String>,
73    hostname: Option<String>,
74    port: Option<String>,
75    pathname: Option<String>,
76    search: Option<String>,
77    hash: Option<String>,
78  ) -> Result<UrlPatternInit, Error> {
79    let mut result = UrlPatternInit {
80      protocol,
81      username,
82      password,
83      hostname,
84      port,
85      pathname,
86      search,
87      hash,
88      base_url: None,
89    };
90
91    let base_url = if let Some(parsed_base_url) = &self.base_url {
92      if self.protocol.is_none() {
93        result.protocol =
94          Some(process_base_url(parsed_base_url.scheme(), &kind));
95      }
96
97      if kind != ProcessType::Pattern
98        && (self.protocol.is_none()
99          && self.hostname.is_none()
100          && self.port.is_none()
101          && self.username.is_none())
102      {
103        result.username =
104          Some(process_base_url(parsed_base_url.username(), &kind));
105      }
106
107      if kind != ProcessType::Pattern
108        && (self.protocol.is_none()
109          && self.hostname.is_none()
110          && self.port.is_none()
111          && self.username.is_none()
112          && self.password.is_none())
113      {
114        result.password = Some(process_base_url(
115          parsed_base_url.password().unwrap_or_default(),
116          &kind,
117        ));
118      }
119
120      if self.protocol.is_none() && self.hostname.is_none() {
121        result.hostname = Some(process_base_url(
122          parsed_base_url.host_str().unwrap_or_default(),
123          &kind,
124        ));
125      }
126
127      if self.protocol.is_none()
128        && self.hostname.is_none()
129        && self.port.is_none()
130      {
131        result.port =
132          Some(process_base_url(url::quirks::port(parsed_base_url), &kind));
133      }
134
135      if self.protocol.is_none()
136        && self.hostname.is_none()
137        && self.port.is_none()
138        && self.pathname.is_none()
139      {
140        result.pathname = Some(process_base_url(
141          url::quirks::pathname(parsed_base_url),
142          &kind,
143        ));
144      }
145
146      if self.protocol.is_none()
147        && self.hostname.is_none()
148        && self.port.is_none()
149        && self.pathname.is_none()
150        && self.search.is_none()
151      {
152        result.search = Some(process_base_url(
153          parsed_base_url.query().unwrap_or_default(),
154          &kind,
155        ));
156      }
157
158      if self.protocol.is_none()
159        && self.hostname.is_none()
160        && self.port.is_none()
161        && self.pathname.is_none()
162        && self.search.is_none()
163        && self.hash.is_none()
164      {
165        result.hash = Some(process_base_url(
166          parsed_base_url.fragment().unwrap_or_default(),
167          &kind,
168        ));
169      }
170
171      Some(parsed_base_url)
172    } else {
173      None
174    };
175
176    if let Some(protocol) = &self.protocol {
177      result.protocol = Some(canonicalize_and_process::process_protocol_init(
178        protocol, &kind,
179      )?);
180    }
181    if let Some(username) = &self.username {
182      result.username = Some(canonicalize_and_process::process_username_init(
183        username, &kind,
184      )?);
185    }
186    if let Some(password) = &self.password {
187      result.password = Some(canonicalize_and_process::process_password_init(
188        password, &kind,
189      )?);
190    }
191    if let Some(hostname) = &self.hostname {
192      result.hostname = Some(canonicalize_and_process::process_hostname_init(
193        hostname, &kind,
194      )?);
195    }
196    if let Some(port) = &self.port {
197      result.port = Some(canonicalize_and_process::process_port_init(
198        port,
199        result.protocol.as_deref(),
200        &kind,
201      )?);
202    }
203    if let Some(pathname) = &self.pathname {
204      result.pathname = Some(pathname.clone());
205
206      if let Some(base_url) = base_url {
207        if !base_url.cannot_be_a_base()
208          && !is_absolute_pathname(pathname, &kind)
209        {
210          let baseurl_path = url::quirks::pathname(base_url);
211          let slash_index = baseurl_path.rfind('/');
212          if let Some(slash_index) = slash_index {
213            let new_pathname = baseurl_path[..=slash_index].to_string();
214            result.pathname =
215              Some(format!("{}{}", new_pathname, result.pathname.unwrap()));
216          }
217        }
218      }
219
220      result.pathname = Some(canonicalize_and_process::process_pathname_init(
221        &result.pathname.unwrap(),
222        result.protocol.as_deref(),
223        &kind,
224      )?);
225    }
226    if let Some(search) = &self.search {
227      result.search = Some(canonicalize_and_process::process_search_init(
228        search, &kind,
229      )?);
230    }
231    if let Some(hash) = &self.hash {
232      result.hash =
233        Some(canonicalize_and_process::process_hash_init(hash, &kind)?);
234    }
235    Ok(result)
236  }
237}
238
239// Ref: https://wicg.github.io/urlpattern/#is-an-absolute-pathname
240fn is_absolute_pathname(
241  input: &str,
242  kind: &canonicalize_and_process::ProcessType,
243) -> bool {
244  if input.is_empty() {
245    return false;
246  }
247  if input.starts_with('/') {
248    return true;
249  }
250  if kind == &canonicalize_and_process::ProcessType::Url {
251    return false;
252  }
253  // TODO: input code point length
254  if input.len() < 2 {
255    return false;
256  }
257
258  input.starts_with("\\/") || input.starts_with("{/")
259}
260
261// Ref: https://wicg.github.io/urlpattern/#urlpattern
262/// A UrlPattern that can be matched against.
263///
264/// # Examples
265///
266/// ```
267/// use urlpattern::UrlPattern;
268/// use urlpattern::UrlPatternInit;
269/// use urlpattern::UrlPatternMatchInput;
270///
271///# fn main() {
272/// // Create the UrlPattern to match against.
273/// let init = UrlPatternInit {
274///   pathname: Some("/users/:id".to_owned()),
275///   ..Default::default()
276/// };
277/// let pattern = <UrlPattern>::parse(init, Default::default()).unwrap();
278///
279/// // Match the pattern against a URL.
280/// let url = "https://example.com/users/123".parse().unwrap();
281/// let result = pattern.exec(UrlPatternMatchInput::Url(url)).unwrap().unwrap();
282/// assert_eq!(result.pathname.groups.get("id").unwrap().as_ref().unwrap(), "123");
283///# }
284/// ```
285#[derive(Debug)]
286pub struct UrlPattern<R: RegExp = regex::Regex> {
287  protocol: Component<R>,
288  username: Component<R>,
289  password: Component<R>,
290  hostname: Component<R>,
291  port: Component<R>,
292  pathname: Component<R>,
293  search: Component<R>,
294  hash: Component<R>,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub enum UrlPatternMatchInput {
299  Init(UrlPatternInit),
300  Url(Url),
301}
302
303impl<R: RegExp> UrlPattern<R> {
304  // Ref: https://wicg.github.io/urlpattern/#dom-urlpattern-urlpattern
305  /// Parse a [UrlPatternInit] into a [UrlPattern].
306  pub fn parse(
307    init: UrlPatternInit,
308    options: UrlPatternOptions,
309  ) -> Result<Self, Error> {
310    Self::parse_internal(init, true, options)
311  }
312
313  pub(crate) fn parse_internal(
314    init: UrlPatternInit,
315    report_regex_errors: bool,
316    options: UrlPatternOptions,
317  ) -> Result<Self, Error> {
318    let mut processed_init = init.process(
319      ProcessType::Pattern,
320      None,
321      None,
322      None,
323      None,
324      None,
325      None,
326      None,
327      None,
328    )?;
329
330    //  If processedInit["protocol"] is a special scheme and processedInit["port"] is its corresponding default port
331    if let Some(protocol) = &processed_init.protocol {
332      if is_special_scheme(protocol) {
333        let default_port = special_scheme_default_port(protocol);
334        if default_port == processed_init.port.as_deref() {
335          processed_init.port = Some(String::new())
336        }
337      }
338    }
339
340    let protocol = Component::compile(
341      processed_init.protocol.as_deref(),
342      canonicalize_and_process::canonicalize_protocol,
343      parser::Options::default(),
344    )?
345    .optionally_transpose_regex_error(report_regex_errors)?;
346
347    let hostname_is_ipv6 = processed_init
348      .hostname
349      .as_deref()
350      .map(hostname_pattern_is_ipv6_address)
351      .unwrap_or(false);
352
353    let hostname = if hostname_is_ipv6 {
354      Component::compile(
355        processed_init.hostname.as_deref(),
356        canonicalize_and_process::canonicalize_ipv6_hostname,
357        parser::Options::hostname(),
358      )?
359      .optionally_transpose_regex_error(report_regex_errors)?
360    } else {
361      Component::compile(
362        processed_init.hostname.as_deref(),
363        canonicalize_and_process::canonicalize_hostname,
364        parser::Options::hostname(),
365      )?
366      .optionally_transpose_regex_error(report_regex_errors)?
367    };
368
369    let compile_options = parser::Options {
370      ignore_case: options.ignore_case,
371      ..Default::default()
372    };
373
374    let pathname = if protocol.protocol_component_matches_special_scheme() {
375      Component::compile(
376        processed_init.pathname.as_deref(),
377        canonicalize_and_process::canonicalize_pathname,
378        parser::Options {
379          ignore_case: options.ignore_case,
380          ..parser::Options::pathname()
381        },
382      )?
383      .optionally_transpose_regex_error(report_regex_errors)?
384    } else {
385      Component::compile(
386        processed_init.pathname.as_deref(),
387        canonicalize_and_process::canonicalize_an_opaque_pathname,
388        compile_options.clone(),
389      )?
390      .optionally_transpose_regex_error(report_regex_errors)?
391    };
392
393    Ok(UrlPattern {
394      protocol,
395      username: Component::compile(
396        processed_init.username.as_deref(),
397        canonicalize_and_process::canonicalize_username,
398        parser::Options::default(),
399      )?
400      .optionally_transpose_regex_error(report_regex_errors)?,
401      password: Component::compile(
402        processed_init.password.as_deref(),
403        canonicalize_and_process::canonicalize_password,
404        parser::Options::default(),
405      )?
406      .optionally_transpose_regex_error(report_regex_errors)?,
407      hostname,
408      port: Component::compile(
409        processed_init.port.as_deref(),
410        |port| canonicalize_and_process::canonicalize_port(port, None),
411        parser::Options::default(),
412      )?
413      .optionally_transpose_regex_error(report_regex_errors)?,
414      pathname,
415      search: Component::compile(
416        processed_init.search.as_deref(),
417        canonicalize_and_process::canonicalize_search,
418        compile_options.clone(),
419      )?
420      .optionally_transpose_regex_error(report_regex_errors)?,
421      hash: Component::compile(
422        processed_init.hash.as_deref(),
423        canonicalize_and_process::canonicalize_hash,
424        compile_options,
425      )?
426      .optionally_transpose_regex_error(report_regex_errors)?,
427    })
428  }
429
430  /// The pattern used to match against the protocol of the URL.
431  pub fn protocol(&self) -> &str {
432    &self.protocol.pattern_string
433  }
434
435  /// The pattern used to match against the username of the URL.
436  pub fn username(&self) -> &str {
437    &self.username.pattern_string
438  }
439
440  /// The pattern used to match against the password of the URL.
441  pub fn password(&self) -> &str {
442    &self.password.pattern_string
443  }
444
445  /// The pattern used to match against the hostname of the URL.
446  pub fn hostname(&self) -> &str {
447    &self.hostname.pattern_string
448  }
449
450  /// The pattern used to match against the port of the URL.
451  pub fn port(&self) -> &str {
452    &self.port.pattern_string
453  }
454
455  /// The pattern used to match against the pathname of the URL.
456  pub fn pathname(&self) -> &str {
457    &self.pathname.pattern_string
458  }
459
460  /// The pattern used to match against the search string of the URL.
461  pub fn search(&self) -> &str {
462    &self.search.pattern_string
463  }
464
465  /// The pattern used to match against the hash fragment of the URL.
466  pub fn hash(&self) -> &str {
467    &self.hash.pattern_string
468  }
469
470  /// Returns whether the URLPattern contains one or more groups which uses regular expression matching.
471  pub fn has_regexp_groups(&self) -> bool {
472    self.protocol.has_regexp_group
473      || self.username.has_regexp_group
474      || self.password.has_regexp_group
475      || self.hostname.has_regexp_group
476      || self.port.has_regexp_group
477      || self.pathname.has_regexp_group
478      || self.search.has_regexp_group
479      || self.hash.has_regexp_group
480  }
481
482  // Ref: https://wicg.github.io/urlpattern/#dom-urlpattern-test
483  /// Test if a given [UrlPatternInput] (with optional base url), matches the
484  /// pattern.
485  pub fn test(&self, input: UrlPatternMatchInput) -> Result<bool, Error> {
486    self.matches(input).map(|res| res.is_some())
487  }
488
489  // Ref: https://wicg.github.io/urlpattern/#dom-urlpattern-exec
490  /// Execute the pattern against a [UrlPatternInput] (with optional base url),
491  /// returning a [UrlPatternResult] if the pattern matches. If the pattern
492  /// doesn't match, returns `None`.
493  pub fn exec(
494    &self,
495    input: UrlPatternMatchInput,
496  ) -> Result<Option<UrlPatternResult>, Error> {
497    self.matches(input)
498  }
499
500  // Ref: https://wicg.github.io/urlpattern/#match
501  fn matches(
502    &self,
503    input: UrlPatternMatchInput,
504  ) -> Result<Option<UrlPatternResult>, Error> {
505    let input = match quirks::parse_match_input(input) {
506      Some(input) => input,
507      None => return Ok(None),
508    };
509
510    let protocol_exec_result = self.protocol.matcher.matches(&input.protocol);
511    let username_exec_result = self.username.matcher.matches(&input.username);
512    let password_exec_result = self.password.matcher.matches(&input.password);
513    let hostname_exec_result = self.hostname.matcher.matches(&input.hostname);
514    let port_exec_result = self.port.matcher.matches(&input.port);
515    let pathname_exec_result = self.pathname.matcher.matches(&input.pathname);
516    let search_exec_result = self.search.matcher.matches(&input.search);
517    let hash_exec_result = self.hash.matcher.matches(&input.hash);
518
519    match (
520      protocol_exec_result,
521      username_exec_result,
522      password_exec_result,
523      hostname_exec_result,
524      port_exec_result,
525      pathname_exec_result,
526      search_exec_result,
527      hash_exec_result,
528    ) {
529      (
530        Some(protocol_exec_result),
531        Some(username_exec_result),
532        Some(password_exec_result),
533        Some(hostname_exec_result),
534        Some(port_exec_result),
535        Some(pathname_exec_result),
536        Some(search_exec_result),
537        Some(hash_exec_result),
538      ) => Ok(Some(UrlPatternResult {
539        protocol: self
540          .protocol
541          .create_match_result(input.protocol.clone(), protocol_exec_result),
542        username: self
543          .username
544          .create_match_result(input.username.clone(), username_exec_result),
545        password: self
546          .password
547          .create_match_result(input.password.clone(), password_exec_result),
548        hostname: self
549          .hostname
550          .create_match_result(input.hostname.clone(), hostname_exec_result),
551        port: self
552          .port
553          .create_match_result(input.port.clone(), port_exec_result),
554        pathname: self
555          .pathname
556          .create_match_result(input.pathname.clone(), pathname_exec_result),
557        search: self
558          .search
559          .create_match_result(input.search.clone(), search_exec_result),
560        hash: self
561          .hash
562          .create_match_result(input.hash.clone(), hash_exec_result),
563      })),
564      _ => Ok(None),
565    }
566  }
567}
568
569// Ref: https://wicg.github.io/urlpattern/#hostname-pattern-is-an-ipv6-address
570fn hostname_pattern_is_ipv6_address(input: &str) -> bool {
571  // TODO: code point length
572  if input.len() < 2 {
573    return false;
574  }
575
576  input.starts_with('[') || input.starts_with("{[") || input.starts_with("\\[")
577}
578
579// Ref: https://wicg.github.io/urlpattern/#dictdef-urlpatternresult
580/// A result of a URL pattern match.
581#[derive(Debug, Clone, PartialEq, Eq)]
582pub struct UrlPatternResult {
583  pub protocol: UrlPatternComponentResult,
584  pub username: UrlPatternComponentResult,
585  pub password: UrlPatternComponentResult,
586  pub hostname: UrlPatternComponentResult,
587  pub port: UrlPatternComponentResult,
588  pub pathname: UrlPatternComponentResult,
589  pub search: UrlPatternComponentResult,
590  pub hash: UrlPatternComponentResult,
591}
592
593// Ref: https://wicg.github.io/urlpattern/#dictdef-urlpatterncomponentresult
594/// A result of a URL pattern match on a single component.
595#[derive(Debug, Clone, PartialEq, Eq)]
596pub struct UrlPatternComponentResult {
597  /// The matched input for this component.
598  pub input: String,
599  /// The values for all named groups in the pattern.
600  pub groups: std::collections::HashMap<String, Option<String>>,
601}
602
603#[cfg(test)]
604mod tests {
605  use regex::Regex;
606  use std::collections::HashMap;
607
608  use serde::Deserialize;
609  use serde::Serialize;
610  use url::Url;
611
612  use crate::quirks;
613  use crate::quirks::StringOrInit;
614  use crate::UrlPatternComponentResult;
615  use crate::UrlPatternOptions;
616  use crate::UrlPatternResult;
617
618  use super::UrlPattern;
619  use super::UrlPatternInit;
620
621  #[derive(Debug, Deserialize)]
622  #[serde(untagged)]
623  #[allow(clippy::large_enum_variant)]
624  enum ExpectedMatch {
625    String(String),
626    MatchResult(MatchResult),
627  }
628
629  #[derive(Debug, Deserialize)]
630  struct ComponentResult {
631    input: String,
632    groups: HashMap<String, Option<String>>,
633  }
634
635  #[allow(clippy::large_enum_variant)]
636  #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
637  #[serde(untagged)]
638  pub enum StringOrInitOrOptions {
639    Options(UrlPatternOptions),
640    StringOrInit(quirks::StringOrInit),
641  }
642
643  #[derive(Debug, Deserialize)]
644  struct TestCase {
645    skip: Option<String>,
646    pattern: Vec<StringOrInitOrOptions>,
647    #[serde(default)]
648    inputs: Vec<quirks::StringOrInit>,
649    expected_obj: Option<quirks::StringOrInit>,
650    expected_match: Option<ExpectedMatch>,
651    #[serde(default)]
652    exactly_empty_components: Vec<String>,
653  }
654
655  #[derive(Debug, Deserialize)]
656  struct MatchResult {
657    #[serde(deserialize_with = "deserialize_match_result_inputs")]
658    #[serde(default)]
659    inputs: Option<(quirks::StringOrInit, Option<String>)>,
660
661    protocol: Option<ComponentResult>,
662    username: Option<ComponentResult>,
663    password: Option<ComponentResult>,
664    hostname: Option<ComponentResult>,
665    port: Option<ComponentResult>,
666    pathname: Option<ComponentResult>,
667    search: Option<ComponentResult>,
668    hash: Option<ComponentResult>,
669  }
670
671  fn deserialize_match_result_inputs<'de, D>(
672    deserializer: D,
673  ) -> Result<Option<(quirks::StringOrInit, Option<String>)>, D::Error>
674  where
675    D: serde::Deserializer<'de>,
676  {
677    #[derive(Debug, Deserialize)]
678    #[serde(untagged)]
679    enum MatchResultInputs {
680      OneArgument((quirks::StringOrInit,)),
681      TwoArguments(quirks::StringOrInit, String),
682    }
683
684    let res = Option::<MatchResultInputs>::deserialize(deserializer)?;
685    Ok(match res {
686      Some(MatchResultInputs::OneArgument((a,))) => Some((a, None)),
687      Some(MatchResultInputs::TwoArguments(a, b)) => Some((a, Some(b))),
688      None => None,
689    })
690  }
691
692  fn test_case(case: TestCase) {
693    let mut input = quirks::StringOrInit::Init(Default::default());
694    let mut base_url = None;
695    let mut options = None;
696
697    for (i, pattern_input) in case.pattern.into_iter().enumerate() {
698      match pattern_input {
699        StringOrInitOrOptions::StringOrInit(str_or_init) => {
700          if i == 0 {
701            input = str_or_init;
702          } else if i == 1 {
703            base_url = match str_or_init {
704              StringOrInit::String(str) => Some(str.clone()),
705              StringOrInit::Init(_) => None,
706            };
707          } else if matches!(&case.expected_obj, Some(StringOrInit::String(s)) if s == "error")
708          {
709            println!("Expected not to pass due to bad parameters");
710            println!("✅ Passed");
711            return;
712          } else {
713            panic!("Failed to parse testcase");
714          }
715        }
716        StringOrInitOrOptions::Options(opts) => {
717          options = Some(opts);
718        }
719      }
720    }
721
722    println!("\n=====");
723    println!(
724      "Pattern: {}, {}",
725      serde_json::to_string(&input).unwrap(),
726      serde_json::to_string(&base_url).unwrap()
727    );
728    if let Some(options) = &options {
729      println!("Options: {}", serde_json::to_string(&options).unwrap(),);
730    }
731
732    if let Some(reason) = case.skip {
733      println!("🟠 Skipping: {reason}");
734      return;
735    }
736
737    let init_res = quirks::process_construct_pattern_input(
738      input.clone(),
739      base_url.as_deref(),
740    );
741
742    let res = init_res.and_then(|init_res| {
743      UrlPattern::<Regex>::parse(init_res, options.unwrap_or_default())
744    });
745    let expected_obj = match case.expected_obj {
746      Some(StringOrInit::String(s)) if s == "error" => {
747        assert!(res.is_err());
748        println!("✅ Passed");
749        return;
750      }
751      Some(StringOrInit::String(_)) => unreachable!(),
752      Some(StringOrInit::Init(init)) => {
753        let base_url = init.base_url.map(|url| url.parse().unwrap());
754        UrlPatternInit {
755          protocol: init.protocol,
756          username: init.username,
757          password: init.password,
758          hostname: init.hostname,
759          port: init.port,
760          pathname: init.pathname,
761          search: init.search,
762          hash: init.hash,
763          base_url,
764        }
765      }
766      None => UrlPatternInit::default(),
767    };
768    let pattern = res.expect("failed to parse pattern");
769
770    if let StringOrInit::Init(quirks::UrlPatternInit {
771      base_url: Some(url),
772      ..
773    }) = &input
774    {
775      base_url = Some(url.clone())
776    }
777
778    macro_rules! assert_field {
779      ($field:ident) => {{
780        let mut expected = expected_obj.$field;
781        if expected == None {
782          if case
783            .exactly_empty_components
784            .contains(&stringify!($field).to_owned())
785          {
786            expected = Some(String::new())
787          } else if let StringOrInit::Init(quirks::UrlPatternInit {
788            $field: Some($field),
789            ..
790          }) = &input
791          {
792            expected = Some($field.to_owned())
793          } else if {
794            if let StringOrInit::Init(init) = &input {
795              match stringify!($field) {
796                "protocol" => false,
797                "hostname" => init.protocol.is_some(),
798                "port" => init.protocol.is_some() || init.hostname.is_some(),
799                "username" => false,
800                "password" => false,
801                "pathname" => {
802                  init.protocol.is_some()
803                    || init.hostname.is_some()
804                    || init.port.is_some()
805                }
806                "search" => {
807                  init.protocol.is_some()
808                    || init.hostname.is_some()
809                    || init.port.is_some()
810                    || init.pathname.is_some()
811                }
812                "hash" => {
813                  init.protocol.is_some()
814                    || init.hostname.is_some()
815                    || init.port.is_some()
816                    || init.pathname.is_some()
817                    || init.search.is_some()
818                }
819                _ => unreachable!(),
820              }
821            } else {
822              false
823            }
824          } {
825            expected = Some("*".to_owned())
826          } else if let Some(base_url) =
827            base_url.as_ref().and_then(|base_url| {
828              if !matches!(stringify!($field), "username" | "password") {
829                Some(base_url)
830              } else {
831                None
832              }
833            })
834          {
835            let base_url = Url::parse(base_url).unwrap();
836            let field = url::quirks::$field(&base_url);
837            let field: String = match stringify!($field) {
838              "protocol" if !field.is_empty() => {
839                field[..field.len() - 1].to_owned()
840              }
841              "search" | "hash" if !field.is_empty() => field[1..].to_owned(),
842              _ => field.to_owned(),
843            };
844            expected = Some(field)
845          } else {
846            expected = Some("*".to_owned())
847          }
848        }
849
850        let expected = expected.unwrap();
851        let pattern = &pattern.$field.pattern_string;
852
853        assert_eq!(
854          &expected,
855          pattern,
856          "pattern for {} does not match",
857          stringify!($field)
858        );
859      }};
860    }
861
862    assert_field!(protocol);
863    assert_field!(username);
864    assert_field!(password);
865    assert_field!(hostname);
866    assert_field!(port);
867    assert_field!(pathname);
868    assert_field!(search);
869    assert_field!(hash);
870
871    let input = case.inputs.first().cloned();
872    let base_url = case.inputs.get(1).map(|input| match input {
873      StringOrInit::String(str) => str.clone(),
874      StringOrInit::Init(_) => unreachable!(),
875    });
876
877    println!(
878      "Input: {}, {}",
879      serde_json::to_string(&input).unwrap(),
880      serde_json::to_string(&base_url).unwrap(),
881    );
882
883    let input = input.unwrap_or_else(|| StringOrInit::Init(Default::default()));
884
885    let expected_input = (input.clone(), base_url.clone());
886
887    let match_input = quirks::process_match_input(input, base_url.as_deref());
888
889    if let Some(ExpectedMatch::String(s)) = &case.expected_match {
890      if s == "error" {
891        assert!(match_input.is_err());
892        println!("✅ Passed");
893        return;
894      }
895    };
896
897    let input = match_input.expect("failed to parse match input");
898
899    if input.is_none() {
900      assert!(case.expected_match.is_none());
901      println!("✅ Passed");
902      return;
903    }
904    let test_res = if let Some((input, _)) = input.clone() {
905      pattern.test(input)
906    } else {
907      Ok(false)
908    };
909    let exec_res = if let Some((input, _)) = input.clone() {
910      pattern.exec(input)
911    } else {
912      Ok(None)
913    };
914    if let Some(ExpectedMatch::String(s)) = &case.expected_match {
915      if s == "error" {
916        assert!(test_res.is_err());
917        assert!(exec_res.is_err());
918        println!("✅ Passed");
919        return;
920      }
921    };
922
923    let expected_match = case.expected_match.map(|x| match x {
924      ExpectedMatch::String(_) => unreachable!(),
925      ExpectedMatch::MatchResult(x) => x,
926    });
927
928    let test = test_res.unwrap();
929    let actual_match = exec_res.unwrap();
930
931    assert_eq!(
932      expected_match.is_some(),
933      test,
934      "pattern.test result is not correct"
935    );
936
937    let expected_match = match expected_match {
938      Some(x) => x,
939      None => {
940        assert!(actual_match.is_none(), "expected match to be None");
941        println!("✅ Passed");
942        return;
943      }
944    };
945
946    let actual_match = actual_match.expect("expected match to be Some");
947
948    let expected_inputs = expected_match.inputs.unwrap_or(expected_input);
949
950    let (_, inputs) = input.unwrap();
951
952    assert_eq!(inputs, expected_inputs, "expected inputs to be identical");
953
954    let exactly_empty_components = case.exactly_empty_components;
955
956    macro_rules! convert_result {
957      ($component:ident) => {
958        expected_match
959          .$component
960          .map(|c| UrlPatternComponentResult {
961            input: c.input,
962            groups: c.groups,
963          })
964          .unwrap_or_else(|| {
965            let mut groups = HashMap::new();
966            if !exactly_empty_components
967              .contains(&stringify!($component).to_owned())
968            {
969              groups.insert("0".to_owned(), Some("".to_owned()));
970            }
971            UrlPatternComponentResult {
972              input: "".to_owned(),
973              groups,
974            }
975          })
976      };
977    }
978
979    let expected_result = UrlPatternResult {
980      protocol: convert_result!(protocol),
981      username: convert_result!(username),
982      password: convert_result!(password),
983      hostname: convert_result!(hostname),
984      port: convert_result!(port),
985      pathname: convert_result!(pathname),
986      search: convert_result!(search),
987      hash: convert_result!(hash),
988    };
989
990    assert_eq!(
991      actual_match, expected_result,
992      "pattern.exec result is not correct"
993    );
994
995    println!("✅ Passed");
996  }
997
998  #[test]
999  fn test_cases() {
1000    let testdata = include_str!("./testdata/urlpatterntestdata.json");
1001    let cases: Vec<TestCase> = serde_json::from_str(testdata).unwrap();
1002    for case in cases {
1003      test_case(case);
1004    }
1005  }
1006
1007  #[test]
1008  fn issue26() {
1009    UrlPattern::<Regex>::parse(
1010      UrlPatternInit {
1011        pathname: Some("/:foo.".to_owned()),
1012        ..Default::default()
1013      },
1014      Default::default(),
1015    )
1016    .unwrap();
1017  }
1018
1019  #[test]
1020  fn issue46() {
1021    quirks::process_construct_pattern_input(
1022      quirks::StringOrInit::String(":café://:foo".to_owned()),
1023      None,
1024    )
1025    .unwrap();
1026  }
1027
1028  #[test]
1029  fn has_regexp_group() {
1030    let pattern = <UrlPattern>::parse(
1031      UrlPatternInit {
1032        pathname: Some("/:foo.".to_owned()),
1033        ..Default::default()
1034      },
1035      Default::default(),
1036    )
1037    .unwrap();
1038    assert!(!pattern.has_regexp_groups());
1039
1040    let pattern = <UrlPattern>::parse(
1041      UrlPatternInit {
1042        pathname: Some("/(.*?)".to_owned()),
1043        ..Default::default()
1044      },
1045      Default::default(),
1046    )
1047    .unwrap();
1048    assert!(pattern.has_regexp_groups());
1049  }
1050}