1mod 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#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct UrlPatternOptions {
34 pub ignore_case: bool,
35}
36
37#[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 #[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
239fn 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 if input.len() < 2 {
255 return false;
256 }
257
258 input.starts_with("\\/") || input.starts_with("{/")
259}
260
261#[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 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 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 pub fn protocol(&self) -> &str {
432 &self.protocol.pattern_string
433 }
434
435 pub fn username(&self) -> &str {
437 &self.username.pattern_string
438 }
439
440 pub fn password(&self) -> &str {
442 &self.password.pattern_string
443 }
444
445 pub fn hostname(&self) -> &str {
447 &self.hostname.pattern_string
448 }
449
450 pub fn port(&self) -> &str {
452 &self.port.pattern_string
453 }
454
455 pub fn pathname(&self) -> &str {
457 &self.pathname.pattern_string
458 }
459
460 pub fn search(&self) -> &str {
462 &self.search.pattern_string
463 }
464
465 pub fn hash(&self) -> &str {
467 &self.hash.pattern_string
468 }
469
470 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 pub fn test(&self, input: UrlPatternMatchInput) -> Result<bool, Error> {
486 self.matches(input).map(|res| res.is_some())
487 }
488
489 pub fn exec(
494 &self,
495 input: UrlPatternMatchInput,
496 ) -> Result<Option<UrlPatternResult>, Error> {
497 self.matches(input)
498 }
499
500 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
569fn hostname_pattern_is_ipv6_address(input: &str) -> bool {
571 if input.len() < 2 {
573 return false;
574 }
575
576 input.starts_with('[') || input.starts_with("{[") || input.starts_with("\\[")
577}
578
579#[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#[derive(Debug, Clone, PartialEq, Eq)]
596pub struct UrlPatternComponentResult {
597 pub input: String,
599 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}