@@ -41,6 +41,30 @@ impl IndexUrl {
41
41
///
42
42
/// Normalizes non-file URLs by removing trailing slashes for consistency.
43
43
pub fn parse ( path : & str , root_dir : Option < & Path > ) -> Result < Self , IndexUrlError > {
44
+ Self :: parse_with_trailing_slash_policy ( path, root_dir, TrailingSlashPolicy :: Remove )
45
+ }
46
+
47
+ /// Parse an [`IndexUrl`] from a string, relative to an optional root directory.
48
+ ///
49
+ /// If no root directory is provided, relative paths are resolved against the current working
50
+ /// directory.
51
+ ///
52
+ /// Preserves trailing slash if present in `path`.
53
+ pub fn parse_preserving_trailing_slash ( path : & str ) -> Result < Self , IndexUrlError > {
54
+ Self :: parse_with_trailing_slash_policy ( path, None , TrailingSlashPolicy :: Preserve )
55
+ }
56
+
57
+ /// Parse an [`IndexUrl`] from a string, relative to an optional root directory.
58
+ ///
59
+ /// If no root directory is provided, relative paths are resolved against the current working
60
+ /// directory.
61
+ ///
62
+ /// Applies trailing slash policy to non-file URLs.
63
+ fn parse_with_trailing_slash_policy (
64
+ path : & str ,
65
+ root_dir : Option < & Path > ,
66
+ slash_policy : TrailingSlashPolicy ,
67
+ ) -> Result < Self , IndexUrlError > {
44
68
let url = match split_scheme ( path) {
45
69
Some ( ( scheme, ..) ) => {
46
70
match Scheme :: parse ( scheme) {
@@ -67,7 +91,10 @@ impl IndexUrl {
67
91
}
68
92
}
69
93
} ;
70
- Ok ( Self :: from ( url. with_given ( path) ) )
94
+ Ok ( Self :: from_verbatim_url_with_trailing_slash_policy (
95
+ url. with_given ( path) ,
96
+ slash_policy,
97
+ ) )
71
98
}
72
99
73
100
/// Return the root [`Url`] of the index, if applicable.
@@ -91,6 +118,34 @@ impl IndexUrl {
91
118
url. path_segments_mut ( ) . ok ( ) ?. pop_if_empty ( ) . pop ( ) ;
92
119
Some ( url)
93
120
}
121
+
122
+ /// Construct an [`IndexUrl`] from a [`VerbatimUrl`], preserving a trailing
123
+ /// slash if present.
124
+ pub fn from_verbatim_url_preserving_trailing_slash ( url : VerbatimUrl ) -> Self {
125
+ Self :: from_verbatim_url_with_trailing_slash_policy ( url, TrailingSlashPolicy :: Preserve )
126
+ }
127
+
128
+ /// Construct an [`IndexUrl`] from a [`VerbatimUrl`], applying a [`TrailingSlashPolicy`]
129
+ /// to non-file URLs.
130
+ fn from_verbatim_url_with_trailing_slash_policy (
131
+ mut url : VerbatimUrl ,
132
+ slash_policy : TrailingSlashPolicy ,
133
+ ) -> Self {
134
+ if url. scheme ( ) == "file" {
135
+ return Self :: Path ( Arc :: new ( url) ) ;
136
+ }
137
+
138
+ if slash_policy == TrailingSlashPolicy :: Remove {
139
+ if let Ok ( mut path_segments) = url. raw_mut ( ) . path_segments_mut ( ) {
140
+ path_segments. pop_if_empty ( ) ;
141
+ }
142
+ }
143
+ if * url. raw ( ) == * PYPI_URL {
144
+ Self :: Pypi ( Arc :: new ( url) )
145
+ } else {
146
+ Self :: Url ( Arc :: new ( url) )
147
+ }
148
+ }
94
149
}
95
150
96
151
#[ cfg( feature = "schemars" ) ]
@@ -117,18 +172,6 @@ impl IndexUrl {
117
172
}
118
173
}
119
174
120
- /// Return the raw URL for the index with a trailing slash.
121
- pub fn url_with_trailing_slash ( & self ) -> Cow < ' _ , DisplaySafeUrl > {
122
- let path = self . url ( ) . path ( ) ;
123
- if path. ends_with ( '/' ) {
124
- Cow :: Borrowed ( self . url ( ) )
125
- } else {
126
- let mut url = self . url ( ) . clone ( ) ;
127
- url. set_path ( & format ! ( "{path}/" ) ) ;
128
- Cow :: Owned ( url)
129
- }
130
- }
131
-
132
175
/// Convert the index URL into a [`DisplaySafeUrl`].
133
176
pub fn into_url ( self ) -> DisplaySafeUrl {
134
177
match self {
@@ -196,6 +239,16 @@ impl Verbatim for IndexUrl {
196
239
}
197
240
}
198
241
242
+ /// Whether to preserve or remove a trailing slash from a non-file URL.
243
+ #[ derive( Default , Clone , Copy , Eq , PartialEq ) ]
244
+ enum TrailingSlashPolicy {
245
+ /// Preserve trailing slash if present.
246
+ #[ default]
247
+ Preserve ,
248
+ /// Remove trailing slash if present.
249
+ Remove ,
250
+ }
251
+
199
252
/// Checks if a path is disambiguated.
200
253
///
201
254
/// Disambiguated paths are absolute paths, paths with valid schemes,
@@ -270,21 +323,10 @@ impl<'de> serde::de::Deserialize<'de> for IndexUrl {
270
323
}
271
324
272
325
impl From < VerbatimUrl > for IndexUrl {
273
- fn from ( mut url : VerbatimUrl ) -> Self {
274
- if url. scheme ( ) == "file" {
275
- Self :: Path ( Arc :: new ( url) )
276
- } else {
277
- // Remove trailing slashes for consistency. They'll be re-added if necessary when
278
- // querying the Simple API.
279
- if let Ok ( mut path_segments) = url. raw_mut ( ) . path_segments_mut ( ) {
280
- path_segments. pop_if_empty ( ) ;
281
- }
282
- if * url. raw ( ) == * PYPI_URL {
283
- Self :: Pypi ( Arc :: new ( url) )
284
- } else {
285
- Self :: Url ( Arc :: new ( url) )
286
- }
287
- }
326
+ fn from ( url : VerbatimUrl ) -> Self {
327
+ // Remove trailing slashes for consistency. They'll be re-added if necessary when
328
+ // querying the Simple API.
329
+ Self :: from_verbatim_url_with_trailing_slash_policy ( url, TrailingSlashPolicy :: Remove )
288
330
}
289
331
}
290
332
@@ -727,21 +769,40 @@ mod tests {
727
769
}
728
770
729
771
#[ test]
730
- fn test_index_url_with_trailing_slash ( ) {
772
+ fn test_index_url_trailing_slash_policies ( ) {
731
773
let url_with_trailing_slash = DisplaySafeUrl :: parse ( "https://example.com/path/" ) . unwrap ( ) ;
732
-
733
- let index_url_with_given_slash =
734
- IndexUrl :: parse ( "https://example.com/path/" , None ) . unwrap ( ) ;
774
+ let url_without_trailing_slash = DisplaySafeUrl :: parse ( "https://example.com/path" ) . unwrap ( ) ;
775
+ let verbatim_url_with_trailing_slash =
776
+ VerbatimUrl :: from_url ( url_with_trailing_slash. clone ( ) ) ;
777
+ let verbatim_url_without_trailing_slash =
778
+ VerbatimUrl :: from_url ( url_without_trailing_slash. clone ( ) ) ;
779
+
780
+ // Test `From<VerbatimUrl>` implementation.
781
+ // Trailing slash should be removed if present.
735
782
assert_eq ! (
736
- & * index_url_with_given_slash. url_with_trailing_slash( ) ,
737
- & url_with_trailing_slash
783
+ IndexUrl :: from( verbatim_url_with_trailing_slash. clone( ) ) . url( ) ,
784
+ & url_without_trailing_slash
785
+ ) ;
786
+ assert_eq ! (
787
+ IndexUrl :: from( verbatim_url_without_trailing_slash. clone( ) ) . url( ) ,
788
+ & url_without_trailing_slash
738
789
) ;
739
790
740
- let index_url_without_given_slash =
741
- IndexUrl :: parse ( "https://example.com/path" , None ) . unwrap ( ) ;
791
+ // Test `from_verbatim_url_preserving_trailing_slash`.
792
+ // Trailing slash should be preserved if present.
742
793
assert_eq ! (
743
- & * index_url_without_given_slash. url_with_trailing_slash( ) ,
794
+ IndexUrl :: from_verbatim_url_preserving_trailing_slash(
795
+ verbatim_url_with_trailing_slash. clone( )
796
+ )
797
+ . url( ) ,
744
798
& url_with_trailing_slash
745
799
) ;
800
+ assert_eq ! (
801
+ IndexUrl :: from_verbatim_url_preserving_trailing_slash(
802
+ verbatim_url_without_trailing_slash. clone( )
803
+ )
804
+ . url( ) ,
805
+ & url_without_trailing_slash
806
+ ) ;
746
807
}
747
808
}
0 commit comments