1pub fn location_of_link(link: &str) -> Option<String> {
27 let link = link.trim();
28 let (scheme, rest) = link.split_once(':')?;
29 let scheme_ok = !scheme.is_empty()
30 && scheme.starts_with(|c: char| c.is_ascii_alphabetic())
31 && scheme
32 .chars()
33 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'));
34 if !scheme_ok {
35 return None;
36 }
37 let rest = rest.split('#').next().unwrap_or("");
38 let web = scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https");
39 let location = match rest.strip_prefix("//") {
40 Some(after) if web => match after.find(['/', '?']) {
41 Some(at) => after[at..].to_string(),
43 None => String::new(),
44 },
45 Some(after) => after.to_string(),
47 None => rest.to_string(),
48 };
49 Some(normalised(&location))
50}
51
52fn normalised(location: &str) -> String {
54 let trimmed = location.trim_start_matches('/');
55 if trimmed.is_empty() {
56 "/".to_string()
57 } else {
58 format!("/{trimmed}")
59 }
60}
61
62pub fn link_among<I, S>(args: I) -> Option<String>
66where
67 I: IntoIterator<Item = S>,
68 S: AsRef<str>,
69{
70 args.into_iter()
71 .map(|arg| arg.as_ref().to_string())
72 .find(|arg| arg.contains("://") && location_of_link(arg).is_some())
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn a_web_links_path_and_query_are_the_location() {
81 assert_eq!(
82 location_of_link("https://example.com/orders/42?tab=items"),
83 Some("/orders/42?tab=items".to_string())
84 );
85 assert_eq!(
86 location_of_link("http://localhost:8080/a"),
87 Some("/a".to_string()),
88 "a port belongs to the authority"
89 );
90 }
91
92 #[test]
93 fn a_web_link_with_no_path_is_the_root() {
94 assert_eq!(
95 location_of_link("https://example.com"),
96 Some("/".to_string())
97 );
98 assert_eq!(
99 location_of_link("https://example.com/"),
100 Some("/".to_string())
101 );
102 assert_eq!(
103 location_of_link("https://example.com?ref=mail"),
104 Some("/?ref=mail".to_string())
105 );
106 }
107
108 #[test]
109 fn a_custom_schemes_host_is_the_first_segment() {
110 assert_eq!(
111 location_of_link("myapp://orders/42"),
112 Some("/orders/42".to_string())
113 );
114 assert_eq!(
115 location_of_link("myapp://settings"),
116 Some("/settings".to_string())
117 );
118 }
119
120 #[test]
121 fn a_custom_link_without_slashes_or_with_three_names_the_same_place() {
122 assert_eq!(
123 location_of_link("myapp:orders/42"),
124 Some("/orders/42".to_string())
125 );
126 assert_eq!(
127 location_of_link("myapp:///orders/42"),
128 Some("/orders/42".to_string())
129 );
130 assert_eq!(location_of_link("myapp://"), Some("/".to_string()));
131 }
132
133 #[test]
134 fn the_fragment_is_dropped() {
135 assert_eq!(
136 location_of_link("https://example.com/a?x=1#top"),
137 Some("/a?x=1".to_string())
138 );
139 }
140
141 #[test]
142 fn what_is_not_a_link_is_none() {
143 assert_eq!(location_of_link(""), None);
144 assert_eq!(location_of_link("/orders/42"), None, "no scheme");
145 assert_eq!(
146 location_of_link("42:orders"),
147 None,
148 "a scheme starts with a letter"
149 );
150 assert_eq!(location_of_link("not a link"), None);
151 }
152
153 #[test]
154 fn the_link_among_a_programs_arguments_is_the_first_that_looks_like_one() {
155 assert_eq!(
156 link_among(["frus-demo", "--verbose", "myapp://orders/42", "other://x"]),
157 Some("myapp://orders/42".to_string())
158 );
159 assert_eq!(link_among(["frus-demo", "--verbose"]), None);
160 assert_eq!(link_among(["frus-demo", "C:\\Users\\me"]), None);
161 }
162}