aboutsummaryrefslogblamecommitdiff
path: root/src/api/s3_router.rs
blob: e02643336e7a8ac08ff10dcd91ddc763fcadbcc7 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
































































































































































































































































































































































































































































































































                                                                                                                                                                
                                                     




















































































































































































































































































                                                                                                                                                        






















                                                                           











                                                    

                                                   

                                                    














                                                                    

                                                            


























































































































































                                                                                                      
                                                                                                         




                                             
                                                                                                        






                                             









                                                                







                                                                                                   



                                                                                                  



                                                                                                  



                                                                                                  







                                                                                                     



                                                                                                     










































































                                                                                                                                             
                                                        













                                                                                                           
                                                                       



























                                                                                                                                     
                                                                 








































































                                                                                                                                                                                                                                                                                                                                                                    
                                                                 










































































































                                                                                                                                           
use crate::error::{Error, OkOrBadRequest};

use std::borrow::Cow;

use hyper::header::HeaderValue;
use hyper::{HeaderMap, Method, Request};

/// This macro is used to generate very repetitive match {} blocks in this module
/// It is _not_ made to be used anywhere else
macro_rules! s3_match {
    (@extract $enum:expr , $param:ident, [ $($endpoint:ident,)* ]) => {{
        // usage: s3_match {@extract my_enum, field_name, [ VariantWithField1, VariantWithField2 ..] }
        // returns Some(field_value), or None if the variant was not one of the listed variants.
        use Endpoint::*;
        match $enum {
            $(
            $endpoint {$param, ..} => Some($param),
            )*
            _ => None
        }
    }};
    (@gen_parser ($keyword:expr, $key:expr, $bucket:expr, $query:expr, $header:expr),
        key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*],
        no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{
        // usage: s3_match {@gen_parser (keyword, key, bucket, query, header),
        //   key: [
        //      SOME_KEYWORD => VariantWithKey,
        //      ...
        //   ],
        //   no_key: [
        //      SOME_KEYWORD => VariantWithoutKey,
        //      ...
        //   ]
        // }
        // See in from_{method} for more detailed usage.
        use Endpoint::*;
        use keywords::*;
        match ($keyword, !$key.is_empty()){
            $(
            ($kw_k, true) if true $(&& $query.$required_k.is_some())? $(&& $header.contains_key($header_k))? => Ok($api_k {
                bucket: $bucket,
                key: $key,
                $($(
                    $param_k: s3_match!(@@parse_param $query, $conv_k, $param_k),
                )*)?
            }),
            )*
            $(
            ($kw_nk, false) $(if $query.$required_nk.is_some())? $(if $header.contains($header_nk))? => Ok($api_nk {
                bucket: $bucket,
                $($(
                    $param_nk: s3_match!(@@parse_param $query, $conv_nk, $param_nk),
                )*)?
            }),
            )*
            (kw, _) => Err(Error::BadRequest(format!("Invalid endpoint: {}", kw)))
        }
    }};

    (@@parse_param $query:expr, query_opt, $param:ident) => {{
        // extract optional query parameter
		$query.$param.take().map(|param| param.into_owned())
    }};
    (@@parse_param $query:expr, query, $param:ident) => {{
        // extract mendatory query parameter
        $query.$param.take().ok_or_bad_request("Missing argument for endpoint")?.into_owned()
    }};
    (@@parse_param $query:expr, opt_parse, $param:ident) => {{
        // extract and parse optional query parameter
        // missing parameter is file, however parse error is reported as an error
		$query.$param
            .take()
            .map(|param| param.parse())
            .transpose()
            .map_err(|_| Error::BadRequest("Failed to parse query parameter".to_owned()))?
    }};
    (@@parse_param $query:expr, parse, $param:ident) => {{
        // extract and parse mandatory query parameter
        // both missing and un-parseable parameters are reported as errors
        $query.$param.take().ok_or_bad_request("Missing argument for endpoint")?
            .parse()
            .map_err(|_| Error::BadRequest("Failed to parse query parameter".to_owned()))?
    }};
    (@func
    $(#[$doc:meta])*
     pub enum Endpoint {
        $(
            $(#[$outer:meta])*
            $variant:ident $({
                bucket: String,
                $($name:ident: $ty:ty,)*
            })?,
        )*
    }) => {
    $(#[$doc])*
        pub enum Endpoint {
            $(
                $(#[$outer])*
                $variant $({
                    bucket: String,
                    $($name: $ty, )*
                })?,
            )*
        }
        impl Endpoint {
            pub fn name(&self) -> &'static str {
                match self {
                    $(Endpoint::$variant $({ $($name: _,)* .. })? => stringify!($variant),)*
                }
            }

            /// Get the bucket the request target. Returns None for requests not related to a bucket.
            pub fn get_bucket(&self) -> Option<&str> {
                match self {
                    $(
                        Endpoint::$variant $({ bucket, $($name: _,)* .. })? => s3_match!{@if ($(bucket $($name)*)?) then (Some(bucket)) else (None)},
                    )*
                }
            }
        }
    };
    (@if ($($cond:tt)+) then ($($then:tt)*) else ($($else:tt)*)) => {
        $($then)*
    };
    (@if () then ($($then:tt)*) else ($($else:tt)*)) => {
        $($else)*
    };
}

s3_match! {@func

/// List of all S3 API endpoints.
///
/// For each endpoint, it contains the parameters this endpoint receive by url (bucket, key and
/// query parameters). Parameters it may receive by header are left out, however headers are
/// considered when required to determine between one endpoint or another (for CopyObject and
/// UploadObject, for instance).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Endpoint {
	AbortMultipartUpload {
		bucket: String,
		key: String,
		upload_id: String,
	},
	CompleteMultipartUpload {
		bucket: String,
		key: String,
		upload_id: String,
	},
	CopyObject {
		bucket: String,
		key: String,
	},
	CreateBucket {
		bucket: String,
	},
	CreateMultipartUpload {
		bucket: String,
		key: String,
	},
	DeleteBucket {
		bucket: String,
	},
	DeleteBucketAnalyticsConfiguration {
		bucket: String,
		id: String,
	},
	DeleteBucketCors {
		bucket: String,
	},
	DeleteBucketEncryption {
		bucket: String,
	},
	DeleteBucketIntelligentTieringConfiguration {
		bucket: String,
		id: String,
	},
	DeleteBucketInventoryConfiguration {
		bucket: String,
		id: String,
	},
	DeleteBucketLifecycle {
		bucket: String,
	},
	DeleteBucketMetricsConfiguration {
		bucket: String,
		id: String,
	},
	DeleteBucketOwnershipControls {
		bucket: String,
	},
	DeleteBucketPolicy {
		bucket: String,
	},
	DeleteBucketReplication {
		bucket: String,
	},
	DeleteBucketTagging {
		bucket: String,
	},
	DeleteBucketWebsite {
		bucket: String,
	},
	DeleteObject {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	DeleteObjects {
		bucket: String,
	},
	DeleteObjectTagging {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	DeletePublicAccessBlock {
		bucket: String,
	},
	GetBucketAccelerateConfiguration {
		bucket: String,
	},
	GetBucketAcl {
		bucket: String,
	},
	GetBucketAnalyticsConfiguration {
		bucket: String,
		id: String,
	},
	GetBucketCors {
		bucket: String,
	},
	GetBucketEncryption {
		bucket: String,
	},
	GetBucketIntelligentTieringConfiguration {
		bucket: String,
		id: String,
	},
	GetBucketInventoryConfiguration {
		bucket: String,
		id: String,
	},
	GetBucketLifecycleConfiguration {
		bucket: String,
	},
	GetBucketLocation {
		bucket: String,
	},
	GetBucketLogging {
		bucket: String,
	},
	GetBucketMetricsConfiguration {
		bucket: String,
		id: String,
	},
	GetBucketNotificationConfiguration {
		bucket: String,
	},
	GetBucketOwnershipControls {
		bucket: String,
	},
	GetBucketPolicy {
		bucket: String,
	},
	GetBucketPolicyStatus {
		bucket: String,
	},
	GetBucketReplication {
		bucket: String,
	},
	GetBucketRequestPayment {
		bucket: String,
	},
	GetBucketTagging {
		bucket: String,
	},
	GetBucketVersioning {
		bucket: String,
	},
	GetBucketWebsite {
		bucket: String,
	},
	/// There are actually many more query parameters, used to add headers to the answer. They were
	/// not added here as they are best handled in a dedicated route.
	GetObject {
		bucket: String,
		key: String,
		part_number: Option<u64>,
		version_id: Option<String>,
	},
	GetObjectAcl {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	GetObjectLegalHold {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	GetObjectLockConfiguration {
		bucket: String,
	},
	GetObjectRetention {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	GetObjectTagging {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	GetObjectTorrent {
		bucket: String,
		key: String,
	},
	GetPublicAccessBlock {
		bucket: String,
	},
	HeadBucket {
		bucket: String,
	},
	HeadObject {
		bucket: String,
		key: String,
		part_number: Option<u64>,
		version_id: Option<String>,
	},
	ListBucketAnalyticsConfigurations {
		bucket: String,
		continuation_token: Option<String>,
	},
	ListBucketIntelligentTieringConfigurations {
		bucket: String,
		continuation_token: Option<String>,
	},
	ListBucketInventoryConfigurations {
		bucket: String,
		continuation_token: Option<String>,
	},
	ListBucketMetricsConfigurations {
		bucket: String,
		continuation_token: Option<String>,
	},
	ListBuckets,
	ListMultipartUploads {
		bucket: String,
		delimiter: Option<char>,
		encoding_type: Option<String>,
		key_marker: Option<String>,
		max_uploads: Option<u64>,
		prefix: Option<String>,
		upload_id_marker: Option<String>,
	},
	ListObjects {
		bucket: String,
		delimiter: Option<char>,
		encoding_type: Option<String>,
		marker: Option<String>,
		max_keys: Option<usize>,
		prefix: Option<String>,
	},
	ListObjectsV2 {
		bucket: String,
		// This value should always be 2. It is not checked when constructing the struct
		list_type: String,
		continuation_token: Option<String>,
		delimiter: Option<char>,
		encoding_type: Option<String>,
		fetch_owner: Option<bool>,
		max_keys: Option<usize>,
		prefix: Option<String>,
		start_after: Option<String>,
	},
	ListObjectVersions {
		bucket: String,
		delimiter: Option<char>,
		encoding_type: Option<String>,
		key_marker: Option<String>,
		max_keys: Option<u64>,
		prefix: Option<String>,
		version_id_marker: Option<String>,
	},
	ListParts {
		bucket: String,
		key: String,
		max_parts: Option<u64>,
		part_number_marker: Option<u64>,
		upload_id: String,
	},
	PutBucketAccelerateConfiguration {
		bucket: String,
	},
	PutBucketAcl {
		bucket: String,
	},
	PutBucketAnalyticsConfiguration {
		bucket: String,
		id: String,
	},
	PutBucketCors {
		bucket: String,
	},
	PutBucketEncryption {
		bucket: String,
	},
	PutBucketIntelligentTieringConfiguration {
		bucket: String,
		id: String,
	},
	PutBucketInventoryConfiguration {
		bucket: String,
		id: String,
	},
	PutBucketLifecycleConfiguration {
		bucket: String,
	},
	PutBucketLogging {
		bucket: String,
	},
	PutBucketMetricsConfiguration {
		bucket: String,
		id: String,
	},
	PutBucketNotificationConfiguration {
		bucket: String,
	},
	PutBucketOwnershipControls {
		bucket: String,
	},
	PutBucketPolicy {
		bucket: String,
	},
	PutBucketReplication {
		bucket: String,
	},
	PutBucketRequestPayment {
		bucket: String,
	},
	PutBucketTagging {
		bucket: String,
	},
	PutBucketVersioning {
		bucket: String,
	},
	PutBucketWebsite {
		bucket: String,
	},
	PutObject {
		bucket: String,
		key: String,
	},
	PutObjectAcl {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	PutObjectLegalHold {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	PutObjectLockConfiguration {
		bucket: String,
	},
	PutObjectRetention {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	PutObjectTagging {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	PutPublicAccessBlock {
		bucket: String,
	},
	RestoreObject {
		bucket: String,
		key: String,
		version_id: Option<String>,
	},
	SelectObjectContent {
		bucket: String,
		key: String,
		// This value should always be 2. It is not checked when constructing the struct
		select_type: String,
	},
	UploadPart {
		bucket: String,
		key: String,
		part_number: u64,
		upload_id: String,
	},
	UploadPartCopy {
		bucket: String,
		key: String,
		part_number: u64,
		upload_id: String,
	},
}}

impl Endpoint {
	/// Determine which S3 endpoint a request is for using the request, and a bucket which was
	/// possibly extracted from the Host header.
	pub fn from_request<T>(req: &Request<T>, bucket: Option<String>) -> Result<Self, Error> {
		let uri = req.uri();
		let path = uri.path().trim_start_matches('/');
		let query = uri.query();
		if bucket.is_none() && path.is_empty() {
			return Ok(Self::ListBuckets);
		}

		let (bucket, key) = if let Some(bucket) = bucket {
			(bucket, path)
		} else {
			path.split_once('/')
				.map(|(b, p)| (b.to_owned(), p.trim_start_matches('/')))
				.unwrap_or((path.to_owned(), ""))
		};

		let key = percent_encoding::percent_decode_str(key)
			.decode_utf8()?
			.into_owned();

		let mut query = QueryParameters::from_query(query.unwrap_or_default())?;

		let res = match *req.method() {
			Method::GET => Self::from_get(bucket, key, &mut query)?,
			Method::HEAD => Self::from_head(bucket, key, &mut query)?,
			Method::POST => Self::from_post(bucket, key, &mut query)?,
			Method::PUT => Self::from_put(bucket, key, &mut query, req.headers())?,
			Method::DELETE => Self::from_delete(bucket, key, &mut query)?,
			_ => return Err(Error::BadRequest("Unknown method".to_owned())),
		};

		if let Some(message) = query.nonempty_message() {
			debug!("Unused query parameter: {}", message)
		}
		Ok(res)
	}

	/// Determine which endpoint a request is for, knowing it is a GET.
	fn from_get(
		bucket: String,
		key: String,
		query: &mut QueryParameters<'_>,
	) -> Result<Self, Error> {
		s3_match! {
			@gen_parser
			(query.keyword.take().unwrap_or_default().as_ref(), key, bucket, query, None),
			key: [
				EMPTY if upload_id => ListParts (query::upload_id, opt_parse::max_parts, opt_parse::part_number_marker),
				EMPTY => GetObject (query_opt::version_id, opt_parse::part_number),
				ACL => GetObjectAcl (query_opt::version_id),
				LEGAL_HOLD => GetObjectLegalHold (query_opt::version_id),
				RETENTION => GetObjectRetention (query_opt::version_id),
				TAGGING => GetObjectTagging (query_opt::version_id),
				TORRENT => GetObjectTorrent,
			],
			no_key: [
				EMPTY if list_type => ListObjectsV2 (query::list_type, query_opt::continuation_token,
													 opt_parse::delimiter, query_opt::encoding_type,
													 opt_parse::fetch_owner, opt_parse::max_keys,
													 query_opt::prefix, query_opt::start_after),
				EMPTY => ListObjects (opt_parse::delimiter, query_opt::encoding_type, query_opt::marker,
									  opt_parse::max_keys, opt_parse::prefix),
				ACCELERATE => GetBucketAccelerateConfiguration,
				ACL => GetBucketAcl,
				ANALYTICS if id => GetBucketAnalyticsConfiguration (query::id),
				ANALYTICS => ListBucketAnalyticsConfigurations (query_opt::continuation_token),
				CORS => GetBucketCors,
				ENCRYPTION => GetBucketEncryption,
				INTELLIGENT_TIERING if id => GetBucketIntelligentTieringConfiguration (query::id),
				INTELLIGENT_TIERING => ListBucketIntelligentTieringConfigurations (query_opt::continuation_token),
				INVENTORY if id => GetBucketInventoryConfiguration (query::id),
				INVENTORY => ListBucketInventoryConfigurations (query_opt::continuation_token),
				LIFECYCLE => GetBucketLifecycleConfiguration,
				LOCATION => GetBucketLocation,
				LOGGING => GetBucketLogging,
				METRICS if id => GetBucketMetricsConfiguration (query::id),
				METRICS => ListBucketMetricsConfigurations (query_opt::continuation_token),
				NOTIFICATION => GetBucketNotificationConfiguration,
				OBJECT_LOCK => GetObjectLockConfiguration,
				OWNERSHIP_CONTROLS => GetBucketOwnershipControls,
				POLICY => GetBucketPolicy,
				POLICY_STATUS => GetBucketPolicyStatus,
				PUBLIC_ACCESS_BLOCK => GetPublicAccessBlock,
				REPLICATION => GetBucketReplication,
				REQUEST_PAYMENT => GetBucketRequestPayment,
				TAGGING => GetBucketTagging,
				UPLOADS => ListMultipartUploads (opt_parse::delimiter, query_opt::encoding_type,
												 query_opt::key_marker, opt_parse::max_uploads,
												 query_opt::prefix, query_opt::upload_id_marker),
				VERSIONING => GetBucketVersioning,
				VERSIONS => ListObjectVersions (opt_parse::delimiter, query_opt::encoding_type,
												query_opt::key_marker, opt_parse::max_keys,
												query_opt::prefix, query_opt::version_id_marker),
				WEBSITE => GetBucketWebsite,
			]
		}
	}

	/// Determine which endpoint a request is for, knowing it is a HEAD.
	fn from_head(
		bucket: String,
		key: String,
		query: &mut QueryParameters<'_>,
	) -> Result<Self, Error> {
		s3_match! {
			@gen_parser
			(query.keyword.take().unwrap_or_default().as_ref(), key, bucket, query, None),
			key: [
				EMPTY => HeadObject(opt_parse::part_number, query_opt::version_id),
			],
			no_key: [
				EMPTY => HeadBucket,
			]
		}
	}

	/// Determine which endpoint a request is for, knowing it is a POST.
	fn from_post(
		bucket: String,
		key: String,
		query: &mut QueryParameters<'_>,
	) -> Result<Self, Error> {
		s3_match! {
			@gen_parser
			(query.keyword.take().unwrap_or_default().as_ref(), key, bucket, query, None),
			key: [
				EMPTY if upload_id  => CompleteMultipartUpload (query::upload_id),
				RESTORE => RestoreObject (query_opt::version_id),
				SELECT => SelectObjectContent (query::select_type),
				UPLOADS => CreateMultipartUpload,
			],
			no_key: [
				DELETE => DeleteObjects,
			]
		}
	}

	/// Determine which endpoint a request is for, knowing it is a PUT.
	fn from_put(
		bucket: String,
		key: String,
		query: &mut QueryParameters<'_>,
		headers: &HeaderMap<HeaderValue>,
	) -> Result<Self, Error> {
		s3_match! {
			@gen_parser
			(query.keyword.take().unwrap_or_default().as_ref(), key, bucket, query, headers),
			key: [
				EMPTY if part_number header "x-amz-copy-source" => UploadPartCopy (parse::part_number, query::upload_id),
				EMPTY header "x-amz-copy-source" => CopyObject,
				EMPTY if part_number => UploadPart (parse::part_number, query::upload_id),
				EMPTY => PutObject,
				ACL => PutObjectAcl (query_opt::version_id),
				LEGAL_HOLD => PutObjectLegalHold (query_opt::version_id),
				RETENTION => PutObjectRetention (query_opt::version_id),
				TAGGING => PutObjectTagging (query_opt::version_id),

			],
			no_key: [
				EMPTY => CreateBucket,
				ACCELERATE => PutBucketAccelerateConfiguration,
				ACL => PutBucketAcl,
				ANALYTICS => PutBucketAnalyticsConfiguration (query::id),
				CORS => PutBucketCors,
				ENCRYPTION => PutBucketEncryption,
				INTELLIGENT_TIERING => PutBucketIntelligentTieringConfiguration(query::id),
				INVENTORY => PutBucketInventoryConfiguration(query::id),
				LIFECYCLE => PutBucketLifecycleConfiguration,
				LOGGING => PutBucketLogging,
				METRICS => PutBucketMetricsConfiguration(query::id),
				NOTIFICATION => PutBucketNotificationConfiguration,
				OBJECT_LOCK => PutObjectLockConfiguration,
				OWNERSHIP_CONTROLS => PutBucketOwnershipControls,
				POLICY => PutBucketPolicy,
				PUBLIC_ACCESS_BLOCK => PutPublicAccessBlock,
				REPLICATION => PutBucketReplication,
				REQUEST_PAYMENT => PutBucketRequestPayment,
				TAGGING => PutBucketTagging,
				VERSIONING => PutBucketVersioning,
				WEBSITE => PutBucketWebsite,
			]
		}
	}

	/// Determine which endpoint a request is for, knowing it is a DELETE.
	fn from_delete(
		bucket: String,
		key: String,
		query: &mut QueryParameters<'_>,
	) -> Result<Self, Error> {
		s3_match! {
			@gen_parser
			(query.keyword.take().unwrap_or_default().as_ref(), key, bucket, query, None),
			key: [
				EMPTY if upload_id => AbortMultipartUpload (query::upload_id),
				EMPTY => DeleteObject (query_opt::version_id),
				TAGGING => DeleteObjectTagging (query_opt::version_id),
			],
			no_key: [
				EMPTY => DeleteBucket,
				ANALYTICS => DeleteBucketAnalyticsConfiguration (query::id),
				CORS => DeleteBucketCors,
				ENCRYPTION => DeleteBucketEncryption,
				INTELLIGENT_TIERING => DeleteBucketIntelligentTieringConfiguration (query::id),
				INVENTORY => DeleteBucketInventoryConfiguration (query::id),
				LIFECYCLE => DeleteBucketLifecycle,
				METRICS => DeleteBucketMetricsConfiguration (query::id),
				OWNERSHIP_CONTROLS => DeleteBucketOwnershipControls,
				POLICY => DeleteBucketPolicy,
				PUBLIC_ACCESS_BLOCK => DeletePublicAccessBlock,
				REPLICATION => DeleteBucketReplication,
				TAGGING => DeleteBucketTagging,
				WEBSITE => DeleteBucketWebsite,
			]
		}
	}

	/// Get the key the request target. Returns None for requests which don't use a key.
	#[allow(dead_code)]
	pub fn get_key(&self) -> Option<&str> {
		s3_match! {
			@extract
			self,
			key,
			[
				AbortMultipartUpload,
				CompleteMultipartUpload,
				CopyObject,
				CreateMultipartUpload,
				DeleteObject,
				DeleteObjectTagging,
				GetObject,
				GetObjectAcl,
				GetObjectLegalHold,
				GetObjectRetention,
				GetObjectTagging,
				GetObjectTorrent,
				HeadObject,
				ListParts,
				PutObject,
				PutObjectAcl,
				PutObjectLegalHold,
				PutObjectRetention,
				PutObjectTagging,
				RestoreObject,
				SelectObjectContent,
				UploadPart,
				UploadPartCopy,
			]
		}
	}

	/// Get the kind of authorization which is required to perform the operation.
	pub fn authorization_type(&self) -> Authorization<'_> {
		let bucket = if let Some(bucket) = self.get_bucket() {
			bucket
		} else {
			return Authorization::None;
		};
		let readonly = s3_match! {
			@extract
			self,
			bucket,
			[
				GetBucketAccelerateConfiguration,
				GetBucketAcl,
				GetBucketAnalyticsConfiguration,
				GetBucketCors,
				GetBucketEncryption,
				GetBucketIntelligentTieringConfiguration,
				GetBucketInventoryConfiguration,
				GetBucketLifecycleConfiguration,
				GetBucketLocation,
				GetBucketLogging,
				GetBucketMetricsConfiguration,
				GetBucketNotificationConfiguration,
				GetBucketOwnershipControls,
				GetBucketPolicy,
				GetBucketPolicyStatus,
				GetBucketReplication,
				GetBucketRequestPayment,
				GetBucketTagging,
				GetBucketVersioning,
				GetObject,
				GetObjectAcl,
				GetObjectLegalHold,
				GetObjectLockConfiguration,
				GetObjectRetention,
				GetObjectTagging,
				GetObjectTorrent,
				GetPublicAccessBlock,
				HeadBucket,
				HeadObject,
				ListBucketAnalyticsConfigurations,
				ListBucketIntelligentTieringConfigurations,
				ListBucketInventoryConfigurations,
				ListBucketMetricsConfigurations,
				ListMultipartUploads,
				ListObjects,
				ListObjectsV2,
				ListObjectVersions,
				ListParts,
				SelectObjectContent,
			]
		}
		.is_some();
		let owner = s3_match! {
			@extract
			self,
			bucket,
			[
				DeleteBucket,
				GetBucketWebsite,
				PutBucketWebsite,
				DeleteBucketWebsite,
			]
		}
		.is_some();
		if readonly {
			Authorization::Read(bucket)
		} else if owner {
			Authorization::Owner(bucket)
		} else {
			Authorization::Write(bucket)
		}
	}
}

/// What kind of authorization is required to perform a given action
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Authorization<'a> {
	/// No authorization is required
	None,
	/// Having Read permission on bucket .0 is required
	Read(&'a str),
	/// Having Write permission on bucket .0 is required
	Write(&'a str),
	/// Having Owner permission on bucket .0 is required
	Owner(&'a str),
}

/// This macro is used to generate part of the code in this module. It must be called only one, and
/// is useless outside of this module.
macro_rules! generateQueryParameters {
    ( $($rest:expr => $name:ident),* ) => {
        /// Struct containing all query parameters used in endpoints. Think of it as an HashMap,
        /// but with keys statically known.
        #[derive(Debug, Default)]
        struct QueryParameters<'a> {
            keyword: Option<Cow<'a, str>>,
            $(
            $name: Option<Cow<'a, str>>,
            )*
        }

        impl<'a> QueryParameters<'a> {
            /// Build this struct from the query part of an URI.
            fn from_query(query: &'a str) -> Result<Self, Error> {
                let mut res: Self = Default::default();
                for (k, v) in url::form_urlencoded::parse(query.as_bytes()) {
                    let repeated = match k.as_ref() {
                        $(
                            $rest => if !v.is_empty() {
                                res.$name.replace(v).is_some()
                            } else {
                                false
                            },
                        )*
                        _ => {
                            if k.starts_with("response-") || k.starts_with("X-Amz-") {
                                false
                            } else if v.as_ref().is_empty() {
                                if res.keyword.replace(k).is_some() {
                                    return Err(Error::BadRequest("Multiple keywords".to_owned()));
                                }
                                continue;
                            } else {
                                debug!("Received an unknown query parameter: '{}'", k);
                                false
                            }
                        }
                    };
                    if repeated {
                        return Err(Error::BadRequest(format!(
                            "Query parameter repeated: '{}'",
                            k
                        )));
                    }
                }
                Ok(res)
            }

            /// Get an error message in case not all parameters where used when extracting them to
            /// build an Enpoint variant
            fn nonempty_message(&self) -> Option<&str> {
                if self.keyword.is_some() {
                    Some("Keyword not used")
                } $(
                    else if self.$name.is_some() {
                        Some(concat!("'", $rest, "'"))
                    }
                )* else {
                    None
                }
            }
        }
    }
}

// parameter name => struct field
generateQueryParameters! {
	"continuation-token" => continuation_token,
	"delimiter" => delimiter,
	"encoding-type" => encoding_type,
	"fetch-owner" => fetch_owner,
	"id" => id,
	"key-marker" => key_marker,
	"list-type" => list_type,
	"marker" => marker,
	"max-keys" => max_keys,
	"max-parts" => max_parts,
	"max-uploads" => max_uploads,
	"partNumber" => part_number,
	"part-number-marker" => part_number_marker,
	"prefix" => prefix,
	"select-type" => select_type,
	"start-after" => start_after,
	"uploadId" => upload_id,
	"upload-id-marker" => upload_id_marker,
	"versionId" => version_id,
	"version-id-marker" => version_id_marker
}

mod keywords {
	//! This module contain all query parameters with no associated value S3 uses to differentiate
	//! endpoints.
	pub const EMPTY: &str = "";

	pub const ACCELERATE: &str = "accelerate";
	pub const ACL: &str = "acl";
	pub const ANALYTICS: &str = "analytics";
	pub const CORS: &str = "cors";
	pub const DELETE: &str = "delete";
	pub const ENCRYPTION: &str = "encryption";
	pub const INTELLIGENT_TIERING: &str = "intelligent-tiering";
	pub const INVENTORY: &str = "inventory";
	pub const LEGAL_HOLD: &str = "legal-hold";
	pub const LIFECYCLE: &str = "lifecycle";
	pub const LOCATION: &str = "location";
	pub const LOGGING: &str = "logging";
	pub const METRICS: &str = "metrics";
	pub const NOTIFICATION: &str = "notification";
	pub const OBJECT_LOCK: &str = "object-lock";
	pub const OWNERSHIP_CONTROLS: &str = "ownershipControls";
	pub const POLICY: &str = "policy";
	pub const POLICY_STATUS: &str = "policyStatus";
	pub const PUBLIC_ACCESS_BLOCK: &str = "publicAccessBlock";
	pub const REPLICATION: &str = "replication";
	pub const REQUEST_PAYMENT: &str = "requestPayment";
	pub const RESTORE: &str = "restore";
	pub const RETENTION: &str = "retention";
	pub const SELECT: &str = "select";
	pub const TAGGING: &str = "tagging";
	pub const TORRENT: &str = "torrent";
	pub const UPLOADS: &str = "uploads";
	pub const VERSIONING: &str = "versioning";
	pub const VERSIONS: &str = "versions";
	pub const WEBSITE: &str = "website";
}

#[cfg(test)]
mod tests {
	use super::*;

	fn parse(
		method: &str,
		uri: &str,
		bucket: Option<String>,
		header: Option<(&str, &str)>,
	) -> Endpoint {
		let mut req = Request::builder().method(method).uri(uri);
		if let Some((k, v)) = header {
			req = req.header(k, v)
		}
		let req = req.body(()).unwrap();

		Endpoint::from_request(&req, bucket).unwrap()
	}

	macro_rules! test_cases {
        ($($method:ident $uri:expr => $variant:ident )*) => {{
            $(
            assert!(
                matches!(
                    parse(test_cases!{@actual_method $method}, $uri, Some("my_bucket".to_owned()), None),
                    Endpoint::$variant { .. }
                )
            );
            assert!(
                matches!(
                    parse(test_cases!{@actual_method $method}, concat!("/my_bucket", $uri), None, None),
                    Endpoint::$variant { .. }
                )
            );

            test_cases!{@auth $method $uri}
            )*
        }};

		(@actual_method HEAD) => {{ "HEAD" }};
		(@actual_method GET) => {{ "GET" }};
		(@actual_method OWNER_GET) => {{ "GET" }};
		(@actual_method PUT) => {{ "PUT" }};
		(@actual_method OWNER_PUT) => {{ "PUT" }};
		(@actual_method POST) => {{ "POST" }};
		(@actual_method DELETE) => {{ "DELETE" }};
		(@actual_method OWNER_DELETE) => {{ "DELETE" }};

        (@auth HEAD $uri:expr) => {{
            assert_eq!(parse("HEAD", concat!("/my_bucket", $uri), None, None).authorization_type(),
                Authorization::Read("my_bucket"))
        }};
        (@auth GET $uri:expr) => {{
            assert_eq!(parse("GET", concat!("/my_bucket", $uri), None, None).authorization_type(),
                Authorization::Read("my_bucket"))
        }};
        (@auth OWNER_GET $uri:expr) => {{
            assert_eq!(parse("GET", concat!("/my_bucket", $uri), None, None).authorization_type(),
                Authorization::Owner("my_bucket"))
        }};
        (@auth PUT $uri:expr) => {{
            assert_eq!(parse("PUT", concat!("/my_bucket", $uri), None, None).authorization_type(),
                Authorization::Write("my_bucket"))
        }};
        (@auth OWNER_PUT $uri:expr) => {{
            assert_eq!(parse("PUT", concat!("/my_bucket", $uri), None, None).authorization_type(),
                Authorization::Owner("my_bucket"))
        }};
        (@auth POST $uri:expr) => {{
            assert_eq!(parse("POST", concat!("/my_bucket", $uri), None, None).authorization_type(),
                Authorization::Write("my_bucket"))
        }};
        (@auth DELETE $uri:expr) => {{
            assert_eq!(parse("DELETE", concat!("/my_bucket", $uri), None, None).authorization_type(),
                Authorization::Write("my_bucket"))
        }};
        (@auth OWNER_DELETE $uri:expr) => {{
            assert_eq!(parse("DELETE", concat!("/my_bucket", $uri), None, None).authorization_type(),
                Authorization::Owner("my_bucket"))
        }};
    }

	#[test]
	fn test_bucket_extraction() {
		assert_eq!(
			parse("GET", "/my/key", Some("my_bucket".to_owned()), None).get_bucket(),
			parse("GET", "/my_bucket/my/key", None, None).get_bucket()
		);
		assert_eq!(
			parse("GET", "/my_bucket/my/key", None, None)
				.get_bucket()
				.unwrap(),
			"my_bucket"
		);
		assert!(parse("GET", "/", None, None).get_bucket().is_none());
	}

	#[test]
	fn test_key() {
		assert_eq!(
			parse("GET", "/my/key", Some("my_bucket".to_owned()), None).get_key(),
			parse("GET", "/my_bucket/my/key", None, None).get_key()
		);
		assert_eq!(
			parse("GET", "/my_bucket/my/key", None, None)
				.get_key()
				.unwrap(),
			"my/key"
		);
		assert_eq!(
			parse("GET", "/my_bucket/my/key?acl", None, None)
				.get_key()
				.unwrap(),
			"my/key"
		);
		assert!(parse("GET", "/my_bucket/?list-type=2", None, None)
			.get_key()
			.is_none());

		assert_eq!(
			parse("GET", "/my_bucket/%26%2B%3F%25%C3%A9/something", None, None)
				.get_key()
				.unwrap(),
			"&+?%é/something"
		);

		/*
		 * this case is failing. We should verify how clients encode space in url
		assert_eq!(
			parse("GET", "/my_bucket/+", None, None).get_key().unwrap(),
			" ");
		 */
	}

	#[test]
	fn invalid_endpoint() {
		let req = Request::builder()
			.method("GET")
			.uri("/bucket/key?website")
			.body(())
			.unwrap();

		assert!(Endpoint::from_request(&req, None).is_err())
	}

	#[test]
	fn test_aws_doc_examples() {
		test_cases!(
			DELETE "/example-object?uploadId=VXBsb2FkIElEIGZvciBlbHZpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZ" => AbortMultipartUpload
			DELETE "/Key+?uploadId=UploadId" => AbortMultipartUpload
			POST "/example-object?uploadId=AAAsb2FkIElEIGZvciBlbHZpbmcncyWeeS1tb3ZpZS5tMnRzIRRwbG9hZA" => CompleteMultipartUpload
			POST "/Key+?uploadId=UploadId" => CompleteMultipartUpload
			PUT "/" => CreateBucket
			POST "/example-object?uploads" => CreateMultipartUpload
			POST "/{Key+}?uploads" => CreateMultipartUpload
			OWNER_DELETE "/" => DeleteBucket
			DELETE "/?analytics&id=list1" => DeleteBucketAnalyticsConfiguration
			DELETE "/?analytics&id=Id" => DeleteBucketAnalyticsConfiguration
			DELETE "/?cors" => DeleteBucketCors
			DELETE "/?encryption" => DeleteBucketEncryption
			DELETE "/?intelligent-tiering&id=Id" => DeleteBucketIntelligentTieringConfiguration
			DELETE "/?inventory&id=list1" => DeleteBucketInventoryConfiguration
			DELETE "/?inventory&id=Id" => DeleteBucketInventoryConfiguration
			DELETE "/?lifecycle" => DeleteBucketLifecycle
			DELETE "/?metrics&id=ExampleMetrics" => DeleteBucketMetricsConfiguration
			DELETE "/?metrics&id=Id" => DeleteBucketMetricsConfiguration
			DELETE "/?ownershipControls" => DeleteBucketOwnershipControls
			DELETE "/?policy" => DeleteBucketPolicy
			DELETE "/?replication" => DeleteBucketReplication
			DELETE "/?tagging" => DeleteBucketTagging
			OWNER_DELETE "/?website" => DeleteBucketWebsite
			DELETE "/my-second-image.jpg" => DeleteObject
			DELETE "/my-third-image.jpg?versionId=UIORUnfndfiufdisojhr398493jfdkjFJjkndnqUifhnw89493jJFJ" => DeleteObject
			DELETE "/Key+?versionId=VersionId" => DeleteObject
			POST "/?delete" => DeleteObjects
			DELETE "/exampleobject?tagging" => DeleteObjectTagging
			DELETE "/{Key+}?tagging&versionId=VersionId" => DeleteObjectTagging
			DELETE "/?publicAccessBlock" => DeletePublicAccessBlock
			GET "/?accelerate" => GetBucketAccelerateConfiguration
			GET "/?acl" => GetBucketAcl
			GET "/?analytics&id=Id" => GetBucketAnalyticsConfiguration
			GET "/?cors" => GetBucketCors
			GET "/?encryption" => GetBucketEncryption
			GET "/?intelligent-tiering&id=Id" => GetBucketIntelligentTieringConfiguration
			GET "/?inventory&id=list1" => GetBucketInventoryConfiguration
			GET "/?inventory&id=Id" => GetBucketInventoryConfiguration
			GET "/?lifecycle" => GetBucketLifecycleConfiguration
			GET "/?location" => GetBucketLocation
			GET "/?logging" => GetBucketLogging
			GET "/?metrics&id=Documents" => GetBucketMetricsConfiguration
			GET "/?metrics&id=Id" => GetBucketMetricsConfiguration
			GET "/?notification" => GetBucketNotificationConfiguration
			GET "/?ownershipControls" => GetBucketOwnershipControls
			GET "/?policy" => GetBucketPolicy
			GET "/?policyStatus" => GetBucketPolicyStatus
			GET "/?replication" => GetBucketReplication
			GET "/?requestPayment" => GetBucketRequestPayment
			GET "/?tagging" => GetBucketTagging
			GET "/?versioning" => GetBucketVersioning
			OWNER_GET "/?website" => GetBucketWebsite
			GET "/my-image.jpg" => GetObject
			GET "/myObject?versionId=3/L4kqtJlcpXroDTDmpUMLUo" => GetObject
			GET "/Junk3.txt?response-cache-control=No-cache&response-content-disposition=attachment%3B%20filename%3Dtesting.txt&response-content-encoding=x-gzip&response-content-language=mi%2C%20en&response-expires=Thu%2C%2001%20Dec%201994%2016:00:00%20GMT" => GetObject
			GET "/Key+?partNumber=1&response-cache-control=ResponseCacheControl&response-content-disposition=ResponseContentDisposition&response-content-encoding=ResponseContentEncoding&response-content-language=ResponseContentLanguage&response-content-type=ResponseContentType&response-expires=ResponseExpires&versionId=VersionId" => GetObject
			GET "/my-image.jpg?acl" => GetObjectAcl
			GET "/my-image.jpg?versionId=3/L4kqtJlcpXroDVBH40Nr8X8gdRQBpUMLUo&acl" => GetObjectAcl
			GET "/{Key+}?acl&versionId=VersionId" => GetObjectAcl
			GET "/{Key+}?legal-hold&versionId=VersionId" => GetObjectLegalHold
			GET "/?object-lock" => GetObjectLockConfiguration
			GET "/{Key+}?retention&versionId=VersionId" => GetObjectRetention
			GET "/example-object?tagging" => GetObjectTagging
			GET "/{Key+}?tagging&versionId=VersionId" => GetObjectTagging
			GET "/quotes/Nelson?torrent" => GetObjectTorrent
			GET "/{Key+}?torrent" => GetObjectTorrent
			GET "/?publicAccessBlock" => GetPublicAccessBlock
			HEAD "/" => HeadBucket
			HEAD "/my-image.jpg" => HeadObject
			HEAD "/my-image.jpg?versionId=3HL4kqCxf3vjVBH40Nrjfkd" => HeadObject
			HEAD "/Key+?partNumber=3&versionId=VersionId" => HeadObject
			GET "/?analytics" => ListBucketAnalyticsConfigurations
			GET "/?analytics&continuation-token=ContinuationToken" => ListBucketAnalyticsConfigurations
			GET "/?intelligent-tiering" => ListBucketIntelligentTieringConfigurations
			GET "/?intelligent-tiering&continuation-token=ContinuationToken" => ListBucketIntelligentTieringConfigurations
			GET "/?inventory" => ListBucketInventoryConfigurations
			GET "/?inventory&continuation-token=ContinuationToken" => ListBucketInventoryConfigurations
			GET "/?metrics" => ListBucketMetricsConfigurations
			GET "/?metrics&continuation-token=ContinuationToken" => ListBucketMetricsConfigurations
			GET "/?uploads&max-uploads=3" => ListMultipartUploads
			GET "/?uploads&delimiter=/" => ListMultipartUploads
			GET "/?uploads&delimiter=/&prefix=photos/2006/" => ListMultipartUploads
			GET "/?uploads&delimiter=D&encoding-type=EncodingType&key-marker=KeyMarker&max-uploads=1&prefix=Prefix&upload-id-marker=UploadIdMarker" => ListMultipartUploads
			GET "/" => ListObjects
			GET "/?prefix=N&marker=Ned&max-keys=40" => ListObjects
			GET "/?delimiter=/" => ListObjects
			GET "/?prefix=photos/2006/&delimiter=/" => ListObjects

			GET "/?delimiter=D&encoding-type=EncodingType&marker=Marker&max-keys=1&prefix=Prefix" => ListObjects
			GET "/?list-type=2" => ListObjectsV2
			GET "/?list-type=2&max-keys=3&prefix=E&start-after=ExampleGuide.pdf" => ListObjectsV2
			GET "/?list-type=2&delimiter=/" => ListObjectsV2
			GET "/?list-type=2&prefix=photos/2006/&delimiter=/" => ListObjectsV2
			GET "/?list-type=2" => ListObjectsV2
			GET "/?list-type=2&continuation-token=1ueGcxLPRx1Tr/XYExHnhbYLgveDs2J/wm36Hy4vbOwM=" => ListObjectsV2
			GET "/?list-type=2&continuation-token=ContinuationToken&delimiter=D&encoding-type=EncodingType&fetch-owner=true&max-keys=1&prefix=Prefix&start-after=StartAfter" => ListObjectsV2
			GET "/?versions" => ListObjectVersions
			GET "/?versions&key-marker=key2" => ListObjectVersions
			GET "/?versions&key-marker=key3&version-id-marker=t46ZenlYTZBnj" => ListObjectVersions
			GET "/?versions&key-marker=key3&version-id-marker=t46Z0menlYTZBnj&max-keys=3" => ListObjectVersions
			GET "/?versions&delimiter=/" => ListObjectVersions
			GET "/?versions&prefix=photos/2006/&delimiter=/" => ListObjectVersions
			GET "/?versions&delimiter=D&encoding-type=EncodingType&key-marker=KeyMarker&max-keys=2&prefix=Prefix&version-id-marker=VersionIdMarker" => ListObjectVersions
			GET "/example-object?uploadId=XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA&max-parts=2&part-number-marker=1" => ListParts
			GET "/Key+?max-parts=2&part-number-marker=2&uploadId=UploadId" => ListParts
			PUT "/?accelerate" => PutBucketAccelerateConfiguration
			PUT "/?acl" => PutBucketAcl
			PUT "/?analytics&id=report1" => PutBucketAnalyticsConfiguration
			PUT "/?analytics&id=Id" => PutBucketAnalyticsConfiguration
			PUT "/?cors" => PutBucketCors
			PUT "/?encryption" => PutBucketEncryption
			PUT "/?intelligent-tiering&id=Id" => PutBucketIntelligentTieringConfiguration
			PUT "/?inventory&id=report1" => PutBucketInventoryConfiguration
			PUT "/?inventory&id=Id" => PutBucketInventoryConfiguration
			PUT "/?lifecycle" => PutBucketLifecycleConfiguration
			PUT "/?logging" => PutBucketLogging
			PUT "/?metrics&id=EntireBucket" => PutBucketMetricsConfiguration
			PUT "/?metrics&id=Id" => PutBucketMetricsConfiguration
			PUT "/?notification" => PutBucketNotificationConfiguration
			PUT "/?ownershipControls" => PutBucketOwnershipControls
			PUT "/?policy" => PutBucketPolicy
			PUT "/?replication" => PutBucketReplication
			PUT "/?requestPayment" => PutBucketRequestPayment
			PUT "/?tagging" => PutBucketTagging
			PUT "/?versioning" => PutBucketVersioning
			OWNER_PUT "/?website" => PutBucketWebsite
			PUT "/my-image.jpg" => PutObject
			PUT "/Key+" => PutObject
			PUT "/my-image.jpg?acl" => PutObjectAcl
			PUT "/my-image.jpg?acl&versionId=3HL4kqtJlcpXroDTDmJ+rmSpXd3dIbrHY+MTRCxf3vjVBH40Nrjfkd" => PutObjectAcl
			PUT "/{Key+}?acl&versionId=VersionId" => PutObjectAcl
			PUT "/{Key+}?legal-hold&versionId=VersionId" => PutObjectLegalHold
			PUT "/?object-lock" => PutObjectLockConfiguration
			PUT "/{Key+}?retention&versionId=VersionId" => PutObjectRetention
			PUT "/object-key?tagging" => PutObjectTagging
			PUT "/{Key+}?tagging&versionId=VersionId" => PutObjectTagging
			PUT "/?publicAccessBlock" => PutPublicAccessBlock
			POST "/object-one.csv?restore" => RestoreObject
			POST "/{Key+}?restore&versionId=VersionId" => RestoreObject
			PUT "/my-movie.m2ts?partNumber=1&uploadId=VCVsb2FkIElEIGZvciBlbZZpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZR" => UploadPart
			PUT "/Key+?partNumber=2&uploadId=UploadId" => UploadPart
		);
		// no bucket, won't work with the rest of the test suite
		assert!(matches!(
			parse("GET", "/", None, None),
			Endpoint::ListBuckets { .. }
		));
		assert!(matches!(
			parse("GET", "/", None, None).authorization_type(),
			Authorization::None
		));

		// require a header
		assert!(matches!(
			parse(
				"PUT",
				"/Key+",
				Some("my_bucket".to_owned()),
				Some(("x-amz-copy-source", "some/key"))
			),
			Endpoint::CopyObject { .. }
		));
		assert!(matches!(
			parse(
				"PUT",
				"/my_bucket/Key+",
				None,
				Some(("x-amz-copy-source", "some/key"))
			),
			Endpoint::CopyObject { .. }
		));
		assert!(matches!(
			parse(
				"PUT",
				"/my_bucket/Key+",
				None,
				Some(("x-amz-copy-source", "some/key"))
			)
			.authorization_type(),
			Authorization::Write("my_bucket")
		));

		// require a header
		assert!(matches!(
			parse(
				"PUT",
				"/Key+?partNumber=2&uploadId=UploadId",
				Some("my_bucket".to_owned()),
				Some(("x-amz-copy-source", "some/key"))
			),
			Endpoint::UploadPartCopy { .. }
		));
		assert!(matches!(
			parse(
				"PUT",
				"/my_bucket/Key+?partNumber=2&uploadId=UploadId",
				None,
				Some(("x-amz-copy-source", "some/key"))
			),
			Endpoint::UploadPartCopy { .. }
		));
		assert!(matches!(
			parse(
				"PUT",
				"/my_bucket/Key+?partNumber=2&uploadId=UploadId",
				None,
				Some(("x-amz-copy-source", "some/key"))
			)
			.authorization_type(),
			Authorization::Write("my_bucket")
		));

		// POST request, but with GET semantic for permissions purpose
		assert!(matches!(
			parse(
				"POST",
				"/{Key+}?select&select-type=2",
				Some("my_bucket".to_owned()),
				None
			),
			Endpoint::SelectObjectContent { .. }
		));
		assert!(matches!(
			parse("POST", "/my_bucket/{Key+}?select&select-type=2", None, None),
			Endpoint::SelectObjectContent { .. }
		));
		assert!(matches!(
			parse("POST", "/my_bucket/{Key+}?select&select-type=2", None, None)
				.authorization_type(),
			Authorization::Read("my_bucket")
		));
	}
}