api-tools 0.6.1

An API tools library for Rust
Documentation
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
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <style>html, body {
  margin: 0;
  padding: 0;
}

.app {
  margin: 10px;
  padding: 0;
}

.files-list {
  margin: 10px 0 0;
  width: 100%;
  border-collapse: collapse;
}
.files-list__head {
  border: 1px solid #999;
}
.files-list__head > tr > th {
  padding: 10px;
  border: 1px solid #999;
  text-align: left;
  font-weight: normal;
  background: #ddd;
}
.files-list__body {
}
.files-list__file {
  cursor: pointer;
}
.files-list__file:hover {
  background: #ccf;
}
.files-list__file > td {
  padding: 10px;
  border: 1px solid #999;
}
.files-list__file > td:first-child::before {
  content: '\01F4C4';
  margin-right: 1em;
}
.files-list__file_low {
  background: #fcc;
}
.files-list__file_medium {
  background: #ffc;
}
.files-list__file_high {
  background: #cfc;
}
.files-list__file_folder > td:first-child::before {
  content: '\01F4C1';
  margin-right: 1em;
}

.file-header {
  border: 1px solid #999;
  display: flex;
  justify-content: space-between;
  align-items: center;
  position: sticky;
  top: 0;
  background: white;
}

.file-header__back {
  margin: 10px;
  cursor: pointer;
  flex-shrink: 0;
  flex-grow: 0;
  text-decoration: underline;
  color: #338;
}

.file-header__name {
  margin: 10px;
  flex-shrink: 2;
  flex-grow: 2;
}

.file-header__stat {
  margin: 10px;
  flex-shrink: 0;
  flex-grow: 0;
}

.file-content {
  margin: 10px 0 0;
  border: 1px solid #999;
  padding: 10px;
  counter-reset: line;
  display: flex;
  flex-direction: column;
}

.code-line::before {
    content: counter(line);
    margin-right: 10px;
}
.code-line {
  margin: 0;
  padding: 0.3em;
  height: 1em;
  counter-increment: line;
}
.code-line_covered {
  background: #cfc;
}
.code-line_uncovered {
  background: #fcc;
}
</style>
</head>
<body>
    <div id="root"></div>
    <script>
        var data = {"files":[{"path":["/","Users","fabien","lab","rust","api-tools","src","lib.rs"],"content":"//! # Api Tools - A toolkit for API in Rust\n//!\n//! Toolkit for API in Rust\n//!\n//! API Tools is a Rust library providing utilities for developing robust, consistent, and secure APIs.\n//! It offers ready-to-use layers, extractors, error handling, and helpers designed to simplify API development,\n//! especially with the Axum framework.\n//! The toolkit aims to standardize common API patterns and reduce boilerplate in your Rust projects.\n//!\n//! ## Features list\n//!\n//! | Name         | Description                       | Default |\n//! | ------------ | --------------------------------- | :-----: |\n//! | `axum`       | Enable Axum feature               |   ❌    |\n//! | `prometheus` | Enable Prometheus metrics feature |   ❌    |\n//! | `full`       | Enable all features               |   ❌    |\n//!\n//! ## Components\n//!\n//! ### Value objects\n//!\n//! | Name          | Description                                                                                |\n//! | ------------- | ------------------------------------------------------------------------------------------ |\n//! | `UtcDateTime` | A wrapper around `chrono::DateTime` to handle date and time values in UTC                  |\n//! | `Timezone`    | A wrapper around `chrono_tz::Tz` to handle time zones                                      |\n//! | `Pagination`  | A struct to handle pagination parameters, including page number, page size and total count |\n//! | `QuerySort`   | A struct to handle sorting query parameters, including field and direction                 |\n//!\n//! ### Security\n//!\n//! | Name          | Description                                                                                |\n//! |---------------|----------------------------------|\n//! | `Jwt` | A wrapper for JWT generation and parsing |\n//!\n//! ### Axum\n//!\n//! #### Layers\n//!\n//! | Name                   | Description                                                                                                                              |\n//! | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |\n//! | `BasicAuthLayer`       | Provides HTTP Basic Authentication middleware for protecting routes with username and password                                           |\n//! | `CorsLayer`            | Adds Cross-Origin Resource Sharing (CORS) headers to responses, allowing or restricting resource sharing between different origins       |\n//! | `HttpErrorsLayer`      | Middleware for intercepting and customizing HTTP error responses, enabling standardized error handling across your API                   |\n//! | `LoggerLayer`          | Logs incoming requests and outgoing responses, useful for debugging and monitoring API activity                                          |\n//! | `RequestId`            | Middleware that generates and attaches a unique request identifier (UUID) to each incoming request for traceability                      |\n//! | `TimeLimiterLayer`     | Middleware that restricts API usage to specific time slots. Outside of these allowed periods, it returns a 503 Service Unavailable error |\n//! | `PrometheusLayer`      | Middleware that collects and exposes Prometheus-compatible metrics for monitoring API performance and usage                              |\n//! | `SecurityHeadersLayer` | Middleware add security headers like (CSP, etc.)                                                                                         |\n//!\n//! ##### Utility functions\n//!\n//! | Name                  | Description                                                              |\n//! | --------------------- | ------------------------------------------------------------------------ |\n//! | `body_from_parts`     | Construct a response body from `Parts`, status code, message and headers |\n//! | `header_value_to_str` | Convert `HeaderValue` to `\u0026str`                                          |\n//!\n//! #### Extractors\n//!\n//! | Name               | Description                                                            |\n//! | ------------------ | ---------------------------------------------------------------------- |\n//! | `ExtractRequestId` | Extracts the unique request identifier (UUID) from the request headers |\n//! | `Path`             | Extracts and deserializes path parameters from the request URL         |\n//! | `Query`            | Extracts and deserializes query string parameters from the request URL |\n//!\n//! #### Response helpers\n//!\n//! | Name               | Description                                                                                                 |\n//! | ------------------ | ----------------------------------------------------------------------------------------------------------- |\n//! | `ApiSuccess`       | Represents a successful API response (Status code and data in JSON). It implements the `IntoResponse` trait |\n//! | `ApiError`         | Represents a list of HTTP errors                                                                            |\n//! | `ApiErrorResponse` | Encapsulates the details of an API error response, including the status code and the error message          |\n//!\n//! #### Handlers\n//!\n//! | Name                | Description                                                                                       |\n//! | ------------------- | ------------------------------------------------------------------------------------------------- |\n//! | `PrometheusHandler` | Handler that exposes Prometheus metrics endpoint, allowing metrics scraping by Prometheus servers |\n\n#[macro_use]\nextern crate tracing;\n\npub mod security;\npub mod server;\npub mod value_objects;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","security","jwt","access_token.rs"],"content":"//! Access token entity\n\nuse crate::{server::axum::response::ApiError, value_objects::datetime::UtcDateTime};\nuse axum::{extract::FromRequestParts, http::request::Parts};\nuse hyper::{HeaderMap, header};\nuse serde::Deserialize;\n\n/// Access Token Value represents the value of the access token\npub type AccessTokenValue = String;\n\n/// Access Token\n#[derive(Debug, Clone, PartialEq, Deserialize)]\npub struct AccessToken {\n    /// Token\n    pub token: AccessTokenValue,\n\n    /// Expiration time\n    pub expired_at: UtcDateTime,\n}\n\nimpl AccessToken {\n    /// Create a new access token\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// use api_tools::security::jwt::access_token::AccessToken;\n    /// use api_tools::value_objects::datetime::UtcDateTime;\n    ///\n    /// let token = \"my_access_token\".to_string();\n    /// let expired_at = UtcDateTime::now();\n    /// let access_token = AccessToken::new(token, expired_at.clone());\n    ///\n    /// assert_eq!(access_token.token, \"my_access_token\".to_string());\n    /// assert_eq!(access_token.expired_at, expired_at);\n    /// ```\n    pub fn new(token: String, expired_at: UtcDateTime) -\u003e Self {\n        Self { token, expired_at }\n    }\n\n    /// Extract bearer token from headers\n    pub fn extract_bearer_token_from_headers(headers: \u0026HeaderMap) -\u003e Option\u003cSelf\u003e {\n        headers\n            .get(header::AUTHORIZATION)\n            .and_then(|h| h.to_str().ok())\n            .and_then(|h| {\n                let words = h.split(\"Bearer\").collect::\u003cVec\u003c\u0026str\u003e\u003e();\n                words.get(1).map(|w| w.trim())\n            })\n            .map(|token| AccessToken::new(token.to_string(), UtcDateTime::now()))\n    }\n}\n\n/// JWT extractor from HTTP headers\nimpl\u003cS\u003e FromRequestParts\u003cS\u003e for AccessToken\nwhere\n    S: Send + Sync,\n{\n    type Rejection = ApiError;\n\n    async fn from_request_parts(parts: \u0026mut Parts, _state: \u0026S) -\u003e Result\u003cSelf, Self::Rejection\u003e {\n        Self::extract_bearer_token_from_headers(\u0026parts.headers)\n            .ok_or(ApiError::Unauthorized(\"Missing or invalid token\".to_string()))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use axum::http::HeaderValue;\n\n    #[test]\n    fn test_extract_bearer_token_from_headers() {\n        let mut headers = HeaderMap::new();\n        headers.insert(header::AUTHORIZATION, HeaderValue::from_static(\"Bearer my_token\"));\n\n        let token = AccessToken::extract_bearer_token_from_headers(\u0026headers);\n        assert!(token.is_some());\n        assert_eq!(token.unwrap().token, \"my_token\");\n    }\n\n    #[test]\n    fn test_extract_bearer_token_from_headers_invalid() {\n        let mut headers = HeaderMap::new();\n        headers.insert(header::AUTHORIZATION, HeaderValue::from_static(\"Invalid my_token\"));\n\n        let token = AccessToken::extract_bearer_token_from_headers(\u0026headers);\n        assert!(token.is_none());\n    }\n}\n","traces":[{"line":37,"address":[],"length":0,"stats":{"Line":1}},{"line":42,"address":[],"length":0,"stats":{"Line":2}},{"line":43,"address":[],"length":0,"stats":{"Line":2}},{"line":44,"address":[],"length":0,"stats":{"Line":2}},{"line":45,"address":[],"length":0,"stats":{"Line":6}},{"line":46,"address":[],"length":0,"stats":{"Line":4}},{"line":47,"address":[],"length":0,"stats":{"Line":2}},{"line":48,"address":[],"length":0,"stats":{"Line":5}},{"line":50,"address":[],"length":0,"stats":{"Line":5}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":0}},{"line":63,"address":[],"length":0,"stats":{"Line":0}}],"covered":9,"coverable":12},{"path":["/","Users","fabien","lab","rust","api-tools","src","security","jwt","mod.rs"],"content":"//! JWT module\n\npub mod access_token;\npub mod payload;\n\nuse crate::server::axum::response::ApiError;\nuse crate::{security::jwt::access_token::AccessToken, value_objects::datetime::UtcDateTime};\nuse jsonwebtoken::errors::ErrorKind::ExpiredSignature;\nuse jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation, decode, encode};\nuse serde::{Deserialize, Serialize};\nuse std::fmt::{Debug, Formatter};\nuse thiserror::Error;\n\nconst JWT_ACCESS_LIFETIME_IN_MINUTES: i64 = 15; // 15 minutes\nconst JWT_REFRESH_LIFETIME_IN_HOURS: i64 = 7 * 24; // 7 days\n\n/// JWT errors\n#[derive(Debug, Clone, PartialEq, Error)]\npub enum JwtError {\n    #[error(\"Parse token error: {0}\")]\n    ParseError(String),\n\n    #[error(\"Generate token error: {0}\")]\n    GenerateError(String),\n\n    #[error(\"Invalid or unsupported algorithm: {0}\")]\n    InvalidAlgorithm(String),\n\n    #[error(\"Encoding key error: {0}\")]\n    EncodingKeyError(String),\n\n    #[error(\"Decoding key error: {0}\")]\n    DecodingKeyError(String),\n\n    #[error(\"Expired token\")]\n    ExpiredToken,\n}\n\n/// JWT error\nimpl From\u003cJwtError\u003e for ApiError {\n    fn from(value: JwtError) -\u003e Self {\n        Self::InternalServerError(value.to_string())\n    }\n}\n\n/// JWT representation\n#[derive(Clone)]\npub struct Jwt {\n    /// The algorithm supported for signing/verifying JWT\n    algorithm: Algorithm,\n\n    /// Access Token lifetime (in minute)\n    /// The default value is 15 minutes.\n    access_lifetime: i64,\n\n    /// Refresh Token lifetime (in hour)\n    /// The default value is 7 days.\n    refresh_lifetime: i64,\n\n    /// Encoding key\n    encoding_key: Option\u003cEncodingKey\u003e,\n\n    /// Decoding key\n    decoding_key: Option\u003cDecodingKey\u003e,\n}\n\nimpl Default for Jwt {\n    fn default() -\u003e Self {\n        Self {\n            algorithm: Algorithm::HS512,\n            access_lifetime: JWT_ACCESS_LIFETIME_IN_MINUTES,\n            refresh_lifetime: JWT_REFRESH_LIFETIME_IN_HOURS,\n            encoding_key: None,\n            decoding_key: None,\n        }\n    }\n}\n\nimpl Debug for Jwt {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        write!(\n            f,\n            \"JWT =\u003e algo: {:?}, access_lifetime: {}, refresh_lifetime: {}\",\n            self.algorithm, self.access_lifetime, self.refresh_lifetime\n        )\n    }\n}\n\nimpl Jwt {\n    /// Initialize a new `Jwt`\n    pub fn init(\n        algorithm: \u0026str,\n        access_lifetime: i64,\n        refresh_lifetime: i64,\n        secret: Option\u003c\u0026str\u003e,\n        private_key: Option\u003c\u0026str\u003e,\n        public_key: Option\u003c\u0026str\u003e,\n    ) -\u003e Result\u003cSelf, JwtError\u003e {\n        let mut jwt = Jwt {\n            algorithm: Self::algorithm_from_str(algorithm)?,\n            access_lifetime,\n            refresh_lifetime,\n            ..Default::default()\n        };\n\n        // Encoding key\n        match (secret, private_key, jwt.use_secret()) {\n            (Some(secret), _, true) =\u003e jwt.set_encoding_key(secret.trim())?,\n            (_, Some(private_key), false) =\u003e jwt.set_encoding_key(private_key.trim())?,\n            _ =\u003e return Err(JwtError::EncodingKeyError(\"invalid JWT encoding key\".to_owned())),\n        }\n\n        // Decoding key\n        match (secret, public_key, jwt.use_secret()) {\n            (Some(secret), _, true) =\u003e jwt.set_decoding_key(secret.trim())?,\n            (_, Some(public_key), false) =\u003e jwt.set_decoding_key(public_key.trim())?,\n            _ =\u003e return Err(JwtError::DecodingKeyError(\"invalid JWT decoding key\".to_owned())),\n        }\n\n        Ok(jwt)\n    }\n\n    /// Get access token lifetime\n    pub fn access_lifetime(\u0026self) -\u003e i64 {\n        self.access_lifetime\n    }\n\n    /// Get refresh token lifetime\n    pub fn refresh_lifetime(\u0026self) -\u003e i64 {\n        self.refresh_lifetime\n    }\n\n    /// Update access token lifetime (in minute)\n    pub fn set_access_lifetime(\u0026mut self, duration: i64) {\n        self.access_lifetime = duration;\n    }\n\n    /// Update refresh token lifetime (in day)\n    pub fn set_refresh_lifetime(\u0026mut self, duration: i64) {\n        self.refresh_lifetime = duration;\n    }\n\n    /// Update encoding key\n    pub fn set_encoding_key(\u0026mut self, secret: \u0026str) -\u003e Result\u003c(), JwtError\u003e {\n        let key = match self.algorithm {\n            Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 =\u003e EncodingKey::from_secret(secret.as_bytes()),\n            Algorithm::ES256 | Algorithm::ES384 =\u003e EncodingKey::from_ec_pem(secret.as_bytes())\n                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,\n            Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 =\u003e EncodingKey::from_rsa_pem(secret.as_bytes())\n                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,\n            Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 =\u003e EncodingKey::from_rsa_pem(secret.as_bytes())\n                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,\n            Algorithm::EdDSA =\u003e EncodingKey::from_ed_pem(secret.as_bytes())\n                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,\n        };\n\n        self.encoding_key = Some(key);\n\n        Ok(())\n    }\n\n    /// Update decoding key\n    pub fn set_decoding_key(\u0026mut self, secret: \u0026str) -\u003e Result\u003c(), JwtError\u003e {\n        let key = match self.algorithm {\n            Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 =\u003e DecodingKey::from_secret(secret.as_bytes()),\n            Algorithm::ES256 | Algorithm::ES384 =\u003e DecodingKey::from_ec_pem(secret.as_bytes())\n                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,\n            Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 =\u003e DecodingKey::from_rsa_pem(secret.as_bytes())\n                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,\n            Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 =\u003e DecodingKey::from_rsa_pem(secret.as_bytes())\n                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,\n            Algorithm::EdDSA =\u003e DecodingKey::from_ed_pem(secret.as_bytes())\n                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,\n        };\n\n        self.decoding_key = Some(key);\n\n        Ok(())\n    }\n\n    /// Generate JWT\n    pub fn generate\u003cP: Debug + Serialize\u003e(\u0026self, payload: P, expired_at: UtcDateTime) -\u003e Result\u003cAccessToken, JwtError\u003e {\n        let header = jsonwebtoken::Header::new(self.algorithm);\n\n        match self.encoding_key.clone() {\n            Some(encoding_key) =\u003e {\n                let token = encode(\u0026header, \u0026payload, \u0026encoding_key)\n                    .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?;\n\n                Ok(AccessToken { token, expired_at })\n            }\n            _ =\u003e Err(JwtError::EncodingKeyError(\"empty key\".to_owned())),\n        }\n    }\n\n    /// Parse JWT\n    pub fn parse\u003cP: Debug + for\u003c'de\u003e Deserialize\u003c'de\u003e\u003e(\u0026self, token: \u0026AccessToken) -\u003e Result\u003cP, JwtError\u003e {\n        let validation = Validation::new(self.algorithm);\n\n        match self.decoding_key.clone() {\n            Some(decoding_key) =\u003e {\n                let token = decode::\u003cP\u003e(\u0026token.token, \u0026decoding_key, \u0026validation).map_err(|err| match err.kind() {\n                    ExpiredSignature =\u003e JwtError::ExpiredToken,\n                    _ =\u003e JwtError::DecodingKeyError(err.to_string()),\n                })?;\n\n                Ok(token.claims)\n            }\n            _ =\u003e Err(JwtError::DecodingKeyError(\"empty key\".to_owned())),\n        }\n    }\n\n    /// Return true if a secret key is used instead of a pair of keys\n    fn use_secret(\u0026self) -\u003e bool {\n        self.algorithm == Algorithm::HS256 || self.algorithm == Algorithm::HS384 || self.algorithm == Algorithm::HS512\n    }\n\n    /// Convert `\u0026str` to `Algorithm`\n    fn algorithm_from_str(algo: \u0026str) -\u003e Result\u003cAlgorithm, JwtError\u003e {\n        Ok(match algo {\n            \"HS256\" =\u003e Algorithm::HS256,\n            \"HS384\" =\u003e Algorithm::HS384,\n            \"HS512\" =\u003e Algorithm::HS512,\n            \"ES256\" =\u003e Algorithm::ES256,\n            \"ES384\" =\u003e Algorithm::ES384,\n            _ =\u003e {\n                return Err(JwtError::InvalidAlgorithm(algo.to_string()));\n            }\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_jwt_use_secret() {\n        let jwt = Jwt::default();\n        assert!(jwt.use_secret());\n\n        let mut jwt = Jwt::default();\n        jwt.algorithm = Algorithm::ES256;\n        assert!(!jwt.use_secret());\n\n        jwt.algorithm = Algorithm::HS256;\n        assert!(jwt.use_secret());\n    }\n\n    #[test]\n    fn test_jwt_algorithm_from_str() {\n        assert_eq!(Jwt::algorithm_from_str(\"HS256\").unwrap(), Algorithm::HS256);\n        assert_eq!(Jwt::algorithm_from_str(\"HS384\").unwrap(), Algorithm::HS384);\n        assert_eq!(Jwt::algorithm_from_str(\"HS512\").unwrap(), Algorithm::HS512);\n        assert_eq!(Jwt::algorithm_from_str(\"ES256\").unwrap(), Algorithm::ES256);\n        assert_eq!(Jwt::algorithm_from_str(\"ES384\").unwrap(), Algorithm::ES384);\n\n        let invalid_algo = Jwt::algorithm_from_str(\"ES512\");\n        assert!(invalid_algo.is_err());\n        if let Err(e) = invalid_algo {\n            assert_eq!(e, JwtError::InvalidAlgorithm(\"ES512\".to_string()));\n        }\n    }\n\n    #[test]\n    fn test_jwt_default() {\n        let jwt = Jwt::default();\n        assert_eq!(jwt.algorithm, Algorithm::HS512);\n        assert_eq!(jwt.access_lifetime, JWT_ACCESS_LIFETIME_IN_MINUTES);\n        assert_eq!(jwt.refresh_lifetime, JWT_REFRESH_LIFETIME_IN_HOURS);\n        assert!(jwt.encoding_key.is_none());\n        assert!(jwt.decoding_key.is_none());\n    }\n\n    #[test]\n    fn test_jwt_debug() {\n        let jwt = Jwt::default();\n        let debug_str = format!(\"{:?}\", jwt);\n\n        assert_eq!(\n            debug_str,\n            format!(\"JWT =\u003e algo: HS512, access_lifetime: 15, refresh_lifetime: {}\", 7 * 24)\n        );\n    }\n}\n","traces":[{"line":41,"address":[],"length":0,"stats":{"Line":0}},{"line":42,"address":[],"length":0,"stats":{"Line":0}},{"line":68,"address":[],"length":0,"stats":{"Line":4}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":81,"address":[],"length":0,"stats":{"Line":1}},{"line":82,"address":[],"length":0,"stats":{"Line":1}},{"line":84,"address":[],"length":0,"stats":{"Line":1}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":100,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":0}},{"line":108,"address":[],"length":0,"stats":{"Line":0}},{"line":109,"address":[],"length":0,"stats":{"Line":0}},{"line":110,"address":[],"length":0,"stats":{"Line":0}},{"line":114,"address":[],"length":0,"stats":{"Line":0}},{"line":115,"address":[],"length":0,"stats":{"Line":0}},{"line":116,"address":[],"length":0,"stats":{"Line":0}},{"line":117,"address":[],"length":0,"stats":{"Line":0}},{"line":120,"address":[],"length":0,"stats":{"Line":0}},{"line":124,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":0}},{"line":129,"address":[],"length":0,"stats":{"Line":0}},{"line":130,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":135,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":0}},{"line":144,"address":[],"length":0,"stats":{"Line":0}},{"line":145,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":0}},{"line":147,"address":[],"length":0,"stats":{"Line":0}},{"line":148,"address":[],"length":0,"stats":{"Line":0}},{"line":149,"address":[],"length":0,"stats":{"Line":0}},{"line":150,"address":[],"length":0,"stats":{"Line":0}},{"line":151,"address":[],"length":0,"stats":{"Line":0}},{"line":152,"address":[],"length":0,"stats":{"Line":0}},{"line":153,"address":[],"length":0,"stats":{"Line":0}},{"line":154,"address":[],"length":0,"stats":{"Line":0}},{"line":157,"address":[],"length":0,"stats":{"Line":0}},{"line":159,"address":[],"length":0,"stats":{"Line":0}},{"line":163,"address":[],"length":0,"stats":{"Line":0}},{"line":164,"address":[],"length":0,"stats":{"Line":0}},{"line":165,"address":[],"length":0,"stats":{"Line":0}},{"line":166,"address":[],"length":0,"stats":{"Line":0}},{"line":167,"address":[],"length":0,"stats":{"Line":0}},{"line":168,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":170,"address":[],"length":0,"stats":{"Line":0}},{"line":171,"address":[],"length":0,"stats":{"Line":0}},{"line":172,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":0}},{"line":176,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":183,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":0}},{"line":186,"address":[],"length":0,"stats":{"Line":0}},{"line":187,"address":[],"length":0,"stats":{"Line":0}},{"line":188,"address":[],"length":0,"stats":{"Line":0}},{"line":190,"address":[],"length":0,"stats":{"Line":0}},{"line":192,"address":[],"length":0,"stats":{"Line":0}},{"line":197,"address":[],"length":0,"stats":{"Line":0}},{"line":198,"address":[],"length":0,"stats":{"Line":0}},{"line":200,"address":[],"length":0,"stats":{"Line":0}},{"line":201,"address":[],"length":0,"stats":{"Line":0}},{"line":202,"address":[],"length":0,"stats":{"Line":0}},{"line":203,"address":[],"length":0,"stats":{"Line":0}},{"line":204,"address":[],"length":0,"stats":{"Line":0}},{"line":207,"address":[],"length":0,"stats":{"Line":0}},{"line":209,"address":[],"length":0,"stats":{"Line":0}},{"line":214,"address":[],"length":0,"stats":{"Line":3}},{"line":215,"address":[],"length":0,"stats":{"Line":7}},{"line":219,"address":[],"length":0,"stats":{"Line":6}},{"line":220,"address":[],"length":0,"stats":{"Line":6}},{"line":221,"address":[],"length":0,"stats":{"Line":7}},{"line":222,"address":[],"length":0,"stats":{"Line":6}},{"line":223,"address":[],"length":0,"stats":{"Line":5}},{"line":224,"address":[],"length":0,"stats":{"Line":4}},{"line":225,"address":[],"length":0,"stats":{"Line":3}},{"line":227,"address":[],"length":0,"stats":{"Line":1}}],"covered":15,"coverable":79},{"path":["/","Users","fabien","lab","rust","api-tools","src","security","jwt","payload.rs"],"content":"//! JWT Payload module\n\nuse crate::security::jwt::Jwt;\nuse serde::{Deserialize, Serialize};\nuse std::fmt::Debug;\nuse thiserror::Error;\n\n/// Payload errors\n#[derive(Debug, Clone, PartialEq, Error)]\npub enum PayloadError {\n    #[error(\"Missing token\")]\n    MissingToken,\n\n    #[error(\"Invalid token: {0}\")]\n    ParseTokenError(String),\n\n    #[error(\"Invalid headers\")]\n    InvalidHeaders,\n}\n\npub trait PayloadExtractor\u003cH, P: Debug + Serialize + for\u003c'de\u003e Deserialize\u003c'de\u003e\u003e {\n    /// Extract payload from request headers\n    fn try_from_headers(headers: \u0026H, jwt: \u0026Jwt) -\u003e Result\u003cP, PayloadError\u003e;\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","security","mod.rs"],"content":"//! Security module\n\npub mod jwt;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","extractors.rs"],"content":"//! Extractor modules for Axum\n\nuse crate::server::axum::layers::request_id::REQUEST_ID_HEADER;\nuse crate::server::axum::response::ApiError;\nuse axum::extract::FromRequestParts;\nuse axum::extract::path::ErrorKind;\nuse axum::extract::rejection::PathRejection;\nuse axum::http::request::Parts;\nuse axum::http::{HeaderValue, StatusCode};\nuse serde::de::DeserializeOwned;\n\n/// Request ID extractor from HTTP headers\npub struct RequestId(pub HeaderValue);\n\nimpl\u003cS\u003e FromRequestParts\u003cS\u003e for RequestId\nwhere\n    S: Send + Sync,\n{\n    type Rejection = ();\n\n    async fn from_request_parts(parts: \u0026mut Parts, _state: \u0026S) -\u003e Result\u003cSelf, Self::Rejection\u003e {\n        match parts.headers.get(REQUEST_ID_HEADER.clone()) {\n            Some(id) =\u003e Ok(RequestId(id.clone())),\n            _ =\u003e Ok(RequestId(HeaderValue::from_static(\"\"))),\n        }\n    }\n}\n\n/// `Path` extractor customizes the error from `axum::extract::Path`\npub struct Path\u003cT\u003e(pub T);\n\nimpl\u003cS, T\u003e FromRequestParts\u003cS\u003e for Path\u003cT\u003e\nwhere\n    T: DeserializeOwned + Send,\n    S: Send + Sync,\n{\n    type Rejection = (StatusCode, ApiError);\n\n    async fn from_request_parts(parts: \u0026mut Parts, state: \u0026S) -\u003e Result\u003cSelf, Self::Rejection\u003e {\n        match axum::extract::Path::\u003cT\u003e::from_request_parts(parts, state).await {\n            Ok(value) =\u003e Ok(Self(value.0)),\n            Err(rejection) =\u003e {\n                let (status, body) = match rejection {\n                    PathRejection::FailedToDeserializePathParams(inner) =\u003e {\n                        let mut status = StatusCode::BAD_REQUEST;\n\n                        let kind = inner.into_kind();\n                        let body = match \u0026kind {\n                            ErrorKind::WrongNumberOfParameters { .. } =\u003e ApiError::BadRequest(kind.to_string()),\n                            ErrorKind::ParseErrorAtKey { .. } =\u003e ApiError::BadRequest(kind.to_string()),\n                            ErrorKind::ParseErrorAtIndex { .. } =\u003e ApiError::BadRequest(kind.to_string()),\n                            ErrorKind::ParseError { .. } =\u003e ApiError::BadRequest(kind.to_string()),\n                            ErrorKind::InvalidUtf8InPathParam { .. } =\u003e ApiError::BadRequest(kind.to_string()),\n                            ErrorKind::UnsupportedType { .. } =\u003e {\n                                // this error is caused by the programmer using an unsupported type\n                                // (such as nested maps) so respond with `500` instead\n                                status = StatusCode::INTERNAL_SERVER_ERROR;\n                                ApiError::InternalServerError(kind.to_string())\n                            }\n                            ErrorKind::Message(msg) =\u003e ApiError::BadRequest(msg.clone()),\n                            _ =\u003e ApiError::BadRequest(format!(\"Unhandled deserialization error: {kind}\")),\n                        };\n\n                        (status, body)\n                    }\n                    PathRejection::MissingPathParams(error) =\u003e (\n                        StatusCode::INTERNAL_SERVER_ERROR,\n                        ApiError::InternalServerError(error.to_string()),\n                    ),\n                    _ =\u003e (\n                        StatusCode::INTERNAL_SERVER_ERROR,\n                        ApiError::InternalServerError(format!(\"Unhandled path rejection: {rejection}\")),\n                    ),\n                };\n\n                Err((status, body))\n            }\n        }\n    }\n}\n\n/// `Query` extractor customizes the error from `axum::extract::Query`\npub struct Query\u003cT\u003e(pub T);\n\nimpl\u003cT, S\u003e FromRequestParts\u003cS\u003e for Query\u003cT\u003e\nwhere\n    T: DeserializeOwned,\n    S: Send + Sync,\n{\n    type Rejection = (StatusCode, ApiError);\n\n    async fn from_request_parts(parts: \u0026mut Parts, _state: \u0026S) -\u003e Result\u003cSelf, Self::Rejection\u003e {\n        let query = parts.uri.query().unwrap_or_default();\n        let value = serde_urlencoded::from_str(query)\n            .map_err(|err| (StatusCode::BAD_REQUEST, ApiError::BadRequest(err.to_string())))?;\n\n        Ok(Query(value))\n    }\n}\n","traces":[{"line":21,"address":[],"length":0,"stats":{"Line":0}},{"line":22,"address":[],"length":0,"stats":{"Line":0}},{"line":23,"address":[],"length":0,"stats":{"Line":0}},{"line":24,"address":[],"length":0,"stats":{"Line":0}},{"line":39,"address":[],"length":0,"stats":{"Line":0}},{"line":40,"address":[],"length":0,"stats":{"Line":0}},{"line":41,"address":[],"length":0,"stats":{"Line":0}},{"line":42,"address":[],"length":0,"stats":{"Line":0}},{"line":43,"address":[],"length":0,"stats":{"Line":0}},{"line":44,"address":[],"length":0,"stats":{"Line":0}},{"line":45,"address":[],"length":0,"stats":{"Line":0}},{"line":47,"address":[],"length":0,"stats":{"Line":0}},{"line":48,"address":[],"length":0,"stats":{"Line":0}},{"line":49,"address":[],"length":0,"stats":{"Line":0}},{"line":50,"address":[],"length":0,"stats":{"Line":0}},{"line":51,"address":[],"length":0,"stats":{"Line":0}},{"line":52,"address":[],"length":0,"stats":{"Line":0}},{"line":53,"address":[],"length":0,"stats":{"Line":0}},{"line":54,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":58,"address":[],"length":0,"stats":{"Line":0}},{"line":60,"address":[],"length":0,"stats":{"Line":0}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":64,"address":[],"length":0,"stats":{"Line":0}},{"line":66,"address":[],"length":0,"stats":{"Line":0}},{"line":67,"address":[],"length":0,"stats":{"Line":0}},{"line":68,"address":[],"length":0,"stats":{"Line":0}},{"line":70,"address":[],"length":0,"stats":{"Line":0}},{"line":71,"address":[],"length":0,"stats":{"Line":0}},{"line":72,"address":[],"length":0,"stats":{"Line":0}},{"line":76,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":0}},{"line":93,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":95,"address":[],"length":0,"stats":{"Line":0}},{"line":97,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":36},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","handlers","mod.rs"],"content":"//! Axum handlers\n\n#[cfg(feature = \"prometheus\")]\npub mod prometheus;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","handlers","prometheus.rs"],"content":"//! Prometheus metrics handler for Axum\n\nuse crate::server::axum::response::ApiError;\nuse metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle};\n\n/// Buckets for HTTP request duration in seconds\nconst SECONDS_DURATION_BUCKETS: \u0026[f64; 11] = \u0026[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0];\n\n/// Prometheus metrics handler for Axum\npub struct PrometheusHandler {}\n\nimpl PrometheusHandler {\n    /// Return a new `PrometheusHandle`\n    pub fn get_handle() -\u003e Result\u003cPrometheusHandle, ApiError\u003e {\n        PrometheusBuilder::new()\n            .set_buckets_for_metric(\n                Matcher::Full(\"http_requests_duration_seconds\".to_string()),\n                SECONDS_DURATION_BUCKETS,\n            )\n            .map_err(|err| ApiError::InternalServerError(err.to_string()))?\n            .install_recorder()\n            .map_err(|err| ApiError::InternalServerError(err.to_string()))\n    }\n}\n","traces":[{"line":14,"address":[],"length":0,"stats":{"Line":0}},{"line":15,"address":[],"length":0,"stats":{"Line":0}},{"line":17,"address":[],"length":0,"stats":{"Line":0}},{"line":18,"address":[],"length":0,"stats":{"Line":0}},{"line":20,"address":[],"length":0,"stats":{"Line":0}},{"line":22,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":6},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","basic_auth.rs"],"content":"//! Basic Auth layer\n\nuse super::body_from_parts;\nuse axum::{\n    body::Body,\n    http::{HeaderValue, Request, header},\n    response::Response,\n};\nuse futures::future::BoxFuture;\nuse http_auth_basic::Credentials;\nuse hyper::StatusCode;\nuse std::task::{Context, Poll};\nuse tower::{Layer, Service};\n\n#[derive(Clone)]\npub struct BasicAuthLayer {\n    pub username: String,\n    pub password: String,\n}\n\nimpl BasicAuthLayer {\n    /// Create a new `BasicAuthLayer`\n    pub fn new(username: \u0026str, password: \u0026str) -\u003e Self {\n        Self {\n            username: username.to_string(),\n            password: password.to_string(),\n        }\n    }\n}\n\nimpl\u003cS\u003e Layer\u003cS\u003e for BasicAuthLayer {\n    type Service = BasicAuthMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        BasicAuthMiddleware {\n            inner,\n            username: self.username.clone(),\n            password: self.password.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct BasicAuthMiddleware\u003cS\u003e {\n    inner: S,\n    username: String,\n    password: String,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for BasicAuthMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let auth = request\n            .headers()\n            .get(header::AUTHORIZATION)\n            .and_then(|h| h.to_str().ok())\n            .map(str::to_string);\n        let username = self.username.clone();\n        let password = self.password.clone();\n\n        let future = self.inner.call(request);\n        Box::pin(async move {\n            let mut response = Response::default();\n\n            let ok = match auth {\n                None =\u003e false,\n                Some(auth) =\u003e match Credentials::from_header(auth) {\n                    Err(_) =\u003e false,\n                    Ok(cred) =\u003e cred.user_id == username \u0026\u0026 cred.password == password,\n                },\n            };\n            response = match ok {\n                true =\u003e future.await?,\n                false =\u003e {\n                    let (mut parts, _body) = response.into_parts();\n                    let msg = body_from_parts(\n                        \u0026mut parts,\n                        StatusCode::UNAUTHORIZED,\n                        \"Unauthorized\",\n                        Some(vec![(\n                            header::WWW_AUTHENTICATE,\n                            HeaderValue::from_static(\"basic realm=RESTRICTED\"),\n                        )]),\n                    );\n                    Response::from_parts(parts, Body::from(msg))\n                }\n            };\n\n            Ok(response)\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use axum::{\n        body::Body,\n        http::{Request, StatusCode, header},\n        response::Response,\n    };\n    use base64::{Engine as _, engine::general_purpose};\n    use std::convert::Infallible;\n    use tower::ServiceExt;\n\n    async fn dummy_service(_req: Request\u003cBody\u003e) -\u003e Result\u003cResponse, Infallible\u003e {\n        Ok(Response::builder()\n            .status(StatusCode::OK)\n            .body(Body::from(\"ok\"))\n            .unwrap())\n    }\n\n    #[tokio::test]\n    async fn test_basic_auth_layer() {\n        let username = \"user\";\n        let password = \"pass\";\n        let layer = BasicAuthLayer::new(username, password);\n        let service = layer.layer(tower::service_fn(dummy_service));\n\n        // Request without Authorization header\n        let req = Request::builder().uri(\"/\").body(Body::empty()).unwrap();\n        let resp = service.clone().oneshot(req).await.unwrap();\n        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);\n\n        // Request with invalid credentials\n        let bad_auth = format!(\"Basic {}\", general_purpose::STANDARD.encode(\"\"));\n        let req = Request::builder()\n            .uri(\"/\")\n            .header(header::AUTHORIZATION, bad_auth)\n            .body(Body::empty())\n            .unwrap();\n        let resp = service.clone().oneshot(req).await.unwrap();\n        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);\n\n        // Request with bad credentials\n        let bad_auth = format!(\"Basic {}\", general_purpose::STANDARD.encode(\"user:wrong\"));\n        let req = Request::builder()\n            .uri(\"/\")\n            .header(header::AUTHORIZATION, bad_auth)\n            .body(Body::empty())\n            .unwrap();\n        let resp = service.clone().oneshot(req).await.unwrap();\n        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);\n\n        // Request with good credentials\n        let good_auth = format!(\n            \"Basic {}\",\n            general_purpose::STANDARD.encode(format!(\"{}:{}\", username, password))\n        );\n        let req = Request::builder()\n            .uri(\"/\")\n            .header(header::AUTHORIZATION, good_auth)\n            .body(Body::empty())\n            .unwrap();\n        let resp = service.oneshot(req).await.unwrap();\n        assert_eq!(resp.status(), StatusCode::OK);\n    }\n}\n","traces":[{"line":23,"address":[],"length":0,"stats":{"Line":1}},{"line":25,"address":[],"length":0,"stats":{"Line":1}},{"line":26,"address":[],"length":0,"stats":{"Line":1}},{"line":34,"address":[],"length":0,"stats":{"Line":1}},{"line":37,"address":[],"length":0,"stats":{"Line":1}},{"line":38,"address":[],"length":0,"stats":{"Line":1}},{"line":60,"address":[],"length":0,"stats":{"Line":4}},{"line":61,"address":[],"length":0,"stats":{"Line":4}},{"line":64,"address":[],"length":0,"stats":{"Line":4}},{"line":65,"address":[],"length":0,"stats":{"Line":4}},{"line":67,"address":[],"length":0,"stats":{"Line":4}},{"line":68,"address":[],"length":0,"stats":{"Line":11}},{"line":69,"address":[],"length":0,"stats":{"Line":4}},{"line":70,"address":[],"length":0,"stats":{"Line":4}},{"line":71,"address":[],"length":0,"stats":{"Line":4}},{"line":73,"address":[],"length":0,"stats":{"Line":4}},{"line":74,"address":[],"length":0,"stats":{"Line":8}},{"line":75,"address":[],"length":0,"stats":{"Line":4}},{"line":77,"address":[],"length":0,"stats":{"Line":8}},{"line":78,"address":[],"length":0,"stats":{"Line":1}},{"line":79,"address":[],"length":0,"stats":{"Line":3}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":81,"address":[],"length":0,"stats":{"Line":4}},{"line":84,"address":[],"length":0,"stats":{"Line":8}},{"line":85,"address":[],"length":0,"stats":{"Line":1}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":87,"address":[],"length":0,"stats":{"Line":3}},{"line":88,"address":[],"length":0,"stats":{"Line":3}},{"line":89,"address":[],"length":0,"stats":{"Line":3}},{"line":90,"address":[],"length":0,"stats":{"Line":3}},{"line":91,"address":[],"length":0,"stats":{"Line":3}},{"line":92,"address":[],"length":0,"stats":{"Line":3}},{"line":93,"address":[],"length":0,"stats":{"Line":3}},{"line":94,"address":[],"length":0,"stats":{"Line":3}},{"line":97,"address":[],"length":0,"stats":{"Line":3}},{"line":101,"address":[],"length":0,"stats":{"Line":0}}],"covered":34,"coverable":36},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","cors.rs"],"content":"//! CORS layer for Axum\n\nuse axum::http::{HeaderName, HeaderValue, Method};\nuse tower_http::cors::{AllowOrigin, Any, CorsLayer};\n\n/// CORS configuration\n///\n/// # Example\n///\n/// ```rust\n/// use axum::http::{header, HeaderName, HeaderValue, Method};\n/// use api_tools::server::axum::layers::cors::CorsConfig;\n///\n/// let cors_config = CorsConfig {\n///     allow_origin: \"*\",\n///     allow_methods: vec![Method::GET, Method::POST, Method::PUT, Method::PATCH, Method::DELETE],\n///     allow_headers: vec![header::AUTHORIZATION, header::ACCEPT, header::CONTENT_TYPE, header::ORIGIN],\n/// };\n/// ```\npub struct CorsConfig\u003c'a\u003e {\n    pub allow_origin: \u0026'a str,\n    pub allow_methods: Vec\u003cMethod\u003e,\n    pub allow_headers: Vec\u003cHeaderName\u003e,\n}\n\n/// CORS layer\n///\n/// This function creates a CORS layer for Axum with the specified configuration.\n///\n/// # Example\n///\n/// ```rust\n/// use axum::http::{header, HeaderName, HeaderValue, Method};\n/// use api_tools::server::axum::layers::cors::{cors, CorsConfig};\n///\n/// let cors_config = CorsConfig {\n///     allow_origin: \"*\",\n///     allow_methods: vec![Method::GET, Method::POST, Method::PUT, Method::PATCH, Method::DELETE],\n///     allow_headers: vec![header::AUTHORIZATION, header::ACCEPT, header::CONTENT_TYPE, header::ORIGIN],\n/// };\n///\n/// let layer = cors(cors_config);\n/// ```\npub fn cors(config: CorsConfig) -\u003e CorsLayer {\n    let allow_origin = config.allow_origin;\n\n    let layer = CorsLayer::new()\n        .allow_methods(config.allow_methods)\n        .allow_headers(config.allow_headers);\n\n    if allow_origin == \"*\" {\n        layer.allow_origin(Any)\n    } else {\n        let origins = allow_origin\n            .split(',')\n            .filter(|url| *url != \"*\" \u0026\u0026 !url.is_empty())\n            .filter_map(|url| url.parse().ok())\n            .collect::\u003cVec\u003cHeaderValue\u003e\u003e();\n\n        if origins.is_empty() {\n            layer.allow_origin(Any)\n        } else {\n            layer\n                .allow_origin(AllowOrigin::predicate(move |origin: \u0026HeaderValue, _| {\n                    origins.contains(origin)\n                }))\n                .allow_credentials(true)\n        }\n    }\n}\n","traces":[{"line":44,"address":[],"length":0,"stats":{"Line":0}},{"line":45,"address":[],"length":0,"stats":{"Line":0}},{"line":47,"address":[],"length":0,"stats":{"Line":0}},{"line":48,"address":[],"length":0,"stats":{"Line":0}},{"line":49,"address":[],"length":0,"stats":{"Line":0}},{"line":51,"address":[],"length":0,"stats":{"Line":0}},{"line":52,"address":[],"length":0,"stats":{"Line":0}},{"line":54,"address":[],"length":0,"stats":{"Line":0}},{"line":56,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":60,"address":[],"length":0,"stats":{"Line":0}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":63,"address":[],"length":0,"stats":{"Line":0}},{"line":64,"address":[],"length":0,"stats":{"Line":0}},{"line":65,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":15},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","http_errors.rs"],"content":"//! Override some HTTP errors\n\nuse crate::server::axum::response::ApiError;\nuse axum::body::Body;\nuse axum::http::{Request, StatusCode};\nuse axum::response::{IntoResponse, Response};\nuse futures::future::BoxFuture;\nuse std::task::{Context, Poll};\nuse tower::{Layer, Service};\n\n/// Configuration for the `HttpErrorsLayer`\n#[derive(Clone, Debug)]\npub struct HttpErrorsConfig {\n    /// Maximum size of the body in bytes\n    pub body_max_size: usize,\n}\n\n#[derive(Clone)]\npub struct HttpErrorsLayer {\n    pub config: HttpErrorsConfig,\n}\n\nimpl HttpErrorsLayer {\n    /// Create a new `HttpErrorsLayer`\n    pub fn new(config: \u0026HttpErrorsConfig) -\u003e Self {\n        Self { config: config.clone() }\n    }\n}\n\nimpl\u003cS\u003e Layer\u003cS\u003e for HttpErrorsLayer {\n    type Service = HttpErrorsMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        HttpErrorsMiddleware {\n            inner,\n            config: self.config.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct HttpErrorsMiddleware\u003cS\u003e {\n    inner: S,\n    config: HttpErrorsConfig,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for HttpErrorsMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + Clone + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let mut inner = self.inner.clone();\n        let config = self.config.clone();\n\n        Box::pin(async move {\n            let response: Response = inner.call(request).await?;\n\n            // Vérifie le content-type\n            let headers = response.headers();\n            if let Some(content_type) = headers.get(\"content-type\") {\n                let content_type = content_type.to_str().unwrap_or_default();\n                if content_type.starts_with(\"image/\")\n                    || content_type.starts_with(\"audio/\")\n                    || content_type.starts_with(\"video/\")\n                {\n                    return Ok(response);\n                }\n            }\n\n            let (parts, body) = response.into_parts();\n            match axum::body::to_bytes(body, config.body_max_size).await {\n                Ok(body) =\u003e match String::from_utf8(body.to_vec()) {\n                    Ok(body) =\u003e match parts.status {\n                        StatusCode::METHOD_NOT_ALLOWED =\u003e Ok(ApiError::MethodNotAllowed.into_response()),\n                        StatusCode::UNPROCESSABLE_ENTITY =\u003e Ok(ApiError::UnprocessableEntity(body).into_response()),\n                        _ =\u003e Ok(Response::from_parts(parts, Body::from(body))),\n                    },\n                    Err(err) =\u003e Ok(ApiError::InternalServerError(err.to_string()).into_response()),\n                },\n                Err(_) =\u003e Ok(ApiError::PayloadTooLarge.into_response()),\n            }\n        })\n    }\n}\n","traces":[{"line":25,"address":[],"length":0,"stats":{"Line":0}},{"line":26,"address":[],"length":0,"stats":{"Line":0}},{"line":33,"address":[],"length":0,"stats":{"Line":0}},{"line":36,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":58,"address":[],"length":0,"stats":{"Line":0}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":0}},{"line":63,"address":[],"length":0,"stats":{"Line":0}},{"line":65,"address":[],"length":0,"stats":{"Line":0}},{"line":66,"address":[],"length":0,"stats":{"Line":0}},{"line":69,"address":[],"length":0,"stats":{"Line":0}},{"line":70,"address":[],"length":0,"stats":{"Line":0}},{"line":71,"address":[],"length":0,"stats":{"Line":0}},{"line":72,"address":[],"length":0,"stats":{"Line":0}},{"line":73,"address":[],"length":0,"stats":{"Line":0}},{"line":74,"address":[],"length":0,"stats":{"Line":0}},{"line":76,"address":[],"length":0,"stats":{"Line":0}},{"line":80,"address":[],"length":0,"stats":{"Line":0}},{"line":81,"address":[],"length":0,"stats":{"Line":0}},{"line":82,"address":[],"length":0,"stats":{"Line":0}},{"line":83,"address":[],"length":0,"stats":{"Line":0}},{"line":84,"address":[],"length":0,"stats":{"Line":0}},{"line":85,"address":[],"length":0,"stats":{"Line":0}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":88,"address":[],"length":0,"stats":{"Line":0}},{"line":90,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":27},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","logger.rs"],"content":"//! Logger layer\n\nuse super::header_value_to_str;\nuse axum::body::HttpBody;\nuse axum::http::StatusCode;\nuse axum::{body::Body, http::Request, response::Response};\nuse bytesize::ByteSize;\nuse futures::future::BoxFuture;\nuse std::{\n    fmt::Display,\n    task::{Context, Poll},\n    time::{Duration, Instant},\n};\nuse tower::{Layer, Service};\n\n#[derive(Debug, Default)]\nstruct LoggerMessage {\n    method: String,\n    request_id: String,\n    host: String,\n    path: String,\n    uri: String,\n    user_agent: String,\n    status_code: u16,\n    version: String,\n    latency: Duration,\n    body_size: u64,\n}\n\nimpl Display for LoggerMessage {\n    fn fmt(\u0026self, f: \u0026mut std::fmt::Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        write!(\n            f,\n            \"status_code: {}, method: {}, path: {}, uri: {}, host: {}, request_id: {}, user_agent: {}, version: {}, latency: {:?}, body_size: {}\",\n            self.status_code,\n            self.method,\n            self.path,\n            self.uri,\n            self.host,\n            self.request_id,\n            self.user_agent,\n            self.version,\n            self.latency,\n            ByteSize::b(self.body_size),\n        )\n    }\n}\n\n#[derive(Clone)]\npub struct LoggerLayer;\n\nimpl\u003cS\u003e Layer\u003cS\u003e for LoggerLayer {\n    type Service = LoggerMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        LoggerMiddleware { inner }\n    }\n}\n\n#[derive(Clone)]\npub struct LoggerMiddleware\u003cS\u003e {\n    inner: S,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for LoggerMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let now = Instant::now();\n        let request_headers = request.headers();\n\n        let mut message = LoggerMessage {\n            method: request.method().to_string(),\n            path: request.uri().path().to_string(),\n            uri: request.uri().to_string(),\n            host: header_value_to_str(request_headers.get(\"host\")).to_string(),\n            request_id: header_value_to_str(request_headers.get(\"x-request-id\")).to_string(),\n            user_agent: header_value_to_str(request_headers.get(\"user-agent\")).to_string(),\n            ..Default::default()\n        };\n\n        let future = self.inner.call(request);\n        Box::pin(async move {\n            let response: Response = future.await?;\n\n            message.status_code = response.status().as_u16();\n            message.version = format!(\"{:?}\", response.version());\n            message.latency = now.elapsed();\n            message.body_size = response.body().size_hint().lower();\n\n            if response.status() \u003e= StatusCode::INTERNAL_SERVER_ERROR\n                \u0026\u0026 response.status() != StatusCode::SERVICE_UNAVAILABLE\n            {\n                error!(\n                    status_code = %message.status_code,\n                    method = %message.method,\n                    path = %message.path,\n                    uri = %message.uri,\n                    host = %message.host,\n                    request_id = %message.request_id,\n                    user_agent = %message.user_agent,\n                    version = %message.version,\n                    latency = %format!(\"{:?}\", message.latency),\n                    body_size = %ByteSize::b(message.body_size),\n                );\n            } else if !message.path.starts_with(\"/metrics\") {\n                info!(\n                    status_code = %message.status_code,\n                    method = %message.method,\n                    path = %message.path,\n                    uri = %message.uri,\n                    host = %message.host,\n                    request_id = %message.request_id,\n                    user_agent = %message.user_agent,\n                    version = %message.version,\n                    latency = %format!(\"{:?}\", message.latency),\n                    body_size = %ByteSize::b(message.body_size),\n                );\n            }\n\n            Ok(response)\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::time::Duration;\n\n    #[test]\n    fn test_logger_message_fmt() {\n        let message = LoggerMessage {\n            method: \"GET\".to_string(),\n            request_id: \"abc-123\".to_string(),\n            host: \"localhost\".to_string(),\n            path: \"/test\".to_string(),\n            uri: \"/test?query=1\".to_string(),\n            user_agent: \"TestAgent/1.0\".to_string(),\n            status_code: 200,\n            version: \"HTTP/1.1\".to_string(),\n            latency: Duration::from_millis(42),\n            body_size: 1_524,\n        };\n        let expected = String::from(\n            \"status_code: 200, method: GET, path: /test, uri: /test?query=1, host: localhost, request_id: abc-123, user_agent: TestAgent/1.0, version: HTTP/1.1, latency: 42ms, body_size: 1.5 KiB\",\n        );\n\n        assert_eq!(message.to_string(), expected);\n    }\n}\n","traces":[{"line":31,"address":[],"length":0,"stats":{"Line":1}},{"line":32,"address":[],"length":0,"stats":{"Line":1}},{"line":33,"address":[],"length":0,"stats":{"Line":1}},{"line":35,"address":[],"length":0,"stats":{"Line":1}},{"line":36,"address":[],"length":0,"stats":{"Line":1}},{"line":37,"address":[],"length":0,"stats":{"Line":1}},{"line":38,"address":[],"length":0,"stats":{"Line":1}},{"line":39,"address":[],"length":0,"stats":{"Line":1}},{"line":40,"address":[],"length":0,"stats":{"Line":1}},{"line":41,"address":[],"length":0,"stats":{"Line":1}},{"line":42,"address":[],"length":0,"stats":{"Line":1}},{"line":43,"address":[],"length":0,"stats":{"Line":1}},{"line":44,"address":[],"length":0,"stats":{"Line":1}},{"line":55,"address":[],"length":0,"stats":{"Line":0}},{"line":75,"address":[],"length":0,"stats":{"Line":0}},{"line":76,"address":[],"length":0,"stats":{"Line":0}},{"line":79,"address":[],"length":0,"stats":{"Line":0}},{"line":80,"address":[],"length":0,"stats":{"Line":0}},{"line":81,"address":[],"length":0,"stats":{"Line":0}},{"line":84,"address":[],"length":0,"stats":{"Line":0}},{"line":85,"address":[],"length":0,"stats":{"Line":0}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":87,"address":[],"length":0,"stats":{"Line":0}},{"line":88,"address":[],"length":0,"stats":{"Line":0}},{"line":89,"address":[],"length":0,"stats":{"Line":0}},{"line":93,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":95,"address":[],"length":0,"stats":{"Line":0}},{"line":97,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":0}},{"line":99,"address":[],"length":0,"stats":{"Line":0}},{"line":100,"address":[],"length":0,"stats":{"Line":0}},{"line":102,"address":[],"length":0,"stats":{"Line":0}},{"line":103,"address":[],"length":0,"stats":{"Line":0}},{"line":105,"address":[],"length":0,"stats":{"Line":0}},{"line":106,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":0}},{"line":108,"address":[],"length":0,"stats":{"Line":0}},{"line":109,"address":[],"length":0,"stats":{"Line":0}},{"line":110,"address":[],"length":0,"stats":{"Line":0}},{"line":111,"address":[],"length":0,"stats":{"Line":0}},{"line":112,"address":[],"length":0,"stats":{"Line":0}},{"line":113,"address":[],"length":0,"stats":{"Line":0}},{"line":114,"address":[],"length":0,"stats":{"Line":0}},{"line":115,"address":[],"length":0,"stats":{"Line":0}},{"line":117,"address":[],"length":0,"stats":{"Line":0}},{"line":118,"address":[],"length":0,"stats":{"Line":0}},{"line":119,"address":[],"length":0,"stats":{"Line":0}},{"line":120,"address":[],"length":0,"stats":{"Line":0}},{"line":121,"address":[],"length":0,"stats":{"Line":0}},{"line":122,"address":[],"length":0,"stats":{"Line":0}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":124,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":0}},{"line":126,"address":[],"length":0,"stats":{"Line":0}},{"line":127,"address":[],"length":0,"stats":{"Line":0}},{"line":128,"address":[],"length":0,"stats":{"Line":0}},{"line":132,"address":[],"length":0,"stats":{"Line":0}}],"covered":13,"coverable":58},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","mod.rs"],"content":"//! Axum layers\n\npub mod basic_auth;\npub mod cors;\npub mod http_errors;\npub mod logger;\n#[cfg(feature = \"prometheus\")]\npub mod prometheus;\npub mod request_id;\npub mod security_headers;\npub mod time_limiter;\n\nuse crate::server::axum::response::ApiErrorResponse;\nuse axum::http::header::CONTENT_TYPE;\nuse axum::http::response::Parts;\nuse axum::http::{HeaderName, HeaderValue, StatusCode};\nuse bytes::Bytes;\nuse std::str::from_utf8;\n\n/// Construct a response body from `Parts`, status code, message and headers\npub fn body_from_parts(\n    parts: \u0026mut Parts,\n    status_code: StatusCode,\n    message: \u0026str,\n    headers: Option\u003cVec\u003c(HeaderName, HeaderValue)\u003e\u003e,\n) -\u003e Bytes {\n    // Status\n    parts.status = status_code;\n\n    // Headers\n    parts\n        .headers\n        .insert(CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()));\n    if let Some(headers) = headers {\n        for header in headers {\n            parts.headers.insert(header.0, header.1);\n        }\n    }\n\n    // Body\n    let msg = serde_json::json!(ApiErrorResponse::new(status_code, message, None));\n\n    Bytes::from(msg.to_string())\n}\n\n/// Convert `HeaderValue` to `\u0026str`\npub fn header_value_to_str(value: Option\u003c\u0026HeaderValue\u003e) -\u003e \u0026str {\n    match value {\n        Some(value) =\u003e from_utf8(value.as_bytes()).unwrap_or_default(),\n        None =\u003e \"\",\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_header_value_to_str() {\n        let header_value = HeaderValue::from_static(\"test_value\");\n        let result = header_value_to_str(Some(\u0026header_value));\n        assert_eq!(result, \"test_value\");\n\n        let none_result = header_value_to_str(None);\n        assert_eq!(none_result, \"\");\n    }\n}\n","traces":[{"line":21,"address":[],"length":0,"stats":{"Line":3}},{"line":28,"address":[],"length":0,"stats":{"Line":3}},{"line":31,"address":[],"length":0,"stats":{"Line":3}},{"line":32,"address":[],"length":0,"stats":{"Line":3}},{"line":33,"address":[],"length":0,"stats":{"Line":3}},{"line":34,"address":[],"length":0,"stats":{"Line":6}},{"line":35,"address":[],"length":0,"stats":{"Line":9}},{"line":41,"address":[],"length":0,"stats":{"Line":3}},{"line":43,"address":[],"length":0,"stats":{"Line":3}},{"line":47,"address":[],"length":0,"stats":{"Line":2}},{"line":48,"address":[],"length":0,"stats":{"Line":2}},{"line":49,"address":[],"length":0,"stats":{"Line":1}},{"line":50,"address":[],"length":0,"stats":{"Line":1}}],"covered":13,"coverable":13},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","prometheus.rs"],"content":"//! Prometheus' metrics layer\n\nuse axum::body::Body;\nuse axum::extract::MatchedPath;\nuse axum::http::Request;\nuse axum::response::Response;\nuse bytesize::ByteSize;\nuse futures::future::BoxFuture;\nuse metrics::{counter, gauge, histogram};\nuse std::fmt;\nuse std::path::PathBuf;\nuse std::task::{Context, Poll};\nuse std::time::Instant;\nuse sysinfo::{Disks, System};\nuse tower::{Layer, Service};\n\n/// Prometheus metrics layer for Axum\n#[derive(Clone)]\npub struct PrometheusLayer {\n    /// Service name\n    pub service_name: String,\n\n    /// Disk mount points to monitor\n    pub disk_mount_points: Vec\u003cPathBuf\u003e,\n}\n\nimpl\u003cS\u003e Layer\u003cS\u003e for PrometheusLayer {\n    type Service = PrometheusMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        PrometheusMiddleware {\n            inner,\n            service_name: self.service_name.clone(),\n            disk_mount_points: self.disk_mount_points.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct PrometheusMiddleware\u003cS\u003e {\n    inner: S,\n    service_name: String,\n    disk_mount_points: Vec\u003cPathBuf\u003e,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for PrometheusMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let path = if let Some(matched_path) = request.extensions().get::\u003cMatchedPath\u003e() {\n            matched_path.as_str().to_owned()\n        } else {\n            request.uri().path().to_owned()\n        };\n        let method = request.method().to_string();\n        let service_name = self.service_name.clone();\n        let disk_mount_points = self.disk_mount_points.clone();\n\n        let start = Instant::now();\n        let future = self.inner.call(request);\n        Box::pin(async move {\n            let response = future.await?;\n\n            // Exclude metrics endpoint\n            if path != \"/metrics\" {\n                let latency = start.elapsed().as_secs_f64();\n                let status = response.status().as_u16().to_string();\n                let labels = [\n                    (\"method\", method),\n                    (\"path\", path),\n                    (\"service\", service_name.clone()),\n                    (\"status\", status),\n                ];\n\n                counter!(\"http_requests_total\", \u0026labels).increment(1);\n                histogram!(\"http_requests_duration_seconds\", \u0026labels).record(latency);\n            }\n\n            // System metrics\n            let system_metrics = SystemMetrics::new(\u0026disk_mount_points).await;\n            system_metrics.add_metrics(service_name);\n\n            Ok(response)\n        })\n    }\n}\n\n#[derive(Debug, Clone)]\nstruct SystemMetrics {\n    /// Average CPU usage in percent\n    cpu_usage: f32,\n\n    /// Total memory in bytes\n    total_memory: u64,\n\n    /// Used memory in bytes\n    used_memory: u64,\n\n    /// Total swap space in bytes\n    total_swap: u64,\n\n    /// Used swap space in bytes\n    used_swap: u64,\n\n    /// Total disk space in bytes for a specified mount point\n    total_disks_space: u64,\n\n    /// Used disk space in bytes for a specified mount point\n    used_disks_space: u64,\n}\n\nimpl SystemMetrics {\n    /// Creates a new `SystemMetrics` instance, refreshing the system information\n    async fn new(disk_mount_points: \u0026[PathBuf]) -\u003e Self {\n        let mut sys = System::new_all();\n\n        // CPU\n        sys.refresh_cpu_usage();\n        let mut cpu_usage = sys.global_cpu_usage();\n        tokio::time::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await;\n        sys.refresh_cpu_usage();\n        cpu_usage += sys.global_cpu_usage();\n        cpu_usage /= 2.0;\n\n        // Memory\n        sys.refresh_memory();\n        let total_memory = sys.total_memory();\n        let used_memory = sys.used_memory();\n\n        // Swap\n        let total_swap = sys.total_swap();\n        let used_swap = sys.used_swap();\n\n        // Disks\n        let disks = Disks::new_with_refreshed_list();\n        let mut total_disks_space = 0;\n        let mut used_disks_space = 0;\n        for disk in \u0026disks {\n            if disk_mount_points.contains(\u0026disk.mount_point().to_path_buf()) {\n                total_disks_space += disk.total_space();\n                used_disks_space += disk.total_space() - disk.available_space();\n            }\n        }\n\n        Self {\n            cpu_usage,\n            total_memory,\n            used_memory,\n            total_swap,\n            used_swap,\n            total_disks_space,\n            used_disks_space,\n        }\n    }\n\n    /// Adds the system metrics to Prometheus gauges\n    fn add_metrics(\u0026self, service_name: String) {\n        gauge!(\"system_cpu_usage\", \"service\" =\u003e service_name.clone()).set(self.cpu_usage);\n        gauge!(\"system_total_memory\", \"service\" =\u003e service_name.clone()).set(self.total_memory as f64);\n        gauge!(\"system_used_memory\", \"service\" =\u003e service_name.clone()).set(self.used_memory as f64);\n        gauge!(\"system_total_swap\", \"service\" =\u003e service_name.clone()).set(self.total_swap as f64);\n        gauge!(\"system_used_swap\", \"service\" =\u003e service_name.clone()).set(self.used_swap as f64);\n        gauge!(\"system_total_disks_space\", \"service\" =\u003e service_name.clone()).set(self.total_disks_space as f64);\n        gauge!(\"system_used_disks_usage\", \"service\" =\u003e service_name).set(self.used_disks_space as f64);\n    }\n}\n\nimpl fmt::Display for SystemMetrics {\n    fn fmt(\u0026self, f: \u0026mut fmt::Formatter\u003c'_\u003e) -\u003e fmt::Result {\n        write!(\n            f,\n            \"CPUs:       {:.1}%\\n\\\n             Memory:     {} / {}\\n\\\n             Swap:       {} / {}\\n\\\n             Disk usage: {} / {}\",\n            self.cpu_usage,\n            ByteSize::b(self.used_memory),\n            ByteSize::b(self.total_memory),\n            ByteSize::b(self.used_swap),\n            ByteSize::b(self.total_swap),\n            ByteSize::b(self.used_disks_space),\n            ByteSize::b(self.total_disks_space),\n        )\n    }\n}\n","traces":[{"line":30,"address":[],"length":0,"stats":{"Line":0}},{"line":33,"address":[],"length":0,"stats":{"Line":0}},{"line":34,"address":[],"length":0,"stats":{"Line":0}},{"line":56,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":60,"address":[],"length":0,"stats":{"Line":0}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":0}},{"line":64,"address":[],"length":0,"stats":{"Line":0}},{"line":66,"address":[],"length":0,"stats":{"Line":0}},{"line":67,"address":[],"length":0,"stats":{"Line":0}},{"line":68,"address":[],"length":0,"stats":{"Line":0}},{"line":70,"address":[],"length":0,"stats":{"Line":0}},{"line":71,"address":[],"length":0,"stats":{"Line":0}},{"line":72,"address":[],"length":0,"stats":{"Line":0}},{"line":73,"address":[],"length":0,"stats":{"Line":0}},{"line":76,"address":[],"length":0,"stats":{"Line":0}},{"line":77,"address":[],"length":0,"stats":{"Line":0}},{"line":78,"address":[],"length":0,"stats":{"Line":0}},{"line":79,"address":[],"length":0,"stats":{"Line":0}},{"line":80,"address":[],"length":0,"stats":{"Line":0}},{"line":81,"address":[],"length":0,"stats":{"Line":0}},{"line":82,"address":[],"length":0,"stats":{"Line":0}},{"line":83,"address":[],"length":0,"stats":{"Line":0}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":87,"address":[],"length":0,"stats":{"Line":0}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":0}},{"line":126,"address":[],"length":0,"stats":{"Line":0}},{"line":129,"address":[],"length":0,"stats":{"Line":0}},{"line":130,"address":[],"length":0,"stats":{"Line":0}},{"line":131,"address":[],"length":0,"stats":{"Line":0}},{"line":132,"address":[],"length":0,"stats":{"Line":0}},{"line":133,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":137,"address":[],"length":0,"stats":{"Line":0}},{"line":138,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":142,"address":[],"length":0,"stats":{"Line":0}},{"line":143,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":0}},{"line":147,"address":[],"length":0,"stats":{"Line":0}},{"line":148,"address":[],"length":0,"stats":{"Line":0}},{"line":149,"address":[],"length":0,"stats":{"Line":0}},{"line":150,"address":[],"length":0,"stats":{"Line":0}},{"line":151,"address":[],"length":0,"stats":{"Line":0}},{"line":152,"address":[],"length":0,"stats":{"Line":0}},{"line":168,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":170,"address":[],"length":0,"stats":{"Line":0}},{"line":171,"address":[],"length":0,"stats":{"Line":0}},{"line":172,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":0}},{"line":175,"address":[],"length":0,"stats":{"Line":0}},{"line":180,"address":[],"length":0,"stats":{"Line":0}},{"line":181,"address":[],"length":0,"stats":{"Line":0}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":187,"address":[],"length":0,"stats":{"Line":0}},{"line":188,"address":[],"length":0,"stats":{"Line":0}},{"line":189,"address":[],"length":0,"stats":{"Line":0}},{"line":190,"address":[],"length":0,"stats":{"Line":0}},{"line":191,"address":[],"length":0,"stats":{"Line":0}},{"line":192,"address":[],"length":0,"stats":{"Line":0}},{"line":193,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":67},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","request_id.rs"],"content":"//! Request ID middleware\n\nuse axum::http::{HeaderName, Request};\nuse std::sync::LazyLock;\nuse tower_http::request_id::{MakeRequestId, RequestId};\nuse uuid::Uuid;\n\n#[derive(Clone, Copy)]\npub struct MakeRequestUuid;\n\n/// Request ID header\npub static REQUEST_ID_HEADER: LazyLock\u003cHeaderName\u003e = LazyLock::new(|| HeaderName::from_static(\"x-request-id\"));\n\nimpl MakeRequestId for MakeRequestUuid {\n    fn make_request_id\u003cB\u003e(\u0026mut self, _request: \u0026Request\u003cB\u003e) -\u003e Option\u003cRequestId\u003e {\n        let id = Uuid::new_v4().to_string().parse();\n        match id {\n            Ok(id) =\u003e Some(RequestId::new(id)),\n            _ =\u003e None,\n        }\n    }\n}\n","traces":[{"line":12,"address":[],"length":0,"stats":{"Line":0}},{"line":15,"address":[],"length":0,"stats":{"Line":0}},{"line":16,"address":[],"length":0,"stats":{"Line":0}},{"line":17,"address":[],"length":0,"stats":{"Line":0}},{"line":18,"address":[],"length":0,"stats":{"Line":0}},{"line":19,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":6},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","security_headers.rs"],"content":"//! Security layer (standard security headers: CSP, HSTS, etc.)\n\nuse axum::{\n    body::Body,\n    extract::Request,\n    http::{HeaderName, HeaderValue, header},\n    response::Response,\n};\nuse futures::future::BoxFuture;\nuse std::task::{Context, Poll};\nuse tower::{Layer, Service};\n\n/// Configuration for security headers\n#[derive(Clone, Debug)]\npub struct SecurityHeadersConfig {\n    pub content_security_policy: HeaderValue,\n    pub strict_transport_security: HeaderValue,\n    pub x_content_type_options: HeaderValue,\n    pub x_frame_options: HeaderValue,\n    pub x_xss_protection: HeaderValue,\n    pub referrer_policy: HeaderValue,\n    pub permissions_policy: HeaderValue,\n}\n\nimpl Default for SecurityHeadersConfig {\n    fn default() -\u003e Self {\n        SecurityHeadersConfig {\n            content_security_policy: HeaderValue::from_static(\"default-src 'self';\"),\n            strict_transport_security: HeaderValue::from_static(\"max-age=31536000; includeSubDomains; preload\"),\n            x_content_type_options: HeaderValue::from_static(\"nosniff\"),\n            x_frame_options: HeaderValue::from_static(\"DENY\"),\n            x_xss_protection: HeaderValue::from_static(\"1; mode=block\"),\n            referrer_policy: HeaderValue::from_static(\"no-referrer\"),\n            permissions_policy: HeaderValue::from_static(\"geolocation=(self), microphone=(), camera=()\"),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct SecurityHeadersLayer {\n    pub config: SecurityHeadersConfig,\n}\n\nimpl SecurityHeadersLayer {\n    /// Create a new `SecurityLayer`\n    pub fn new(config: SecurityHeadersConfig) -\u003e Self {\n        Self { config }\n    }\n}\n\nimpl\u003cS\u003e Layer\u003cS\u003e for SecurityHeadersLayer {\n    type Service = SecurityHeadersMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        SecurityHeadersMiddleware {\n            inner,\n            config: self.config.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct SecurityHeadersMiddleware\u003cS\u003e {\n    inner: S,\n    config: SecurityHeadersConfig,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for SecurityHeadersMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + Clone + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let config = self.config.clone();\n        let future = self.inner.call(request);\n\n        Box::pin(async move {\n            let mut response: Response = future.await?;\n\n            let headers = response.headers_mut();\n            headers.insert(header::CONTENT_SECURITY_POLICY, config.content_security_policy);\n            headers.insert(header::STRICT_TRANSPORT_SECURITY, config.strict_transport_security);\n            headers.insert(header::X_CONTENT_TYPE_OPTIONS, config.x_content_type_options);\n            headers.insert(header::X_FRAME_OPTIONS, config.x_frame_options);\n            headers.insert(header::X_XSS_PROTECTION, config.x_xss_protection);\n            headers.insert(header::REFERRER_POLICY, config.referrer_policy);\n            headers.insert(HeaderName::from_static(\"permissions-policy\"), config.permissions_policy);\n\n            Ok(response)\n        })\n    }\n}\n","traces":[{"line":26,"address":[],"length":0,"stats":{"Line":0}},{"line":28,"address":[],"length":0,"stats":{"Line":0}},{"line":29,"address":[],"length":0,"stats":{"Line":0}},{"line":30,"address":[],"length":0,"stats":{"Line":0}},{"line":31,"address":[],"length":0,"stats":{"Line":0}},{"line":32,"address":[],"length":0,"stats":{"Line":0}},{"line":33,"address":[],"length":0,"stats":{"Line":0}},{"line":34,"address":[],"length":0,"stats":{"Line":0}},{"line":46,"address":[],"length":0,"stats":{"Line":0}},{"line":54,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":78,"address":[],"length":0,"stats":{"Line":0}},{"line":79,"address":[],"length":0,"stats":{"Line":0}},{"line":82,"address":[],"length":0,"stats":{"Line":0}},{"line":83,"address":[],"length":0,"stats":{"Line":0}},{"line":84,"address":[],"length":0,"stats":{"Line":0}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":87,"address":[],"length":0,"stats":{"Line":0}},{"line":89,"address":[],"length":0,"stats":{"Line":0}},{"line":90,"address":[],"length":0,"stats":{"Line":0}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":0}},{"line":93,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":95,"address":[],"length":0,"stats":{"Line":0}},{"line":96,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":27},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","time_limiter.rs"],"content":"//! Time limiter layer\n\nuse crate::server::axum::{layers::body_from_parts, response::ApiError};\nuse axum::body::Body;\nuse axum::http::{Request, StatusCode};\nuse axum::response::Response;\nuse chrono::Local;\nuse futures::future::BoxFuture;\nuse std::fmt::Display;\nuse std::task::{Context, Poll};\nuse tower::{Layer, Service};\n\n/// TimeSlots represents a collection of time intervals\n/// where each interval is defined by a start and end time.\n#[derive(Debug, Clone, PartialEq)]\npub struct TimeSlots(Vec\u003cTimeSlot\u003e);\n\nimpl TimeSlots {\n    /// Get time slots vector\n    ///\n    /// # Example\n    /// ```\n    /// use api_tools::server::axum::layers::time_limiter::TimeSlots;\n    ///\n    /// let time_slots: TimeSlots = \"08:00-12:00,13:00-17:00\".into();\n    /// assert_eq!(time_slots.values().len(), 2);\n    /// assert_eq!(time_slots.values()[0].start, \"08:00\");\n    /// assert_eq!(time_slots.values()[0].end, \"12:00\");\n    /// assert_eq!(time_slots.values()[1].start, \"13:00\");\n    /// assert_eq!(time_slots.values()[1].end, \"17:00\");\n    /// ```\n    pub fn values(\u0026self) -\u003e \u0026Vec\u003cTimeSlot\u003e {\n        \u0026self.0\n    }\n\n    /// Check if a time is in the time slots list\n    ///\n    /// # Example\n    /// ```\n    /// use api_tools::server::axum::layers::time_limiter::TimeSlots;\n    ///\n    /// let time_slots: TimeSlots = \"08:00-12:00,13:00-17:00\".into();\n    /// let now = \"09:00\";\n    /// assert_eq!(time_slots.contains(now), true);\n    ///\n    /// let now = \"08:00\";\n    /// assert_eq!(time_slots.contains(now), true);\n    ///\n    /// let now = \"17:00\";\n    /// assert_eq!(time_slots.contains(now), true);\n    ///\n    /// let now = \"12:30\";\n    /// assert_eq!(time_slots.contains(now), false);\n    ///\n    /// let time_slots: TimeSlots = \"\".into();\n    /// let now = \"09:00\";\n    /// assert_eq!(time_slots.contains(now), false);\n    /// ```\n    pub fn contains(\u0026self, time: \u0026str) -\u003e bool {\n        self.0.iter().any(|slot| *slot.start \u003c= *time \u0026\u0026 *time \u003c= *slot.end)\n    }\n}\n\nimpl Display for TimeSlots {\n    fn fmt(\u0026self, f: \u0026mut std::fmt::Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        let mut slots = String::new();\n        for (i, slot) in self.0.iter().enumerate() {\n            slots.push_str(\u0026format!(\"{} - {}\", slot.start, slot.end));\n\n            if i \u003c self.0.len() - 1 {\n                slots.push_str(\", \");\n            }\n        }\n        write!(f, \"{}\", slots)\n    }\n}\n\nimpl From\u003c\u0026str\u003e for TimeSlots {\n    fn from(value: \u0026str) -\u003e Self {\n        Self(\n            value\n                .split(',')\n                .filter_map(|part| part.try_into().ok())\n                .collect::\u003cVec\u003c_\u003e\u003e(),\n        )\n    }\n}\n\n#[derive(Debug, Clone, PartialEq)]\npub struct TimeSlot {\n    pub start: String,\n    pub end: String,\n}\n\nimpl TryFrom\u003c\u0026str\u003e for TimeSlot {\n    type Error = ApiError;\n\n    fn try_from(value: \u0026str) -\u003e Result\u003cSelf, Self::Error\u003e {\n        let (start, end) = value.split_once('-').ok_or(ApiError::InternalServerError(\n            \"Time slots configuration error\".to_string(),\n        ))?;\n\n        if start.len() != 5 || end.len() != 5 {\n            return Err(ApiError::InternalServerError(\n                \"Time slots configuration error\".to_string(),\n            ));\n        }\n\n        Ok(Self {\n            start: start.to_string(),\n            end: end.to_string(),\n        })\n    }\n}\n\n#[derive(Clone)]\npub struct TimeLimiterLayer {\n    pub time_slots: TimeSlots,\n}\n\nimpl TimeLimiterLayer {\n    /// Create a new `TimeLimiterLayer`\n    pub fn new(time_slots: TimeSlots) -\u003e Self {\n        Self { time_slots }\n    }\n}\n\nimpl\u003cS\u003e Layer\u003cS\u003e for TimeLimiterLayer {\n    type Service = TimeLimiterMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        TimeLimiterMiddleware {\n            inner,\n            time_slots: self.time_slots.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct TimeLimiterMiddleware\u003cS\u003e {\n    inner: S,\n    time_slots: TimeSlots,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for TimeLimiterMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let now = Local::now().format(\"%H:%M\").to_string();\n        let is_authorized = !self.time_slots.contains(\u0026now);\n        let time_slots = self.time_slots.clone();\n\n        let future = self.inner.call(request);\n        Box::pin(async move {\n            let mut response = Response::default();\n\n            response = match is_authorized {\n                true =\u003e future.await?,\n                false =\u003e {\n                    let (mut parts, _body) = response.into_parts();\n                    let msg = body_from_parts(\n                        \u0026mut parts,\n                        StatusCode::SERVICE_UNAVAILABLE,\n                        format!(\"Service unavailable during these times: {}\", time_slots).as_str(),\n                        None,\n                    );\n                    Response::from_parts(parts, Body::from(msg))\n                }\n            };\n\n            Ok(response)\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_timeslots_from_str() {\n        let time_slots: TimeSlots = \"08:00-12:00,13:00-17:00\".into();\n        assert_eq!(time_slots.values().len(), 2);\n        assert_eq!(time_slots.values()[0].start, \"08:00\");\n        assert_eq!(time_slots.values()[0].end, \"12:00\");\n        assert_eq!(time_slots.values()[1].start, \"13:00\");\n        assert_eq!(time_slots.values()[1].end, \"17:00\");\n    }\n\n    #[test]\n    fn test_timeslot_try_from_valid() {\n        let slot: TimeSlot = \"10:00-11:00\".try_into().unwrap();\n        assert_eq!(slot.start, \"10:00\");\n        assert_eq!(slot.end, \"11:00\");\n    }\n\n    #[test]\n    fn test_timeslot_try_from_invalid_format() {\n        let slot = TimeSlot::try_from(\"1000-1100\");\n        assert!(slot.is_err());\n        let slot = TimeSlot::try_from(\"10:00/11:00\");\n        assert!(slot.is_err());\n    }\n\n    #[test]\n    fn test_timeslots_display() {\n        let time_slots: TimeSlots = \"08:00-12:00,13:00-17:00\".into();\n        let display = format!(\"{}\", time_slots);\n        assert_eq!(display, \"08:00 - 12:00, 13:00 - 17:00\");\n    }\n\n    #[test]\n    fn test_timeslots_empty_display() {\n        let time_slots: TimeSlots = \"\".into();\n        let display = format!(\"{}\", time_slots);\n        assert_eq!(display, \"\");\n    }\n}\n","traces":[{"line":32,"address":[],"length":0,"stats":{"Line":5}},{"line":33,"address":[],"length":0,"stats":{"Line":5}},{"line":59,"address":[],"length":0,"stats":{"Line":0}},{"line":60,"address":[],"length":0,"stats":{"Line":0}},{"line":65,"address":[],"length":0,"stats":{"Line":2}},{"line":66,"address":[],"length":0,"stats":{"Line":2}},{"line":67,"address":[],"length":0,"stats":{"Line":4}},{"line":70,"address":[],"length":0,"stats":{"Line":1}},{"line":71,"address":[],"length":0,"stats":{"Line":1}},{"line":74,"address":[],"length":0,"stats":{"Line":2}},{"line":79,"address":[],"length":0,"stats":{"Line":3}},{"line":81,"address":[],"length":0,"stats":{"Line":3}},{"line":82,"address":[],"length":0,"stats":{"Line":3}},{"line":83,"address":[],"length":0,"stats":{"Line":11}},{"line":84,"address":[],"length":0,"stats":{"Line":3}},{"line":98,"address":[],"length":0,"stats":{"Line":8}},{"line":99,"address":[],"length":0,"stats":{"Line":14}},{"line":100,"address":[],"length":0,"stats":{"Line":8}},{"line":103,"address":[],"length":0,"stats":{"Line":5}},{"line":104,"address":[],"length":0,"stats":{"Line":1}},{"line":105,"address":[],"length":0,"stats":{"Line":1}},{"line":109,"address":[],"length":0,"stats":{"Line":5}},{"line":110,"address":[],"length":0,"stats":{"Line":5}},{"line":111,"address":[],"length":0,"stats":{"Line":5}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":131,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":155,"address":[],"length":0,"stats":{"Line":0}},{"line":156,"address":[],"length":0,"stats":{"Line":0}},{"line":159,"address":[],"length":0,"stats":{"Line":0}},{"line":160,"address":[],"length":0,"stats":{"Line":0}},{"line":161,"address":[],"length":0,"stats":{"Line":0}},{"line":162,"address":[],"length":0,"stats":{"Line":0}},{"line":164,"address":[],"length":0,"stats":{"Line":0}},{"line":165,"address":[],"length":0,"stats":{"Line":0}},{"line":166,"address":[],"length":0,"stats":{"Line":0}},{"line":168,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":170,"address":[],"length":0,"stats":{"Line":0}},{"line":171,"address":[],"length":0,"stats":{"Line":0}},{"line":172,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":0}},{"line":175,"address":[],"length":0,"stats":{"Line":0}},{"line":176,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":182,"address":[],"length":0,"stats":{"Line":0}}],"covered":22,"coverable":47},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","mod.rs"],"content":"//! Axum server\n\npub mod extractors;\npub mod handlers;\npub mod layers;\npub mod response;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","response.rs"],"content":"//! API response module\n\nuse axum::Json;\nuse axum::http::StatusCode;\nuse axum::response::{IntoResponse, Response};\nuse opentelemetry::TraceId;\nuse opentelemetry::trace::TraceContextExt;\nuse serde::Serialize;\nuse thiserror::Error;\nuse tracing_opentelemetry::OpenTelemetrySpanExt;\n\n/// API response success\n#[derive(Debug, Clone)]\npub struct ApiSuccess\u003cT: Serialize + PartialEq\u003e(StatusCode, Json\u003cT\u003e);\n\nimpl\u003cT\u003e PartialEq for ApiSuccess\u003cT\u003e\nwhere\n    T: Serialize + PartialEq,\n{\n    fn eq(\u0026self, other: \u0026Self) -\u003e bool {\n        self.0 == other.0 \u0026\u0026 self.1.0 == other.1.0\n    }\n}\n\nimpl\u003cT: Serialize + PartialEq\u003e ApiSuccess\u003cT\u003e {\n    pub fn new(status: StatusCode, data: T) -\u003e Self {\n        ApiSuccess(status, Json(data))\n    }\n}\n\nimpl\u003cT: Serialize + PartialEq\u003e IntoResponse for ApiSuccess\u003cT\u003e {\n    fn into_response(self) -\u003e Response {\n        (self.0, self.1).into_response()\n    }\n}\n\n/// Generic response structure shared by all API responses.\n#[derive(Debug, Clone, PartialEq, Serialize)]\npub(crate) struct ApiErrorResponse\u003cT: Serialize + PartialEq\u003e {\n    code: u16,\n    message: T,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    trace_id: Option\u003cString\u003e,\n}\n\nimpl\u003cT: Serialize + PartialEq\u003e ApiErrorResponse\u003cT\u003e {\n    pub(crate) fn new(status_code: StatusCode, message: T, trace_id: Option\u003cString\u003e) -\u003e Self {\n        Self {\n            code: status_code.as_u16(),\n            message,\n            trace_id,\n        }\n    }\n}\n\n/// API error\n#[derive(Debug, Clone, PartialEq, Error)]\npub enum ApiError {\n    #[error(\"Bad request: {0}\")]\n    BadRequest(String),\n\n    #[error(\"Unauthorized: {0}\")]\n    Unauthorized(String),\n\n    #[error(\"Forbidden: {0}\")]\n    Forbidden(String),\n\n    #[error(\"Not found: {0}\")]\n    NotFound(String),\n\n    #[error(\"Unprocessable entity: {0}\")]\n    UnprocessableEntity(String),\n\n    #[error(\"Internal server error: {0}\")]\n    InternalServerError(String),\n\n    #[error(\"Timeout\")]\n    Timeout,\n\n    #[error(\"Too many requests\")]\n    TooManyRequests,\n\n    #[error(\"Method not allowed\")]\n    MethodNotAllowed,\n\n    #[error(\"Payload too large\")]\n    PayloadTooLarge,\n\n    #[error(\"Service unavailable\")]\n    ServiceUnavailable,\n}\n\nimpl ApiError {\n    fn response(code: StatusCode, message: \u0026str) -\u003e impl IntoResponse + '_ {\n        let ctx = tracing::Span::current().context();\n        let trace_id = ctx.span().span_context().trace_id();\n        let trace_id = if trace_id == TraceId::INVALID {\n            None\n        } else {\n            Some(trace_id.to_string())\n        };\n\n        match code {\n            StatusCode::REQUEST_TIMEOUT =\u003e (\n                StatusCode::REQUEST_TIMEOUT,\n                Json(ApiErrorResponse::new(StatusCode::REQUEST_TIMEOUT, message, trace_id)),\n            ),\n            StatusCode::TOO_MANY_REQUESTS =\u003e (\n                StatusCode::TOO_MANY_REQUESTS,\n                Json(ApiErrorResponse::new(StatusCode::TOO_MANY_REQUESTS, message, trace_id)),\n            ),\n            StatusCode::METHOD_NOT_ALLOWED =\u003e (\n                StatusCode::METHOD_NOT_ALLOWED,\n                Json(ApiErrorResponse::new(StatusCode::METHOD_NOT_ALLOWED, message, trace_id)),\n            ),\n            StatusCode::PAYLOAD_TOO_LARGE =\u003e (\n                StatusCode::PAYLOAD_TOO_LARGE,\n                Json(ApiErrorResponse::new(StatusCode::PAYLOAD_TOO_LARGE, message, trace_id)),\n            ),\n            StatusCode::BAD_REQUEST =\u003e (\n                StatusCode::BAD_REQUEST,\n                Json(ApiErrorResponse::new(StatusCode::BAD_REQUEST, message, trace_id)),\n            ),\n            StatusCode::UNAUTHORIZED =\u003e (\n                StatusCode::UNAUTHORIZED,\n                Json(ApiErrorResponse::new(StatusCode::UNAUTHORIZED, message, None)),\n            ),\n            StatusCode::FORBIDDEN =\u003e (\n                StatusCode::FORBIDDEN,\n                Json(ApiErrorResponse::new(StatusCode::FORBIDDEN, message, trace_id)),\n            ),\n            StatusCode::NOT_FOUND =\u003e (\n                StatusCode::NOT_FOUND,\n                Json(ApiErrorResponse::new(StatusCode::NOT_FOUND, message, trace_id)),\n            ),\n            StatusCode::SERVICE_UNAVAILABLE =\u003e (\n                StatusCode::SERVICE_UNAVAILABLE,\n                Json(ApiErrorResponse::new(\n                    StatusCode::SERVICE_UNAVAILABLE,\n                    message,\n                    trace_id,\n                )),\n            ),\n            StatusCode::UNPROCESSABLE_ENTITY =\u003e (\n                StatusCode::UNPROCESSABLE_ENTITY,\n                Json(ApiErrorResponse::new(\n                    StatusCode::UNPROCESSABLE_ENTITY,\n                    message,\n                    trace_id,\n                )),\n            ),\n            _ =\u003e (\n                StatusCode::INTERNAL_SERVER_ERROR,\n                Json(ApiErrorResponse::new(\n                    StatusCode::INTERNAL_SERVER_ERROR,\n                    message,\n                    trace_id,\n                )),\n            ),\n        }\n    }\n}\n\nimpl IntoResponse for ApiError {\n    fn into_response(self) -\u003e Response {\n        match self {\n            ApiError::Timeout =\u003e Self::response(StatusCode::REQUEST_TIMEOUT, \"Request timeout\").into_response(),\n            ApiError::TooManyRequests =\u003e {\n                Self::response(StatusCode::TOO_MANY_REQUESTS, \"Too many requests\").into_response()\n            }\n            ApiError::MethodNotAllowed =\u003e {\n                Self::response(StatusCode::METHOD_NOT_ALLOWED, \"Method not allowed\").into_response()\n            }\n            ApiError::PayloadTooLarge =\u003e {\n                Self::response(StatusCode::PAYLOAD_TOO_LARGE, \"Payload too large\").into_response()\n            }\n            ApiError::ServiceUnavailable =\u003e {\n                Self::response(StatusCode::SERVICE_UNAVAILABLE, \"Service unavailable\").into_response()\n            }\n            ApiError::BadRequest(message) =\u003e Self::response(StatusCode::BAD_REQUEST, \u0026message).into_response(),\n            ApiError::Unauthorized(message) =\u003e Self::response(StatusCode::UNAUTHORIZED, \u0026message).into_response(),\n            ApiError::Forbidden(message) =\u003e Self::response(StatusCode::FORBIDDEN, \u0026message).into_response(),\n            ApiError::NotFound(message) =\u003e Self::response(StatusCode::NOT_FOUND, \u0026message).into_response(),\n            ApiError::UnprocessableEntity(message) =\u003e {\n                Self::response(StatusCode::UNPROCESSABLE_ENTITY, \u0026message).into_response()\n            }\n            ApiError::InternalServerError(message) =\u003e {\n                Self::response(StatusCode::INTERNAL_SERVER_ERROR, \u0026message).into_response()\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde_json::json;\n\n    #[test]\n    fn test_api_success_partial_eq() {\n        let success1 = ApiSuccess::new(StatusCode::OK, json!({\"data\": \"test\"}));\n        let success2 = ApiSuccess::new(StatusCode::OK, json!({\"data\": \"test\"}));\n        assert_eq!(success1, success2);\n\n        let success3 = ApiSuccess::new(StatusCode::BAD_REQUEST, json!({\"data\": \"test\"}));\n        assert_ne!(success1, success3);\n    }\n\n    #[tokio::test]\n    async fn test_api_success_into_response() {\n        let data = json!({\"hello\": \"world\"});\n        let api_success = ApiSuccess::new(StatusCode::OK, data.clone());\n        let response = api_success.into_response();\n        assert_eq!(response.status(), StatusCode::OK);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(body_str, data.to_string());\n    }\n\n    #[test]\n    fn test_new_api_error_response() {\n        let error = ApiErrorResponse::new(StatusCode::BAD_REQUEST, \"Bad request\", None);\n        assert_eq!(error.code, 400);\n        assert_eq!(error.message, \"Bad request\");\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_bad_request() {\n        let error = ApiError::BadRequest(\"Invalid input\".to_string());\n        assert_eq!(error.to_string(), \"Bad request: Invalid input\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::BAD_REQUEST);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(body_str, json!({ \"code\": 400, \"message\": \"Invalid input\" }).to_string());\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_unauthorized() {\n        let error = ApiError::Unauthorized(\"Not authorized\".to_string());\n        assert_eq!(error.to_string(), \"Unauthorized: Not authorized\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 401, \"message\": \"Not authorized\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_forbidden() {\n        let error = ApiError::Forbidden(\"Access denied\".to_string());\n        assert_eq!(error.to_string(), \"Forbidden: Access denied\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::FORBIDDEN);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(body_str, json!({ \"code\": 403, \"message\": \"Access denied\" }).to_string());\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_not_found() {\n        let error = ApiError::NotFound(\"Resource missing\".to_string());\n        assert_eq!(error.to_string(), \"Not found: Resource missing\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::NOT_FOUND);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 404, \"message\": \"Resource missing\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_unprocessable_entity() {\n        let error = ApiError::UnprocessableEntity(\"Invalid data\".to_string());\n        assert_eq!(error.to_string(), \"Unprocessable entity: Invalid data\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(body_str, json!({ \"code\": 422, \"message\": \"Invalid data\" }).to_string());\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_internal_server_error() {\n        let error = ApiError::InternalServerError(\"Unexpected\".to_string());\n        assert_eq!(error.to_string(), \"Internal server error: Unexpected\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(body_str, json!({ \"code\": 500, \"message\": \"Unexpected\" }).to_string());\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_timeout() {\n        let error = ApiError::Timeout;\n        assert_eq!(error.to_string(), \"Timeout\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 408, \"message\": \"Request timeout\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_too_many_requests() {\n        let error = ApiError::TooManyRequests;\n        assert_eq!(error.to_string(), \"Too many requests\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 429, \"message\": \"Too many requests\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_method_not_allowed() {\n        let error = ApiError::MethodNotAllowed;\n        assert_eq!(error.to_string(), \"Method not allowed\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 405, \"message\": \"Method not allowed\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_payload_too_large() {\n        let error = ApiError::PayloadTooLarge;\n        assert_eq!(error.to_string(), \"Payload too large\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 413, \"message\": \"Payload too large\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_service_unavailable() {\n        let error = ApiError::ServiceUnavailable;\n        assert_eq!(error.to_string(), \"Service unavailable\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 503, \"message\": \"Service unavailable\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_response() {\n        let response = ApiError::response(StatusCode::INTERNAL_SERVER_ERROR, \"Internal server error\");\n        let response = response.into_response();\n        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 500, \"message\": \"Internal server error\" }).to_string()\n        );\n    }\n}\n","traces":[{"line":20,"address":[],"length":0,"stats":{"Line":2}},{"line":21,"address":[],"length":0,"stats":{"Line":3}},{"line":26,"address":[],"length":0,"stats":{"Line":4}},{"line":27,"address":[],"length":0,"stats":{"Line":4}},{"line":32,"address":[],"length":0,"stats":{"Line":1}},{"line":33,"address":[],"length":0,"stats":{"Line":1}},{"line":47,"address":[],"length":0,"stats":{"Line":16}},{"line":49,"address":[],"length":0,"stats":{"Line":16}},{"line":94,"address":[],"length":0,"stats":{"Line":12}},{"line":95,"address":[],"length":0,"stats":{"Line":12}},{"line":96,"address":[],"length":0,"stats":{"Line":12}},{"line":97,"address":[],"length":0,"stats":{"Line":24}},{"line":98,"address":[],"length":0,"stats":{"Line":12}},{"line":100,"address":[],"length":0,"stats":{"Line":0}},{"line":103,"address":[],"length":0,"stats":{"Line":12}},{"line":104,"address":[],"length":0,"stats":{"Line":1}},{"line":105,"address":[],"length":0,"stats":{"Line":1}},{"line":106,"address":[],"length":0,"stats":{"Line":1}},{"line":108,"address":[],"length":0,"stats":{"Line":1}},{"line":109,"address":[],"length":0,"stats":{"Line":1}},{"line":110,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":1}},{"line":113,"address":[],"length":0,"stats":{"Line":1}},{"line":114,"address":[],"length":0,"stats":{"Line":1}},{"line":116,"address":[],"length":0,"stats":{"Line":1}},{"line":117,"address":[],"length":0,"stats":{"Line":1}},{"line":118,"address":[],"length":0,"stats":{"Line":1}},{"line":120,"address":[],"length":0,"stats":{"Line":1}},{"line":121,"address":[],"length":0,"stats":{"Line":1}},{"line":122,"address":[],"length":0,"stats":{"Line":1}},{"line":124,"address":[],"length":0,"stats":{"Line":1}},{"line":125,"address":[],"length":0,"stats":{"Line":1}},{"line":126,"address":[],"length":0,"stats":{"Line":1}},{"line":128,"address":[],"length":0,"stats":{"Line":1}},{"line":129,"address":[],"length":0,"stats":{"Line":1}},{"line":130,"address":[],"length":0,"stats":{"Line":1}},{"line":132,"address":[],"length":0,"stats":{"Line":1}},{"line":133,"address":[],"length":0,"stats":{"Line":1}},{"line":134,"address":[],"length":0,"stats":{"Line":1}},{"line":136,"address":[],"length":0,"stats":{"Line":1}},{"line":137,"address":[],"length":0,"stats":{"Line":1}},{"line":138,"address":[],"length":0,"stats":{"Line":1}},{"line":139,"address":[],"length":0,"stats":{"Line":1}},{"line":140,"address":[],"length":0,"stats":{"Line":1}},{"line":141,"address":[],"length":0,"stats":{"Line":1}},{"line":144,"address":[],"length":0,"stats":{"Line":1}},{"line":145,"address":[],"length":0,"stats":{"Line":1}},{"line":146,"address":[],"length":0,"stats":{"Line":1}},{"line":147,"address":[],"length":0,"stats":{"Line":1}},{"line":148,"address":[],"length":0,"stats":{"Line":1}},{"line":149,"address":[],"length":0,"stats":{"Line":1}},{"line":152,"address":[],"length":0,"stats":{"Line":2}},{"line":153,"address":[],"length":0,"stats":{"Line":2}},{"line":154,"address":[],"length":0,"stats":{"Line":2}},{"line":155,"address":[],"length":0,"stats":{"Line":2}},{"line":156,"address":[],"length":0,"stats":{"Line":2}},{"line":157,"address":[],"length":0,"stats":{"Line":2}},{"line":165,"address":[],"length":0,"stats":{"Line":11}},{"line":166,"address":[],"length":0,"stats":{"Line":11}},{"line":167,"address":[],"length":0,"stats":{"Line":1}},{"line":169,"address":[],"length":0,"stats":{"Line":1}},{"line":172,"address":[],"length":0,"stats":{"Line":1}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":178,"address":[],"length":0,"stats":{"Line":1}},{"line":180,"address":[],"length":0,"stats":{"Line":1}},{"line":181,"address":[],"length":0,"stats":{"Line":1}},{"line":182,"address":[],"length":0,"stats":{"Line":1}},{"line":183,"address":[],"length":0,"stats":{"Line":1}},{"line":184,"address":[],"length":0,"stats":{"Line":1}},{"line":185,"address":[],"length":0,"stats":{"Line":1}},{"line":187,"address":[],"length":0,"stats":{"Line":1}},{"line":188,"address":[],"length":0,"stats":{"Line":1}}],"covered":71,"coverable":72},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","mod.rs"],"content":"//! Server module\n\n// Experimental: #[cfg_attr(docsrs, doc(cfg(feature = \"axum\")))]\n#[cfg(feature = \"axum\")]\npub mod axum;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","value_objects","datetime.rs"],"content":"//! Datetime represents a date and time value in the UTC timezone.\n\nuse chrono::{DateTime, TimeDelta, Utc};\nuse serde::Deserialize;\nuse std::fmt::{Display, Formatter};\nuse std::ops::Add;\nuse thiserror::Error;\n\n/// UTC Datetime possible errors\n#[derive(Debug, Clone, PartialEq, Error)]\npub enum UtcDateTimeError {\n    #[error(\"Invalid date time: {0}\")]\n    InvalidDateTime(String),\n}\n\n/// Date time with UTC timezone\n#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Deserialize)]\npub struct UtcDateTime {\n    value: DateTime\u003cUtc\u003e,\n}\n\nimpl UtcDateTime {\n    /// Create a new date time for now\n    pub fn now() -\u003e Self {\n        Self { value: Utc::now() }\n    }\n\n    /// Create a new date time\n    pub fn new(value: DateTime\u003cUtc\u003e) -\u003e Self {\n        Self { value }\n    }\n\n    /// Create a new date time from RFC3339 string\n    ///\n    /// # Example\n    /// ```rust\n    /// use api_tools::value_objects::datetime::UtcDateTime;\n    ///\n    /// let datetime = UtcDateTime::from_rfc3339(\"2024-08-28T12:00:00Z\");\n    /// assert_eq!(datetime.unwrap().to_string(), \"2024-08-28T12:00:00Z\".to_owned());\n    ///\n    /// let invalid_datetime = UtcDateTime::from_rfc3339(\"2024-08-T12:00:00Z\");\n    /// ```\n    pub fn from_rfc3339(value: \u0026str) -\u003e Result\u003cSelf, UtcDateTimeError\u003e {\n        let dt = DateTime::parse_from_rfc3339(value)\n            .map_err(|e| UtcDateTimeError::InvalidDateTime(format!(\"{e}: {value}\")))?;\n\n        Ok(Self {\n            value: dt.with_timezone(\u0026Utc),\n        })\n    }\n\n    /// Get timestamp value\n    pub fn timestamp(\u0026self) -\u003e i64 {\n        self.value.timestamp()\n    }\n\n    /// Get date time value\n    pub fn value(\u0026self) -\u003e DateTime\u003cUtc\u003e {\n        self.value\n    }\n\n    /// Create a new date time from a timestamp\n    pub fn add(\u0026self, rhs: TimeDelta) -\u003e Self {\n        Self {\n            value: self.value.add(rhs),\n        }\n    }\n}\n\nimpl From\u003cDateTime\u003cUtc\u003e\u003e for UtcDateTime {\n    fn from(value: DateTime\u003cUtc\u003e) -\u003e Self {\n        Self { value }\n    }\n}\n\nimpl Display for UtcDateTime {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        write!(f, \"{}\", self.value.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn test_utc_date_time_display() {\n        let dt = DateTime::parse_from_rfc3339(\"2024-08-28T12:00:00Z\")\n            .unwrap()\n            .with_timezone(\u0026Utc);\n        let datetime = UtcDateTime::from(dt);\n\n        assert_eq!(datetime.to_string(), \"2024-08-28T12:00:00Z\");\n    }\n\n    #[test]\n    fn test_from_rfc3339() {\n        let datetime = UtcDateTime::from_rfc3339(\"2024-08-28T12:00:00Z\");\n        assert!(datetime.is_ok());\n        assert_eq!(datetime.unwrap().to_string(), \"2024-08-28T12:00:00Z\".to_owned());\n\n        let invalid_datetime = UtcDateTime::from_rfc3339(\"2024-08-T12:00:00Z\");\n        assert!(invalid_datetime.is_err());\n    }\n\n    #[test]\n    fn test_value() {\n        let dt = DateTime::parse_from_rfc3339(\"2024-08-28T12:00:00Z\")\n            .unwrap()\n            .with_timezone(\u0026Utc);\n        let datetime = UtcDateTime::from(dt);\n\n        assert_eq!(datetime.value(), dt);\n    }\n\n    #[test]\n    fn test_timestamp() {\n        let dt = DateTime::parse_from_rfc3339(\"2024-08-28T12:00:00Z\")\n            .unwrap()\n            .with_timezone(\u0026Utc);\n        let datetime = UtcDateTime::from(dt);\n\n        assert_eq!(datetime.timestamp(), 1724846400);\n    }\n\n    #[test]\n    fn test_add() {\n        let dt = DateTime::parse_from_rfc3339(\"2024-08-28T12:00:00Z\")\n            .unwrap()\n            .with_timezone(\u0026Utc);\n        let datetime = UtcDateTime::from(dt);\n        let new_datetime = datetime.add(TimeDelta::seconds(3600));\n        assert_eq!(new_datetime.to_string(), \"2024-08-28T13:00:00Z\".to_owned());\n    }\n}\n","traces":[{"line":24,"address":[],"length":0,"stats":{"Line":1}},{"line":25,"address":[],"length":0,"stats":{"Line":1}},{"line":29,"address":[],"length":0,"stats":{"Line":0}},{"line":44,"address":[],"length":0,"stats":{"Line":2}},{"line":45,"address":[],"length":0,"stats":{"Line":3}},{"line":46,"address":[],"length":0,"stats":{"Line":6}},{"line":54,"address":[],"length":0,"stats":{"Line":1}},{"line":55,"address":[],"length":0,"stats":{"Line":1}},{"line":59,"address":[],"length":0,"stats":{"Line":1}},{"line":60,"address":[],"length":0,"stats":{"Line":1}},{"line":64,"address":[],"length":0,"stats":{"Line":1}},{"line":66,"address":[],"length":0,"stats":{"Line":1}},{"line":72,"address":[],"length":0,"stats":{"Line":4}},{"line":78,"address":[],"length":0,"stats":{"Line":3}},{"line":79,"address":[],"length":0,"stats":{"Line":3}}],"covered":14,"coverable":15},{"path":["/","Users","fabien","lab","rust","api-tools","src","value_objects","mod.rs"],"content":"//! Value objects list\n\npub mod datetime;\npub mod pagination;\npub mod query_sort;\npub mod timezone;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","value_objects","pagination.rs"],"content":"//! Pagination value object representation\n\n/// Pagination min limit\npub const PAGINATION_MIN_LIMIT: u32 = 50;\n\n/// Pagination max limit\npub const PAGINATION_MAX_LIMIT: u32 = 500;\n\n/// Pagination default limit\npub const PAGINATION_DEFAULT_LIMIT: u32 = 200;\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct Pagination {\n    page: u32,\n    limit: u32,\n    max_limit: Option\u003cu32\u003e,\n}\n\nimpl Pagination {\n    /// Create new pagination\n    ///\n    /// `max_limit` is optional and will be clamped between `PAGINATION_MIN_LIMIT` and `PAGINATION_MAX_LIMIT`\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// use api_tools::value_objects::pagination::{Pagination, PAGINATION_MAX_LIMIT, PAGINATION_MIN_LIMIT};\n    ///\n    /// let pagination = Pagination::new(1, 100, None);\n    /// assert_eq!(pagination.page(), 1);\n    /// assert_eq!(pagination.limit(), 100);\n    ///\n    /// // Invalid page\n    /// let pagination = Pagination::new(0, 100, None);\n    /// assert_eq!(pagination.page(), 1);\n    /// assert_eq!(pagination.limit(), 100);\n    ///\n    /// // Limit too small\n    /// let pagination = Pagination::new(2, 10, None);\n    /// assert_eq!(pagination.page(), 2);\n    /// assert_eq!(pagination.limit(), PAGINATION_MIN_LIMIT);\n    ///\n    /// // Limit too big\n    /// let pagination = Pagination::new(2, 1_000, None);\n    /// assert_eq!(pagination.page(), 2);\n    /// assert_eq!(pagination.limit(), PAGINATION_MAX_LIMIT);\n    ///\n    /// // Limit too big and max limit greater than max\n    /// let pagination = Pagination::new(2, 1_000, Some(800));\n    /// assert_eq!(pagination.page(), 2);\n    /// assert_eq!(pagination.limit(), PAGINATION_MAX_LIMIT);\n    /// ```\n    pub fn new(page: u32, limit: u32, max_limit: Option\u003cu32\u003e) -\u003e Self {\n        let page = if page == 0 { 1 } else { page };\n\n        let mut max = max_limit.unwrap_or(PAGINATION_MAX_LIMIT);\n\n        if max \u003e PAGINATION_MAX_LIMIT {\n            max = PAGINATION_MAX_LIMIT;\n        }\n\n        let limit = if limit \u003e max {\n            max\n        } else if limit \u003c PAGINATION_MIN_LIMIT {\n            PAGINATION_MIN_LIMIT\n        } else {\n            limit\n        };\n\n        Self { page, limit, max_limit }\n    }\n\n    /// Get page\n    pub fn page(\u0026self) -\u003e u32 {\n        self.page\n    }\n\n    /// Get limit\n    pub fn limit(\u0026self) -\u003e u32 {\n        self.limit\n    }\n\n    /// Set a max limit (between `PAGINATION_MIN_LIMIT` and `PAGINATION_MAX_LIMIT`)\n    pub fn set_max_limit(\u0026mut self, max_limit: u32) {\n        let max_limit = max_limit.clamp(PAGINATION_MIN_LIMIT, PAGINATION_MAX_LIMIT);\n        self.max_limit = Some(max_limit);\n    }\n}\n\nimpl Default for Pagination {\n    /// Default pagination\n    fn default() -\u003e Self {\n        let default = if PAGINATION_DEFAULT_LIMIT \u003e PAGINATION_MAX_LIMIT {\n            PAGINATION_MAX_LIMIT\n        } else {\n            PAGINATION_DEFAULT_LIMIT\n        };\n        Self::new(1, default, None)\n    }\n}\n\n/// Pagination for response\n#[derive(Debug, Clone, PartialEq)]\n\npub struct PaginationResponse {\n    pub page: u32,\n    pub limit: u32,\n    pub total: i64,\n}\n\nimpl PaginationResponse {\n    /// Create a new pagination response\n    pub fn new(page: u32, limit: u32, total: i64) -\u003e Self {\n        Self { page, limit, total }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn test_set_max_limit() {\n        let mut pagination = Pagination::default();\n        assert_eq!(pagination.max_limit, None);\n\n        pagination.set_max_limit(100);\n        assert_eq!(pagination.max_limit, Some(100));\n\n        pagination.set_max_limit(300);\n        assert_eq!(pagination.max_limit, Some(300));\n\n        pagination.set_max_limit(20);\n        assert_eq!(pagination.max_limit, Some(PAGINATION_MIN_LIMIT));\n\n        pagination.set_max_limit(600);\n        assert_eq!(pagination.max_limit, Some(PAGINATION_MAX_LIMIT));\n    }\n\n    #[test]\n    fn test_default() {\n        let pagination = Pagination::default();\n        assert_eq!(pagination.page(), 1);\n        assert_eq!(pagination.limit(), PAGINATION_DEFAULT_LIMIT);\n        assert_eq!(pagination.max_limit, None);\n    }\n}\n","traces":[{"line":53,"address":[],"length":0,"stats":{"Line":2}},{"line":54,"address":[],"length":0,"stats":{"Line":6}},{"line":56,"address":[],"length":0,"stats":{"Line":2}},{"line":58,"address":[],"length":0,"stats":{"Line":2}},{"line":59,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":4}},{"line":63,"address":[],"length":0,"stats":{"Line":0}},{"line":64,"address":[],"length":0,"stats":{"Line":2}},{"line":65,"address":[],"length":0,"stats":{"Line":0}},{"line":67,"address":[],"length":0,"stats":{"Line":2}},{"line":74,"address":[],"length":0,"stats":{"Line":1}},{"line":75,"address":[],"length":0,"stats":{"Line":1}},{"line":79,"address":[],"length":0,"stats":{"Line":1}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":84,"address":[],"length":0,"stats":{"Line":4}},{"line":85,"address":[],"length":0,"stats":{"Line":4}},{"line":86,"address":[],"length":0,"stats":{"Line":4}},{"line":92,"address":[],"length":0,"stats":{"Line":2}},{"line":93,"address":[],"length":0,"stats":{"Line":4}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":96,"address":[],"length":0,"stats":{"Line":2}},{"line":98,"address":[],"length":0,"stats":{"Line":2}},{"line":113,"address":[],"length":0,"stats":{"Line":0}}],"covered":18,"coverable":23},{"path":["/","Users","fabien","lab","rust","api-tools","src","value_objects","query_sort.rs"],"content":"//! Query sorts value object representation\n\nuse std::fmt::Display;\n\n/// Filter sort field\npub type QuerySortField = String;\n\n/// Filter sort direction (ASC or DESC)\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub enum QuerySortDirection {\n    /// Ascending sort (`'+'` prefix)\n    /// Example: `?sort=+id`\n    #[default]\n    Asc,\n\n    /// Descending sort (`'-'` prefix)\n    /// Example: `?sort=-name`\n    Desc,\n}\n\nimpl Display for QuerySortDirection {\n    fn fmt(\u0026self, f: \u0026mut std::fmt::Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        write!(\n            f,\n            \"{}\",\n            match self {\n                Self::Asc =\u003e \"ASC\",\n                Self::Desc =\u003e \"DESC\",\n            }\n        )\n    }\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct QuerySort {\n    pub field: QuerySortField,\n    pub direction: QuerySortDirection,\n}\n\nimpl QuerySort {\n    /// Create a new query sort\n    pub fn new(field: QuerySortField, direction: QuerySortDirection) -\u003e Self {\n        Self { field, direction }\n    }\n}\n\n/// Filter sorts\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct QuerySorts(pub Vec\u003cQuerySort\u003e);\n\nimpl From\u003c\u0026str\u003e for QuerySorts {\n    /// Create a new query sorting from a string\n    ///\n    /// # Example\n    /// ```\n    /// use api_tools::value_objects::query_sort::{QuerySort, QuerySortDirection, QuerySorts};\n    ///\n    /// let sorts = QuerySorts::from(\"+id,-name\");\n    /// assert_eq!(\n    ///     sorts.0,\n    ///     vec![\n    ///         QuerySort {\n    ///             field: \"id\".to_string(),\n    ///             direction: QuerySortDirection::Asc\n    ///         },\n    ///         QuerySort {\n    ///             field: \"name\".to_string(),\n    ///             direction: QuerySortDirection::Desc\n    ///         },\n    ///     ]\n    /// );\n    /// ```\n    fn from(value: \u0026str) -\u003e Self {\n        let mut sorts = Vec::new();\n        let parts = value.split(',');\n\n        for part in parts {\n            let prefix = part.chars().next();\n            if let Some(prefix) = prefix {\n                if prefix == '+' {\n                    sorts.push(QuerySort::new(part[1..].to_string(), QuerySortDirection::Asc));\n                } else if prefix == '-' {\n                    sorts.push(QuerySort::new(part[1..].to_string(), QuerySortDirection::Desc));\n                }\n            }\n        }\n\n        Self(sorts)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_query_sort_direction_default() {\n        assert_eq!(QuerySortDirection::default(), QuerySortDirection::Asc);\n    }\n\n    #[test]\n    fn test_query_sort_direction_display() {\n        assert_eq!(\"ASC\", QuerySortDirection::Asc.to_string());\n        assert_eq!(\"DESC\", QuerySortDirection::Desc.to_string());\n    }\n\n    #[test]\n    fn test_filter_sorts_from_str() {\n        let sorts = QuerySorts::from(\"\");\n        assert!(sorts.0.is_empty());\n\n        let sorts = QuerySorts::from(\"+id,-name\");\n        assert_eq!(sorts.0.len(), 2);\n        assert_eq!(\n            sorts.0,\n            vec![\n                QuerySort {\n                    field: \"id\".to_string(),\n                    direction: QuerySortDirection::Asc\n                },\n                QuerySort {\n                    field: \"name\".to_string(),\n                    direction: QuerySortDirection::Desc\n                },\n            ]\n        );\n\n        let sorts = QuerySorts::from(\"id\");\n        assert!(sorts.0.is_empty());\n    }\n}\n","traces":[{"line":22,"address":[],"length":0,"stats":{"Line":2}},{"line":23,"address":[],"length":0,"stats":{"Line":2}},{"line":24,"address":[],"length":0,"stats":{"Line":2}},{"line":26,"address":[],"length":0,"stats":{"Line":2}},{"line":27,"address":[],"length":0,"stats":{"Line":1}},{"line":28,"address":[],"length":0,"stats":{"Line":1}},{"line":42,"address":[],"length":0,"stats":{"Line":2}},{"line":73,"address":[],"length":0,"stats":{"Line":3}},{"line":74,"address":[],"length":0,"stats":{"Line":3}},{"line":75,"address":[],"length":0,"stats":{"Line":3}},{"line":77,"address":[],"length":0,"stats":{"Line":11}},{"line":79,"address":[],"length":0,"stats":{"Line":3}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":81,"address":[],"length":0,"stats":{"Line":1}},{"line":82,"address":[],"length":0,"stats":{"Line":4}},{"line":83,"address":[],"length":0,"stats":{"Line":1}},{"line":88,"address":[],"length":0,"stats":{"Line":3}}],"covered":17,"coverable":17},{"path":["/","Users","fabien","lab","rust","api-tools","src","value_objects","timezone.rs"],"content":"//! Timezone value object representation\n\nuse chrono_tz::Tz;\nuse std::fmt::Display;\nuse std::str::FromStr;\nuse thiserror::Error;\n\n#[derive(Debug, Clone, PartialEq, Error)]\npub enum TimezoneError {\n    #[error(\"Invalid timezone: {0}\")]\n    Invalid(String),\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct Timezone {\n    value: Tz,\n}\n\nimpl Timezone {\n    /// Create a new timezone\n    pub fn new(tz: Tz) -\u003e Self {\n        Self { value: tz }\n    }\n}\n\nimpl TryFrom\u003c\u0026str\u003e for Timezone {\n    type Error = TimezoneError;\n\n    fn try_from(value: \u0026str) -\u003e Result\u003cSelf, Self::Error\u003e {\n        let tz = Tz::from_str(value).map_err(|e| TimezoneError::Invalid(e.to_string()))?;\n\n        Ok(Self::new(tz))\n    }\n}\n\nimpl Display for Timezone {\n    fn fmt(\u0026self, f: \u0026mut std::fmt::Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        write!(f, \"{}\", self.value)\n    }\n}\n\nimpl Default for Timezone {\n    fn default() -\u003e Self {\n        Self::new(Tz::Europe__Paris)\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use chrono_tz::Tz::Europe__Paris;\n\n    #[test]\n    fn test_try_from_str() {\n        let tz = Timezone::try_from(\"Europe/Paris\").unwrap();\n        assert_eq!(tz.value, Europe__Paris);\n\n        let tz = Timezone::try_from(\"Invalid\");\n        assert!(tz.is_err());\n    }\n\n    #[test]\n    fn test_display() {\n        let tz = Timezone::new(Europe__Paris);\n        assert_eq!(tz.to_string(), \"Europe/Paris\");\n    }\n\n    #[test]\n    fn test_default() {\n        let tz = Timezone::default();\n        assert_eq!(tz.value, Europe__Paris);\n    }\n}\n","traces":[{"line":21,"address":[],"length":0,"stats":{"Line":3}},{"line":29,"address":[],"length":0,"stats":{"Line":2}},{"line":30,"address":[],"length":0,"stats":{"Line":7}},{"line":37,"address":[],"length":0,"stats":{"Line":1}},{"line":38,"address":[],"length":0,"stats":{"Line":1}},{"line":43,"address":[],"length":0,"stats":{"Line":1}},{"line":44,"address":[],"length":0,"stats":{"Line":1}}],"covered":7,"coverable":7}]};
        var previousData = {"files":[{"path":["/","Users","fabien","lab","rust","api-tools","src","lib.rs"],"content":"//! # Api Tools - A toolkit for API in Rust\n//!\n//! Toolkit for API in Rust\n//!\n//! API Tools is a Rust library providing utilities for developing robust, consistent, and secure APIs.\n//! It offers ready-to-use layers, extractors, error handling, and helpers designed to simplify API development,\n//! especially with the Axum framework.\n//! The toolkit aims to standardize common API patterns and reduce boilerplate in your Rust projects.\n//!\n//! ## Features list\n//!\n//! | Name         | Description                       | Default |\n//! | ------------ | --------------------------------- | :-----: |\n//! | `axum`       | Enable Axum feature               |   ❌    |\n//! | `prometheus` | Enable Prometheus metrics feature |   ❌    |\n//! | `full`       | Enable all features               |   ❌    |\n//!\n//! ## Components\n//!\n//! ### Value objects\n//!\n//! | Name          | Description                                                                                |\n//! | ------------- | ------------------------------------------------------------------------------------------ |\n//! | `UtcDateTime` | A wrapper around `chrono::DateTime` to handle date and time values in UTC                  |\n//! | `Timezone`    | A wrapper around `chrono_tz::Tz` to handle time zones                                      |\n//! | `Pagination`  | A struct to handle pagination parameters, including page number, page size and total count |\n//! | `QuerySort`   | A struct to handle sorting query parameters, including field and direction                 |\n//!\n//! ### Security\n//!\n//! | Name          | Description                                                                                |\n//! |---------------|----------------------------------|\n//! | `Jwt` | A wrapper for JWT generation and parsing |\n//!\n//! ### Axum\n//!\n//! #### Layers\n//!\n//! | Name                   | Description                                                                                                                              |\n//! | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |\n//! | `BasicAuthLayer`       | Provides HTTP Basic Authentication middleware for protecting routes with username and password                                           |\n//! | `CorsLayer`            | Adds Cross-Origin Resource Sharing (CORS) headers to responses, allowing or restricting resource sharing between different origins       |\n//! | `HttpErrorsLayer`      | Middleware for intercepting and customizing HTTP error responses, enabling standardized error handling across your API                   |\n//! | `LoggerLayer`          | Logs incoming requests and outgoing responses, useful for debugging and monitoring API activity                                          |\n//! | `RequestId`            | Middleware that generates and attaches a unique request identifier (UUID) to each incoming request for traceability                      |\n//! | `TimeLimiterLayer`     | Middleware that restricts API usage to specific time slots. Outside of these allowed periods, it returns a 503 Service Unavailable error |\n//! | `PrometheusLayer`      | Middleware that collects and exposes Prometheus-compatible metrics for monitoring API performance and usage                              |\n//! | `SecurityHeadersLayer` | Middleware add security headers like (CSP, etc.)                                                                                         |\n//!\n//! ##### Utility functions\n//!\n//! | Name                  | Description                                                              |\n//! | --------------------- | ------------------------------------------------------------------------ |\n//! | `body_from_parts`     | Construct a response body from `Parts`, status code, message and headers |\n//! | `header_value_to_str` | Convert `HeaderValue` to `\u0026str`                                          |\n//!\n//! #### Extractors\n//!\n//! | Name               | Description                                                            |\n//! | ------------------ | ---------------------------------------------------------------------- |\n//! | `ExtractRequestId` | Extracts the unique request identifier (UUID) from the request headers |\n//! | `Path`             | Extracts and deserializes path parameters from the request URL         |\n//! | `Query`            | Extracts and deserializes query string parameters from the request URL |\n//!\n//! #### Response helpers\n//!\n//! | Name               | Description                                                                                                 |\n//! | ------------------ | ----------------------------------------------------------------------------------------------------------- |\n//! | `ApiSuccess`       | Represents a successful API response (Status code and data in JSON). It implements the `IntoResponse` trait |\n//! | `ApiError`         | Represents a list of HTTP errors                                                                            |\n//! | `ApiErrorResponse` | Encapsulates the details of an API error response, including the status code and the error message          |\n//!\n//! #### Handlers\n//!\n//! | Name                | Description                                                                                       |\n//! | ------------------- | ------------------------------------------------------------------------------------------------- |\n//! | `PrometheusHandler` | Handler that exposes Prometheus metrics endpoint, allowing metrics scraping by Prometheus servers |\n\n#[macro_use]\nextern crate tracing;\n\npub mod security;\npub mod server;\npub mod value_objects;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","security","jwt","access_token.rs"],"content":"//! Access token entity\n\nuse crate::{server::axum::response::ApiError, value_objects::datetime::UtcDateTime};\nuse axum::{extract::FromRequestParts, http::request::Parts};\nuse hyper::{HeaderMap, header};\nuse serde::Deserialize;\n\n/// Access Token Value represents the value of the access token\npub type AccessTokenValue = String;\n\n/// Access Token\n#[derive(Debug, Clone, PartialEq, Deserialize)]\npub struct AccessToken {\n    /// Token\n    pub token: AccessTokenValue,\n\n    /// Expiration time\n    pub expired_at: UtcDateTime,\n}\n\nimpl AccessToken {\n    /// Create a new access token\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// use api_tools::security::jwt::access_token::AccessToken;\n    /// use api_tools::value_objects::datetime::UtcDateTime;\n    ///\n    /// let token = \"my_access_token\".to_string();\n    /// let expired_at = UtcDateTime::now();\n    /// let access_token = AccessToken::new(token, expired_at.clone());\n    ///\n    /// assert_eq!(access_token.token, \"my_access_token\".to_string());\n    /// assert_eq!(access_token.expired_at, expired_at);\n    /// ```\n    pub fn new(token: String, expired_at: UtcDateTime) -\u003e Self {\n        Self { token, expired_at }\n    }\n\n    /// Extract bearer token from headers\n    pub fn extract_bearer_token_from_headers(headers: \u0026HeaderMap) -\u003e Option\u003cSelf\u003e {\n        headers\n            .get(header::AUTHORIZATION)\n            .and_then(|h| h.to_str().ok())\n            .and_then(|h| {\n                let words = h.split(\"Bearer\").collect::\u003cVec\u003c\u0026str\u003e\u003e();\n                words.get(1).map(|w| w.trim())\n            })\n            .map(|token| AccessToken::new(token.to_string(), UtcDateTime::now()))\n    }\n}\n\n/// JWT extractor from HTTP headers\nimpl\u003cS\u003e FromRequestParts\u003cS\u003e for AccessToken\nwhere\n    S: Send + Sync,\n{\n    type Rejection = ApiError;\n\n    async fn from_request_parts(parts: \u0026mut Parts, _state: \u0026S) -\u003e Result\u003cSelf, Self::Rejection\u003e {\n        Self::extract_bearer_token_from_headers(\u0026parts.headers)\n            .ok_or(ApiError::Unauthorized(\"Missing or invalid token\".to_string()))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use axum::http::HeaderValue;\n\n    #[test]\n    fn test_extract_bearer_token_from_headers() {\n        let mut headers = HeaderMap::new();\n        headers.insert(header::AUTHORIZATION, HeaderValue::from_static(\"Bearer my_token\"));\n\n        let token = AccessToken::extract_bearer_token_from_headers(\u0026headers);\n        assert!(token.is_some());\n        assert_eq!(token.unwrap().token, \"my_token\");\n    }\n\n    #[test]\n    fn test_extract_bearer_token_from_headers_invalid() {\n        let mut headers = HeaderMap::new();\n        headers.insert(header::AUTHORIZATION, HeaderValue::from_static(\"Invalid my_token\"));\n\n        let token = AccessToken::extract_bearer_token_from_headers(\u0026headers);\n        assert!(token.is_none());\n    }\n}\n","traces":[{"line":23,"address":[],"length":0,"stats":{"Line":0}},{"line":28,"address":[],"length":0,"stats":{"Line":0}},{"line":29,"address":[],"length":0,"stats":{"Line":0}},{"line":30,"address":[],"length":0,"stats":{"Line":0}},{"line":31,"address":[],"length":0,"stats":{"Line":0}},{"line":32,"address":[],"length":0,"stats":{"Line":0}},{"line":33,"address":[],"length":0,"stats":{"Line":0}},{"line":34,"address":[],"length":0,"stats":{"Line":0}},{"line":36,"address":[],"length":0,"stats":{"Line":0}},{"line":47,"address":[],"length":0,"stats":{"Line":0}},{"line":48,"address":[],"length":0,"stats":{"Line":0}},{"line":49,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":12},{"path":["/","Users","fabien","lab","rust","api-tools","src","security","jwt","mod.rs"],"content":"//! JWT module\n\npub mod access_token;\npub mod payload;\n\nuse crate::server::axum::response::ApiError;\nuse crate::{security::jwt::access_token::AccessToken, value_objects::datetime::UtcDateTime};\nuse jsonwebtoken::errors::ErrorKind::ExpiredSignature;\nuse jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation, decode, encode};\nuse serde::{Deserialize, Serialize};\nuse std::fmt::{Debug, Formatter};\nuse thiserror::Error;\n\nconst JWT_ACCESS_LIFETIME_IN_MINUTES: i64 = 15; // 15 minutes\nconst JWT_REFRESH_LIFETIME_IN_HOURS: i64 = 7 * 24; // 7 days\n\n/// JWT errors\n#[derive(Debug, Clone, PartialEq, Error)]\npub enum JwtError {\n    #[error(\"Parse token error: {0}\")]\n    ParseError(String),\n\n    #[error(\"Generate token error: {0}\")]\n    GenerateError(String),\n\n    #[error(\"Invalid or unsupported algorithm: {0}\")]\n    InvalidAlgorithm(String),\n\n    #[error(\"Encoding key error: {0}\")]\n    EncodingKeyError(String),\n\n    #[error(\"Decoding key error: {0}\")]\n    DecodingKeyError(String),\n\n    #[error(\"Expired token\")]\n    ExpiredToken,\n}\n\n/// JWT error\nimpl From\u003cJwtError\u003e for ApiError {\n    fn from(value: JwtError) -\u003e Self {\n        Self::InternalServerError(value.to_string())\n    }\n}\n\n/// JWT representation\n#[derive(Clone)]\npub struct Jwt {\n    /// The algorithm supported for signing/verifying JWT\n    algorithm: Algorithm,\n\n    /// Access Token lifetime (in minute)\n    /// The default value is 15 minutes.\n    access_lifetime: i64,\n\n    /// Refresh Token lifetime (in hour)\n    /// The default value is 7 days.\n    refresh_lifetime: i64,\n\n    /// Encoding key\n    encoding_key: Option\u003cEncodingKey\u003e,\n\n    /// Decoding key\n    decoding_key: Option\u003cDecodingKey\u003e,\n}\n\nimpl Default for Jwt {\n    fn default() -\u003e Self {\n        Self {\n            algorithm: Algorithm::HS512,\n            access_lifetime: JWT_ACCESS_LIFETIME_IN_MINUTES,\n            refresh_lifetime: JWT_REFRESH_LIFETIME_IN_HOURS,\n            encoding_key: None,\n            decoding_key: None,\n        }\n    }\n}\n\nimpl Debug for Jwt {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        write!(\n            f,\n            \"JWT =\u003e algo: {:?}, access_lifetime: {}, refresh_lifetime: {}\",\n            self.algorithm, self.access_lifetime, self.refresh_lifetime\n        )\n    }\n}\n\nimpl Jwt {\n    /// Initialize a new `Jwt`\n    pub fn init(\n        algorithm: \u0026str,\n        access_lifetime: i64,\n        refresh_lifetime: i64,\n        secret: Option\u003c\u0026str\u003e,\n        private_key: Option\u003c\u0026str\u003e,\n        public_key: Option\u003c\u0026str\u003e,\n    ) -\u003e Result\u003cSelf, JwtError\u003e {\n        let mut jwt = Jwt {\n            algorithm: Self::algorithm_from_str(algorithm)?,\n            access_lifetime,\n            refresh_lifetime,\n            ..Default::default()\n        };\n\n        // Encoding key\n        match (secret, private_key, jwt.use_secret()) {\n            (Some(secret), _, true) =\u003e jwt.set_encoding_key(secret.trim())?,\n            (_, Some(private_key), false) =\u003e jwt.set_encoding_key(private_key.trim())?,\n            _ =\u003e return Err(JwtError::EncodingKeyError(\"invalid JWT encoding key\".to_owned())),\n        }\n\n        // Decoding key\n        match (secret, public_key, jwt.use_secret()) {\n            (Some(secret), _, true) =\u003e jwt.set_decoding_key(secret.trim())?,\n            (_, Some(public_key), false) =\u003e jwt.set_decoding_key(public_key.trim())?,\n            _ =\u003e return Err(JwtError::DecodingKeyError(\"invalid JWT decoding key\".to_owned())),\n        }\n\n        Ok(jwt)\n    }\n\n    /// Get access token lifetime\n    pub fn access_lifetime(\u0026self) -\u003e i64 {\n        self.access_lifetime\n    }\n\n    /// Get refresh token lifetime\n    pub fn refresh_lifetime(\u0026self) -\u003e i64 {\n        self.refresh_lifetime\n    }\n\n    /// Update access token lifetime (in minute)\n    pub fn set_access_lifetime(\u0026mut self, duration: i64) {\n        self.access_lifetime = duration;\n    }\n\n    /// Update refresh token lifetime (in day)\n    pub fn set_refresh_lifetime(\u0026mut self, duration: i64) {\n        self.refresh_lifetime = duration;\n    }\n\n    /// Update encoding key\n    pub fn set_encoding_key(\u0026mut self, secret: \u0026str) -\u003e Result\u003c(), JwtError\u003e {\n        let key = match self.algorithm {\n            Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 =\u003e EncodingKey::from_secret(secret.as_bytes()),\n            Algorithm::ES256 | Algorithm::ES384 =\u003e EncodingKey::from_ec_pem(secret.as_bytes())\n                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,\n            Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 =\u003e EncodingKey::from_rsa_pem(secret.as_bytes())\n                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,\n            Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 =\u003e EncodingKey::from_rsa_pem(secret.as_bytes())\n                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,\n            Algorithm::EdDSA =\u003e EncodingKey::from_ed_pem(secret.as_bytes())\n                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,\n        };\n\n        self.encoding_key = Some(key);\n\n        Ok(())\n    }\n\n    /// Update decoding key\n    pub fn set_decoding_key(\u0026mut self, secret: \u0026str) -\u003e Result\u003c(), JwtError\u003e {\n        let key = match self.algorithm {\n            Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 =\u003e DecodingKey::from_secret(secret.as_bytes()),\n            Algorithm::ES256 | Algorithm::ES384 =\u003e DecodingKey::from_ec_pem(secret.as_bytes())\n                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,\n            Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 =\u003e DecodingKey::from_rsa_pem(secret.as_bytes())\n                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,\n            Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 =\u003e DecodingKey::from_rsa_pem(secret.as_bytes())\n                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,\n            Algorithm::EdDSA =\u003e DecodingKey::from_ed_pem(secret.as_bytes())\n                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,\n        };\n\n        self.decoding_key = Some(key);\n\n        Ok(())\n    }\n\n    /// Generate JWT\n    pub fn generate\u003cP: Debug + Serialize\u003e(\u0026self, payload: P, expired_at: UtcDateTime) -\u003e Result\u003cAccessToken, JwtError\u003e {\n        let header = jsonwebtoken::Header::new(self.algorithm);\n\n        match self.encoding_key.clone() {\n            Some(encoding_key) =\u003e {\n                let token = encode(\u0026header, \u0026payload, \u0026encoding_key)\n                    .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?;\n\n                Ok(AccessToken { token, expired_at })\n            }\n            _ =\u003e Err(JwtError::EncodingKeyError(\"empty key\".to_owned())),\n        }\n    }\n\n    /// Parse JWT\n    pub fn parse\u003cP: Debug + for\u003c'de\u003e Deserialize\u003c'de\u003e\u003e(\u0026self, token: \u0026AccessToken) -\u003e Result\u003cP, JwtError\u003e {\n        let validation = Validation::new(self.algorithm);\n\n        match self.decoding_key.clone() {\n            Some(decoding_key) =\u003e {\n                let token = decode::\u003cP\u003e(\u0026token.token, \u0026decoding_key, \u0026validation).map_err(|err| match err.kind() {\n                    ExpiredSignature =\u003e JwtError::ExpiredToken,\n                    _ =\u003e JwtError::DecodingKeyError(err.to_string()),\n                })?;\n\n                Ok(token.claims)\n            }\n            _ =\u003e Err(JwtError::DecodingKeyError(\"empty key\".to_owned())),\n        }\n    }\n\n    /// Return true if a secret key is used instead of a pair of keys\n    fn use_secret(\u0026self) -\u003e bool {\n        self.algorithm == Algorithm::HS256 || self.algorithm == Algorithm::HS384 || self.algorithm == Algorithm::HS512\n    }\n\n    /// Convert `\u0026str` to `Algorithm`\n    fn algorithm_from_str(algo: \u0026str) -\u003e Result\u003cAlgorithm, JwtError\u003e {\n        Ok(match algo {\n            \"HS256\" =\u003e Algorithm::HS256,\n            \"HS384\" =\u003e Algorithm::HS384,\n            \"HS512\" =\u003e Algorithm::HS512,\n            \"ES256\" =\u003e Algorithm::ES256,\n            \"ES384\" =\u003e Algorithm::ES384,\n            _ =\u003e {\n                return Err(JwtError::InvalidAlgorithm(algo.to_string()));\n            }\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_jwt_use_secret() {\n        let jwt = Jwt::default();\n        assert!(jwt.use_secret());\n\n        let mut jwt = Jwt::default();\n        jwt.algorithm = Algorithm::ES256;\n        assert!(!jwt.use_secret());\n\n        jwt.algorithm = Algorithm::HS256;\n        assert!(jwt.use_secret());\n    }\n\n    #[test]\n    fn test_jwt_algorithm_from_str() {\n        assert_eq!(Jwt::algorithm_from_str(\"HS256\").unwrap(), Algorithm::HS256);\n        assert_eq!(Jwt::algorithm_from_str(\"HS384\").unwrap(), Algorithm::HS384);\n        assert_eq!(Jwt::algorithm_from_str(\"HS512\").unwrap(), Algorithm::HS512);\n        assert_eq!(Jwt::algorithm_from_str(\"ES256\").unwrap(), Algorithm::ES256);\n        assert_eq!(Jwt::algorithm_from_str(\"ES384\").unwrap(), Algorithm::ES384);\n\n        let invalid_algo = Jwt::algorithm_from_str(\"ES512\");\n        assert!(invalid_algo.is_err());\n        if let Err(e) = invalid_algo {\n            assert_eq!(e, JwtError::InvalidAlgorithm(\"ES512\".to_string()));\n        }\n    }\n\n    #[test]\n    fn test_jwt_default() {\n        let jwt = Jwt::default();\n        assert_eq!(jwt.algorithm, Algorithm::HS512);\n        assert_eq!(jwt.access_lifetime, JWT_ACCESS_LIFETIME_IN_MINUTES);\n        assert_eq!(jwt.refresh_lifetime, JWT_REFRESH_LIFETIME_IN_HOURS);\n        assert!(jwt.encoding_key.is_none());\n        assert!(jwt.decoding_key.is_none());\n    }\n\n    #[test]\n    fn test_jwt_debug() {\n        let jwt = Jwt::default();\n        let debug_str = format!(\"{:?}\", jwt);\n\n        assert_eq!(\n            debug_str,\n            format!(\"JWT =\u003e algo: HS512, access_lifetime: 15, refresh_lifetime: {}\", 7 * 24)\n        );\n    }\n}\n","traces":[{"line":41,"address":[],"length":0,"stats":{"Line":0}},{"line":42,"address":[],"length":0,"stats":{"Line":0}},{"line":68,"address":[],"length":0,"stats":{"Line":4}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":81,"address":[],"length":0,"stats":{"Line":1}},{"line":82,"address":[],"length":0,"stats":{"Line":1}},{"line":84,"address":[],"length":0,"stats":{"Line":1}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":100,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":0}},{"line":108,"address":[],"length":0,"stats":{"Line":0}},{"line":109,"address":[],"length":0,"stats":{"Line":0}},{"line":110,"address":[],"length":0,"stats":{"Line":0}},{"line":114,"address":[],"length":0,"stats":{"Line":0}},{"line":115,"address":[],"length":0,"stats":{"Line":0}},{"line":116,"address":[],"length":0,"stats":{"Line":0}},{"line":117,"address":[],"length":0,"stats":{"Line":0}},{"line":120,"address":[],"length":0,"stats":{"Line":0}},{"line":124,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":0}},{"line":129,"address":[],"length":0,"stats":{"Line":0}},{"line":130,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":135,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":0}},{"line":144,"address":[],"length":0,"stats":{"Line":0}},{"line":145,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":0}},{"line":147,"address":[],"length":0,"stats":{"Line":0}},{"line":148,"address":[],"length":0,"stats":{"Line":0}},{"line":149,"address":[],"length":0,"stats":{"Line":0}},{"line":150,"address":[],"length":0,"stats":{"Line":0}},{"line":151,"address":[],"length":0,"stats":{"Line":0}},{"line":152,"address":[],"length":0,"stats":{"Line":0}},{"line":153,"address":[],"length":0,"stats":{"Line":0}},{"line":154,"address":[],"length":0,"stats":{"Line":0}},{"line":157,"address":[],"length":0,"stats":{"Line":0}},{"line":159,"address":[],"length":0,"stats":{"Line":0}},{"line":163,"address":[],"length":0,"stats":{"Line":0}},{"line":164,"address":[],"length":0,"stats":{"Line":0}},{"line":165,"address":[],"length":0,"stats":{"Line":0}},{"line":166,"address":[],"length":0,"stats":{"Line":0}},{"line":167,"address":[],"length":0,"stats":{"Line":0}},{"line":168,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":170,"address":[],"length":0,"stats":{"Line":0}},{"line":171,"address":[],"length":0,"stats":{"Line":0}},{"line":172,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":0}},{"line":176,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":183,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":0}},{"line":186,"address":[],"length":0,"stats":{"Line":0}},{"line":187,"address":[],"length":0,"stats":{"Line":0}},{"line":188,"address":[],"length":0,"stats":{"Line":0}},{"line":190,"address":[],"length":0,"stats":{"Line":0}},{"line":192,"address":[],"length":0,"stats":{"Line":0}},{"line":197,"address":[],"length":0,"stats":{"Line":0}},{"line":198,"address":[],"length":0,"stats":{"Line":0}},{"line":200,"address":[],"length":0,"stats":{"Line":0}},{"line":201,"address":[],"length":0,"stats":{"Line":0}},{"line":202,"address":[],"length":0,"stats":{"Line":0}},{"line":203,"address":[],"length":0,"stats":{"Line":0}},{"line":204,"address":[],"length":0,"stats":{"Line":0}},{"line":207,"address":[],"length":0,"stats":{"Line":0}},{"line":209,"address":[],"length":0,"stats":{"Line":0}},{"line":214,"address":[],"length":0,"stats":{"Line":3}},{"line":215,"address":[],"length":0,"stats":{"Line":7}},{"line":219,"address":[],"length":0,"stats":{"Line":6}},{"line":220,"address":[],"length":0,"stats":{"Line":6}},{"line":221,"address":[],"length":0,"stats":{"Line":7}},{"line":222,"address":[],"length":0,"stats":{"Line":6}},{"line":223,"address":[],"length":0,"stats":{"Line":5}},{"line":224,"address":[],"length":0,"stats":{"Line":4}},{"line":225,"address":[],"length":0,"stats":{"Line":3}},{"line":227,"address":[],"length":0,"stats":{"Line":1}}],"covered":15,"coverable":79},{"path":["/","Users","fabien","lab","rust","api-tools","src","security","jwt","payload.rs"],"content":"//! JWT Payload module\n\nuse crate::security::jwt::Jwt;\nuse serde::{Deserialize, Serialize};\nuse std::fmt::Debug;\nuse thiserror::Error;\n\n/// Payload errors\n#[derive(Debug, Clone, PartialEq, Error)]\npub enum PayloadError {\n    #[error(\"Missing token\")]\n    MissingToken,\n\n    #[error(\"Invalid token: {0}\")]\n    ParseTokenError(String),\n\n    #[error(\"Invalid headers\")]\n    InvalidHeaders,\n}\n\npub trait PayloadExtractor\u003cH, P: Debug + Serialize + for\u003c'de\u003e Deserialize\u003c'de\u003e\u003e {\n    /// Extract payload from request headers\n    fn try_from_headers(headers: \u0026H, jwt: \u0026Jwt) -\u003e Result\u003cP, PayloadError\u003e;\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","security","mod.rs"],"content":"//! Security module\n\npub mod jwt;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","extractors.rs"],"content":"//! Extractor modules for Axum\n\nuse crate::server::axum::layers::request_id::REQUEST_ID_HEADER;\nuse crate::server::axum::response::ApiError;\nuse axum::extract::FromRequestParts;\nuse axum::extract::path::ErrorKind;\nuse axum::extract::rejection::PathRejection;\nuse axum::http::request::Parts;\nuse axum::http::{HeaderValue, StatusCode};\nuse serde::de::DeserializeOwned;\n\n/// Request ID extractor from HTTP headers\npub struct RequestId(pub HeaderValue);\n\nimpl\u003cS\u003e FromRequestParts\u003cS\u003e for RequestId\nwhere\n    S: Send + Sync,\n{\n    type Rejection = ();\n\n    async fn from_request_parts(parts: \u0026mut Parts, _state: \u0026S) -\u003e Result\u003cSelf, Self::Rejection\u003e {\n        match parts.headers.get(REQUEST_ID_HEADER.clone()) {\n            Some(id) =\u003e Ok(RequestId(id.clone())),\n            _ =\u003e Ok(RequestId(HeaderValue::from_static(\"\"))),\n        }\n    }\n}\n\n/// `Path` extractor customizes the error from `axum::extract::Path`\npub struct Path\u003cT\u003e(pub T);\n\nimpl\u003cS, T\u003e FromRequestParts\u003cS\u003e for Path\u003cT\u003e\nwhere\n    T: DeserializeOwned + Send,\n    S: Send + Sync,\n{\n    type Rejection = (StatusCode, ApiError);\n\n    async fn from_request_parts(parts: \u0026mut Parts, state: \u0026S) -\u003e Result\u003cSelf, Self::Rejection\u003e {\n        match axum::extract::Path::\u003cT\u003e::from_request_parts(parts, state).await {\n            Ok(value) =\u003e Ok(Self(value.0)),\n            Err(rejection) =\u003e {\n                let (status, body) = match rejection {\n                    PathRejection::FailedToDeserializePathParams(inner) =\u003e {\n                        let mut status = StatusCode::BAD_REQUEST;\n\n                        let kind = inner.into_kind();\n                        let body = match \u0026kind {\n                            ErrorKind::WrongNumberOfParameters { .. } =\u003e ApiError::BadRequest(kind.to_string()),\n                            ErrorKind::ParseErrorAtKey { .. } =\u003e ApiError::BadRequest(kind.to_string()),\n                            ErrorKind::ParseErrorAtIndex { .. } =\u003e ApiError::BadRequest(kind.to_string()),\n                            ErrorKind::ParseError { .. } =\u003e ApiError::BadRequest(kind.to_string()),\n                            ErrorKind::InvalidUtf8InPathParam { .. } =\u003e ApiError::BadRequest(kind.to_string()),\n                            ErrorKind::UnsupportedType { .. } =\u003e {\n                                // this error is caused by the programmer using an unsupported type\n                                // (such as nested maps) so respond with `500` instead\n                                status = StatusCode::INTERNAL_SERVER_ERROR;\n                                ApiError::InternalServerError(kind.to_string())\n                            }\n                            ErrorKind::Message(msg) =\u003e ApiError::BadRequest(msg.clone()),\n                            _ =\u003e ApiError::BadRequest(format!(\"Unhandled deserialization error: {kind}\")),\n                        };\n\n                        (status, body)\n                    }\n                    PathRejection::MissingPathParams(error) =\u003e (\n                        StatusCode::INTERNAL_SERVER_ERROR,\n                        ApiError::InternalServerError(error.to_string()),\n                    ),\n                    _ =\u003e (\n                        StatusCode::INTERNAL_SERVER_ERROR,\n                        ApiError::InternalServerError(format!(\"Unhandled path rejection: {rejection}\")),\n                    ),\n                };\n\n                Err((status, body))\n            }\n        }\n    }\n}\n\n/// `Query` extractor customizes the error from `axum::extract::Query`\npub struct Query\u003cT\u003e(pub T);\n\nimpl\u003cT, S\u003e FromRequestParts\u003cS\u003e for Query\u003cT\u003e\nwhere\n    T: DeserializeOwned,\n    S: Send + Sync,\n{\n    type Rejection = (StatusCode, ApiError);\n\n    async fn from_request_parts(parts: \u0026mut Parts, _state: \u0026S) -\u003e Result\u003cSelf, Self::Rejection\u003e {\n        let query = parts.uri.query().unwrap_or_default();\n        let value = serde_urlencoded::from_str(query)\n            .map_err(|err| (StatusCode::BAD_REQUEST, ApiError::BadRequest(err.to_string())))?;\n\n        Ok(Query(value))\n    }\n}\n","traces":[{"line":21,"address":[],"length":0,"stats":{"Line":0}},{"line":22,"address":[],"length":0,"stats":{"Line":0}},{"line":23,"address":[],"length":0,"stats":{"Line":0}},{"line":24,"address":[],"length":0,"stats":{"Line":0}},{"line":39,"address":[],"length":0,"stats":{"Line":0}},{"line":40,"address":[],"length":0,"stats":{"Line":0}},{"line":41,"address":[],"length":0,"stats":{"Line":0}},{"line":42,"address":[],"length":0,"stats":{"Line":0}},{"line":43,"address":[],"length":0,"stats":{"Line":0}},{"line":44,"address":[],"length":0,"stats":{"Line":0}},{"line":45,"address":[],"length":0,"stats":{"Line":0}},{"line":47,"address":[],"length":0,"stats":{"Line":0}},{"line":48,"address":[],"length":0,"stats":{"Line":0}},{"line":49,"address":[],"length":0,"stats":{"Line":0}},{"line":50,"address":[],"length":0,"stats":{"Line":0}},{"line":51,"address":[],"length":0,"stats":{"Line":0}},{"line":52,"address":[],"length":0,"stats":{"Line":0}},{"line":53,"address":[],"length":0,"stats":{"Line":0}},{"line":54,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":58,"address":[],"length":0,"stats":{"Line":0}},{"line":60,"address":[],"length":0,"stats":{"Line":0}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":64,"address":[],"length":0,"stats":{"Line":0}},{"line":66,"address":[],"length":0,"stats":{"Line":0}},{"line":67,"address":[],"length":0,"stats":{"Line":0}},{"line":68,"address":[],"length":0,"stats":{"Line":0}},{"line":70,"address":[],"length":0,"stats":{"Line":0}},{"line":71,"address":[],"length":0,"stats":{"Line":0}},{"line":72,"address":[],"length":0,"stats":{"Line":0}},{"line":76,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":0}},{"line":93,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":95,"address":[],"length":0,"stats":{"Line":0}},{"line":97,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":36},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","handlers","mod.rs"],"content":"//! Axum handlers\n\n#[cfg(feature = \"prometheus\")]\npub mod prometheus;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","handlers","prometheus.rs"],"content":"//! Prometheus metrics handler for Axum\n\nuse crate::server::axum::response::ApiError;\nuse metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle};\n\n/// Buckets for HTTP request duration in seconds\nconst SECONDS_DURATION_BUCKETS: \u0026[f64; 11] = \u0026[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0];\n\n/// Prometheus metrics handler for Axum\npub struct PrometheusHandler {}\n\nimpl PrometheusHandler {\n    /// Return a new `PrometheusHandle`\n    pub fn get_handle() -\u003e Result\u003cPrometheusHandle, ApiError\u003e {\n        PrometheusBuilder::new()\n            .set_buckets_for_metric(\n                Matcher::Full(\"http_requests_duration_seconds\".to_string()),\n                SECONDS_DURATION_BUCKETS,\n            )\n            .map_err(|err| ApiError::InternalServerError(err.to_string()))?\n            .install_recorder()\n            .map_err(|err| ApiError::InternalServerError(err.to_string()))\n    }\n}\n","traces":[{"line":14,"address":[],"length":0,"stats":{"Line":0}},{"line":15,"address":[],"length":0,"stats":{"Line":0}},{"line":17,"address":[],"length":0,"stats":{"Line":0}},{"line":18,"address":[],"length":0,"stats":{"Line":0}},{"line":20,"address":[],"length":0,"stats":{"Line":0}},{"line":22,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":6},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","basic_auth.rs"],"content":"//! Basic Auth layer\n\nuse super::body_from_parts;\nuse axum::{\n    body::Body,\n    http::{HeaderValue, Request, header},\n    response::Response,\n};\nuse futures::future::BoxFuture;\nuse http_auth_basic::Credentials;\nuse hyper::StatusCode;\nuse std::task::{Context, Poll};\nuse tower::{Layer, Service};\n\n#[derive(Clone)]\npub struct BasicAuthLayer {\n    pub username: String,\n    pub password: String,\n}\n\nimpl BasicAuthLayer {\n    /// Create a new `BasicAuthLayer`\n    pub fn new(username: \u0026str, password: \u0026str) -\u003e Self {\n        Self {\n            username: username.to_string(),\n            password: password.to_string(),\n        }\n    }\n}\n\nimpl\u003cS\u003e Layer\u003cS\u003e for BasicAuthLayer {\n    type Service = BasicAuthMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        BasicAuthMiddleware {\n            inner,\n            username: self.username.clone(),\n            password: self.password.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct BasicAuthMiddleware\u003cS\u003e {\n    inner: S,\n    username: String,\n    password: String,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for BasicAuthMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let auth = request\n            .headers()\n            .get(header::AUTHORIZATION)\n            .and_then(|h| h.to_str().ok())\n            .map(str::to_string);\n        let username = self.username.clone();\n        let password = self.password.clone();\n\n        let future = self.inner.call(request);\n        Box::pin(async move {\n            let mut response = Response::default();\n\n            let ok = match auth {\n                None =\u003e false,\n                Some(auth) =\u003e match Credentials::from_header(auth) {\n                    Err(_) =\u003e false,\n                    Ok(cred) =\u003e cred.user_id == username \u0026\u0026 cred.password == password,\n                },\n            };\n            response = match ok {\n                true =\u003e future.await?,\n                false =\u003e {\n                    let (mut parts, _body) = response.into_parts();\n                    let msg = body_from_parts(\n                        \u0026mut parts,\n                        StatusCode::UNAUTHORIZED,\n                        \"Unauthorized\",\n                        Some(vec![(\n                            header::WWW_AUTHENTICATE,\n                            HeaderValue::from_static(\"basic realm=RESTRICTED\"),\n                        )]),\n                    );\n                    Response::from_parts(parts, Body::from(msg))\n                }\n            };\n\n            Ok(response)\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use axum::{\n        body::Body,\n        http::{Request, StatusCode, header},\n        response::Response,\n    };\n    use base64::{Engine as _, engine::general_purpose};\n    use std::convert::Infallible;\n    use tower::ServiceExt;\n\n    async fn dummy_service(_req: Request\u003cBody\u003e) -\u003e Result\u003cResponse, Infallible\u003e {\n        Ok(Response::builder()\n            .status(StatusCode::OK)\n            .body(Body::from(\"ok\"))\n            .unwrap())\n    }\n\n    #[tokio::test]\n    async fn test_basic_auth_layer() {\n        let username = \"user\";\n        let password = \"pass\";\n        let layer = BasicAuthLayer::new(username, password);\n        let service = layer.layer(tower::service_fn(dummy_service));\n\n        // Request without Authorization header\n        let req = Request::builder().uri(\"/\").body(Body::empty()).unwrap();\n        let resp = service.clone().oneshot(req).await.unwrap();\n        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);\n\n        // Request with invalid credentials\n        let bad_auth = format!(\"Basic {}\", general_purpose::STANDARD.encode(\"\"));\n        let req = Request::builder()\n            .uri(\"/\")\n            .header(header::AUTHORIZATION, bad_auth)\n            .body(Body::empty())\n            .unwrap();\n        let resp = service.clone().oneshot(req).await.unwrap();\n        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);\n\n        // Request with bad credentials\n        let bad_auth = format!(\"Basic {}\", general_purpose::STANDARD.encode(\"user:wrong\"));\n        let req = Request::builder()\n            .uri(\"/\")\n            .header(header::AUTHORIZATION, bad_auth)\n            .body(Body::empty())\n            .unwrap();\n        let resp = service.clone().oneshot(req).await.unwrap();\n        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);\n\n        // Request with good credentials\n        let good_auth = format!(\n            \"Basic {}\",\n            general_purpose::STANDARD.encode(format!(\"{}:{}\", username, password))\n        );\n        let req = Request::builder()\n            .uri(\"/\")\n            .header(header::AUTHORIZATION, good_auth)\n            .body(Body::empty())\n            .unwrap();\n        let resp = service.oneshot(req).await.unwrap();\n        assert_eq!(resp.status(), StatusCode::OK);\n    }\n}\n","traces":[{"line":23,"address":[],"length":0,"stats":{"Line":1}},{"line":25,"address":[],"length":0,"stats":{"Line":1}},{"line":26,"address":[],"length":0,"stats":{"Line":1}},{"line":34,"address":[],"length":0,"stats":{"Line":1}},{"line":37,"address":[],"length":0,"stats":{"Line":1}},{"line":38,"address":[],"length":0,"stats":{"Line":1}},{"line":60,"address":[],"length":0,"stats":{"Line":4}},{"line":61,"address":[],"length":0,"stats":{"Line":4}},{"line":64,"address":[],"length":0,"stats":{"Line":4}},{"line":65,"address":[],"length":0,"stats":{"Line":4}},{"line":67,"address":[],"length":0,"stats":{"Line":4}},{"line":68,"address":[],"length":0,"stats":{"Line":11}},{"line":69,"address":[],"length":0,"stats":{"Line":4}},{"line":70,"address":[],"length":0,"stats":{"Line":4}},{"line":71,"address":[],"length":0,"stats":{"Line":4}},{"line":73,"address":[],"length":0,"stats":{"Line":4}},{"line":74,"address":[],"length":0,"stats":{"Line":8}},{"line":75,"address":[],"length":0,"stats":{"Line":4}},{"line":77,"address":[],"length":0,"stats":{"Line":8}},{"line":78,"address":[],"length":0,"stats":{"Line":1}},{"line":79,"address":[],"length":0,"stats":{"Line":3}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":81,"address":[],"length":0,"stats":{"Line":4}},{"line":84,"address":[],"length":0,"stats":{"Line":8}},{"line":85,"address":[],"length":0,"stats":{"Line":1}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":87,"address":[],"length":0,"stats":{"Line":3}},{"line":88,"address":[],"length":0,"stats":{"Line":3}},{"line":89,"address":[],"length":0,"stats":{"Line":3}},{"line":90,"address":[],"length":0,"stats":{"Line":3}},{"line":91,"address":[],"length":0,"stats":{"Line":3}},{"line":92,"address":[],"length":0,"stats":{"Line":3}},{"line":93,"address":[],"length":0,"stats":{"Line":3}},{"line":94,"address":[],"length":0,"stats":{"Line":3}},{"line":97,"address":[],"length":0,"stats":{"Line":3}},{"line":101,"address":[],"length":0,"stats":{"Line":0}}],"covered":34,"coverable":36},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","cors.rs"],"content":"//! CORS layer for Axum\n\nuse axum::http::{HeaderName, HeaderValue, Method};\nuse tower_http::cors::{AllowOrigin, Any, CorsLayer};\n\n/// CORS configuration\n///\n/// # Example\n///\n/// ```rust\n/// use axum::http::{header, HeaderName, HeaderValue, Method};\n/// use api_tools::server::axum::layers::cors::CorsConfig;\n///\n/// let cors_config = CorsConfig {\n///     allow_origin: \"*\",\n///     allow_methods: vec![Method::GET, Method::POST, Method::PUT, Method::PATCH, Method::DELETE],\n///     allow_headers: vec![header::AUTHORIZATION, header::ACCEPT, header::CONTENT_TYPE, header::ORIGIN],\n/// };\n/// ```\npub struct CorsConfig\u003c'a\u003e {\n    pub allow_origin: \u0026'a str,\n    pub allow_methods: Vec\u003cMethod\u003e,\n    pub allow_headers: Vec\u003cHeaderName\u003e,\n}\n\n/// CORS layer\n///\n/// This function creates a CORS layer for Axum with the specified configuration.\n///\n/// # Example\n///\n/// ```rust\n/// use axum::http::{header, HeaderName, HeaderValue, Method};\n/// use api_tools::server::axum::layers::cors::{cors, CorsConfig};\n///\n/// let cors_config = CorsConfig {\n///     allow_origin: \"*\",\n///     allow_methods: vec![Method::GET, Method::POST, Method::PUT, Method::PATCH, Method::DELETE],\n///     allow_headers: vec![header::AUTHORIZATION, header::ACCEPT, header::CONTENT_TYPE, header::ORIGIN],\n/// };\n///\n/// let layer = cors(cors_config);\n/// ```\npub fn cors(config: CorsConfig) -\u003e CorsLayer {\n    let allow_origin = config.allow_origin;\n\n    let layer = CorsLayer::new()\n        .allow_methods(config.allow_methods)\n        .allow_headers(config.allow_headers);\n\n    if allow_origin == \"*\" {\n        layer.allow_origin(Any)\n    } else {\n        let origins = allow_origin\n            .split(',')\n            .filter(|url| *url != \"*\" \u0026\u0026 !url.is_empty())\n            .filter_map(|url| url.parse().ok())\n            .collect::\u003cVec\u003cHeaderValue\u003e\u003e();\n\n        if origins.is_empty() {\n            layer.allow_origin(Any)\n        } else {\n            layer\n                .allow_origin(AllowOrigin::predicate(move |origin: \u0026HeaderValue, _| {\n                    origins.contains(origin)\n                }))\n                .allow_credentials(true)\n        }\n    }\n}\n","traces":[{"line":44,"address":[],"length":0,"stats":{"Line":0}},{"line":45,"address":[],"length":0,"stats":{"Line":0}},{"line":47,"address":[],"length":0,"stats":{"Line":0}},{"line":48,"address":[],"length":0,"stats":{"Line":0}},{"line":49,"address":[],"length":0,"stats":{"Line":0}},{"line":51,"address":[],"length":0,"stats":{"Line":0}},{"line":52,"address":[],"length":0,"stats":{"Line":0}},{"line":54,"address":[],"length":0,"stats":{"Line":0}},{"line":56,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":60,"address":[],"length":0,"stats":{"Line":0}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":63,"address":[],"length":0,"stats":{"Line":0}},{"line":64,"address":[],"length":0,"stats":{"Line":0}},{"line":65,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":15},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","http_errors.rs"],"content":"//! Override some HTTP errors\n\nuse crate::server::axum::response::ApiError;\nuse axum::body::Body;\nuse axum::http::{Request, StatusCode};\nuse axum::response::{IntoResponse, Response};\nuse futures::future::BoxFuture;\nuse std::task::{Context, Poll};\nuse tower::{Layer, Service};\n\n/// Configuration for the `HttpErrorsLayer`\n#[derive(Clone, Debug)]\npub struct HttpErrorsConfig {\n    /// Maximum size of the body in bytes\n    pub body_max_size: usize,\n}\n\n#[derive(Clone)]\npub struct HttpErrorsLayer {\n    pub config: HttpErrorsConfig,\n}\n\nimpl HttpErrorsLayer {\n    /// Create a new `HttpErrorsLayer`\n    pub fn new(config: \u0026HttpErrorsConfig) -\u003e Self {\n        Self { config: config.clone() }\n    }\n}\n\nimpl\u003cS\u003e Layer\u003cS\u003e for HttpErrorsLayer {\n    type Service = HttpErrorsMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        HttpErrorsMiddleware {\n            inner,\n            config: self.config.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct HttpErrorsMiddleware\u003cS\u003e {\n    inner: S,\n    config: HttpErrorsConfig,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for HttpErrorsMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + Clone + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let mut inner = self.inner.clone();\n        let config = self.config.clone();\n\n        Box::pin(async move {\n            let response: Response = inner.call(request).await?;\n\n            // Vérifie le content-type\n            let headers = response.headers();\n            if let Some(content_type) = headers.get(\"content-type\") {\n                let content_type = content_type.to_str().unwrap_or_default();\n                if content_type.starts_with(\"image/\")\n                    || content_type.starts_with(\"audio/\")\n                    || content_type.starts_with(\"video/\")\n                {\n                    return Ok(response);\n                }\n            }\n\n            let (parts, body) = response.into_parts();\n            match axum::body::to_bytes(body, config.body_max_size).await {\n                Ok(body) =\u003e match String::from_utf8(body.to_vec()) {\n                    Ok(body) =\u003e match parts.status {\n                        StatusCode::METHOD_NOT_ALLOWED =\u003e Ok(ApiError::MethodNotAllowed.into_response()),\n                        StatusCode::UNPROCESSABLE_ENTITY =\u003e Ok(ApiError::UnprocessableEntity(body).into_response()),\n                        _ =\u003e Ok(Response::from_parts(parts, Body::from(body))),\n                    },\n                    Err(err) =\u003e Ok(ApiError::InternalServerError(err.to_string()).into_response()),\n                },\n                Err(_) =\u003e Ok(ApiError::PayloadTooLarge.into_response()),\n            }\n        })\n    }\n}\n","traces":[{"line":25,"address":[],"length":0,"stats":{"Line":0}},{"line":26,"address":[],"length":0,"stats":{"Line":0}},{"line":33,"address":[],"length":0,"stats":{"Line":0}},{"line":36,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":58,"address":[],"length":0,"stats":{"Line":0}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":0}},{"line":63,"address":[],"length":0,"stats":{"Line":0}},{"line":65,"address":[],"length":0,"stats":{"Line":0}},{"line":66,"address":[],"length":0,"stats":{"Line":0}},{"line":69,"address":[],"length":0,"stats":{"Line":0}},{"line":70,"address":[],"length":0,"stats":{"Line":0}},{"line":71,"address":[],"length":0,"stats":{"Line":0}},{"line":72,"address":[],"length":0,"stats":{"Line":0}},{"line":73,"address":[],"length":0,"stats":{"Line":0}},{"line":74,"address":[],"length":0,"stats":{"Line":0}},{"line":76,"address":[],"length":0,"stats":{"Line":0}},{"line":80,"address":[],"length":0,"stats":{"Line":0}},{"line":81,"address":[],"length":0,"stats":{"Line":0}},{"line":82,"address":[],"length":0,"stats":{"Line":0}},{"line":83,"address":[],"length":0,"stats":{"Line":0}},{"line":84,"address":[],"length":0,"stats":{"Line":0}},{"line":85,"address":[],"length":0,"stats":{"Line":0}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":88,"address":[],"length":0,"stats":{"Line":0}},{"line":90,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":27},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","logger.rs"],"content":"//! Logger layer\n\nuse super::header_value_to_str;\nuse axum::body::HttpBody;\nuse axum::http::StatusCode;\nuse axum::{body::Body, http::Request, response::Response};\nuse bytesize::ByteSize;\nuse futures::future::BoxFuture;\nuse std::{\n    fmt::Display,\n    task::{Context, Poll},\n    time::{Duration, Instant},\n};\nuse tower::{Layer, Service};\n\n#[derive(Debug, Default)]\nstruct LoggerMessage {\n    method: String,\n    request_id: String,\n    host: String,\n    path: String,\n    uri: String,\n    user_agent: String,\n    status_code: u16,\n    version: String,\n    latency: Duration,\n    body_size: u64,\n}\n\nimpl Display for LoggerMessage {\n    fn fmt(\u0026self, f: \u0026mut std::fmt::Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        write!(\n            f,\n            \"status_code: {}, method: {}, path: {}, uri: {}, host: {}, request_id: {}, user_agent: {}, version: {}, latency: {:?}, body_size: {}\",\n            self.status_code,\n            self.method,\n            self.path,\n            self.uri,\n            self.host,\n            self.request_id,\n            self.user_agent,\n            self.version,\n            self.latency,\n            ByteSize::b(self.body_size),\n        )\n    }\n}\n\n#[derive(Clone)]\npub struct LoggerLayer;\n\nimpl\u003cS\u003e Layer\u003cS\u003e for LoggerLayer {\n    type Service = LoggerMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        LoggerMiddleware { inner }\n    }\n}\n\n#[derive(Clone)]\npub struct LoggerMiddleware\u003cS\u003e {\n    inner: S,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for LoggerMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let now = Instant::now();\n        let request_headers = request.headers();\n\n        let mut message = LoggerMessage {\n            method: request.method().to_string(),\n            path: request.uri().path().to_string(),\n            uri: request.uri().to_string(),\n            host: header_value_to_str(request_headers.get(\"host\")).to_string(),\n            request_id: header_value_to_str(request_headers.get(\"x-request-id\")).to_string(),\n            user_agent: header_value_to_str(request_headers.get(\"user-agent\")).to_string(),\n            ..Default::default()\n        };\n\n        let future = self.inner.call(request);\n        Box::pin(async move {\n            let response: Response = future.await?;\n\n            message.status_code = response.status().as_u16();\n            message.version = format!(\"{:?}\", response.version());\n            message.latency = now.elapsed();\n            message.body_size = response.body().size_hint().lower();\n\n            if response.status() \u003e= StatusCode::INTERNAL_SERVER_ERROR\n                \u0026\u0026 response.status() != StatusCode::SERVICE_UNAVAILABLE\n            {\n                error!(\n                    status_code = %message.status_code,\n                    method = %message.method,\n                    path = %message.path,\n                    uri = %message.uri,\n                    host = %message.host,\n                    request_id = %message.request_id,\n                    user_agent = %message.user_agent,\n                    version = %message.version,\n                    latency = %format!(\"{:?}\", message.latency),\n                    body_size = %ByteSize::b(message.body_size),\n                );\n            } else if !message.path.starts_with(\"/metrics\") {\n                info!(\n                    status_code = %message.status_code,\n                    method = %message.method,\n                    path = %message.path,\n                    uri = %message.uri,\n                    host = %message.host,\n                    request_id = %message.request_id,\n                    user_agent = %message.user_agent,\n                    version = %message.version,\n                    latency = %format!(\"{:?}\", message.latency),\n                    body_size = %ByteSize::b(message.body_size),\n                );\n            }\n\n            Ok(response)\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::time::Duration;\n\n    #[test]\n    fn test_logger_message_fmt() {\n        let message = LoggerMessage {\n            method: \"GET\".to_string(),\n            request_id: \"abc-123\".to_string(),\n            host: \"localhost\".to_string(),\n            path: \"/test\".to_string(),\n            uri: \"/test?query=1\".to_string(),\n            user_agent: \"TestAgent/1.0\".to_string(),\n            status_code: 200,\n            version: \"HTTP/1.1\".to_string(),\n            latency: Duration::from_millis(42),\n            body_size: 1_524,\n        };\n        let expected = String::from(\n            \"status_code: 200, method: GET, path: /test, uri: /test?query=1, host: localhost, request_id: abc-123, user_agent: TestAgent/1.0, version: HTTP/1.1, latency: 42ms, body_size: 1.5 KiB\",\n        );\n\n        assert_eq!(message.to_string(), expected);\n    }\n}\n","traces":[{"line":31,"address":[],"length":0,"stats":{"Line":1}},{"line":32,"address":[],"length":0,"stats":{"Line":1}},{"line":33,"address":[],"length":0,"stats":{"Line":1}},{"line":35,"address":[],"length":0,"stats":{"Line":1}},{"line":36,"address":[],"length":0,"stats":{"Line":1}},{"line":37,"address":[],"length":0,"stats":{"Line":1}},{"line":38,"address":[],"length":0,"stats":{"Line":1}},{"line":39,"address":[],"length":0,"stats":{"Line":1}},{"line":40,"address":[],"length":0,"stats":{"Line":1}},{"line":41,"address":[],"length":0,"stats":{"Line":1}},{"line":42,"address":[],"length":0,"stats":{"Line":1}},{"line":43,"address":[],"length":0,"stats":{"Line":1}},{"line":44,"address":[],"length":0,"stats":{"Line":1}},{"line":55,"address":[],"length":0,"stats":{"Line":0}},{"line":75,"address":[],"length":0,"stats":{"Line":0}},{"line":76,"address":[],"length":0,"stats":{"Line":0}},{"line":79,"address":[],"length":0,"stats":{"Line":0}},{"line":80,"address":[],"length":0,"stats":{"Line":0}},{"line":81,"address":[],"length":0,"stats":{"Line":0}},{"line":84,"address":[],"length":0,"stats":{"Line":0}},{"line":85,"address":[],"length":0,"stats":{"Line":0}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":87,"address":[],"length":0,"stats":{"Line":0}},{"line":88,"address":[],"length":0,"stats":{"Line":0}},{"line":89,"address":[],"length":0,"stats":{"Line":0}},{"line":93,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":95,"address":[],"length":0,"stats":{"Line":0}},{"line":97,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":0}},{"line":99,"address":[],"length":0,"stats":{"Line":0}},{"line":100,"address":[],"length":0,"stats":{"Line":0}},{"line":102,"address":[],"length":0,"stats":{"Line":0}},{"line":103,"address":[],"length":0,"stats":{"Line":0}},{"line":105,"address":[],"length":0,"stats":{"Line":0}},{"line":106,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":0}},{"line":108,"address":[],"length":0,"stats":{"Line":0}},{"line":109,"address":[],"length":0,"stats":{"Line":0}},{"line":110,"address":[],"length":0,"stats":{"Line":0}},{"line":111,"address":[],"length":0,"stats":{"Line":0}},{"line":112,"address":[],"length":0,"stats":{"Line":0}},{"line":113,"address":[],"length":0,"stats":{"Line":0}},{"line":114,"address":[],"length":0,"stats":{"Line":0}},{"line":115,"address":[],"length":0,"stats":{"Line":0}},{"line":117,"address":[],"length":0,"stats":{"Line":0}},{"line":118,"address":[],"length":0,"stats":{"Line":0}},{"line":119,"address":[],"length":0,"stats":{"Line":0}},{"line":120,"address":[],"length":0,"stats":{"Line":0}},{"line":121,"address":[],"length":0,"stats":{"Line":0}},{"line":122,"address":[],"length":0,"stats":{"Line":0}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":124,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":0}},{"line":126,"address":[],"length":0,"stats":{"Line":0}},{"line":127,"address":[],"length":0,"stats":{"Line":0}},{"line":128,"address":[],"length":0,"stats":{"Line":0}},{"line":132,"address":[],"length":0,"stats":{"Line":0}}],"covered":13,"coverable":58},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","mod.rs"],"content":"//! Axum layers\n\npub mod basic_auth;\npub mod cors;\npub mod http_errors;\npub mod logger;\n#[cfg(feature = \"prometheus\")]\npub mod prometheus;\npub mod request_id;\npub mod security_headers;\npub mod time_limiter;\n\nuse crate::server::axum::response::ApiErrorResponse;\nuse axum::http::header::CONTENT_TYPE;\nuse axum::http::response::Parts;\nuse axum::http::{HeaderName, HeaderValue, StatusCode};\nuse bytes::Bytes;\nuse std::str::from_utf8;\n\n/// Construct a response body from `Parts`, status code, message and headers\npub fn body_from_parts(\n    parts: \u0026mut Parts,\n    status_code: StatusCode,\n    message: \u0026str,\n    headers: Option\u003cVec\u003c(HeaderName, HeaderValue)\u003e\u003e,\n) -\u003e Bytes {\n    // Status\n    parts.status = status_code;\n\n    // Headers\n    parts\n        .headers\n        .insert(CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()));\n    if let Some(headers) = headers {\n        for header in headers {\n            parts.headers.insert(header.0, header.1);\n        }\n    }\n\n    // Body\n    let msg = serde_json::json!(ApiErrorResponse::new(status_code, message, None));\n\n    Bytes::from(msg.to_string())\n}\n\n/// Convert `HeaderValue` to `\u0026str`\npub fn header_value_to_str(value: Option\u003c\u0026HeaderValue\u003e) -\u003e \u0026str {\n    match value {\n        Some(value) =\u003e from_utf8(value.as_bytes()).unwrap_or_default(),\n        None =\u003e \"\",\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_header_value_to_str() {\n        let header_value = HeaderValue::from_static(\"test_value\");\n        let result = header_value_to_str(Some(\u0026header_value));\n        assert_eq!(result, \"test_value\");\n\n        let none_result = header_value_to_str(None);\n        assert_eq!(none_result, \"\");\n    }\n}\n","traces":[{"line":21,"address":[],"length":0,"stats":{"Line":3}},{"line":28,"address":[],"length":0,"stats":{"Line":3}},{"line":31,"address":[],"length":0,"stats":{"Line":3}},{"line":32,"address":[],"length":0,"stats":{"Line":3}},{"line":33,"address":[],"length":0,"stats":{"Line":3}},{"line":34,"address":[],"length":0,"stats":{"Line":6}},{"line":35,"address":[],"length":0,"stats":{"Line":9}},{"line":41,"address":[],"length":0,"stats":{"Line":3}},{"line":43,"address":[],"length":0,"stats":{"Line":3}},{"line":47,"address":[],"length":0,"stats":{"Line":2}},{"line":48,"address":[],"length":0,"stats":{"Line":2}},{"line":49,"address":[],"length":0,"stats":{"Line":1}},{"line":50,"address":[],"length":0,"stats":{"Line":1}}],"covered":13,"coverable":13},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","prometheus.rs"],"content":"//! Prometheus' metrics layer\n\nuse axum::body::Body;\nuse axum::extract::MatchedPath;\nuse axum::http::Request;\nuse axum::response::Response;\nuse bytesize::ByteSize;\nuse futures::future::BoxFuture;\nuse metrics::{counter, gauge, histogram};\nuse std::fmt;\nuse std::path::PathBuf;\nuse std::task::{Context, Poll};\nuse std::time::Instant;\nuse sysinfo::{Disks, System};\nuse tower::{Layer, Service};\n\n/// Prometheus metrics layer for Axum\n#[derive(Clone)]\npub struct PrometheusLayer {\n    /// Service name\n    pub service_name: String,\n\n    /// Disk mount points to monitor\n    pub disk_mount_points: Vec\u003cPathBuf\u003e,\n}\n\nimpl\u003cS\u003e Layer\u003cS\u003e for PrometheusLayer {\n    type Service = PrometheusMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        PrometheusMiddleware {\n            inner,\n            service_name: self.service_name.clone(),\n            disk_mount_points: self.disk_mount_points.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct PrometheusMiddleware\u003cS\u003e {\n    inner: S,\n    service_name: String,\n    disk_mount_points: Vec\u003cPathBuf\u003e,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for PrometheusMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let path = if let Some(matched_path) = request.extensions().get::\u003cMatchedPath\u003e() {\n            matched_path.as_str().to_owned()\n        } else {\n            request.uri().path().to_owned()\n        };\n        let method = request.method().to_string();\n        let service_name = self.service_name.clone();\n        let disk_mount_points = self.disk_mount_points.clone();\n\n        let start = Instant::now();\n        let future = self.inner.call(request);\n        Box::pin(async move {\n            let response = future.await?;\n\n            // Exclude metrics endpoint\n            if path != \"/metrics\" {\n                let latency = start.elapsed().as_secs_f64();\n                let status = response.status().as_u16().to_string();\n                let labels = [\n                    (\"method\", method),\n                    (\"path\", path),\n                    (\"service\", service_name.clone()),\n                    (\"status\", status),\n                ];\n\n                counter!(\"http_requests_total\", \u0026labels).increment(1);\n                histogram!(\"http_requests_duration_seconds\", \u0026labels).record(latency);\n            }\n\n            // System metrics\n            let system_metrics = SystemMetrics::new(\u0026disk_mount_points).await;\n            system_metrics.add_metrics(service_name);\n\n            Ok(response)\n        })\n    }\n}\n\n#[derive(Debug, Clone)]\nstruct SystemMetrics {\n    /// Average CPU usage in percent\n    cpu_usage: f32,\n\n    /// Total memory in bytes\n    total_memory: u64,\n\n    /// Used memory in bytes\n    used_memory: u64,\n\n    /// Total swap space in bytes\n    total_swap: u64,\n\n    /// Used swap space in bytes\n    used_swap: u64,\n\n    /// Total disk space in bytes for a specified mount point\n    total_disks_space: u64,\n\n    /// Used disk space in bytes for a specified mount point\n    used_disks_space: u64,\n}\n\nimpl SystemMetrics {\n    /// Creates a new `SystemMetrics` instance, refreshing the system information\n    async fn new(disk_mount_points: \u0026[PathBuf]) -\u003e Self {\n        let mut sys = System::new_all();\n\n        // CPU\n        sys.refresh_cpu_usage();\n        let mut cpu_usage = sys.global_cpu_usage();\n        tokio::time::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await;\n        sys.refresh_cpu_usage();\n        cpu_usage += sys.global_cpu_usage();\n        cpu_usage /= 2.0;\n\n        // Memory\n        sys.refresh_memory();\n        let total_memory = sys.total_memory();\n        let used_memory = sys.used_memory();\n\n        // Swap\n        let total_swap = sys.total_swap();\n        let used_swap = sys.used_swap();\n\n        // Disks\n        let disks = Disks::new_with_refreshed_list();\n        let mut total_disks_space = 0;\n        let mut used_disks_space = 0;\n        for disk in \u0026disks {\n            if disk_mount_points.contains(\u0026disk.mount_point().to_path_buf()) {\n                total_disks_space += disk.total_space();\n                used_disks_space += disk.total_space() - disk.available_space();\n            }\n        }\n\n        Self {\n            cpu_usage,\n            total_memory,\n            used_memory,\n            total_swap,\n            used_swap,\n            total_disks_space,\n            used_disks_space,\n        }\n    }\n\n    /// Adds the system metrics to Prometheus gauges\n    fn add_metrics(\u0026self, service_name: String) {\n        gauge!(\"system_cpu_usage\", \"service\" =\u003e service_name.clone()).set(self.cpu_usage);\n        gauge!(\"system_total_memory\", \"service\" =\u003e service_name.clone()).set(self.total_memory as f64);\n        gauge!(\"system_used_memory\", \"service\" =\u003e service_name.clone()).set(self.used_memory as f64);\n        gauge!(\"system_total_swap\", \"service\" =\u003e service_name.clone()).set(self.total_swap as f64);\n        gauge!(\"system_used_swap\", \"service\" =\u003e service_name.clone()).set(self.used_swap as f64);\n        gauge!(\"system_total_disks_space\", \"service\" =\u003e service_name.clone()).set(self.total_disks_space as f64);\n        gauge!(\"system_used_disks_usage\", \"service\" =\u003e service_name).set(self.used_disks_space as f64);\n    }\n}\n\nimpl fmt::Display for SystemMetrics {\n    fn fmt(\u0026self, f: \u0026mut fmt::Formatter\u003c'_\u003e) -\u003e fmt::Result {\n        write!(\n            f,\n            \"CPUs:       {:.1}%\\n\\\n             Memory:     {} / {}\\n\\\n             Swap:       {} / {}\\n\\\n             Disk usage: {} / {}\",\n            self.cpu_usage,\n            ByteSize::b(self.used_memory),\n            ByteSize::b(self.total_memory),\n            ByteSize::b(self.used_swap),\n            ByteSize::b(self.total_swap),\n            ByteSize::b(self.used_disks_space),\n            ByteSize::b(self.total_disks_space),\n        )\n    }\n}\n","traces":[{"line":30,"address":[],"length":0,"stats":{"Line":0}},{"line":33,"address":[],"length":0,"stats":{"Line":0}},{"line":34,"address":[],"length":0,"stats":{"Line":0}},{"line":56,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":60,"address":[],"length":0,"stats":{"Line":0}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":0}},{"line":64,"address":[],"length":0,"stats":{"Line":0}},{"line":66,"address":[],"length":0,"stats":{"Line":0}},{"line":67,"address":[],"length":0,"stats":{"Line":0}},{"line":68,"address":[],"length":0,"stats":{"Line":0}},{"line":70,"address":[],"length":0,"stats":{"Line":0}},{"line":71,"address":[],"length":0,"stats":{"Line":0}},{"line":72,"address":[],"length":0,"stats":{"Line":0}},{"line":73,"address":[],"length":0,"stats":{"Line":0}},{"line":76,"address":[],"length":0,"stats":{"Line":0}},{"line":77,"address":[],"length":0,"stats":{"Line":0}},{"line":78,"address":[],"length":0,"stats":{"Line":0}},{"line":79,"address":[],"length":0,"stats":{"Line":0}},{"line":80,"address":[],"length":0,"stats":{"Line":0}},{"line":81,"address":[],"length":0,"stats":{"Line":0}},{"line":82,"address":[],"length":0,"stats":{"Line":0}},{"line":83,"address":[],"length":0,"stats":{"Line":0}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":87,"address":[],"length":0,"stats":{"Line":0}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":0}},{"line":126,"address":[],"length":0,"stats":{"Line":0}},{"line":129,"address":[],"length":0,"stats":{"Line":0}},{"line":130,"address":[],"length":0,"stats":{"Line":0}},{"line":131,"address":[],"length":0,"stats":{"Line":0}},{"line":132,"address":[],"length":0,"stats":{"Line":0}},{"line":133,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":137,"address":[],"length":0,"stats":{"Line":0}},{"line":138,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":142,"address":[],"length":0,"stats":{"Line":0}},{"line":143,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":0}},{"line":147,"address":[],"length":0,"stats":{"Line":0}},{"line":148,"address":[],"length":0,"stats":{"Line":0}},{"line":149,"address":[],"length":0,"stats":{"Line":0}},{"line":150,"address":[],"length":0,"stats":{"Line":0}},{"line":151,"address":[],"length":0,"stats":{"Line":0}},{"line":152,"address":[],"length":0,"stats":{"Line":0}},{"line":168,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":170,"address":[],"length":0,"stats":{"Line":0}},{"line":171,"address":[],"length":0,"stats":{"Line":0}},{"line":172,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":0}},{"line":175,"address":[],"length":0,"stats":{"Line":0}},{"line":180,"address":[],"length":0,"stats":{"Line":0}},{"line":181,"address":[],"length":0,"stats":{"Line":0}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":187,"address":[],"length":0,"stats":{"Line":0}},{"line":188,"address":[],"length":0,"stats":{"Line":0}},{"line":189,"address":[],"length":0,"stats":{"Line":0}},{"line":190,"address":[],"length":0,"stats":{"Line":0}},{"line":191,"address":[],"length":0,"stats":{"Line":0}},{"line":192,"address":[],"length":0,"stats":{"Line":0}},{"line":193,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":67},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","request_id.rs"],"content":"//! Request ID middleware\n\nuse axum::http::{HeaderName, Request};\nuse std::sync::LazyLock;\nuse tower_http::request_id::{MakeRequestId, RequestId};\nuse uuid::Uuid;\n\n#[derive(Clone, Copy)]\npub struct MakeRequestUuid;\n\n/// Request ID header\npub static REQUEST_ID_HEADER: LazyLock\u003cHeaderName\u003e = LazyLock::new(|| HeaderName::from_static(\"x-request-id\"));\n\nimpl MakeRequestId for MakeRequestUuid {\n    fn make_request_id\u003cB\u003e(\u0026mut self, _request: \u0026Request\u003cB\u003e) -\u003e Option\u003cRequestId\u003e {\n        let id = Uuid::new_v4().to_string().parse();\n        match id {\n            Ok(id) =\u003e Some(RequestId::new(id)),\n            _ =\u003e None,\n        }\n    }\n}\n","traces":[{"line":12,"address":[],"length":0,"stats":{"Line":0}},{"line":15,"address":[],"length":0,"stats":{"Line":0}},{"line":16,"address":[],"length":0,"stats":{"Line":0}},{"line":17,"address":[],"length":0,"stats":{"Line":0}},{"line":18,"address":[],"length":0,"stats":{"Line":0}},{"line":19,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":6},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","security_headers.rs"],"content":"//! Security layer (standard security headers: CSP, HSTS, etc.)\n\nuse axum::{\n    body::Body,\n    extract::Request,\n    http::{HeaderName, HeaderValue, header},\n    response::Response,\n};\nuse futures::future::BoxFuture;\nuse std::task::{Context, Poll};\nuse tower::{Layer, Service};\n\n/// Configuration for security headers\n#[derive(Clone, Debug)]\npub struct SecurityHeadersConfig {\n    pub content_security_policy: HeaderValue,\n    pub strict_transport_security: HeaderValue,\n    pub x_content_type_options: HeaderValue,\n    pub x_frame_options: HeaderValue,\n    pub x_xss_protection: HeaderValue,\n    pub referrer_policy: HeaderValue,\n    pub permissions_policy: HeaderValue,\n}\n\nimpl Default for SecurityHeadersConfig {\n    fn default() -\u003e Self {\n        SecurityHeadersConfig {\n            content_security_policy: HeaderValue::from_static(\"default-src 'self';\"),\n            strict_transport_security: HeaderValue::from_static(\"max-age=31536000; includeSubDomains; preload\"),\n            x_content_type_options: HeaderValue::from_static(\"nosniff\"),\n            x_frame_options: HeaderValue::from_static(\"DENY\"),\n            x_xss_protection: HeaderValue::from_static(\"1; mode=block\"),\n            referrer_policy: HeaderValue::from_static(\"no-referrer\"),\n            permissions_policy: HeaderValue::from_static(\"geolocation=(self), microphone=(), camera=()\"),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct SecurityHeadersLayer {\n    pub config: SecurityHeadersConfig,\n}\n\nimpl SecurityHeadersLayer {\n    /// Create a new `SecurityLayer`\n    pub fn new(config: SecurityHeadersConfig) -\u003e Self {\n        Self { config }\n    }\n}\n\nimpl\u003cS\u003e Layer\u003cS\u003e for SecurityHeadersLayer {\n    type Service = SecurityHeadersMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        SecurityHeadersMiddleware {\n            inner,\n            config: self.config.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct SecurityHeadersMiddleware\u003cS\u003e {\n    inner: S,\n    config: SecurityHeadersConfig,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for SecurityHeadersMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + Clone + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let config = self.config.clone();\n        let future = self.inner.call(request);\n\n        Box::pin(async move {\n            let mut response: Response = future.await?;\n\n            let headers = response.headers_mut();\n            headers.insert(header::CONTENT_SECURITY_POLICY, config.content_security_policy);\n            headers.insert(header::STRICT_TRANSPORT_SECURITY, config.strict_transport_security);\n            headers.insert(header::X_CONTENT_TYPE_OPTIONS, config.x_content_type_options);\n            headers.insert(header::X_FRAME_OPTIONS, config.x_frame_options);\n            headers.insert(header::X_XSS_PROTECTION, config.x_xss_protection);\n            headers.insert(header::REFERRER_POLICY, config.referrer_policy);\n            headers.insert(HeaderName::from_static(\"permissions-policy\"), config.permissions_policy);\n\n            Ok(response)\n        })\n    }\n}\n","traces":[{"line":26,"address":[],"length":0,"stats":{"Line":0}},{"line":28,"address":[],"length":0,"stats":{"Line":0}},{"line":29,"address":[],"length":0,"stats":{"Line":0}},{"line":30,"address":[],"length":0,"stats":{"Line":0}},{"line":31,"address":[],"length":0,"stats":{"Line":0}},{"line":32,"address":[],"length":0,"stats":{"Line":0}},{"line":33,"address":[],"length":0,"stats":{"Line":0}},{"line":34,"address":[],"length":0,"stats":{"Line":0}},{"line":46,"address":[],"length":0,"stats":{"Line":0}},{"line":54,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":78,"address":[],"length":0,"stats":{"Line":0}},{"line":79,"address":[],"length":0,"stats":{"Line":0}},{"line":82,"address":[],"length":0,"stats":{"Line":0}},{"line":83,"address":[],"length":0,"stats":{"Line":0}},{"line":84,"address":[],"length":0,"stats":{"Line":0}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":87,"address":[],"length":0,"stats":{"Line":0}},{"line":89,"address":[],"length":0,"stats":{"Line":0}},{"line":90,"address":[],"length":0,"stats":{"Line":0}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":0}},{"line":93,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":95,"address":[],"length":0,"stats":{"Line":0}},{"line":96,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":0}}],"covered":0,"coverable":27},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","layers","time_limiter.rs"],"content":"//! Time limiter layer\n\nuse crate::server::axum::{layers::body_from_parts, response::ApiError};\nuse axum::body::Body;\nuse axum::http::{Request, StatusCode};\nuse axum::response::Response;\nuse chrono::Local;\nuse futures::future::BoxFuture;\nuse std::fmt::Display;\nuse std::task::{Context, Poll};\nuse tower::{Layer, Service};\n\n/// TimeSlots represents a collection of time intervals\n/// where each interval is defined by a start and end time.\n#[derive(Debug, Clone, PartialEq)]\npub struct TimeSlots(Vec\u003cTimeSlot\u003e);\n\nimpl TimeSlots {\n    /// Get time slots vector\n    ///\n    /// # Example\n    /// ```\n    /// use api_tools::server::axum::layers::time_limiter::TimeSlots;\n    ///\n    /// let time_slots: TimeSlots = \"08:00-12:00,13:00-17:00\".into();\n    /// assert_eq!(time_slots.values().len(), 2);\n    /// assert_eq!(time_slots.values()[0].start, \"08:00\");\n    /// assert_eq!(time_slots.values()[0].end, \"12:00\");\n    /// assert_eq!(time_slots.values()[1].start, \"13:00\");\n    /// assert_eq!(time_slots.values()[1].end, \"17:00\");\n    /// ```\n    pub fn values(\u0026self) -\u003e \u0026Vec\u003cTimeSlot\u003e {\n        \u0026self.0\n    }\n\n    /// Check if a time is in the time slots list\n    ///\n    /// # Example\n    /// ```\n    /// use api_tools::server::axum::layers::time_limiter::TimeSlots;\n    ///\n    /// let time_slots: TimeSlots = \"08:00-12:00,13:00-17:00\".into();\n    /// let now = \"09:00\";\n    /// assert_eq!(time_slots.contains(now), true);\n    ///\n    /// let now = \"08:00\";\n    /// assert_eq!(time_slots.contains(now), true);\n    ///\n    /// let now = \"17:00\";\n    /// assert_eq!(time_slots.contains(now), true);\n    ///\n    /// let now = \"12:30\";\n    /// assert_eq!(time_slots.contains(now), false);\n    ///\n    /// let time_slots: TimeSlots = \"\".into();\n    /// let now = \"09:00\";\n    /// assert_eq!(time_slots.contains(now), false);\n    /// ```\n    pub fn contains(\u0026self, time: \u0026str) -\u003e bool {\n        self.0.iter().any(|slot| *slot.start \u003c= *time \u0026\u0026 *time \u003c= *slot.end)\n    }\n}\n\nimpl Display for TimeSlots {\n    fn fmt(\u0026self, f: \u0026mut std::fmt::Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        let mut slots = String::new();\n        for (i, slot) in self.0.iter().enumerate() {\n            slots.push_str(\u0026format!(\"{} - {}\", slot.start, slot.end));\n\n            if i \u003c self.0.len() - 1 {\n                slots.push_str(\", \");\n            }\n        }\n        write!(f, \"{}\", slots)\n    }\n}\n\nimpl From\u003c\u0026str\u003e for TimeSlots {\n    fn from(value: \u0026str) -\u003e Self {\n        Self(\n            value\n                .split(',')\n                .filter_map(|part| part.try_into().ok())\n                .collect::\u003cVec\u003c_\u003e\u003e(),\n        )\n    }\n}\n\n#[derive(Debug, Clone, PartialEq)]\npub struct TimeSlot {\n    pub start: String,\n    pub end: String,\n}\n\nimpl TryFrom\u003c\u0026str\u003e for TimeSlot {\n    type Error = ApiError;\n\n    fn try_from(value: \u0026str) -\u003e Result\u003cSelf, Self::Error\u003e {\n        let (start, end) = value.split_once('-').ok_or(ApiError::InternalServerError(\n            \"Time slots configuration error\".to_string(),\n        ))?;\n\n        if start.len() != 5 || end.len() != 5 {\n            return Err(ApiError::InternalServerError(\n                \"Time slots configuration error\".to_string(),\n            ));\n        }\n\n        Ok(Self {\n            start: start.to_string(),\n            end: end.to_string(),\n        })\n    }\n}\n\n#[derive(Clone)]\npub struct TimeLimiterLayer {\n    pub time_slots: TimeSlots,\n}\n\nimpl TimeLimiterLayer {\n    /// Create a new `TimeLimiterLayer`\n    pub fn new(time_slots: TimeSlots) -\u003e Self {\n        Self { time_slots }\n    }\n}\n\nimpl\u003cS\u003e Layer\u003cS\u003e for TimeLimiterLayer {\n    type Service = TimeLimiterMiddleware\u003cS\u003e;\n\n    fn layer(\u0026self, inner: S) -\u003e Self::Service {\n        TimeLimiterMiddleware {\n            inner,\n            time_slots: self.time_slots.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct TimeLimiterMiddleware\u003cS\u003e {\n    inner: S,\n    time_slots: TimeSlots,\n}\n\nimpl\u003cS\u003e Service\u003cRequest\u003cBody\u003e\u003e for TimeLimiterMiddleware\u003cS\u003e\nwhere\n    S: Service\u003cRequest\u003cBody\u003e, Response = Response\u003e + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin\u003cBox\u003cdyn Future + Send + 'a\u003e\u003e`\n    type Future = BoxFuture\u003c'static, Result\u003cSelf::Response, Self::Error\u003e\u003e;\n\n    fn poll_ready(\u0026mut self, cx: \u0026mut Context\u003c'_\u003e) -\u003e Poll\u003cResult\u003c(), Self::Error\u003e\u003e {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(\u0026mut self, request: Request\u003cBody\u003e) -\u003e Self::Future {\n        let now = Local::now().format(\"%H:%M\").to_string();\n        let is_authorized = !self.time_slots.contains(\u0026now);\n        let time_slots = self.time_slots.clone();\n\n        let future = self.inner.call(request);\n        Box::pin(async move {\n            let mut response = Response::default();\n\n            response = match is_authorized {\n                true =\u003e future.await?,\n                false =\u003e {\n                    let (mut parts, _body) = response.into_parts();\n                    let msg = body_from_parts(\n                        \u0026mut parts,\n                        StatusCode::SERVICE_UNAVAILABLE,\n                        format!(\"Service unavailable during these times: {}\", time_slots).as_str(),\n                        None,\n                    );\n                    Response::from_parts(parts, Body::from(msg))\n                }\n            };\n\n            Ok(response)\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_timeslots_from_str() {\n        let time_slots: TimeSlots = \"08:00-12:00,13:00-17:00\".into();\n        assert_eq!(time_slots.values().len(), 2);\n        assert_eq!(time_slots.values()[0].start, \"08:00\");\n        assert_eq!(time_slots.values()[0].end, \"12:00\");\n        assert_eq!(time_slots.values()[1].start, \"13:00\");\n        assert_eq!(time_slots.values()[1].end, \"17:00\");\n    }\n\n    #[test]\n    fn test_timeslot_try_from_valid() {\n        let slot: TimeSlot = \"10:00-11:00\".try_into().unwrap();\n        assert_eq!(slot.start, \"10:00\");\n        assert_eq!(slot.end, \"11:00\");\n    }\n\n    #[test]\n    fn test_timeslot_try_from_invalid_format() {\n        let slot = TimeSlot::try_from(\"1000-1100\");\n        assert!(slot.is_err());\n        let slot = TimeSlot::try_from(\"10:00/11:00\");\n        assert!(slot.is_err());\n    }\n\n    #[test]\n    fn test_timeslots_display() {\n        let time_slots: TimeSlots = \"08:00-12:00,13:00-17:00\".into();\n        let display = format!(\"{}\", time_slots);\n        assert_eq!(display, \"08:00 - 12:00, 13:00 - 17:00\");\n    }\n\n    #[test]\n    fn test_timeslots_empty_display() {\n        let time_slots: TimeSlots = \"\".into();\n        let display = format!(\"{}\", time_slots);\n        assert_eq!(display, \"\");\n    }\n}\n","traces":[{"line":32,"address":[],"length":0,"stats":{"Line":5}},{"line":33,"address":[],"length":0,"stats":{"Line":5}},{"line":59,"address":[],"length":0,"stats":{"Line":0}},{"line":60,"address":[],"length":0,"stats":{"Line":0}},{"line":65,"address":[],"length":0,"stats":{"Line":2}},{"line":66,"address":[],"length":0,"stats":{"Line":2}},{"line":67,"address":[],"length":0,"stats":{"Line":4}},{"line":70,"address":[],"length":0,"stats":{"Line":1}},{"line":71,"address":[],"length":0,"stats":{"Line":1}},{"line":74,"address":[],"length":0,"stats":{"Line":2}},{"line":79,"address":[],"length":0,"stats":{"Line":3}},{"line":81,"address":[],"length":0,"stats":{"Line":3}},{"line":82,"address":[],"length":0,"stats":{"Line":3}},{"line":83,"address":[],"length":0,"stats":{"Line":11}},{"line":84,"address":[],"length":0,"stats":{"Line":3}},{"line":98,"address":[],"length":0,"stats":{"Line":8}},{"line":99,"address":[],"length":0,"stats":{"Line":14}},{"line":100,"address":[],"length":0,"stats":{"Line":8}},{"line":103,"address":[],"length":0,"stats":{"Line":5}},{"line":104,"address":[],"length":0,"stats":{"Line":1}},{"line":105,"address":[],"length":0,"stats":{"Line":1}},{"line":109,"address":[],"length":0,"stats":{"Line":5}},{"line":110,"address":[],"length":0,"stats":{"Line":5}},{"line":111,"address":[],"length":0,"stats":{"Line":5}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":131,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":155,"address":[],"length":0,"stats":{"Line":0}},{"line":156,"address":[],"length":0,"stats":{"Line":0}},{"line":159,"address":[],"length":0,"stats":{"Line":0}},{"line":160,"address":[],"length":0,"stats":{"Line":0}},{"line":161,"address":[],"length":0,"stats":{"Line":0}},{"line":162,"address":[],"length":0,"stats":{"Line":0}},{"line":164,"address":[],"length":0,"stats":{"Line":0}},{"line":165,"address":[],"length":0,"stats":{"Line":0}},{"line":166,"address":[],"length":0,"stats":{"Line":0}},{"line":168,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":170,"address":[],"length":0,"stats":{"Line":0}},{"line":171,"address":[],"length":0,"stats":{"Line":0}},{"line":172,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":0}},{"line":175,"address":[],"length":0,"stats":{"Line":0}},{"line":176,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":182,"address":[],"length":0,"stats":{"Line":0}}],"covered":22,"coverable":47},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","mod.rs"],"content":"//! Axum server\n\npub mod extractors;\npub mod handlers;\npub mod layers;\npub mod response;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","axum","response.rs"],"content":"//! API response module\n\nuse axum::Json;\nuse axum::http::StatusCode;\nuse axum::response::{IntoResponse, Response};\nuse opentelemetry::TraceId;\nuse opentelemetry::trace::TraceContextExt;\nuse serde::Serialize;\nuse thiserror::Error;\nuse tracing_opentelemetry::OpenTelemetrySpanExt;\n\n/// API response success\n#[derive(Debug, Clone)]\npub struct ApiSuccess\u003cT: Serialize + PartialEq\u003e(StatusCode, Json\u003cT\u003e);\n\nimpl\u003cT\u003e PartialEq for ApiSuccess\u003cT\u003e\nwhere\n    T: Serialize + PartialEq,\n{\n    fn eq(\u0026self, other: \u0026Self) -\u003e bool {\n        self.0 == other.0 \u0026\u0026 self.1.0 == other.1.0\n    }\n}\n\nimpl\u003cT: Serialize + PartialEq\u003e ApiSuccess\u003cT\u003e {\n    pub fn new(status: StatusCode, data: T) -\u003e Self {\n        ApiSuccess(status, Json(data))\n    }\n}\n\nimpl\u003cT: Serialize + PartialEq\u003e IntoResponse for ApiSuccess\u003cT\u003e {\n    fn into_response(self) -\u003e Response {\n        (self.0, self.1).into_response()\n    }\n}\n\n/// Generic response structure shared by all API responses.\n#[derive(Debug, Clone, PartialEq, Serialize)]\npub(crate) struct ApiErrorResponse\u003cT: Serialize + PartialEq\u003e {\n    code: u16,\n    message: T,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    trace_id: Option\u003cString\u003e,\n}\n\nimpl\u003cT: Serialize + PartialEq\u003e ApiErrorResponse\u003cT\u003e {\n    pub(crate) fn new(status_code: StatusCode, message: T, trace_id: Option\u003cString\u003e) -\u003e Self {\n        Self {\n            code: status_code.as_u16(),\n            message,\n            trace_id,\n        }\n    }\n}\n\n/// API error\n#[derive(Debug, Clone, PartialEq, Error)]\npub enum ApiError {\n    #[error(\"Bad request: {0}\")]\n    BadRequest(String),\n\n    #[error(\"Unauthorized: {0}\")]\n    Unauthorized(String),\n\n    #[error(\"Forbidden: {0}\")]\n    Forbidden(String),\n\n    #[error(\"Not found: {0}\")]\n    NotFound(String),\n\n    #[error(\"Unprocessable entity: {0}\")]\n    UnprocessableEntity(String),\n\n    #[error(\"Internal server error: {0}\")]\n    InternalServerError(String),\n\n    #[error(\"Timeout\")]\n    Timeout,\n\n    #[error(\"Too many requests\")]\n    TooManyRequests,\n\n    #[error(\"Method not allowed\")]\n    MethodNotAllowed,\n\n    #[error(\"Payload too large\")]\n    PayloadTooLarge,\n\n    #[error(\"Service unavailable\")]\n    ServiceUnavailable,\n}\n\nimpl ApiError {\n    fn response(code: StatusCode, message: \u0026str) -\u003e impl IntoResponse + '_ {\n        let ctx = tracing::Span::current().context();\n        let trace_id = ctx.span().span_context().trace_id();\n        let trace_id = if trace_id == TraceId::INVALID {\n            None\n        } else {\n            Some(trace_id.to_string())\n        };\n\n        match code {\n            StatusCode::REQUEST_TIMEOUT =\u003e (\n                StatusCode::REQUEST_TIMEOUT,\n                Json(ApiErrorResponse::new(StatusCode::REQUEST_TIMEOUT, message, trace_id)),\n            ),\n            StatusCode::TOO_MANY_REQUESTS =\u003e (\n                StatusCode::TOO_MANY_REQUESTS,\n                Json(ApiErrorResponse::new(StatusCode::TOO_MANY_REQUESTS, message, trace_id)),\n            ),\n            StatusCode::METHOD_NOT_ALLOWED =\u003e (\n                StatusCode::METHOD_NOT_ALLOWED,\n                Json(ApiErrorResponse::new(StatusCode::METHOD_NOT_ALLOWED, message, trace_id)),\n            ),\n            StatusCode::PAYLOAD_TOO_LARGE =\u003e (\n                StatusCode::PAYLOAD_TOO_LARGE,\n                Json(ApiErrorResponse::new(StatusCode::PAYLOAD_TOO_LARGE, message, trace_id)),\n            ),\n            StatusCode::BAD_REQUEST =\u003e (\n                StatusCode::BAD_REQUEST,\n                Json(ApiErrorResponse::new(StatusCode::BAD_REQUEST, message, trace_id)),\n            ),\n            StatusCode::UNAUTHORIZED =\u003e (\n                StatusCode::UNAUTHORIZED,\n                Json(ApiErrorResponse::new(StatusCode::UNAUTHORIZED, message, None)),\n            ),\n            StatusCode::FORBIDDEN =\u003e (\n                StatusCode::FORBIDDEN,\n                Json(ApiErrorResponse::new(StatusCode::FORBIDDEN, message, trace_id)),\n            ),\n            StatusCode::NOT_FOUND =\u003e (\n                StatusCode::NOT_FOUND,\n                Json(ApiErrorResponse::new(StatusCode::NOT_FOUND, message, trace_id)),\n            ),\n            StatusCode::SERVICE_UNAVAILABLE =\u003e (\n                StatusCode::SERVICE_UNAVAILABLE,\n                Json(ApiErrorResponse::new(\n                    StatusCode::SERVICE_UNAVAILABLE,\n                    message,\n                    trace_id,\n                )),\n            ),\n            StatusCode::UNPROCESSABLE_ENTITY =\u003e (\n                StatusCode::UNPROCESSABLE_ENTITY,\n                Json(ApiErrorResponse::new(\n                    StatusCode::UNPROCESSABLE_ENTITY,\n                    message,\n                    trace_id,\n                )),\n            ),\n            _ =\u003e (\n                StatusCode::INTERNAL_SERVER_ERROR,\n                Json(ApiErrorResponse::new(\n                    StatusCode::INTERNAL_SERVER_ERROR,\n                    message,\n                    trace_id,\n                )),\n            ),\n        }\n    }\n}\n\nimpl IntoResponse for ApiError {\n    fn into_response(self) -\u003e Response {\n        match self {\n            ApiError::Timeout =\u003e Self::response(StatusCode::REQUEST_TIMEOUT, \"Request timeout\").into_response(),\n            ApiError::TooManyRequests =\u003e {\n                Self::response(StatusCode::TOO_MANY_REQUESTS, \"Too many requests\").into_response()\n            }\n            ApiError::MethodNotAllowed =\u003e {\n                Self::response(StatusCode::METHOD_NOT_ALLOWED, \"Method not allowed\").into_response()\n            }\n            ApiError::PayloadTooLarge =\u003e {\n                Self::response(StatusCode::PAYLOAD_TOO_LARGE, \"Payload too large\").into_response()\n            }\n            ApiError::ServiceUnavailable =\u003e {\n                Self::response(StatusCode::SERVICE_UNAVAILABLE, \"Service unavailable\").into_response()\n            }\n            ApiError::BadRequest(message) =\u003e Self::response(StatusCode::BAD_REQUEST, \u0026message).into_response(),\n            ApiError::Unauthorized(message) =\u003e Self::response(StatusCode::UNAUTHORIZED, \u0026message).into_response(),\n            ApiError::Forbidden(message) =\u003e Self::response(StatusCode::FORBIDDEN, \u0026message).into_response(),\n            ApiError::NotFound(message) =\u003e Self::response(StatusCode::NOT_FOUND, \u0026message).into_response(),\n            ApiError::UnprocessableEntity(message) =\u003e {\n                Self::response(StatusCode::UNPROCESSABLE_ENTITY, \u0026message).into_response()\n            }\n            ApiError::InternalServerError(message) =\u003e {\n                Self::response(StatusCode::INTERNAL_SERVER_ERROR, \u0026message).into_response()\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde_json::json;\n\n    #[test]\n    fn test_api_success_partial_eq() {\n        let success1 = ApiSuccess::new(StatusCode::OK, json!({\"data\": \"test\"}));\n        let success2 = ApiSuccess::new(StatusCode::OK, json!({\"data\": \"test\"}));\n        assert_eq!(success1, success2);\n\n        let success3 = ApiSuccess::new(StatusCode::BAD_REQUEST, json!({\"data\": \"test\"}));\n        assert_ne!(success1, success3);\n    }\n\n    #[tokio::test]\n    async fn test_api_success_into_response() {\n        let data = json!({\"hello\": \"world\"});\n        let api_success = ApiSuccess::new(StatusCode::OK, data.clone());\n        let response = api_success.into_response();\n        assert_eq!(response.status(), StatusCode::OK);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(body_str, data.to_string());\n    }\n\n    #[test]\n    fn test_new_api_error_response() {\n        let error = ApiErrorResponse::new(StatusCode::BAD_REQUEST, \"Bad request\", None);\n        assert_eq!(error.code, 400);\n        assert_eq!(error.message, \"Bad request\");\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_bad_request() {\n        let error = ApiError::BadRequest(\"Invalid input\".to_string());\n        assert_eq!(error.to_string(), \"Bad request: Invalid input\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::BAD_REQUEST);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(body_str, json!({ \"code\": 400, \"message\": \"Invalid input\" }).to_string());\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_unauthorized() {\n        let error = ApiError::Unauthorized(\"Not authorized\".to_string());\n        assert_eq!(error.to_string(), \"Unauthorized: Not authorized\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 401, \"message\": \"Not authorized\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_forbidden() {\n        let error = ApiError::Forbidden(\"Access denied\".to_string());\n        assert_eq!(error.to_string(), \"Forbidden: Access denied\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::FORBIDDEN);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(body_str, json!({ \"code\": 403, \"message\": \"Access denied\" }).to_string());\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_not_found() {\n        let error = ApiError::NotFound(\"Resource missing\".to_string());\n        assert_eq!(error.to_string(), \"Not found: Resource missing\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::NOT_FOUND);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 404, \"message\": \"Resource missing\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_unprocessable_entity() {\n        let error = ApiError::UnprocessableEntity(\"Invalid data\".to_string());\n        assert_eq!(error.to_string(), \"Unprocessable entity: Invalid data\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(body_str, json!({ \"code\": 422, \"message\": \"Invalid data\" }).to_string());\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_internal_server_error() {\n        let error = ApiError::InternalServerError(\"Unexpected\".to_string());\n        assert_eq!(error.to_string(), \"Internal server error: Unexpected\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(body_str, json!({ \"code\": 500, \"message\": \"Unexpected\" }).to_string());\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_timeout() {\n        let error = ApiError::Timeout;\n        assert_eq!(error.to_string(), \"Timeout\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 408, \"message\": \"Request timeout\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_too_many_requests() {\n        let error = ApiError::TooManyRequests;\n        assert_eq!(error.to_string(), \"Too many requests\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 429, \"message\": \"Too many requests\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_method_not_allowed() {\n        let error = ApiError::MethodNotAllowed;\n        assert_eq!(error.to_string(), \"Method not allowed\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 405, \"message\": \"Method not allowed\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_payload_too_large() {\n        let error = ApiError::PayloadTooLarge;\n        assert_eq!(error.to_string(), \"Payload too large\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 413, \"message\": \"Payload too large\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_into_response_service_unavailable() {\n        let error = ApiError::ServiceUnavailable;\n        assert_eq!(error.to_string(), \"Service unavailable\");\n\n        let response = error.into_response();\n        assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 503, \"message\": \"Service unavailable\" }).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_api_error_response() {\n        let response = ApiError::response(StatusCode::INTERNAL_SERVER_ERROR, \"Internal server error\");\n        let response = response.into_response();\n        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);\n\n        let body = response.into_body();\n        let body_bytes = axum::body::to_bytes(body, 1_024).await.unwrap();\n        let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();\n        assert_eq!(\n            body_str,\n            json!({ \"code\": 500, \"message\": \"Internal server error\" }).to_string()\n        );\n    }\n}\n","traces":[{"line":20,"address":[],"length":0,"stats":{"Line":2}},{"line":21,"address":[],"length":0,"stats":{"Line":3}},{"line":26,"address":[],"length":0,"stats":{"Line":4}},{"line":27,"address":[],"length":0,"stats":{"Line":4}},{"line":32,"address":[],"length":0,"stats":{"Line":1}},{"line":33,"address":[],"length":0,"stats":{"Line":1}},{"line":47,"address":[],"length":0,"stats":{"Line":16}},{"line":49,"address":[],"length":0,"stats":{"Line":16}},{"line":94,"address":[],"length":0,"stats":{"Line":12}},{"line":95,"address":[],"length":0,"stats":{"Line":12}},{"line":96,"address":[],"length":0,"stats":{"Line":12}},{"line":97,"address":[],"length":0,"stats":{"Line":24}},{"line":98,"address":[],"length":0,"stats":{"Line":12}},{"line":100,"address":[],"length":0,"stats":{"Line":0}},{"line":103,"address":[],"length":0,"stats":{"Line":12}},{"line":104,"address":[],"length":0,"stats":{"Line":1}},{"line":105,"address":[],"length":0,"stats":{"Line":1}},{"line":106,"address":[],"length":0,"stats":{"Line":1}},{"line":108,"address":[],"length":0,"stats":{"Line":1}},{"line":109,"address":[],"length":0,"stats":{"Line":1}},{"line":110,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":1}},{"line":113,"address":[],"length":0,"stats":{"Line":1}},{"line":114,"address":[],"length":0,"stats":{"Line":1}},{"line":116,"address":[],"length":0,"stats":{"Line":1}},{"line":117,"address":[],"length":0,"stats":{"Line":1}},{"line":118,"address":[],"length":0,"stats":{"Line":1}},{"line":120,"address":[],"length":0,"stats":{"Line":1}},{"line":121,"address":[],"length":0,"stats":{"Line":1}},{"line":122,"address":[],"length":0,"stats":{"Line":1}},{"line":124,"address":[],"length":0,"stats":{"Line":1}},{"line":125,"address":[],"length":0,"stats":{"Line":1}},{"line":126,"address":[],"length":0,"stats":{"Line":1}},{"line":128,"address":[],"length":0,"stats":{"Line":1}},{"line":129,"address":[],"length":0,"stats":{"Line":1}},{"line":130,"address":[],"length":0,"stats":{"Line":1}},{"line":132,"address":[],"length":0,"stats":{"Line":1}},{"line":133,"address":[],"length":0,"stats":{"Line":1}},{"line":134,"address":[],"length":0,"stats":{"Line":1}},{"line":136,"address":[],"length":0,"stats":{"Line":1}},{"line":137,"address":[],"length":0,"stats":{"Line":1}},{"line":138,"address":[],"length":0,"stats":{"Line":1}},{"line":139,"address":[],"length":0,"stats":{"Line":1}},{"line":140,"address":[],"length":0,"stats":{"Line":1}},{"line":141,"address":[],"length":0,"stats":{"Line":1}},{"line":144,"address":[],"length":0,"stats":{"Line":1}},{"line":145,"address":[],"length":0,"stats":{"Line":1}},{"line":146,"address":[],"length":0,"stats":{"Line":1}},{"line":147,"address":[],"length":0,"stats":{"Line":1}},{"line":148,"address":[],"length":0,"stats":{"Line":1}},{"line":149,"address":[],"length":0,"stats":{"Line":1}},{"line":152,"address":[],"length":0,"stats":{"Line":2}},{"line":153,"address":[],"length":0,"stats":{"Line":2}},{"line":154,"address":[],"length":0,"stats":{"Line":2}},{"line":155,"address":[],"length":0,"stats":{"Line":2}},{"line":156,"address":[],"length":0,"stats":{"Line":2}},{"line":157,"address":[],"length":0,"stats":{"Line":2}},{"line":165,"address":[],"length":0,"stats":{"Line":11}},{"line":166,"address":[],"length":0,"stats":{"Line":11}},{"line":167,"address":[],"length":0,"stats":{"Line":1}},{"line":169,"address":[],"length":0,"stats":{"Line":1}},{"line":172,"address":[],"length":0,"stats":{"Line":1}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":178,"address":[],"length":0,"stats":{"Line":1}},{"line":180,"address":[],"length":0,"stats":{"Line":1}},{"line":181,"address":[],"length":0,"stats":{"Line":1}},{"line":182,"address":[],"length":0,"stats":{"Line":1}},{"line":183,"address":[],"length":0,"stats":{"Line":1}},{"line":184,"address":[],"length":0,"stats":{"Line":1}},{"line":185,"address":[],"length":0,"stats":{"Line":1}},{"line":187,"address":[],"length":0,"stats":{"Line":1}},{"line":188,"address":[],"length":0,"stats":{"Line":1}}],"covered":71,"coverable":72},{"path":["/","Users","fabien","lab","rust","api-tools","src","server","mod.rs"],"content":"//! Server module\n\n// Experimental: #[cfg_attr(docsrs, doc(cfg(feature = \"axum\")))]\n#[cfg(feature = \"axum\")]\npub mod axum;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","value_objects","datetime.rs"],"content":"//! Datetime represents a date and time value in the UTC timezone.\n\nuse chrono::{DateTime, TimeDelta, Utc};\nuse serde::Deserialize;\nuse std::fmt::{Display, Formatter};\nuse std::ops::Add;\nuse thiserror::Error;\n\n/// UTC Datetime possible errors\n#[derive(Debug, Clone, PartialEq, Error)]\npub enum UtcDateTimeError {\n    #[error(\"Invalid date time: {0}\")]\n    InvalidDateTime(String),\n}\n\n/// Date time with UTC timezone\n#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Deserialize)]\npub struct UtcDateTime {\n    value: DateTime\u003cUtc\u003e,\n}\n\nimpl UtcDateTime {\n    /// Create a new date time for now\n    pub fn now() -\u003e Self {\n        Self { value: Utc::now() }\n    }\n\n    /// Create a new date time\n    pub fn new(value: DateTime\u003cUtc\u003e) -\u003e Self {\n        Self { value }\n    }\n\n    /// Create a new date time from RFC3339 string\n    ///\n    /// # Example\n    /// ```rust\n    /// use api_tools::value_objects::datetime::UtcDateTime;\n    ///\n    /// let datetime = UtcDateTime::from_rfc3339(\"2024-08-28T12:00:00Z\");\n    /// assert_eq!(datetime.unwrap().to_string(), \"2024-08-28T12:00:00Z\".to_owned());\n    ///\n    /// let invalid_datetime = UtcDateTime::from_rfc3339(\"2024-08-T12:00:00Z\");\n    /// ```\n    pub fn from_rfc3339(value: \u0026str) -\u003e Result\u003cSelf, UtcDateTimeError\u003e {\n        let dt = DateTime::parse_from_rfc3339(value)\n            .map_err(|e| UtcDateTimeError::InvalidDateTime(format!(\"{e}: {value}\")))?;\n\n        Ok(Self {\n            value: dt.with_timezone(\u0026Utc),\n        })\n    }\n\n    /// Get timestamp value\n    pub fn timestamp(\u0026self) -\u003e i64 {\n        self.value.timestamp()\n    }\n\n    /// Get date time value\n    pub fn value(\u0026self) -\u003e DateTime\u003cUtc\u003e {\n        self.value\n    }\n\n    /// Create a new date time from a timestamp\n    pub fn add(\u0026self, rhs: TimeDelta) -\u003e Self {\n        Self {\n            value: self.value.add(rhs),\n        }\n    }\n}\n\nimpl From\u003cDateTime\u003cUtc\u003e\u003e for UtcDateTime {\n    fn from(value: DateTime\u003cUtc\u003e) -\u003e Self {\n        Self { value }\n    }\n}\n\nimpl Display for UtcDateTime {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        write!(f, \"{}\", self.value.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn test_utc_date_time_display() {\n        let dt = DateTime::parse_from_rfc3339(\"2024-08-28T12:00:00Z\")\n            .unwrap()\n            .with_timezone(\u0026Utc);\n        let datetime = UtcDateTime::from(dt);\n\n        assert_eq!(datetime.to_string(), \"2024-08-28T12:00:00Z\");\n    }\n\n    #[test]\n    fn test_from_rfc3339() {\n        let datetime = UtcDateTime::from_rfc3339(\"2024-08-28T12:00:00Z\");\n        assert!(datetime.is_ok());\n        assert_eq!(datetime.unwrap().to_string(), \"2024-08-28T12:00:00Z\".to_owned());\n\n        let invalid_datetime = UtcDateTime::from_rfc3339(\"2024-08-T12:00:00Z\");\n        assert!(invalid_datetime.is_err());\n    }\n\n    #[test]\n    fn test_value() {\n        let dt = DateTime::parse_from_rfc3339(\"2024-08-28T12:00:00Z\")\n            .unwrap()\n            .with_timezone(\u0026Utc);\n        let datetime = UtcDateTime::from(dt);\n\n        assert_eq!(datetime.value(), dt);\n    }\n\n    #[test]\n    fn test_timestamp() {\n        let dt = DateTime::parse_from_rfc3339(\"2024-08-28T12:00:00Z\")\n            .unwrap()\n            .with_timezone(\u0026Utc);\n        let datetime = UtcDateTime::from(dt);\n\n        assert_eq!(datetime.timestamp(), 1724846400);\n    }\n\n    #[test]\n    fn test_add() {\n        let dt = DateTime::parse_from_rfc3339(\"2024-08-28T12:00:00Z\")\n            .unwrap()\n            .with_timezone(\u0026Utc);\n        let datetime = UtcDateTime::from(dt);\n        let new_datetime = datetime.add(TimeDelta::seconds(3600));\n        assert_eq!(new_datetime.to_string(), \"2024-08-28T13:00:00Z\".to_owned());\n    }\n}\n","traces":[{"line":24,"address":[],"length":0,"stats":{"Line":0}},{"line":25,"address":[],"length":0,"stats":{"Line":0}},{"line":29,"address":[],"length":0,"stats":{"Line":0}},{"line":44,"address":[],"length":0,"stats":{"Line":2}},{"line":45,"address":[],"length":0,"stats":{"Line":3}},{"line":46,"address":[],"length":0,"stats":{"Line":6}},{"line":54,"address":[],"length":0,"stats":{"Line":1}},{"line":55,"address":[],"length":0,"stats":{"Line":1}},{"line":59,"address":[],"length":0,"stats":{"Line":1}},{"line":60,"address":[],"length":0,"stats":{"Line":1}},{"line":64,"address":[],"length":0,"stats":{"Line":1}},{"line":66,"address":[],"length":0,"stats":{"Line":1}},{"line":72,"address":[],"length":0,"stats":{"Line":4}},{"line":78,"address":[],"length":0,"stats":{"Line":3}},{"line":79,"address":[],"length":0,"stats":{"Line":3}}],"covered":12,"coverable":15},{"path":["/","Users","fabien","lab","rust","api-tools","src","value_objects","mod.rs"],"content":"//! Value objects list\n\npub mod datetime;\npub mod pagination;\npub mod query_sort;\npub mod timezone;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","fabien","lab","rust","api-tools","src","value_objects","pagination.rs"],"content":"//! Pagination value object representation\n\n/// Pagination min limit\npub const PAGINATION_MIN_LIMIT: u32 = 50;\n\n/// Pagination max limit\npub const PAGINATION_MAX_LIMIT: u32 = 500;\n\n/// Pagination default limit\npub const PAGINATION_DEFAULT_LIMIT: u32 = 200;\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct Pagination {\n    page: u32,\n    limit: u32,\n    max_limit: Option\u003cu32\u003e,\n}\n\nimpl Pagination {\n    /// Create new pagination\n    ///\n    /// `max_limit` is optional and will be clamped between `PAGINATION_MIN_LIMIT` and `PAGINATION_MAX_LIMIT`\n    ///\n    /// # Examples\n    ///\n    /// ```\n    /// use api_tools::value_objects::pagination::{Pagination, PAGINATION_MAX_LIMIT, PAGINATION_MIN_LIMIT};\n    ///\n    /// let pagination = Pagination::new(1, 100, None);\n    /// assert_eq!(pagination.page(), 1);\n    /// assert_eq!(pagination.limit(), 100);\n    ///\n    /// // Invalid page\n    /// let pagination = Pagination::new(0, 100, None);\n    /// assert_eq!(pagination.page(), 1);\n    /// assert_eq!(pagination.limit(), 100);\n    ///\n    /// // Limit too small\n    /// let pagination = Pagination::new(2, 10, None);\n    /// assert_eq!(pagination.page(), 2);\n    /// assert_eq!(pagination.limit(), PAGINATION_MIN_LIMIT);\n    ///\n    /// // Limit too big\n    /// let pagination = Pagination::new(2, 1_000, None);\n    /// assert_eq!(pagination.page(), 2);\n    /// assert_eq!(pagination.limit(), PAGINATION_MAX_LIMIT);\n    ///\n    /// // Limit too big and max limit greater than max\n    /// let pagination = Pagination::new(2, 1_000, Some(800));\n    /// assert_eq!(pagination.page(), 2);\n    /// assert_eq!(pagination.limit(), PAGINATION_MAX_LIMIT);\n    /// ```\n    pub fn new(page: u32, limit: u32, max_limit: Option\u003cu32\u003e) -\u003e Self {\n        let page = if page == 0 { 1 } else { page };\n\n        let mut max = max_limit.unwrap_or(PAGINATION_MAX_LIMIT);\n\n        if max \u003e PAGINATION_MAX_LIMIT {\n            max = PAGINATION_MAX_LIMIT;\n        }\n\n        let limit = if limit \u003e max {\n            max\n        } else if limit \u003c PAGINATION_MIN_LIMIT {\n            PAGINATION_MIN_LIMIT\n        } else {\n            limit\n        };\n\n        Self { page, limit, max_limit }\n    }\n\n    /// Get page\n    pub fn page(\u0026self) -\u003e u32 {\n        self.page\n    }\n\n    /// Get limit\n    pub fn limit(\u0026self) -\u003e u32 {\n        self.limit\n    }\n\n    /// Set a max limit (between `PAGINATION_MIN_LIMIT` and `PAGINATION_MAX_LIMIT`)\n    pub fn set_max_limit(\u0026mut self, max_limit: u32) {\n        let max_limit = max_limit.clamp(PAGINATION_MIN_LIMIT, PAGINATION_MAX_LIMIT);\n        self.max_limit = Some(max_limit);\n    }\n}\n\nimpl Default for Pagination {\n    /// Default pagination\n    fn default() -\u003e Self {\n        let default = if PAGINATION_DEFAULT_LIMIT \u003e PAGINATION_MAX_LIMIT {\n            PAGINATION_MAX_LIMIT\n        } else {\n            PAGINATION_DEFAULT_LIMIT\n        };\n        Self::new(1, default, None)\n    }\n}\n\n/// Pagination for response\n#[derive(Debug, Clone, PartialEq)]\n\npub struct PaginationResponse {\n    pub page: u32,\n    pub limit: u32,\n    pub total: i64,\n}\n\nimpl PaginationResponse {\n    /// Create a new pagination response\n    pub fn new(page: u32, limit: u32, total: i64) -\u003e Self {\n        Self { page, limit, total }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn test_set_max_limit() {\n        let mut pagination = Pagination::default();\n        assert_eq!(pagination.max_limit, None);\n\n        pagination.set_max_limit(100);\n        assert_eq!(pagination.max_limit, Some(100));\n\n        pagination.set_max_limit(300);\n        assert_eq!(pagination.max_limit, Some(300));\n\n        pagination.set_max_limit(20);\n        assert_eq!(pagination.max_limit, Some(PAGINATION_MIN_LIMIT));\n\n        pagination.set_max_limit(600);\n        assert_eq!(pagination.max_limit, Some(PAGINATION_MAX_LIMIT));\n    }\n\n    #[test]\n    fn test_default() {\n        let pagination = Pagination::default();\n        assert_eq!(pagination.page(), 1);\n        assert_eq!(pagination.limit(), PAGINATION_DEFAULT_LIMIT);\n        assert_eq!(pagination.max_limit, None);\n    }\n}\n","traces":[{"line":53,"address":[],"length":0,"stats":{"Line":2}},{"line":54,"address":[],"length":0,"stats":{"Line":6}},{"line":56,"address":[],"length":0,"stats":{"Line":2}},{"line":58,"address":[],"length":0,"stats":{"Line":2}},{"line":59,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":4}},{"line":63,"address":[],"length":0,"stats":{"Line":0}},{"line":64,"address":[],"length":0,"stats":{"Line":2}},{"line":65,"address":[],"length":0,"stats":{"Line":0}},{"line":67,"address":[],"length":0,"stats":{"Line":2}},{"line":74,"address":[],"length":0,"stats":{"Line":1}},{"line":75,"address":[],"length":0,"stats":{"Line":1}},{"line":79,"address":[],"length":0,"stats":{"Line":1}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":84,"address":[],"length":0,"stats":{"Line":4}},{"line":85,"address":[],"length":0,"stats":{"Line":4}},{"line":86,"address":[],"length":0,"stats":{"Line":4}},{"line":92,"address":[],"length":0,"stats":{"Line":2}},{"line":93,"address":[],"length":0,"stats":{"Line":4}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":96,"address":[],"length":0,"stats":{"Line":2}},{"line":98,"address":[],"length":0,"stats":{"Line":2}},{"line":113,"address":[],"length":0,"stats":{"Line":0}}],"covered":18,"coverable":23},{"path":["/","Users","fabien","lab","rust","api-tools","src","value_objects","query_sort.rs"],"content":"//! Query sorts value object representation\n\nuse std::fmt::Display;\n\n/// Filter sort field\npub type QuerySortField = String;\n\n/// Filter sort direction (ASC or DESC)\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub enum QuerySortDirection {\n    /// Ascending sort (`'+'` prefix)\n    /// Example: `?sort=+id`\n    #[default]\n    Asc,\n\n    /// Descending sort (`'-'` prefix)\n    /// Example: `?sort=-name`\n    Desc,\n}\n\nimpl Display for QuerySortDirection {\n    fn fmt(\u0026self, f: \u0026mut std::fmt::Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        write!(\n            f,\n            \"{}\",\n            match self {\n                Self::Asc =\u003e \"ASC\",\n                Self::Desc =\u003e \"DESC\",\n            }\n        )\n    }\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct QuerySort {\n    pub field: QuerySortField,\n    pub direction: QuerySortDirection,\n}\n\nimpl QuerySort {\n    /// Create a new query sort\n    pub fn new(field: QuerySortField, direction: QuerySortDirection) -\u003e Self {\n        Self { field, direction }\n    }\n}\n\n/// Filter sorts\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct QuerySorts(pub Vec\u003cQuerySort\u003e);\n\nimpl From\u003c\u0026str\u003e for QuerySorts {\n    /// Create a new query sorting from a string\n    ///\n    /// # Example\n    /// ```\n    /// use api_tools::value_objects::query_sort::{QuerySort, QuerySortDirection, QuerySorts};\n    ///\n    /// let sorts = QuerySorts::from(\"+id,-name\");\n    /// assert_eq!(\n    ///     sorts.0,\n    ///     vec![\n    ///         QuerySort {\n    ///             field: \"id\".to_string(),\n    ///             direction: QuerySortDirection::Asc\n    ///         },\n    ///         QuerySort {\n    ///             field: \"name\".to_string(),\n    ///             direction: QuerySortDirection::Desc\n    ///         },\n    ///     ]\n    /// );\n    /// ```\n    fn from(value: \u0026str) -\u003e Self {\n        let mut sorts = Vec::new();\n        let parts = value.split(',');\n\n        for part in parts {\n            let prefix = part.chars().next();\n            if let Some(prefix) = prefix {\n                if prefix == '+' {\n                    sorts.push(QuerySort::new(part[1..].to_string(), QuerySortDirection::Asc));\n                } else if prefix == '-' {\n                    sorts.push(QuerySort::new(part[1..].to_string(), QuerySortDirection::Desc));\n                }\n            }\n        }\n\n        Self(sorts)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_query_sort_direction_default() {\n        assert_eq!(QuerySortDirection::default(), QuerySortDirection::Asc);\n    }\n\n    #[test]\n    fn test_query_sort_direction_display() {\n        assert_eq!(\"ASC\", QuerySortDirection::Asc.to_string());\n        assert_eq!(\"DESC\", QuerySortDirection::Desc.to_string());\n    }\n\n    #[test]\n    fn test_filter_sorts_from_str() {\n        let sorts = QuerySorts::from(\"\");\n        assert!(sorts.0.is_empty());\n\n        let sorts = QuerySorts::from(\"+id,-name\");\n        assert_eq!(sorts.0.len(), 2);\n        assert_eq!(\n            sorts.0,\n            vec![\n                QuerySort {\n                    field: \"id\".to_string(),\n                    direction: QuerySortDirection::Asc\n                },\n                QuerySort {\n                    field: \"name\".to_string(),\n                    direction: QuerySortDirection::Desc\n                },\n            ]\n        );\n\n        let sorts = QuerySorts::from(\"id\");\n        assert!(sorts.0.is_empty());\n    }\n}\n","traces":[{"line":22,"address":[],"length":0,"stats":{"Line":2}},{"line":23,"address":[],"length":0,"stats":{"Line":2}},{"line":24,"address":[],"length":0,"stats":{"Line":2}},{"line":26,"address":[],"length":0,"stats":{"Line":2}},{"line":27,"address":[],"length":0,"stats":{"Line":1}},{"line":28,"address":[],"length":0,"stats":{"Line":1}},{"line":42,"address":[],"length":0,"stats":{"Line":2}},{"line":73,"address":[],"length":0,"stats":{"Line":3}},{"line":74,"address":[],"length":0,"stats":{"Line":3}},{"line":75,"address":[],"length":0,"stats":{"Line":3}},{"line":77,"address":[],"length":0,"stats":{"Line":11}},{"line":79,"address":[],"length":0,"stats":{"Line":3}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":81,"address":[],"length":0,"stats":{"Line":1}},{"line":82,"address":[],"length":0,"stats":{"Line":4}},{"line":83,"address":[],"length":0,"stats":{"Line":1}},{"line":88,"address":[],"length":0,"stats":{"Line":3}}],"covered":17,"coverable":17},{"path":["/","Users","fabien","lab","rust","api-tools","src","value_objects","timezone.rs"],"content":"//! Timezone value object representation\n\nuse chrono_tz::Tz;\nuse std::fmt::Display;\nuse std::str::FromStr;\nuse thiserror::Error;\n\n#[derive(Debug, Clone, PartialEq, Error)]\npub enum TimezoneError {\n    #[error(\"Invalid timezone: {0}\")]\n    Invalid(String),\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct Timezone {\n    value: Tz,\n}\n\nimpl Timezone {\n    /// Create a new timezone\n    pub fn new(tz: Tz) -\u003e Self {\n        Self { value: tz }\n    }\n}\n\nimpl TryFrom\u003c\u0026str\u003e for Timezone {\n    type Error = TimezoneError;\n\n    fn try_from(value: \u0026str) -\u003e Result\u003cSelf, Self::Error\u003e {\n        let tz = Tz::from_str(value).map_err(|e| TimezoneError::Invalid(e.to_string()))?;\n\n        Ok(Self::new(tz))\n    }\n}\n\nimpl Display for Timezone {\n    fn fmt(\u0026self, f: \u0026mut std::fmt::Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        write!(f, \"{}\", self.value)\n    }\n}\n\nimpl Default for Timezone {\n    fn default() -\u003e Self {\n        Self::new(Tz::Europe__Paris)\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use chrono_tz::Tz::Europe__Paris;\n\n    #[test]\n    fn test_try_from_str() {\n        let tz = Timezone::try_from(\"Europe/Paris\").unwrap();\n        assert_eq!(tz.value, Europe__Paris);\n\n        let tz = Timezone::try_from(\"Invalid\");\n        assert!(tz.is_err());\n    }\n\n    #[test]\n    fn test_display() {\n        let tz = Timezone::new(Europe__Paris);\n        assert_eq!(tz.to_string(), \"Europe/Paris\");\n    }\n\n    #[test]\n    fn test_default() {\n        let tz = Timezone::default();\n        assert_eq!(tz.value, Europe__Paris);\n    }\n}\n","traces":[{"line":21,"address":[],"length":0,"stats":{"Line":3}},{"line":29,"address":[],"length":0,"stats":{"Line":2}},{"line":30,"address":[],"length":0,"stats":{"Line":7}},{"line":37,"address":[],"length":0,"stats":{"Line":1}},{"line":38,"address":[],"length":0,"stats":{"Line":1}},{"line":43,"address":[],"length":0,"stats":{"Line":1}},{"line":44,"address":[],"length":0,"stats":{"Line":1}}],"covered":7,"coverable":7}]};
    </script>
    <script crossorigin>/** @license React v16.13.1
 * react.production.min.js
 *
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
'use strict';(function(d,r){"object"===typeof exports&&"undefined"!==typeof module?r(exports):"function"===typeof define&&define.amd?define(["exports"],r):(d=d||self,r(d.React={}))})(this,function(d){function r(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
function w(a,b,c){this.props=a;this.context=b;this.refs=ba;this.updater=c||ca}function da(){}function L(a,b,c){this.props=a;this.context=b;this.refs=ba;this.updater=c||ca}function ea(a,b,c){var g,e={},fa=null,d=null;if(null!=b)for(g in void 0!==b.ref&&(d=b.ref),void 0!==b.key&&(fa=""+b.key),b)ha.call(b,g)&&!ia.hasOwnProperty(g)&&(e[g]=b[g]);var h=arguments.length-2;if(1===h)e.children=c;else if(1<h){for(var k=Array(h),f=0;f<h;f++)k[f]=arguments[f+2];e.children=k}if(a&&a.defaultProps)for(g in h=a.defaultProps,
h)void 0===e[g]&&(e[g]=h[g]);return{$$typeof:x,type:a,key:fa,ref:d,props:e,_owner:M.current}}function va(a,b){return{$$typeof:x,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function N(a){return"object"===typeof a&&null!==a&&a.$$typeof===x}function wa(a){var b={"=":"=0",":":"=2"};return"$"+(""+a).replace(/[=:]/g,function(a){return b[a]})}function ja(a,b,c,g){if(C.length){var e=C.pop();e.result=a;e.keyPrefix=b;e.func=c;e.context=g;e.count=0;return e}return{result:a,keyPrefix:b,func:c,
context:g,count:0}}function ka(a){a.result=null;a.keyPrefix=null;a.func=null;a.context=null;a.count=0;10>C.length&&C.push(a)}function O(a,b,c,g){var e=typeof a;if("undefined"===e||"boolean"===e)a=null;var d=!1;if(null===a)d=!0;else switch(e){case "string":case "number":d=!0;break;case "object":switch(a.$$typeof){case x:case xa:d=!0}}if(d)return c(g,a,""===b?"."+P(a,0):b),1;d=0;b=""===b?".":b+":";if(Array.isArray(a))for(var f=0;f<a.length;f++){e=a[f];var h=b+P(e,f);d+=O(e,h,c,g)}else if(null===a||
"object"!==typeof a?h=null:(h=la&&a[la]||a["@@iterator"],h="function"===typeof h?h:null),"function"===typeof h)for(a=h.call(a),f=0;!(e=a.next()).done;)e=e.value,h=b+P(e,f++),d+=O(e,h,c,g);else if("object"===e)throw c=""+a,Error(r(31,"[object Object]"===c?"object with keys {"+Object.keys(a).join(", ")+"}":c,""));return d}function Q(a,b,c){return null==a?0:O(a,"",b,c)}function P(a,b){return"object"===typeof a&&null!==a&&null!=a.key?wa(a.key):b.toString(36)}function ya(a,b,c){a.func.call(a.context,b,
a.count++)}function za(a,b,c){var g=a.result,e=a.keyPrefix;a=a.func.call(a.context,b,a.count++);Array.isArray(a)?R(a,g,c,function(a){return a}):null!=a&&(N(a)&&(a=va(a,e+(!a.key||b&&b.key===a.key?"":(""+a.key).replace(ma,"$&/")+"/")+c)),g.push(a))}function R(a,b,c,g,e){var d="";null!=c&&(d=(""+c).replace(ma,"$&/")+"/");b=ja(b,d,g,e);Q(a,za,b);ka(b)}function t(){var a=na.current;if(null===a)throw Error(r(321));return a}function S(a,b){var c=a.length;a.push(b);a:for(;;){var g=c-1>>>1,e=a[g];if(void 0!==
e&&0<D(e,b))a[g]=b,a[c]=e,c=g;else break a}}function n(a){a=a[0];return void 0===a?null:a}function E(a){var b=a[0];if(void 0!==b){var c=a.pop();if(c!==b){a[0]=c;a:for(var g=0,e=a.length;g<e;){var d=2*(g+1)-1,f=a[d],h=d+1,k=a[h];if(void 0!==f&&0>D(f,c))void 0!==k&&0>D(k,f)?(a[g]=k,a[h]=c,g=h):(a[g]=f,a[d]=c,g=d);else if(void 0!==k&&0>D(k,c))a[g]=k,a[h]=c,g=h;else break a}}return b}return null}function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function F(a){for(var b=n(u);null!==
b;){if(null===b.callback)E(u);else if(b.startTime<=a)E(u),b.sortIndex=b.expirationTime,S(p,b);else break;b=n(u)}}function T(a){y=!1;F(a);if(!v)if(null!==n(p))v=!0,z(U);else{var b=n(u);null!==b&&G(T,b.startTime-a)}}function U(a,b){v=!1;y&&(y=!1,V());H=!0;var c=m;try{F(b);for(l=n(p);null!==l&&(!(l.expirationTime>b)||a&&!W());){var g=l.callback;if(null!==g){l.callback=null;m=l.priorityLevel;var e=g(l.expirationTime<=b);b=q();"function"===typeof e?l.callback=e:l===n(p)&&E(p);F(b)}else E(p);l=n(p)}if(null!==
l)var d=!0;else{var f=n(u);null!==f&&G(T,f.startTime-b);d=!1}return d}finally{l=null,m=c,H=!1}}function oa(a){switch(a){case 1:return-1;case 2:return 250;case 5:return 1073741823;case 4:return 1E4;default:return 5E3}}var f="function"===typeof Symbol&&Symbol.for,x=f?Symbol.for("react.element"):60103,xa=f?Symbol.for("react.portal"):60106,Aa=f?Symbol.for("react.fragment"):60107,Ba=f?Symbol.for("react.strict_mode"):60108,Ca=f?Symbol.for("react.profiler"):60114,Da=f?Symbol.for("react.provider"):60109,
Ea=f?Symbol.for("react.context"):60110,Fa=f?Symbol.for("react.forward_ref"):60112,Ga=f?Symbol.for("react.suspense"):60113,Ha=f?Symbol.for("react.memo"):60115,Ia=f?Symbol.for("react.lazy"):60116,la="function"===typeof Symbol&&Symbol.iterator,pa=Object.getOwnPropertySymbols,Ja=Object.prototype.hasOwnProperty,Ka=Object.prototype.propertyIsEnumerable,I=function(){try{if(!Object.assign)return!1;var a=new String("abc");a[5]="de";if("5"===Object.getOwnPropertyNames(a)[0])return!1;var b={};for(a=0;10>a;a++)b["_"+
String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var c={};"abcdefghijklmnopqrst".split("").forEach(function(a){c[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},c)).join("")?!1:!0}catch(g){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var c=Object(a);for(var g,e=1;e<arguments.length;e++){var d=Object(arguments[e]);
for(var f in d)Ja.call(d,f)&&(c[f]=d[f]);if(pa){g=pa(d);for(var h=0;h<g.length;h++)Ka.call(d,g[h])&&(c[g[h]]=d[g[h]])}}return c},ca={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,c){},enqueueReplaceState:function(a,b,c,d){},enqueueSetState:function(a,b,c,d){}},ba={};w.prototype.isReactComponent={};w.prototype.setState=function(a,b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error(r(85));this.updater.enqueueSetState(this,a,b,"setState")};w.prototype.forceUpdate=
function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate")};da.prototype=w.prototype;f=L.prototype=new da;f.constructor=L;I(f,w.prototype);f.isPureReactComponent=!0;var M={current:null},ha=Object.prototype.hasOwnProperty,ia={key:!0,ref:!0,__self:!0,__source:!0},ma=/\/+/g,C=[],na={current:null},X;if("undefined"===typeof window||"function"!==typeof MessageChannel){var A=null,qa=null,ra=function(){if(null!==A)try{var a=q();A(!0,a);A=null}catch(b){throw setTimeout(ra,0),b;}},La=Date.now();var q=
function(){return Date.now()-La};var z=function(a){null!==A?setTimeout(z,0,a):(A=a,setTimeout(ra,0))};var G=function(a,b){qa=setTimeout(a,b)};var V=function(){clearTimeout(qa)};var W=function(){return!1};f=X=function(){}}else{var Y=window.performance,sa=window.Date,Ma=window.setTimeout,Na=window.clearTimeout;"undefined"!==typeof console&&(f=window.cancelAnimationFrame,"function"!==typeof window.requestAnimationFrame&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"),
"function"!==typeof f&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"));if("object"===typeof Y&&"function"===typeof Y.now)q=function(){return Y.now()};else{var Oa=sa.now();q=function(){return sa.now()-Oa}}var J=!1,K=null,Z=-1,ta=5,ua=0;W=function(){return q()>=ua};f=function(){};X=function(a){0>a||125<a?console.error("forceFrameRate takes a positive int between 0 and 125, forcing framerates higher than 125 fps is not unsupported"):
ta=0<a?Math.floor(1E3/a):5};var B=new MessageChannel,aa=B.port2;B.port1.onmessage=function(){if(null!==K){var a=q();ua=a+ta;try{K(!0,a)?aa.postMessage(null):(J=!1,K=null)}catch(b){throw aa.postMessage(null),b;}}else J=!1};z=function(a){K=a;J||(J=!0,aa.postMessage(null))};G=function(a,b){Z=Ma(function(){a(q())},b)};V=function(){Na(Z);Z=-1}}var p=[],u=[],Pa=1,l=null,m=3,H=!1,v=!1,y=!1,Qa=0;B={ReactCurrentDispatcher:na,ReactCurrentOwner:M,IsSomeRendererActing:{current:!1},assign:I};I(B,{Scheduler:{__proto__:null,
unstable_ImmediatePriority:1,unstable_UserBlockingPriority:2,unstable_NormalPriority:3,unstable_IdlePriority:5,unstable_LowPriority:4,unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=3}var c=m;m=a;try{return b()}finally{m=c}},unstable_next:function(a){switch(m){case 1:case 2:case 3:var b=3;break;default:b=m}var c=m;m=b;try{return a()}finally{m=c}},unstable_scheduleCallback:function(a,b,c){var d=q();if("object"===typeof c&&null!==c){var e=c.delay;
e="number"===typeof e&&0<e?d+e:d;c="number"===typeof c.timeout?c.timeout:oa(a)}else c=oa(a),e=d;c=e+c;a={id:Pa++,callback:b,priorityLevel:a,startTime:e,expirationTime:c,sortIndex:-1};e>d?(a.sortIndex=e,S(u,a),null===n(p)&&a===n(u)&&(y?V():y=!0,G(T,e-d))):(a.sortIndex=c,S(p,a),v||H||(v=!0,z(U)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=m;return function(){var c=m;m=b;try{return a.apply(this,arguments)}finally{m=c}}},unstable_getCurrentPriorityLevel:function(){return m},
unstable_shouldYield:function(){var a=q();F(a);var b=n(p);return b!==l&&null!==l&&null!==b&&null!==b.callback&&b.startTime<=a&&b.expirationTime<l.expirationTime||W()},unstable_requestPaint:f,unstable_continueExecution:function(){v||H||(v=!0,z(U))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return n(p)},get unstable_now(){return q},get unstable_forceFrameRate(){return X},unstable_Profiling:null},SchedulerTracing:{__proto__:null,__interactionsRef:null,__subscriberRef:null,
unstable_clear:function(a){return a()},unstable_getCurrent:function(){return null},unstable_getThreadID:function(){return++Qa},unstable_trace:function(a,b,c){return c()},unstable_wrap:function(a){return a},unstable_subscribe:function(a){},unstable_unsubscribe:function(a){}}});d.Children={map:function(a,b,c){if(null==a)return a;var d=[];R(a,d,null,b,c);return d},forEach:function(a,b,c){if(null==a)return a;b=ja(null,null,b,c);Q(a,ya,b);ka(b)},count:function(a){return Q(a,function(){return null},null)},
toArray:function(a){var b=[];R(a,b,null,function(a){return a});return b},only:function(a){if(!N(a))throw Error(r(143));return a}};d.Component=w;d.Fragment=Aa;d.Profiler=Ca;d.PureComponent=L;d.StrictMode=Ba;d.Suspense=Ga;d.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=B;d.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error(r(267,a));var d=I({},a.props),e=a.key,f=a.ref,m=a._owner;if(null!=b){void 0!==b.ref&&(f=b.ref,m=M.current);void 0!==b.key&&(e=""+b.key);if(a.type&&a.type.defaultProps)var h=
a.type.defaultProps;for(k in b)ha.call(b,k)&&!ia.hasOwnProperty(k)&&(d[k]=void 0===b[k]&&void 0!==h?h[k]:b[k])}var k=arguments.length-2;if(1===k)d.children=c;else if(1<k){h=Array(k);for(var l=0;l<k;l++)h[l]=arguments[l+2];d.children=h}return{$$typeof:x,type:a.type,key:e,ref:f,props:d,_owner:m}};d.createContext=function(a,b){void 0===b&&(b=null);a={$$typeof:Ea,_calculateChangedBits:b,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null};a.Provider={$$typeof:Da,_context:a};return a.Consumer=
a};d.createElement=ea;d.createFactory=function(a){var b=ea.bind(null,a);b.type=a;return b};d.createRef=function(){return{current:null}};d.forwardRef=function(a){return{$$typeof:Fa,render:a}};d.isValidElement=N;d.lazy=function(a){return{$$typeof:Ia,_ctor:a,_status:-1,_result:null}};d.memo=function(a,b){return{$$typeof:Ha,type:a,compare:void 0===b?null:b}};d.useCallback=function(a,b){return t().useCallback(a,b)};d.useContext=function(a,b){return t().useContext(a,b)};d.useDebugValue=function(a,b){};
d.useEffect=function(a,b){return t().useEffect(a,b)};d.useImperativeHandle=function(a,b,c){return t().useImperativeHandle(a,b,c)};d.useLayoutEffect=function(a,b){return t().useLayoutEffect(a,b)};d.useMemo=function(a,b){return t().useMemo(a,b)};d.useReducer=function(a,b,c){return t().useReducer(a,b,c)};d.useRef=function(a){return t().useRef(a)};d.useState=function(a){return t().useState(a)};d.version="16.13.1"});
</script>
    <script crossorigin>/** @license React v16.13.1
 * react-dom.production.min.js
 *
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
/*
 Modernizr 3.0.0pre (Custom Build) | MIT
*/
'use strict';(function(I,ea){"object"===typeof exports&&"undefined"!==typeof module?ea(exports,require("react")):"function"===typeof define&&define.amd?define(["exports","react"],ea):(I=I||self,ea(I.ReactDOM={},I.React))})(this,function(I,ea){function k(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
function ji(a,b,c,d,e,f,g,h,m){yb=!1;gc=null;ki.apply(li,arguments)}function mi(a,b,c,d,e,f,g,h,m){ji.apply(this,arguments);if(yb){if(yb){var n=gc;yb=!1;gc=null}else throw Error(k(198));hc||(hc=!0,pd=n)}}function lf(a,b,c){var d=a.type||"unknown-event";a.currentTarget=mf(c);mi(d,b,void 0,a);a.currentTarget=null}function nf(){if(ic)for(var a in cb){var b=cb[a],c=ic.indexOf(a);if(!(-1<c))throw Error(k(96,a));if(!jc[c]){if(!b.extractEvents)throw Error(k(97,a));jc[c]=b;c=b.eventTypes;for(var d in c){var e=
void 0;var f=c[d],g=b,h=d;if(qd.hasOwnProperty(h))throw Error(k(99,h));qd[h]=f;var m=f.phasedRegistrationNames;if(m){for(e in m)m.hasOwnProperty(e)&&of(m[e],g,h);e=!0}else f.registrationName?(of(f.registrationName,g,h),e=!0):e=!1;if(!e)throw Error(k(98,d,a));}}}}function of(a,b,c){if(db[a])throw Error(k(100,a));db[a]=b;rd[a]=b.eventTypes[c].dependencies}function pf(a){var b=!1,c;for(c in a)if(a.hasOwnProperty(c)){var d=a[c];if(!cb.hasOwnProperty(c)||cb[c]!==d){if(cb[c])throw Error(k(102,c));cb[c]=
d;b=!0}}b&&nf()}function qf(a){if(a=rf(a)){if("function"!==typeof sd)throw Error(k(280));var b=a.stateNode;b&&(b=td(b),sd(a.stateNode,a.type,b))}}function sf(a){eb?fb?fb.push(a):fb=[a]:eb=a}function tf(){if(eb){var a=eb,b=fb;fb=eb=null;qf(a);if(b)for(a=0;a<b.length;a++)qf(b[a])}}function ud(){if(null!==eb||null!==fb)vd(),tf()}function uf(a,b,c){if(wd)return a(b,c);wd=!0;try{return vf(a,b,c)}finally{wd=!1,ud()}}function ni(a){if(wf.call(xf,a))return!0;if(wf.call(yf,a))return!1;if(oi.test(a))return xf[a]=
!0;yf[a]=!0;return!1}function pi(a,b,c,d){if(null!==c&&0===c.type)return!1;switch(typeof b){case "function":case "symbol":return!0;case "boolean":if(d)return!1;if(null!==c)return!c.acceptsBooleans;a=a.toLowerCase().slice(0,5);return"data-"!==a&&"aria-"!==a;default:return!1}}function qi(a,b,c,d){if(null===b||"undefined"===typeof b||pi(a,b,c,d))return!0;if(d)return!1;if(null!==c)switch(c.type){case 3:return!b;case 4:return!1===b;case 5:return isNaN(b);case 6:return isNaN(b)||1>b}return!1}function L(a,
b,c,d,e,f){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f}function xd(a,b,c,d){var e=E.hasOwnProperty(b)?E[b]:null;var f=null!==e?0===e.type:d?!1:!(2<b.length)||"o"!==b[0]&&"O"!==b[0]||"n"!==b[1]&&"N"!==b[1]?!1:!0;f||(qi(b,c,e,d)&&(c=null),d||null===e?ni(b)&&(null===c?a.removeAttribute(b):a.setAttribute(b,""+c)):e.mustUseProperty?a[e.propertyName]=null===c?3===e.type?!1:"":c:(b=e.attributeName,
d=e.attributeNamespace,null===c?a.removeAttribute(b):(e=e.type,c=3===e||4===e&&!0===c?"":""+c,d?a.setAttributeNS(d,b,c):a.setAttribute(b,c))))}function zb(a){if(null===a||"object"!==typeof a)return null;a=zf&&a[zf]||a["@@iterator"];return"function"===typeof a?a:null}function ri(a){if(-1===a._status){a._status=0;var b=a._ctor;b=b();a._result=b;b.then(function(b){0===a._status&&(b=b.default,a._status=1,a._result=b)},function(b){0===a._status&&(a._status=2,a._result=b)})}}function na(a){if(null==a)return null;
if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case Ma:return"Fragment";case gb:return"Portal";case kc:return"Profiler";case Af:return"StrictMode";case lc:return"Suspense";case yd:return"SuspenseList"}if("object"===typeof a)switch(a.$$typeof){case Bf:return"Context.Consumer";case Cf:return"Context.Provider";case zd:var b=a.render;b=b.displayName||b.name||"";return a.displayName||(""!==b?"ForwardRef("+b+")":"ForwardRef");case Ad:return na(a.type);
case Df:return na(a.render);case Ef:if(a=1===a._status?a._result:null)return na(a)}return null}function Bd(a){var b="";do{a:switch(a.tag){case 3:case 4:case 6:case 7:case 10:case 9:var c="";break a;default:var d=a._debugOwner,e=a._debugSource,f=na(a.type);c=null;d&&(c=na(d.type));d=f;f="";e?f=" (at "+e.fileName.replace(si,"")+":"+e.lineNumber+")":c&&(f=" (created by "+c+")");c="\n    in "+(d||"Unknown")+f}b+=c;a=a.return}while(a);return b}function va(a){switch(typeof a){case "boolean":case "number":case "object":case "string":case "undefined":return a;
default:return""}}function Ff(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"===b)}function ti(a){var b=Ff(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,
b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker=null;delete a[b]}}}}function mc(a){a._valueTracker||(a._valueTracker=ti(a))}function Gf(a){if(!a)return!1;var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=Ff(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function Cd(a,b){var c=b.checked;return M({},b,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=
c?c:a._wrapperState.initialChecked})}function Hf(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=va(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function If(a,b){b=b.checked;null!=b&&xd(a,"checked",b,!1)}function Dd(a,b){If(a,b);var c=va(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!=c)a.value=""+c}else a.value!==
""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?Ed(a,b.type,c):b.hasOwnProperty("defaultValue")&&Ed(a,b.type,va(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function Jf(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue;c||b===a.value||(a.value=
b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function Ed(a,b,c){if("number"!==b||a.ownerDocument.activeElement!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function ui(a){var b="";ea.Children.forEach(a,function(a){null!=a&&(b+=a)});return b}function Fd(a,b){a=M({children:void 0},b);if(b=ui(b.children))a.children=b;return a}function hb(a,b,c,d){a=a.options;if(b){b={};
for(var e=0;e<c.length;e++)b["$"+c[e]]=!0;for(c=0;c<a.length;c++)e=b.hasOwnProperty("$"+a[c].value),a[c].selected!==e&&(a[c].selected=e),e&&d&&(a[c].defaultSelected=!0)}else{c=""+va(c);b=null;for(e=0;e<a.length;e++){if(a[e].value===c){a[e].selected=!0;d&&(a[e].defaultSelected=!0);return}null!==b||a[e].disabled||(b=a[e])}null!==b&&(b.selected=!0)}}function Gd(a,b){if(null!=b.dangerouslySetInnerHTML)throw Error(k(91));return M({},b,{value:void 0,defaultValue:void 0,children:""+a._wrapperState.initialValue})}
function Kf(a,b){var c=b.value;if(null==c){c=b.children;b=b.defaultValue;if(null!=c){if(null!=b)throw Error(k(92));if(Array.isArray(c)){if(!(1>=c.length))throw Error(k(93));c=c[0]}b=c}null==b&&(b="");c=b}a._wrapperState={initialValue:va(c)}}function Lf(a,b){var c=va(b.value),d=va(b.defaultValue);null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=""+d)}function Mf(a,b){b=a.textContent;b===a._wrapperState.initialValue&&""!==
b&&null!==b&&(a.value=b)}function Nf(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Hd(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?Nf(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml":a}function nc(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;return c}function oc(a){if(Id[a])return Id[a];
if(!ib[a])return a;var b=ib[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in Of)return Id[a]=b[c];return a}function Jd(a){var b=Pf.get(a);void 0===b&&(b=new Map,Pf.set(a,b));return b}function Na(a){var b=a,c=a;if(a.alternate)for(;b.return;)b=b.return;else{a=b;do b=a,0!==(b.effectTag&1026)&&(c=b.return),a=b.return;while(a)}return 3===b.tag?c:null}function Qf(a){if(13===a.tag){var b=a.memoizedState;null===b&&(a=a.alternate,null!==a&&(b=a.memoizedState));if(null!==b)return b.dehydrated}return null}function Rf(a){if(Na(a)!==
a)throw Error(k(188));}function vi(a){var b=a.alternate;if(!b){b=Na(a);if(null===b)throw Error(k(188));return b!==a?null:a}for(var c=a,d=b;;){var e=c.return;if(null===e)break;var f=e.alternate;if(null===f){d=e.return;if(null!==d){c=d;continue}break}if(e.child===f.child){for(f=e.child;f;){if(f===c)return Rf(e),a;if(f===d)return Rf(e),b;f=f.sibling}throw Error(k(188));}if(c.return!==d.return)c=e,d=f;else{for(var g=!1,h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling}if(!g){for(h=
f.child;h;){if(h===c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling}if(!g)throw Error(k(189));}}if(c.alternate!==d)throw Error(k(190));}if(3!==c.tag)throw Error(k(188));return c.stateNode.current===c?a:b}function Sf(a){a=vi(a);if(!a)return null;for(var b=a;;){if(5===b.tag||6===b.tag)return b;if(b.child)b.child.return=b,b=b.child;else{if(b===a)break;for(;!b.sibling;){if(!b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}}return null}function jb(a,b){if(null==
b)throw Error(k(30));if(null==a)return b;if(Array.isArray(a)){if(Array.isArray(b))return a.push.apply(a,b),a;a.push(b);return a}return Array.isArray(b)?[a].concat(b):[a,b]}function Kd(a,b,c){Array.isArray(a)?a.forEach(b,c):a&&b.call(c,a)}function pc(a){null!==a&&(Ab=jb(Ab,a));a=Ab;Ab=null;if(a){Kd(a,wi);if(Ab)throw Error(k(95));if(hc)throw a=pd,hc=!1,pd=null,a;}}function Ld(a){a=a.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:
a}function Tf(a){if(!wa)return!1;a="on"+a;var b=a in document;b||(b=document.createElement("div"),b.setAttribute(a,"return;"),b="function"===typeof b[a]);return b}function Uf(a){a.topLevelType=null;a.nativeEvent=null;a.targetInst=null;a.ancestors.length=0;10>qc.length&&qc.push(a)}function Vf(a,b,c,d){if(qc.length){var e=qc.pop();e.topLevelType=a;e.eventSystemFlags=d;e.nativeEvent=b;e.targetInst=c;return e}return{topLevelType:a,eventSystemFlags:d,nativeEvent:b,targetInst:c,ancestors:[]}}function Wf(a){var b=
a.targetInst,c=b;do{if(!c){a.ancestors.push(c);break}var d=c;if(3===d.tag)d=d.stateNode.containerInfo;else{for(;d.return;)d=d.return;d=3!==d.tag?null:d.stateNode.containerInfo}if(!d)break;b=c.tag;5!==b&&6!==b||a.ancestors.push(c);c=Bb(d)}while(c);for(c=0;c<a.ancestors.length;c++){b=a.ancestors[c];var e=Ld(a.nativeEvent);d=a.topLevelType;var f=a.nativeEvent,g=a.eventSystemFlags;0===c&&(g|=64);for(var h=null,m=0;m<jc.length;m++){var n=jc[m];n&&(n=n.extractEvents(d,b,f,e,g))&&(h=jb(h,n))}pc(h)}}function Md(a,
b,c){if(!c.has(a)){switch(a){case "scroll":Cb(b,"scroll",!0);break;case "focus":case "blur":Cb(b,"focus",!0);Cb(b,"blur",!0);c.set("blur",null);c.set("focus",null);break;case "cancel":case "close":Tf(a)&&Cb(b,a,!0);break;case "invalid":case "submit":case "reset":break;default:-1===Db.indexOf(a)&&w(a,b)}c.set(a,null)}}function xi(a,b){var c=Jd(b);Nd.forEach(function(a){Md(a,b,c)});yi.forEach(function(a){Md(a,b,c)})}function Od(a,b,c,d,e){return{blockedOn:a,topLevelType:b,eventSystemFlags:c|32,nativeEvent:e,
container:d}}function Xf(a,b){switch(a){case "focus":case "blur":xa=null;break;case "dragenter":case "dragleave":ya=null;break;case "mouseover":case "mouseout":za=null;break;case "pointerover":case "pointerout":Eb.delete(b.pointerId);break;case "gotpointercapture":case "lostpointercapture":Fb.delete(b.pointerId)}}function Gb(a,b,c,d,e,f){if(null===a||a.nativeEvent!==f)return a=Od(b,c,d,e,f),null!==b&&(b=Hb(b),null!==b&&Yf(b)),a;a.eventSystemFlags|=d;return a}function zi(a,b,c,d,e){switch(b){case "focus":return xa=
Gb(xa,a,b,c,d,e),!0;case "dragenter":return ya=Gb(ya,a,b,c,d,e),!0;case "mouseover":return za=Gb(za,a,b,c,d,e),!0;case "pointerover":var f=e.pointerId;Eb.set(f,Gb(Eb.get(f)||null,a,b,c,d,e));return!0;case "gotpointercapture":return f=e.pointerId,Fb.set(f,Gb(Fb.get(f)||null,a,b,c,d,e)),!0}return!1}function Ai(a){var b=Bb(a.target);if(null!==b){var c=Na(b);if(null!==c)if(b=c.tag,13===b){if(b=Qf(c),null!==b){a.blockedOn=b;Pd(a.priority,function(){Bi(c)});return}}else if(3===b&&c.stateNode.hydrate){a.blockedOn=
3===c.tag?c.stateNode.containerInfo:null;return}}a.blockedOn=null}function rc(a){if(null!==a.blockedOn)return!1;var b=Qd(a.topLevelType,a.eventSystemFlags,a.container,a.nativeEvent);if(null!==b){var c=Hb(b);null!==c&&Yf(c);a.blockedOn=b;return!1}return!0}function Zf(a,b,c){rc(a)&&c.delete(b)}function Ci(){for(Rd=!1;0<fa.length;){var a=fa[0];if(null!==a.blockedOn){a=Hb(a.blockedOn);null!==a&&Di(a);break}var b=Qd(a.topLevelType,a.eventSystemFlags,a.container,a.nativeEvent);null!==b?a.blockedOn=b:fa.shift()}null!==
xa&&rc(xa)&&(xa=null);null!==ya&&rc(ya)&&(ya=null);null!==za&&rc(za)&&(za=null);Eb.forEach(Zf);Fb.forEach(Zf)}function Ib(a,b){a.blockedOn===b&&(a.blockedOn=null,Rd||(Rd=!0,$f(ag,Ci)))}function bg(a){if(0<fa.length){Ib(fa[0],a);for(var b=1;b<fa.length;b++){var c=fa[b];c.blockedOn===a&&(c.blockedOn=null)}}null!==xa&&Ib(xa,a);null!==ya&&Ib(ya,a);null!==za&&Ib(za,a);b=function(b){return Ib(b,a)};Eb.forEach(b);Fb.forEach(b);for(b=0;b<Jb.length;b++)c=Jb[b],c.blockedOn===a&&(c.blockedOn=null);for(;0<Jb.length&&
(b=Jb[0],null===b.blockedOn);)Ai(b),null===b.blockedOn&&Jb.shift()}function Sd(a,b){for(var c=0;c<a.length;c+=2){var d=a[c],e=a[c+1],f="on"+(e[0].toUpperCase()+e.slice(1));f={phasedRegistrationNames:{bubbled:f,captured:f+"Capture"},dependencies:[d],eventPriority:b};Td.set(d,b);cg.set(d,f);dg[e]=f}}function w(a,b){Cb(b,a,!1)}function Cb(a,b,c){var d=Td.get(b);switch(void 0===d?2:d){case 0:d=Ei.bind(null,b,1,a);break;case 1:d=Fi.bind(null,b,1,a);break;default:d=sc.bind(null,b,1,a)}c?a.addEventListener(b,
d,!0):a.addEventListener(b,d,!1)}function Ei(a,b,c,d){Oa||vd();var e=sc,f=Oa;Oa=!0;try{eg(e,a,b,c,d)}finally{(Oa=f)||ud()}}function Fi(a,b,c,d){Gi(Hi,sc.bind(null,a,b,c,d))}function sc(a,b,c,d){if(tc)if(0<fa.length&&-1<Nd.indexOf(a))a=Od(null,a,b,c,d),fa.push(a);else{var e=Qd(a,b,c,d);if(null===e)Xf(a,d);else if(-1<Nd.indexOf(a))a=Od(e,a,b,c,d),fa.push(a);else if(!zi(e,a,b,c,d)){Xf(a,d);a=Vf(a,d,null,b);try{uf(Wf,a)}finally{Uf(a)}}}}function Qd(a,b,c,d){c=Ld(d);c=Bb(c);if(null!==c){var e=Na(c);if(null===
e)c=null;else{var f=e.tag;if(13===f){c=Qf(e);if(null!==c)return c;c=null}else if(3===f){if(e.stateNode.hydrate)return 3===e.tag?e.stateNode.containerInfo:null;c=null}else e!==c&&(c=null)}}a=Vf(a,d,c,b);try{uf(Wf,a)}finally{Uf(a)}return null}function fg(a,b,c){return null==b||"boolean"===typeof b||""===b?"":c||"number"!==typeof b||0===b||Kb.hasOwnProperty(a)&&Kb[a]?(""+b).trim():b+"px"}function gg(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf("--"),e=fg(c,b[c],d);"float"===
c&&(c="cssFloat");d?a.setProperty(c,e):a[c]=e}}function Ud(a,b){if(b){if(Ii[a]&&(null!=b.children||null!=b.dangerouslySetInnerHTML))throw Error(k(137,a,""));if(null!=b.dangerouslySetInnerHTML){if(null!=b.children)throw Error(k(60));if(!("object"===typeof b.dangerouslySetInnerHTML&&"__html"in b.dangerouslySetInnerHTML))throw Error(k(61));}if(null!=b.style&&"object"!==typeof b.style)throw Error(k(62,""));}}function Vd(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;
default:return!0}}function oa(a,b){a=9===a.nodeType||11===a.nodeType?a:a.ownerDocument;var c=Jd(a);b=rd[b];for(var d=0;d<b.length;d++)Md(b[d],a,c)}function uc(){}function Wd(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function hg(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function ig(a,b){var c=hg(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;if(a<=b&&d>=b)return{node:c,
offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=hg(c)}}function jg(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?jg(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function kg(){for(var a=window,b=Wd();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;b=Wd(a.document)}return b}
function Xd(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function lg(a,b){switch(a){case "button":case "input":case "select":case "textarea":return!!b.autoFocus}return!1}function Yd(a,b){return"textarea"===a||"option"===a||"noscript"===a||"string"===typeof b.children||"number"===typeof b.children||"object"===typeof b.dangerouslySetInnerHTML&&
null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}function kb(a){for(;null!=a;a=a.nextSibling){var b=a.nodeType;if(1===b||3===b)break}return a}function mg(a){a=a.previousSibling;for(var b=0;a;){if(8===a.nodeType){var c=a.data;if(c===ng||c===Zd||c===$d){if(0===b)return a;b--}else c===og&&b++}a=a.previousSibling}return null}function Bb(a){var b=a[Aa];if(b)return b;for(var c=a.parentNode;c;){if(b=c[Lb]||c[Aa]){c=b.alternate;if(null!==b.child||null!==c&&null!==c.child)for(a=mg(a);null!==
a;){if(c=a[Aa])return c;a=mg(a)}return b}a=c;c=a.parentNode}return null}function Hb(a){a=a[Aa]||a[Lb];return!a||5!==a.tag&&6!==a.tag&&13!==a.tag&&3!==a.tag?null:a}function Pa(a){if(5===a.tag||6===a.tag)return a.stateNode;throw Error(k(33));}function ae(a){return a[vc]||null}function pa(a){do a=a.return;while(a&&5!==a.tag);return a?a:null}function pg(a,b){var c=a.stateNode;if(!c)return null;var d=td(c);if(!d)return null;c=d[b];a:switch(b){case "onClick":case "onClickCapture":case "onDoubleClick":case "onDoubleClickCapture":case "onMouseDown":case "onMouseDownCapture":case "onMouseMove":case "onMouseMoveCapture":case "onMouseUp":case "onMouseUpCapture":case "onMouseEnter":(d=
!d.disabled)||(a=a.type,d=!("button"===a||"input"===a||"select"===a||"textarea"===a));a=!d;break a;default:a=!1}if(a)return null;if(c&&"function"!==typeof c)throw Error(k(231,b,typeof c));return c}function qg(a,b,c){if(b=pg(a,c.dispatchConfig.phasedRegistrationNames[b]))c._dispatchListeners=jb(c._dispatchListeners,b),c._dispatchInstances=jb(c._dispatchInstances,a)}function Ji(a){if(a&&a.dispatchConfig.phasedRegistrationNames){for(var b=a._targetInst,c=[];b;)c.push(b),b=pa(b);for(b=c.length;0<b--;)qg(c[b],
"captured",a);for(b=0;b<c.length;b++)qg(c[b],"bubbled",a)}}function be(a,b,c){a&&c&&c.dispatchConfig.registrationName&&(b=pg(a,c.dispatchConfig.registrationName))&&(c._dispatchListeners=jb(c._dispatchListeners,b),c._dispatchInstances=jb(c._dispatchInstances,a))}function Ki(a){a&&a.dispatchConfig.registrationName&&be(a._targetInst,null,a)}function lb(a){Kd(a,Ji)}function rg(){if(wc)return wc;var a,b=ce,c=b.length,d,e="value"in Ba?Ba.value:Ba.textContent,f=e.length;for(a=0;a<c&&b[a]===e[a];a++);var g=
c-a;for(d=1;d<=g&&b[c-d]===e[f-d];d++);return wc=e.slice(a,1<d?1-d:void 0)}function xc(){return!0}function yc(){return!1}function R(a,b,c,d){this.dispatchConfig=a;this._targetInst=b;this.nativeEvent=c;a=this.constructor.Interface;for(var e in a)a.hasOwnProperty(e)&&((b=a[e])?this[e]=b(c):"target"===e?this.target=d:this[e]=c[e]);this.isDefaultPrevented=(null!=c.defaultPrevented?c.defaultPrevented:!1===c.returnValue)?xc:yc;this.isPropagationStopped=yc;return this}function Li(a,b,c,d){if(this.eventPool.length){var e=
this.eventPool.pop();this.call(e,a,b,c,d);return e}return new this(a,b,c,d)}function Mi(a){if(!(a instanceof this))throw Error(k(279));a.destructor();10>this.eventPool.length&&this.eventPool.push(a)}function sg(a){a.eventPool=[];a.getPooled=Li;a.release=Mi}function tg(a,b){switch(a){case "keyup":return-1!==Ni.indexOf(b.keyCode);case "keydown":return 229!==b.keyCode;case "keypress":case "mousedown":case "blur":return!0;default:return!1}}function ug(a){a=a.detail;return"object"===typeof a&&"data"in
a?a.data:null}function Oi(a,b){switch(a){case "compositionend":return ug(b);case "keypress":if(32!==b.which)return null;vg=!0;return wg;case "textInput":return a=b.data,a===wg&&vg?null:a;default:return null}}function Pi(a,b){if(mb)return"compositionend"===a||!de&&tg(a,b)?(a=rg(),wc=ce=Ba=null,mb=!1,a):null;switch(a){case "paste":return null;case "keypress":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1<b.char.length)return b.char;if(b.which)return String.fromCharCode(b.which)}return null;
case "compositionend":return xg&&"ko"!==b.locale?null:b.data;default:return null}}function yg(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return"input"===b?!!Qi[a.type]:"textarea"===b?!0:!1}function zg(a,b,c){a=R.getPooled(Ag.change,a,b,c);a.type="change";sf(c);lb(a);return a}function Ri(a){pc(a)}function zc(a){var b=Pa(a);if(Gf(b))return a}function Si(a,b){if("change"===a)return b}function Bg(){Mb&&(Mb.detachEvent("onpropertychange",Cg),Nb=Mb=null)}function Cg(a){if("value"===a.propertyName&&
zc(Nb))if(a=zg(Nb,a,Ld(a)),Oa)pc(a);else{Oa=!0;try{ee(Ri,a)}finally{Oa=!1,ud()}}}function Ti(a,b,c){"focus"===a?(Bg(),Mb=b,Nb=c,Mb.attachEvent("onpropertychange",Cg)):"blur"===a&&Bg()}function Ui(a,b){if("selectionchange"===a||"keyup"===a||"keydown"===a)return zc(Nb)}function Vi(a,b){if("click"===a)return zc(b)}function Wi(a,b){if("input"===a||"change"===a)return zc(b)}function Xi(a){var b=this.nativeEvent;return b.getModifierState?b.getModifierState(a):(a=Yi[a])?!!b[a]:!1}function fe(a){return Xi}
function Zi(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}function Ob(a,b){if(Qa(a,b))return!0;if("object"!==typeof a||null===a||"object"!==typeof b||null===b)return!1;var c=Object.keys(a),d=Object.keys(b);if(c.length!==d.length)return!1;for(d=0;d<c.length;d++)if(!$i.call(b,c[d])||!Qa(a[c[d]],b[c[d]]))return!1;return!0}function Dg(a,b){var c=b.window===b?b.document:9===b.nodeType?b:b.ownerDocument;if(ge||null==nb||nb!==Wd(c))return null;c=nb;"selectionStart"in c&&Xd(c)?c={start:c.selectionStart,
end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset});return Pb&&Ob(Pb,c)?null:(Pb=c,a=R.getPooled(Eg.select,he,a,b),a.type="select",a.target=nb,lb(a),a)}function Ac(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;10===a&&(a=13);return 32<=a||13===a?a:0}function q(a,b){0>ob||(a.current=ie[ob],ie[ob]=null,ob--)}function y(a,b,c){ob++;
ie[ob]=a.current;a.current=b}function pb(a,b){var c=a.type.contextTypes;if(!c)return Ca;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function N(a){a=a.childContextTypes;return null!==a&&void 0!==a}function Fg(a,b,c){if(B.current!==Ca)throw Error(k(168));y(B,b);y(G,c)}
function Gg(a,b,c){var d=a.stateNode;a=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in a))throw Error(k(108,na(b)||"Unknown",e));return M({},c,{},d)}function Bc(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||Ca;Ra=B.current;y(B,a);y(G,G.current);return!0}function Hg(a,b,c){var d=a.stateNode;if(!d)throw Error(k(169));c?(a=Gg(a,b,Ra),d.__reactInternalMemoizedMergedChildContext=a,q(G),q(B),y(B,a)):q(G);y(G,c)}function Cc(){switch(aj()){case Dc:return 99;
case Ig:return 98;case Jg:return 97;case Kg:return 96;case Lg:return 95;default:throw Error(k(332));}}function Mg(a){switch(a){case 99:return Dc;case 98:return Ig;case 97:return Jg;case 96:return Kg;case 95:return Lg;default:throw Error(k(332));}}function Da(a,b){a=Mg(a);return bj(a,b)}function Ng(a,b,c){a=Mg(a);return je(a,b,c)}function Og(a){null===qa?(qa=[a],Ec=je(Dc,Pg)):qa.push(a);return Qg}function ha(){if(null!==Ec){var a=Ec;Ec=null;Rg(a)}Pg()}function Pg(){if(!ke&&null!==qa){ke=!0;var a=0;
try{var b=qa;Da(99,function(){for(;a<b.length;a++){var c=b[a];do c=c(!0);while(null!==c)}});qa=null}catch(c){throw null!==qa&&(qa=qa.slice(a+1)),je(Dc,ha),c;}finally{ke=!1}}}function Fc(a,b,c){c/=10;return 1073741821-(((1073741821-a+b/10)/c|0)+1)*c}function aa(a,b){if(a&&a.defaultProps){b=M({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c])}return b}function le(){Gc=qb=Hc=null}function me(a){var b=Ic.current;q(Ic);a.type._context._currentValue=b}function Sg(a,b){for(;null!==a;){var c=
a.alternate;if(a.childExpirationTime<b)a.childExpirationTime=b,null!==c&&c.childExpirationTime<b&&(c.childExpirationTime=b);else if(null!==c&&c.childExpirationTime<b)c.childExpirationTime=b;else break;a=a.return}}function rb(a,b){Hc=a;Gc=qb=null;a=a.dependencies;null!==a&&null!==a.firstContext&&(a.expirationTime>=b&&(ia=!0),a.firstContext=null)}function W(a,b){if(Gc!==a&&!1!==b&&0!==b){if("number"!==typeof b||1073741823===b)Gc=a,b=1073741823;b={context:a,observedBits:b,next:null};if(null===qb){if(null===
Hc)throw Error(k(308));qb=b;Hc.dependencies={expirationTime:0,firstContext:b,responders:null}}else qb=qb.next=b}return a._currentValue}function ne(a){a.updateQueue={baseState:a.memoizedState,baseQueue:null,shared:{pending:null},effects:null}}function oe(a,b){a=a.updateQueue;b.updateQueue===a&&(b.updateQueue={baseState:a.baseState,baseQueue:a.baseQueue,shared:a.shared,effects:a.effects})}function Ea(a,b){a={expirationTime:a,suspenseConfig:b,tag:Tg,payload:null,callback:null,next:null};return a.next=
a}function Fa(a,b){a=a.updateQueue;if(null!==a){a=a.shared;var c=a.pending;null===c?b.next=b:(b.next=c.next,c.next=b);a.pending=b}}function Ug(a,b){var c=a.alternate;null!==c&&oe(c,a);a=a.updateQueue;c=a.baseQueue;null===c?(a.baseQueue=b.next=b,b.next=b):(b.next=c.next,c.next=b)}function Qb(a,b,c,d){var e=a.updateQueue;Ga=!1;var f=e.baseQueue,g=e.shared.pending;if(null!==g){if(null!==f){var h=f.next;f.next=g.next;g.next=h}f=g;e.shared.pending=null;h=a.alternate;null!==h&&(h=h.updateQueue,null!==h&&
(h.baseQueue=g))}if(null!==f){h=f.next;var m=e.baseState,n=0,k=null,ba=null,l=null;if(null!==h){var p=h;do{g=p.expirationTime;if(g<d){var t={expirationTime:p.expirationTime,suspenseConfig:p.suspenseConfig,tag:p.tag,payload:p.payload,callback:p.callback,next:null};null===l?(ba=l=t,k=m):l=l.next=t;g>n&&(n=g)}else{null!==l&&(l=l.next={expirationTime:1073741823,suspenseConfig:p.suspenseConfig,tag:p.tag,payload:p.payload,callback:p.callback,next:null});Vg(g,p.suspenseConfig);a:{var q=a,r=p;g=b;t=c;switch(r.tag){case 1:q=
r.payload;if("function"===typeof q){m=q.call(t,m,g);break a}m=q;break a;case 3:q.effectTag=q.effectTag&-4097|64;case Tg:q=r.payload;g="function"===typeof q?q.call(t,m,g):q;if(null===g||void 0===g)break a;m=M({},m,g);break a;case Jc:Ga=!0}}null!==p.callback&&(a.effectTag|=32,g=e.effects,null===g?e.effects=[p]:g.push(p))}p=p.next;if(null===p||p===h)if(g=e.shared.pending,null===g)break;else p=f.next=g.next,g.next=h,e.baseQueue=f=g,e.shared.pending=null}while(1)}null===l?k=m:l.next=ba;e.baseState=k;e.baseQueue=
l;Kc(n);a.expirationTime=n;a.memoizedState=m}}function Wg(a,b,c){a=b.effects;b.effects=null;if(null!==a)for(b=0;b<a.length;b++){var d=a[b],e=d.callback;if(null!==e){d.callback=null;d=e;e=c;if("function"!==typeof d)throw Error(k(191,d));d.call(e)}}}function Lc(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:M({},b,c);a.memoizedState=c;0===a.expirationTime&&(a.updateQueue.baseState=c)}function Xg(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,
f,g):b.prototype&&b.prototype.isPureReactComponent?!Ob(c,d)||!Ob(e,f):!0}function Yg(a,b,c){var d=!1,e=Ca;var f=b.contextType;"object"===typeof f&&null!==f?f=W(f):(e=N(b)?Ra:B.current,d=b.contextTypes,f=(d=null!==d&&void 0!==d)?pb(a,e):Ca);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=Mc;a.stateNode=b;b._reactInternalFiber=a;d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=e,a.__reactInternalMemoizedMaskedChildContext=f);return b}function Zg(a,
b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&&b.UNSAFE_componentWillReceiveProps(c,d);b.state!==a&&Mc.enqueueReplaceState(b,b.state,null)}function pe(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs=$g;ne(a);var f=b.contextType;"object"===typeof f&&null!==f?e.context=W(f):(f=N(b)?Ra:B.current,e.context=pb(a,f));Qb(a,c,e,d);e.state=a.memoizedState;f=b.getDerivedStateFromProps;
"function"===typeof f&&(Lc(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!==typeof e.UNSAFE_componentWillMount&&"function"!==typeof e.componentWillMount||(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&Mc.enqueueReplaceState(e,e.state,null),Qb(a,c,e,d),e.state=a.memoizedState);"function"===
typeof e.componentDidMount&&(a.effectTag|=4)}function Rb(a,b,c){a=c.ref;if(null!==a&&"function"!==typeof a&&"object"!==typeof a){if(c._owner){c=c._owner;if(c){if(1!==c.tag)throw Error(k(309));var d=c.stateNode}if(!d)throw Error(k(147,a));var e=""+a;if(null!==b&&null!==b.ref&&"function"===typeof b.ref&&b.ref._stringRef===e)return b.ref;b=function(a){var b=d.refs;b===$g&&(b=d.refs={});null===a?delete b[e]:b[e]=a};b._stringRef=e;return b}if("string"!==typeof a)throw Error(k(284));if(!c._owner)throw Error(k(290,
a));}return a}function Nc(a,b){if("textarea"!==a.type)throw Error(k(31,"[object Object]"===Object.prototype.toString.call(b)?"object with keys {"+Object.keys(b).join(", ")+"}":b,""));}function ah(a){function b(b,c){if(a){var d=b.lastEffect;null!==d?(d.nextEffect=c,b.lastEffect=c):b.firstEffect=b.lastEffect=c;c.nextEffect=null;c.effectTag=8}}function c(c,d){if(!a)return null;for(;null!==d;)b(c,d),d=d.sibling;return null}function d(a,b){for(a=new Map;null!==b;)null!==b.key?a.set(b.key,b):a.set(b.index,
b),b=b.sibling;return a}function e(a,b){a=Sa(a,b);a.index=0;a.sibling=null;return a}function f(b,c,d){b.index=d;if(!a)return c;d=b.alternate;if(null!==d)return d=d.index,d<c?(b.effectTag=2,c):d;b.effectTag=2;return c}function g(b){a&&null===b.alternate&&(b.effectTag=2);return b}function h(a,b,c,d){if(null===b||6!==b.tag)return b=qe(c,a.mode,d),b.return=a,b;b=e(b,c);b.return=a;return b}function m(a,b,c,d){if(null!==b&&b.elementType===c.type)return d=e(b,c.props),d.ref=Rb(a,b,c),d.return=a,d;d=Oc(c.type,
c.key,c.props,null,a.mode,d);d.ref=Rb(a,b,c);d.return=a;return d}function n(a,b,c,d){if(null===b||4!==b.tag||b.stateNode.containerInfo!==c.containerInfo||b.stateNode.implementation!==c.implementation)return b=re(c,a.mode,d),b.return=a,b;b=e(b,c.children||[]);b.return=a;return b}function l(a,b,c,d,f){if(null===b||7!==b.tag)return b=Ha(c,a.mode,d,f),b.return=a,b;b=e(b,c);b.return=a;return b}function ba(a,b,c){if("string"===typeof b||"number"===typeof b)return b=qe(""+b,a.mode,c),b.return=a,b;if("object"===
typeof b&&null!==b){switch(b.$$typeof){case Pc:return c=Oc(b.type,b.key,b.props,null,a.mode,c),c.ref=Rb(a,null,b),c.return=a,c;case gb:return b=re(b,a.mode,c),b.return=a,b}if(Qc(b)||zb(b))return b=Ha(b,a.mode,c,null),b.return=a,b;Nc(a,b)}return null}function p(a,b,c,d){var e=null!==b?b.key:null;if("string"===typeof c||"number"===typeof c)return null!==e?null:h(a,b,""+c,d);if("object"===typeof c&&null!==c){switch(c.$$typeof){case Pc:return c.key===e?c.type===Ma?l(a,b,c.props.children,d,e):m(a,b,c,
d):null;case gb:return c.key===e?n(a,b,c,d):null}if(Qc(c)||zb(c))return null!==e?null:l(a,b,c,d,null);Nc(a,c)}return null}function t(a,b,c,d,e){if("string"===typeof d||"number"===typeof d)return a=a.get(c)||null,h(b,a,""+d,e);if("object"===typeof d&&null!==d){switch(d.$$typeof){case Pc:return a=a.get(null===d.key?c:d.key)||null,d.type===Ma?l(b,a,d.props.children,e,d.key):m(b,a,d,e);case gb:return a=a.get(null===d.key?c:d.key)||null,n(b,a,d,e)}if(Qc(d)||zb(d))return a=a.get(c)||null,l(b,a,d,e,null);
Nc(b,d)}return null}function q(e,g,h,m){for(var n=null,k=null,l=g,r=g=0,C=null;null!==l&&r<h.length;r++){l.index>r?(C=l,l=null):C=l.sibling;var O=p(e,l,h[r],m);if(null===O){null===l&&(l=C);break}a&&l&&null===O.alternate&&b(e,l);g=f(O,g,r);null===k?n=O:k.sibling=O;k=O;l=C}if(r===h.length)return c(e,l),n;if(null===l){for(;r<h.length;r++)l=ba(e,h[r],m),null!==l&&(g=f(l,g,r),null===k?n=l:k.sibling=l,k=l);return n}for(l=d(e,l);r<h.length;r++)C=t(l,e,r,h[r],m),null!==C&&(a&&null!==C.alternate&&l.delete(null===
C.key?r:C.key),g=f(C,g,r),null===k?n=C:k.sibling=C,k=C);a&&l.forEach(function(a){return b(e,a)});return n}function w(e,g,h,n){var m=zb(h);if("function"!==typeof m)throw Error(k(150));h=m.call(h);if(null==h)throw Error(k(151));for(var l=m=null,r=g,C=g=0,O=null,v=h.next();null!==r&&!v.done;C++,v=h.next()){r.index>C?(O=r,r=null):O=r.sibling;var q=p(e,r,v.value,n);if(null===q){null===r&&(r=O);break}a&&r&&null===q.alternate&&b(e,r);g=f(q,g,C);null===l?m=q:l.sibling=q;l=q;r=O}if(v.done)return c(e,r),m;
if(null===r){for(;!v.done;C++,v=h.next())v=ba(e,v.value,n),null!==v&&(g=f(v,g,C),null===l?m=v:l.sibling=v,l=v);return m}for(r=d(e,r);!v.done;C++,v=h.next())v=t(r,e,C,v.value,n),null!==v&&(a&&null!==v.alternate&&r.delete(null===v.key?C:v.key),g=f(v,g,C),null===l?m=v:l.sibling=v,l=v);a&&r.forEach(function(a){return b(e,a)});return m}return function(a,d,f,h){var m="object"===typeof f&&null!==f&&f.type===Ma&&null===f.key;m&&(f=f.props.children);var n="object"===typeof f&&null!==f;if(n)switch(f.$$typeof){case Pc:a:{n=
f.key;for(m=d;null!==m;){if(m.key===n){switch(m.tag){case 7:if(f.type===Ma){c(a,m.sibling);d=e(m,f.props.children);d.return=a;a=d;break a}break;default:if(m.elementType===f.type){c(a,m.sibling);d=e(m,f.props);d.ref=Rb(a,m,f);d.return=a;a=d;break a}}c(a,m);break}else b(a,m);m=m.sibling}f.type===Ma?(d=Ha(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=Oc(f.type,f.key,f.props,null,a.mode,h),h.ref=Rb(a,d,f),h.return=a,a=h)}return g(a);case gb:a:{for(m=f.key;null!==d;){if(d.key===m)if(4===d.tag&&d.stateNode.containerInfo===
f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=re(f,a.mode,h);d.return=a;a=d}return g(a)}if("string"===typeof f||"number"===typeof f)return f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):(c(a,d),d=qe(f,a.mode,h),d.return=a,a=d),g(a);if(Qc(f))return q(a,d,f,h);if(zb(f))return w(a,d,f,h);n&&Nc(a,f);if("undefined"===typeof f&&!m)switch(a.tag){case 1:case 0:throw a=
a.type,Error(k(152,a.displayName||a.name||"Component"));}return c(a,d)}}function Ta(a){if(a===Sb)throw Error(k(174));return a}function se(a,b){y(Tb,b);y(Ub,a);y(ja,Sb);a=b.nodeType;switch(a){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:Hd(null,"");break;default:a=8===a?b.parentNode:b,b=a.namespaceURI||null,a=a.tagName,b=Hd(b,a)}q(ja);y(ja,b)}function tb(a){q(ja);q(Ub);q(Tb)}function bh(a){Ta(Tb.current);var b=Ta(ja.current);var c=Hd(b,a.type);b!==c&&(y(Ub,a),y(ja,c))}function te(a){Ub.current===
a&&(q(ja),q(Ub))}function Rc(a){for(var b=a;null!==b;){if(13===b.tag){var c=b.memoizedState;if(null!==c&&(c=c.dehydrated,null===c||c.data===$d||c.data===Zd))return b}else if(19===b.tag&&void 0!==b.memoizedProps.revealOrder){if(0!==(b.effectTag&64))return b}else if(null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}return null}function ue(a,b){return{responder:a,props:b}}
function S(){throw Error(k(321));}function ve(a,b){if(null===b)return!1;for(var c=0;c<b.length&&c<a.length;c++)if(!Qa(a[c],b[c]))return!1;return!0}function we(a,b,c,d,e,f){Ia=f;z=b;b.memoizedState=null;b.updateQueue=null;b.expirationTime=0;Sc.current=null===a||null===a.memoizedState?dj:ej;a=c(d,e);if(b.expirationTime===Ia){f=0;do{b.expirationTime=0;if(!(25>f))throw Error(k(301));f+=1;J=K=null;b.updateQueue=null;Sc.current=fj;a=c(d,e)}while(b.expirationTime===Ia)}Sc.current=Tc;b=null!==K&&null!==K.next;
Ia=0;J=K=z=null;Uc=!1;if(b)throw Error(k(300));return a}function ub(){var a={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};null===J?z.memoizedState=J=a:J=J.next=a;return J}function vb(){if(null===K){var a=z.alternate;a=null!==a?a.memoizedState:null}else a=K.next;var b=null===J?z.memoizedState:J.next;if(null!==b)J=b,K=a;else{if(null===a)throw Error(k(310));K=a;a={memoizedState:K.memoizedState,baseState:K.baseState,baseQueue:K.baseQueue,queue:K.queue,next:null};null===J?z.memoizedState=
J=a:J=J.next=a}return J}function Ua(a,b){return"function"===typeof b?b(a):b}function Vc(a,b,c){b=vb();c=b.queue;if(null===c)throw Error(k(311));c.lastRenderedReducer=a;var d=K,e=d.baseQueue,f=c.pending;if(null!==f){if(null!==e){var g=e.next;e.next=f.next;f.next=g}d.baseQueue=e=f;c.pending=null}if(null!==e){e=e.next;d=d.baseState;var h=g=f=null,m=e;do{var n=m.expirationTime;if(n<Ia){var l={expirationTime:m.expirationTime,suspenseConfig:m.suspenseConfig,action:m.action,eagerReducer:m.eagerReducer,eagerState:m.eagerState,
next:null};null===h?(g=h=l,f=d):h=h.next=l;n>z.expirationTime&&(z.expirationTime=n,Kc(n))}else null!==h&&(h=h.next={expirationTime:1073741823,suspenseConfig:m.suspenseConfig,action:m.action,eagerReducer:m.eagerReducer,eagerState:m.eagerState,next:null}),Vg(n,m.suspenseConfig),d=m.eagerReducer===a?m.eagerState:a(d,m.action);m=m.next}while(null!==m&&m!==e);null===h?f=d:h.next=g;Qa(d,b.memoizedState)||(ia=!0);b.memoizedState=d;b.baseState=f;b.baseQueue=h;c.lastRenderedState=d}return[b.memoizedState,
c.dispatch]}function Wc(a,b,c){b=vb();c=b.queue;if(null===c)throw Error(k(311));c.lastRenderedReducer=a;var d=c.dispatch,e=c.pending,f=b.memoizedState;if(null!==e){c.pending=null;var g=e=e.next;do f=a(f,g.action),g=g.next;while(g!==e);Qa(f,b.memoizedState)||(ia=!0);b.memoizedState=f;null===b.baseQueue&&(b.baseState=f);c.lastRenderedState=f}return[f,d]}function xe(a){var b=ub();"function"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a=b.queue={pending:null,dispatch:null,lastRenderedReducer:Ua,
lastRenderedState:a};a=a.dispatch=ch.bind(null,z,a);return[b.memoizedState,a]}function ye(a,b,c,d){a={tag:a,create:b,destroy:c,deps:d,next:null};b=z.updateQueue;null===b?(b={lastEffect:null},z.updateQueue=b,b.lastEffect=a.next=a):(c=b.lastEffect,null===c?b.lastEffect=a.next=a:(d=c.next,c.next=a,a.next=d,b.lastEffect=a));return a}function dh(a){return vb().memoizedState}function ze(a,b,c,d){var e=ub();z.effectTag|=a;e.memoizedState=ye(1|b,c,void 0,void 0===d?null:d)}function Ae(a,b,c,d){var e=vb();
d=void 0===d?null:d;var f=void 0;if(null!==K){var g=K.memoizedState;f=g.destroy;if(null!==d&&ve(d,g.deps)){ye(b,c,f,d);return}}z.effectTag|=a;e.memoizedState=ye(1|b,c,f,d)}function eh(a,b){return ze(516,4,a,b)}function Xc(a,b){return Ae(516,4,a,b)}function fh(a,b){return Ae(4,2,a,b)}function gh(a,b){if("function"===typeof b)return a=a(),b(a),function(){b(null)};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null}}function hh(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;
return Ae(4,2,gh.bind(null,b,a),c)}function Be(a,b){}function ih(a,b){ub().memoizedState=[a,void 0===b?null:b];return a}function Yc(a,b){var c=vb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&ve(b,d[1]))return d[0];c.memoizedState=[a,b];return a}function jh(a,b){var c=vb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&ve(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a}function Ce(a,b,c){var d=Cc();Da(98>d?98:d,function(){a(!0)});Da(97<d?97:d,function(){var d=
X.suspense;X.suspense=void 0===b?null:b;try{a(!1),c()}finally{X.suspense=d}})}function ch(a,b,c){var d=ka(),e=Vb.suspense;d=Va(d,a,e);e={expirationTime:d,suspenseConfig:e,action:c,eagerReducer:null,eagerState:null,next:null};var f=b.pending;null===f?e.next=e:(e.next=f.next,f.next=e);b.pending=e;f=a.alternate;if(a===z||null!==f&&f===z)Uc=!0,e.expirationTime=Ia,z.expirationTime=Ia;else{if(0===a.expirationTime&&(null===f||0===f.expirationTime)&&(f=b.lastRenderedReducer,null!==f))try{var g=b.lastRenderedState,
h=f(g,c);e.eagerReducer=f;e.eagerState=h;if(Qa(h,g))return}catch(m){}finally{}Ja(a,d)}}function kh(a,b){var c=la(5,null,null,0);c.elementType="DELETED";c.type="DELETED";c.stateNode=b;c.return=a;c.effectTag=8;null!==a.lastEffect?(a.lastEffect.nextEffect=c,a.lastEffect=c):a.firstEffect=a.lastEffect=c}function lh(a,b){switch(a.tag){case 5:var c=a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,!0):!1;case 6:return b=""===a.pendingProps||3!==b.nodeType?
null:b,null!==b?(a.stateNode=b,!0):!1;case 13:return!1;default:return!1}}function De(a){if(Wa){var b=Ka;if(b){var c=b;if(!lh(a,b)){b=kb(c.nextSibling);if(!b||!lh(a,b)){a.effectTag=a.effectTag&-1025|2;Wa=!1;ra=a;return}kh(ra,c)}ra=a;Ka=kb(b.firstChild)}else a.effectTag=a.effectTag&-1025|2,Wa=!1,ra=a}}function mh(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&13!==a.tag;)a=a.return;ra=a}function Zc(a){if(a!==ra)return!1;if(!Wa)return mh(a),Wa=!0,!1;var b=a.type;if(5!==a.tag||"head"!==b&&"body"!==
b&&!Yd(b,a.memoizedProps))for(b=Ka;b;)kh(a,b),b=kb(b.nextSibling);mh(a);if(13===a.tag){a=a.memoizedState;a=null!==a?a.dehydrated:null;if(!a)throw Error(k(317));a:{a=a.nextSibling;for(b=0;a;){if(8===a.nodeType){var c=a.data;if(c===og){if(0===b){Ka=kb(a.nextSibling);break a}b--}else c!==ng&&c!==Zd&&c!==$d||b++}a=a.nextSibling}Ka=null}}else Ka=ra?kb(a.stateNode.nextSibling):null;return!0}function Ee(){Ka=ra=null;Wa=!1}function T(a,b,c,d){b.child=null===a?Fe(b,null,c,d):wb(b,a.child,c,d)}function nh(a,
b,c,d,e){c=c.render;var f=b.ref;rb(b,e);d=we(a,b,c,d,f,e);if(null!==a&&!ia)return b.updateQueue=a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),sa(a,b,e);b.effectTag|=1;T(a,b,d,e);return b.child}function oh(a,b,c,d,e,f){if(null===a){var g=c.type;if("function"===typeof g&&!Ge(g)&&void 0===g.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=g,ph(a,b,g,d,e,f);a=Oc(c.type,null,d,null,b.mode,f);a.ref=b.ref;a.return=b;return b.child=a}g=a.child;if(e<
f&&(e=g.memoizedProps,c=c.compare,c=null!==c?c:Ob,c(e,d)&&a.ref===b.ref))return sa(a,b,f);b.effectTag|=1;a=Sa(g,d);a.ref=b.ref;a.return=b;return b.child=a}function ph(a,b,c,d,e,f){return null!==a&&Ob(a.memoizedProps,d)&&a.ref===b.ref&&(ia=!1,e<f)?(b.expirationTime=a.expirationTime,sa(a,b,f)):He(a,b,c,d,f)}function qh(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.effectTag|=128}function He(a,b,c,d,e){var f=N(c)?Ra:B.current;f=pb(b,f);rb(b,e);c=we(a,b,c,d,f,e);if(null!==a&&!ia)return b.updateQueue=
a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),sa(a,b,e);b.effectTag|=1;T(a,b,c,e);return b.child}function rh(a,b,c,d,e){if(N(c)){var f=!0;Bc(b)}else f=!1;rb(b,e);if(null===b.stateNode)null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),Yg(b,c,d),pe(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var m=g.context,n=c.contextType;"object"===typeof n&&null!==n?n=W(n):(n=N(c)?Ra:B.current,n=pb(b,n));var l=c.getDerivedStateFromProps,k="function"===
typeof l||"function"===typeof g.getSnapshotBeforeUpdate;k||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||m!==n)&&Zg(b,g,d,n);Ga=!1;var p=b.memoizedState;g.state=p;Qb(b,d,g,e);m=b.memoizedState;h!==d||p!==m||G.current||Ga?("function"===typeof l&&(Lc(b,c,l,d),m=b.memoizedState),(h=Ga||Xg(b,c,h,d,p,m,n))?(k||"function"!==typeof g.UNSAFE_componentWillMount&&"function"!==typeof g.componentWillMount||("function"===typeof g.componentWillMount&&
g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.effectTag|=4)):("function"===typeof g.componentDidMount&&(b.effectTag|=4),b.memoizedProps=d,b.memoizedState=m),g.props=d,g.state=m,g.context=n,d=h):("function"===typeof g.componentDidMount&&(b.effectTag|=4),d=!1)}else g=b.stateNode,oe(a,b),h=b.memoizedProps,g.props=b.type===b.elementType?h:aa(b.type,h),m=g.context,n=c.contextType,"object"===typeof n&&null!==
n?n=W(n):(n=N(c)?Ra:B.current,n=pb(b,n)),l=c.getDerivedStateFromProps,(k="function"===typeof l||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||m!==n)&&Zg(b,g,d,n),Ga=!1,m=b.memoizedState,g.state=m,Qb(b,d,g,e),p=b.memoizedState,h!==d||m!==p||G.current||Ga?("function"===typeof l&&(Lc(b,c,l,d),p=b.memoizedState),(l=Ga||Xg(b,c,h,d,m,p,n))?(k||"function"!==typeof g.UNSAFE_componentWillUpdate&&
"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,p,n),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,p,n)),"function"===typeof g.componentDidUpdate&&(b.effectTag|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.effectTag|=256)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&m===
a.memoizedState||(b.effectTag|=256),b.memoizedProps=d,b.memoizedState=p),g.props=d,g.state=p,g.context=n,d=l):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=256),d=!1);return Ie(a,b,c,d,f,e)}function Ie(a,b,c,d,e,f){qh(a,b);var g=0!==(b.effectTag&64);if(!d&&!g)return e&&Hg(b,c,!1),sa(a,b,f);d=b.stateNode;gj.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?
null:d.render();b.effectTag|=1;null!==a&&g?(b.child=wb(b,a.child,null,f),b.child=wb(b,null,h,f)):T(a,b,h,f);b.memoizedState=d.state;e&&Hg(b,c,!0);return b.child}function sh(a){var b=a.stateNode;b.pendingContext?Fg(a,b.pendingContext,b.pendingContext!==b.context):b.context&&Fg(a,b.context,!1);se(a,b.containerInfo)}function th(a,b,c){var d=b.mode,e=b.pendingProps,f=D.current,g=!1,h;(h=0!==(b.effectTag&64))||(h=0!==(f&2)&&(null===a||null!==a.memoizedState));h?(g=!0,b.effectTag&=-65):null!==a&&null===
a.memoizedState||void 0===e.fallback||!0===e.unstable_avoidThisFallback||(f|=1);y(D,f&1);if(null===a){void 0!==e.fallback&&De(b);if(g){g=e.fallback;e=Ha(null,d,0,null);e.return=b;if(0===(b.mode&2))for(a=null!==b.memoizedState?b.child.child:b.child,e.child=a;null!==a;)a.return=e,a=a.sibling;c=Ha(g,d,c,null);c.return=b;e.sibling=c;b.memoizedState=Je;b.child=e;return c}d=e.children;b.memoizedState=null;return b.child=Fe(b,null,d,c)}if(null!==a.memoizedState){a=a.child;d=a.sibling;if(g){e=e.fallback;
c=Sa(a,a.pendingProps);c.return=b;if(0===(b.mode&2)&&(g=null!==b.memoizedState?b.child.child:b.child,g!==a.child))for(c.child=g;null!==g;)g.return=c,g=g.sibling;d=Sa(d,e);d.return=b;c.sibling=d;c.childExpirationTime=0;b.memoizedState=Je;b.child=c;return d}c=wb(b,a.child,e.children,c);b.memoizedState=null;return b.child=c}a=a.child;if(g){g=e.fallback;e=Ha(null,d,0,null);e.return=b;e.child=a;null!==a&&(a.return=e);if(0===(b.mode&2))for(a=null!==b.memoizedState?b.child.child:b.child,e.child=a;null!==
a;)a.return=e,a=a.sibling;c=Ha(g,d,c,null);c.return=b;e.sibling=c;c.effectTag|=2;e.childExpirationTime=0;b.memoizedState=Je;b.child=e;return c}b.memoizedState=null;return b.child=wb(b,a,e.children,c)}function uh(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);Sg(a.return,b)}function Ke(a,b,c,d,e,f){var g=a.memoizedState;null===g?a.memoizedState={isBackwards:b,rendering:null,renderingStartTime:0,last:d,tail:c,tailExpiration:0,tailMode:e,
lastEffect:f}:(g.isBackwards=b,g.rendering=null,g.renderingStartTime=0,g.last=d,g.tail=c,g.tailExpiration=0,g.tailMode=e,g.lastEffect=f)}function vh(a,b,c){var d=b.pendingProps,e=d.revealOrder,f=d.tail;T(a,b,d.children,c);d=D.current;if(0!==(d&2))d=d&1|2,b.effectTag|=64;else{if(null!==a&&0!==(a.effectTag&64))a:for(a=b.child;null!==a;){if(13===a.tag)null!==a.memoizedState&&uh(a,c);else if(19===a.tag)uh(a,c);else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===b)break a;for(;null===a.sibling;){if(null===
a.return||a.return===b)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}d&=1}y(D,d);if(0===(b.mode&2))b.memoizedState=null;else switch(e){case "forwards":c=b.child;for(e=null;null!==c;)a=c.alternate,null!==a&&null===Rc(a)&&(e=c),c=c.sibling;c=e;null===c?(e=b.child,b.child=null):(e=c.sibling,c.sibling=null);Ke(b,!1,e,c,f,b.lastEffect);break;case "backwards":c=null;e=b.child;for(b.child=null;null!==e;){a=e.alternate;if(null!==a&&null===Rc(a)){b.child=e;break}a=e.sibling;e.sibling=c;c=e;e=a}Ke(b,
!0,c,null,f,b.lastEffect);break;case "together":Ke(b,!1,null,null,void 0,b.lastEffect);break;default:b.memoizedState=null}return b.child}function sa(a,b,c){null!==a&&(b.dependencies=a.dependencies);var d=b.expirationTime;0!==d&&Kc(d);if(b.childExpirationTime<c)return null;if(null!==a&&b.child!==a.child)throw Error(k(153));if(null!==b.child){a=b.child;c=Sa(a,a.pendingProps);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=Sa(a,a.pendingProps),c.return=b;c.sibling=null}return b.child}
function $c(a,b){switch(a.tailMode){case "hidden":b=a.tail;for(var c=null;null!==b;)null!==b.alternate&&(c=b),b=b.sibling;null===c?a.tail=null:c.sibling=null;break;case "collapsed":c=a.tail;for(var d=null;null!==c;)null!==c.alternate&&(d=c),c=c.sibling;null===d?b||null===a.tail?a.tail=null:a.tail.sibling=null:d.sibling=null}}function hj(a,b,c){var d=b.pendingProps;switch(b.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return null;case 1:return N(b.type)&&(q(G),q(B)),
null;case 3:return tb(),q(G),q(B),c=b.stateNode,c.pendingContext&&(c.context=c.pendingContext,c.pendingContext=null),null!==a&&null!==a.child||!Zc(b)||(b.effectTag|=4),wh(b),null;case 5:te(b);c=Ta(Tb.current);var e=b.type;if(null!==a&&null!=b.stateNode)ij(a,b,e,d,c),a.ref!==b.ref&&(b.effectTag|=128);else{if(!d){if(null===b.stateNode)throw Error(k(166));return null}a=Ta(ja.current);if(Zc(b)){d=b.stateNode;e=b.type;var f=b.memoizedProps;d[Aa]=b;d[vc]=f;switch(e){case "iframe":case "object":case "embed":w("load",
d);break;case "video":case "audio":for(a=0;a<Db.length;a++)w(Db[a],d);break;case "source":w("error",d);break;case "img":case "image":case "link":w("error",d);w("load",d);break;case "form":w("reset",d);w("submit",d);break;case "details":w("toggle",d);break;case "input":Hf(d,f);w("invalid",d);oa(c,"onChange");break;case "select":d._wrapperState={wasMultiple:!!f.multiple};w("invalid",d);oa(c,"onChange");break;case "textarea":Kf(d,f),w("invalid",d),oa(c,"onChange")}Ud(e,f);a=null;for(var g in f)if(f.hasOwnProperty(g)){var h=
f[g];"children"===g?"string"===typeof h?d.textContent!==h&&(a=["children",h]):"number"===typeof h&&d.textContent!==""+h&&(a=["children",""+h]):db.hasOwnProperty(g)&&null!=h&&oa(c,g)}switch(e){case "input":mc(d);Jf(d,f,!0);break;case "textarea":mc(d);Mf(d);break;case "select":case "option":break;default:"function"===typeof f.onClick&&(d.onclick=uc)}c=a;b.updateQueue=c;null!==c&&(b.effectTag|=4)}else{g=9===c.nodeType?c:c.ownerDocument;"http://www.w3.org/1999/xhtml"===a&&(a=Nf(e));"http://www.w3.org/1999/xhtml"===
a?"script"===e?(a=g.createElement("div"),a.innerHTML="<script>\x3c/script>",a=a.removeChild(a.firstChild)):"string"===typeof d.is?a=g.createElement(e,{is:d.is}):(a=g.createElement(e),"select"===e&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,e);a[Aa]=b;a[vc]=d;jj(a,b,!1,!1);b.stateNode=a;g=Vd(e,d);switch(e){case "iframe":case "object":case "embed":w("load",a);h=d;break;case "video":case "audio":for(h=0;h<Db.length;h++)w(Db[h],a);h=d;break;case "source":w("error",a);
h=d;break;case "img":case "image":case "link":w("error",a);w("load",a);h=d;break;case "form":w("reset",a);w("submit",a);h=d;break;case "details":w("toggle",a);h=d;break;case "input":Hf(a,d);h=Cd(a,d);w("invalid",a);oa(c,"onChange");break;case "option":h=Fd(a,d);break;case "select":a._wrapperState={wasMultiple:!!d.multiple};h=M({},d,{value:void 0});w("invalid",a);oa(c,"onChange");break;case "textarea":Kf(a,d);h=Gd(a,d);w("invalid",a);oa(c,"onChange");break;default:h=d}Ud(e,h);var m=h;for(f in m)if(m.hasOwnProperty(f)){var n=
m[f];"style"===f?gg(a,n):"dangerouslySetInnerHTML"===f?(n=n?n.__html:void 0,null!=n&&xh(a,n)):"children"===f?"string"===typeof n?("textarea"!==e||""!==n)&&Wb(a,n):"number"===typeof n&&Wb(a,""+n):"suppressContentEditableWarning"!==f&&"suppressHydrationWarning"!==f&&"autoFocus"!==f&&(db.hasOwnProperty(f)?null!=n&&oa(c,f):null!=n&&xd(a,f,n,g))}switch(e){case "input":mc(a);Jf(a,d,!1);break;case "textarea":mc(a);Mf(a);break;case "option":null!=d.value&&a.setAttribute("value",""+va(d.value));break;case "select":a.multiple=
!!d.multiple;c=d.value;null!=c?hb(a,!!d.multiple,c,!1):null!=d.defaultValue&&hb(a,!!d.multiple,d.defaultValue,!0);break;default:"function"===typeof h.onClick&&(a.onclick=uc)}lg(e,d)&&(b.effectTag|=4)}null!==b.ref&&(b.effectTag|=128)}return null;case 6:if(a&&null!=b.stateNode)kj(a,b,a.memoizedProps,d);else{if("string"!==typeof d&&null===b.stateNode)throw Error(k(166));c=Ta(Tb.current);Ta(ja.current);Zc(b)?(c=b.stateNode,d=b.memoizedProps,c[Aa]=b,c.nodeValue!==d&&(b.effectTag|=4)):(c=(9===c.nodeType?
c:c.ownerDocument).createTextNode(d),c[Aa]=b,b.stateNode=c)}return null;case 13:q(D);d=b.memoizedState;if(0!==(b.effectTag&64))return b.expirationTime=c,b;c=null!==d;d=!1;null===a?void 0!==b.memoizedProps.fallback&&Zc(b):(e=a.memoizedState,d=null!==e,c||null===e||(e=a.child.sibling,null!==e&&(f=b.firstEffect,null!==f?(b.firstEffect=e,e.nextEffect=f):(b.firstEffect=b.lastEffect=e,e.nextEffect=null),e.effectTag=8)));if(c&&!d&&0!==(b.mode&2))if(null===a&&!0!==b.memoizedProps.unstable_avoidThisFallback||
0!==(D.current&1))F===Xa&&(F=ad);else{if(F===Xa||F===ad)F=bd;0!==Xb&&null!==U&&(Ya(U,P),yh(U,Xb))}if(c||d)b.effectTag|=4;return null;case 4:return tb(),wh(b),null;case 10:return me(b),null;case 17:return N(b.type)&&(q(G),q(B)),null;case 19:q(D);d=b.memoizedState;if(null===d)return null;e=0!==(b.effectTag&64);f=d.rendering;if(null===f)if(e)$c(d,!1);else{if(F!==Xa||null!==a&&0!==(a.effectTag&64))for(f=b.child;null!==f;){a=Rc(f);if(null!==a){b.effectTag|=64;$c(d,!1);e=a.updateQueue;null!==e&&(b.updateQueue=
e,b.effectTag|=4);null===d.lastEffect&&(b.firstEffect=null);b.lastEffect=d.lastEffect;for(d=b.child;null!==d;)e=d,f=c,e.effectTag&=2,e.nextEffect=null,e.firstEffect=null,e.lastEffect=null,a=e.alternate,null===a?(e.childExpirationTime=0,e.expirationTime=f,e.child=null,e.memoizedProps=null,e.memoizedState=null,e.updateQueue=null,e.dependencies=null):(e.childExpirationTime=a.childExpirationTime,e.expirationTime=a.expirationTime,e.child=a.child,e.memoizedProps=a.memoizedProps,e.memoizedState=a.memoizedState,
e.updateQueue=a.updateQueue,f=a.dependencies,e.dependencies=null===f?null:{expirationTime:f.expirationTime,firstContext:f.firstContext,responders:f.responders}),d=d.sibling;y(D,D.current&1|2);return b.child}f=f.sibling}}else{if(!e)if(a=Rc(f),null!==a){if(b.effectTag|=64,e=!0,c=a.updateQueue,null!==c&&(b.updateQueue=c,b.effectTag|=4),$c(d,!0),null===d.tail&&"hidden"===d.tailMode&&!f.alternate)return b=b.lastEffect=d.lastEffect,null!==b&&(b.nextEffect=null),null}else 2*Y()-d.renderingStartTime>d.tailExpiration&&
1<c&&(b.effectTag|=64,e=!0,$c(d,!1),b.expirationTime=b.childExpirationTime=c-1);d.isBackwards?(f.sibling=b.child,b.child=f):(c=d.last,null!==c?c.sibling=f:b.child=f,d.last=f)}return null!==d.tail?(0===d.tailExpiration&&(d.tailExpiration=Y()+500),c=d.tail,d.rendering=c,d.tail=c.sibling,d.lastEffect=b.lastEffect,d.renderingStartTime=Y(),c.sibling=null,b=D.current,y(D,e?b&1|2:b&1),c):null}throw Error(k(156,b.tag));}function lj(a,b){switch(a.tag){case 1:return N(a.type)&&(q(G),q(B)),b=a.effectTag,b&4096?
(a.effectTag=b&-4097|64,a):null;case 3:tb();q(G);q(B);b=a.effectTag;if(0!==(b&64))throw Error(k(285));a.effectTag=b&-4097|64;return a;case 5:return te(a),null;case 13:return q(D),b=a.effectTag,b&4096?(a.effectTag=b&-4097|64,a):null;case 19:return q(D),null;case 4:return tb(),null;case 10:return me(a),null;default:return null}}function Le(a,b){return{value:a,source:b,stack:Bd(b)}}function Me(a,b){var c=b.source,d=b.stack;null===d&&null!==c&&(d=Bd(c));null!==c&&na(c.type);b=b.value;null!==a&&1===a.tag&&
na(a.type);try{console.error(b)}catch(e){setTimeout(function(){throw e;})}}function mj(a,b){try{b.props=a.memoizedProps,b.state=a.memoizedState,b.componentWillUnmount()}catch(c){Za(a,c)}}function zh(a){var b=a.ref;if(null!==b)if("function"===typeof b)try{b(null)}catch(c){Za(a,c)}else b.current=null}function nj(a,b){switch(b.tag){case 0:case 11:case 15:case 22:return;case 1:if(b.effectTag&256&&null!==a){var c=a.memoizedProps,d=a.memoizedState;a=b.stateNode;b=a.getSnapshotBeforeUpdate(b.elementType===
b.type?c:aa(b.type,c),d);a.__reactInternalSnapshotBeforeUpdate=b}return;case 3:case 5:case 6:case 4:case 17:return}throw Error(k(163));}function Ah(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.destroy;c.destroy=void 0;void 0!==d&&d()}c=c.next}while(c!==b)}}function Bh(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.create;c.destroy=d()}c=c.next}while(c!==b)}}function oj(a,b,c,d){switch(c.tag){case 0:case 11:case 15:case 22:Bh(3,
c);return;case 1:a=c.stateNode;c.effectTag&4&&(null===b?a.componentDidMount():(d=c.elementType===c.type?b.memoizedProps:aa(c.type,b.memoizedProps),a.componentDidUpdate(d,b.memoizedState,a.__reactInternalSnapshotBeforeUpdate)));b=c.updateQueue;null!==b&&Wg(c,b,a);return;case 3:b=c.updateQueue;if(null!==b){a=null;if(null!==c.child)switch(c.child.tag){case 5:a=c.child.stateNode;break;case 1:a=c.child.stateNode}Wg(c,b,a)}return;case 5:a=c.stateNode;null===b&&c.effectTag&4&&lg(c.type,c.memoizedProps)&&
a.focus();return;case 6:return;case 4:return;case 12:return;case 13:null===c.memoizedState&&(c=c.alternate,null!==c&&(c=c.memoizedState,null!==c&&(c=c.dehydrated,null!==c&&bg(c))));return;case 19:case 17:case 20:case 21:return}throw Error(k(163));}function Ch(a,b,c){"function"===typeof Ne&&Ne(b);switch(b.tag){case 0:case 11:case 14:case 15:case 22:a=b.updateQueue;if(null!==a&&(a=a.lastEffect,null!==a)){var d=a.next;Da(97<c?97:c,function(){var a=d;do{var c=a.destroy;if(void 0!==c){var g=b;try{c()}catch(h){Za(g,
h)}}a=a.next}while(a!==d)})}break;case 1:zh(b);c=b.stateNode;"function"===typeof c.componentWillUnmount&&mj(b,c);break;case 5:zh(b);break;case 4:Dh(a,b,c)}}function Eh(a){var b=a.alternate;a.return=null;a.child=null;a.memoizedState=null;a.updateQueue=null;a.dependencies=null;a.alternate=null;a.firstEffect=null;a.lastEffect=null;a.pendingProps=null;a.memoizedProps=null;a.stateNode=null;null!==b&&Eh(b)}function Fh(a){return 5===a.tag||3===a.tag||4===a.tag}function Gh(a){a:{for(var b=a.return;null!==
b;){if(Fh(b)){var c=b;break a}b=b.return}throw Error(k(160));}b=c.stateNode;switch(c.tag){case 5:var d=!1;break;case 3:b=b.containerInfo;d=!0;break;case 4:b=b.containerInfo;d=!0;break;default:throw Error(k(161));}c.effectTag&16&&(Wb(b,""),c.effectTag&=-17);a:b:for(c=a;;){for(;null===c.sibling;){if(null===c.return||Fh(c.return)){c=null;break a}c=c.return}c.sibling.return=c.return;for(c=c.sibling;5!==c.tag&&6!==c.tag&&18!==c.tag;){if(c.effectTag&2)continue b;if(null===c.child||4===c.tag)continue b;
else c.child.return=c,c=c.child}if(!(c.effectTag&2)){c=c.stateNode;break a}}d?Oe(a,c,b):Pe(a,c,b)}function Oe(a,b,c){var d=a.tag,e=5===d||6===d;if(e)a=e?a.stateNode:a.stateNode.instance,b?8===c.nodeType?c.parentNode.insertBefore(a,b):c.insertBefore(a,b):(8===c.nodeType?(b=c.parentNode,b.insertBefore(a,c)):(b=c,b.appendChild(a)),c=c._reactRootContainer,null!==c&&void 0!==c||null!==b.onclick||(b.onclick=uc));else if(4!==d&&(a=a.child,null!==a))for(Oe(a,b,c),a=a.sibling;null!==a;)Oe(a,b,c),a=a.sibling}
function Pe(a,b,c){var d=a.tag,e=5===d||6===d;if(e)a=e?a.stateNode:a.stateNode.instance,b?c.insertBefore(a,b):c.appendChild(a);else if(4!==d&&(a=a.child,null!==a))for(Pe(a,b,c),a=a.sibling;null!==a;)Pe(a,b,c),a=a.sibling}function Dh(a,b,c){for(var d=b,e=!1,f,g;;){if(!e){e=d.return;a:for(;;){if(null===e)throw Error(k(160));f=e.stateNode;switch(e.tag){case 5:g=!1;break a;case 3:f=f.containerInfo;g=!0;break a;case 4:f=f.containerInfo;g=!0;break a}e=e.return}e=!0}if(5===d.tag||6===d.tag){a:for(var h=
a,m=d,n=c,l=m;;)if(Ch(h,l,n),null!==l.child&&4!==l.tag)l.child.return=l,l=l.child;else{if(l===m)break a;for(;null===l.sibling;){if(null===l.return||l.return===m)break a;l=l.return}l.sibling.return=l.return;l=l.sibling}g?(h=f,m=d.stateNode,8===h.nodeType?h.parentNode.removeChild(m):h.removeChild(m)):f.removeChild(d.stateNode)}else if(4===d.tag){if(null!==d.child){f=d.stateNode.containerInfo;g=!0;d.child.return=d;d=d.child;continue}}else if(Ch(a,d,c),null!==d.child){d.child.return=d;d=d.child;continue}if(d===
b)break;for(;null===d.sibling;){if(null===d.return||d.return===b)return;d=d.return;4===d.tag&&(e=!1)}d.sibling.return=d.return;d=d.sibling}}function Qe(a,b){switch(b.tag){case 0:case 11:case 14:case 15:case 22:Ah(3,b);return;case 1:return;case 5:var c=b.stateNode;if(null!=c){var d=b.memoizedProps,e=null!==a?a.memoizedProps:d;a=b.type;var f=b.updateQueue;b.updateQueue=null;if(null!==f){c[vc]=d;"input"===a&&"radio"===d.type&&null!=d.name&&If(c,d);Vd(a,e);b=Vd(a,d);for(e=0;e<f.length;e+=2){var g=f[e],
h=f[e+1];"style"===g?gg(c,h):"dangerouslySetInnerHTML"===g?xh(c,h):"children"===g?Wb(c,h):xd(c,g,h,b)}switch(a){case "input":Dd(c,d);break;case "textarea":Lf(c,d);break;case "select":b=c._wrapperState.wasMultiple,c._wrapperState.wasMultiple=!!d.multiple,a=d.value,null!=a?hb(c,!!d.multiple,a,!1):b!==!!d.multiple&&(null!=d.defaultValue?hb(c,!!d.multiple,d.defaultValue,!0):hb(c,!!d.multiple,d.multiple?[]:"",!1))}}}return;case 6:if(null===b.stateNode)throw Error(k(162));b.stateNode.nodeValue=b.memoizedProps;
return;case 3:b=b.stateNode;b.hydrate&&(b.hydrate=!1,bg(b.containerInfo));return;case 12:return;case 13:c=b;null===b.memoizedState?d=!1:(d=!0,c=b.child,Re=Y());if(null!==c)a:for(a=c;;){if(5===a.tag)f=a.stateNode,d?(f=f.style,"function"===typeof f.setProperty?f.setProperty("display","none","important"):f.display="none"):(f=a.stateNode,e=a.memoizedProps.style,e=void 0!==e&&null!==e&&e.hasOwnProperty("display")?e.display:null,f.style.display=fg("display",e));else if(6===a.tag)a.stateNode.nodeValue=d?
"":a.memoizedProps;else if(13===a.tag&&null!==a.memoizedState&&null===a.memoizedState.dehydrated){f=a.child.sibling;f.return=a;a=f;continue}else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===c)break;for(;null===a.sibling;){if(null===a.return||a.return===c)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}Hh(b);return;case 19:Hh(b);return;case 17:return}throw Error(k(163));}function Hh(a){var b=a.updateQueue;if(null!==b){a.updateQueue=null;var c=a.stateNode;null===c&&(c=a.stateNode=
new pj);b.forEach(function(b){var d=qj.bind(null,a,b);c.has(b)||(c.add(b),b.then(d,d))})}}function Ih(a,b,c){c=Ea(c,null);c.tag=3;c.payload={element:null};var d=b.value;c.callback=function(){cd||(cd=!0,Se=d);Me(a,b)};return c}function Jh(a,b,c){c=Ea(c,null);c.tag=3;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){Me(a,b);return d(e)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){"function"!==typeof d&&
(null===La?La=new Set([this]):La.add(this),Me(a,b));var c=b.stack;this.componentDidCatch(b.value,{componentStack:null!==c?c:""})});return c}function ka(){return(p&(ca|ma))!==H?1073741821-(Y()/10|0):0!==dd?dd:dd=1073741821-(Y()/10|0)}function Va(a,b,c){b=b.mode;if(0===(b&2))return 1073741823;var d=Cc();if(0===(b&4))return 99===d?1073741823:1073741822;if((p&ca)!==H)return P;if(null!==c)a=Fc(a,c.timeoutMs|0||5E3,250);else switch(d){case 99:a=1073741823;break;case 98:a=Fc(a,150,100);break;case 97:case 96:a=
Fc(a,5E3,250);break;case 95:a=2;break;default:throw Error(k(326));}null!==U&&a===P&&--a;return a}function ed(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);var d=a.return,e=null;if(null===d&&3===a.tag)e=a.stateNode;else for(;null!==d;){c=d.alternate;d.childExpirationTime<b&&(d.childExpirationTime=b);null!==c&&c.childExpirationTime<b&&(c.childExpirationTime=b);if(null===d.return&&3===d.tag){e=d.stateNode;break}d=d.return}null!==e&&
(U===e&&(Kc(b),F===bd&&Ya(e,P)),yh(e,b));return e}function fd(a){var b=a.lastExpiredTime;if(0!==b)return b;b=a.firstPendingTime;if(!Kh(a,b))return b;var c=a.lastPingedTime;a=a.nextKnownPendingLevel;a=c>a?c:a;return 2>=a&&b!==a?0:a}function V(a){if(0!==a.lastExpiredTime)a.callbackExpirationTime=1073741823,a.callbackPriority=99,a.callbackNode=Og(Te.bind(null,a));else{var b=fd(a),c=a.callbackNode;if(0===b)null!==c&&(a.callbackNode=null,a.callbackExpirationTime=0,a.callbackPriority=90);else{var d=ka();
1073741823===b?d=99:1===b||2===b?d=95:(d=10*(1073741821-b)-10*(1073741821-d),d=0>=d?99:250>=d?98:5250>=d?97:95);if(null!==c){var e=a.callbackPriority;if(a.callbackExpirationTime===b&&e>=d)return;c!==Qg&&Rg(c)}a.callbackExpirationTime=b;a.callbackPriority=d;b=1073741823===b?Og(Te.bind(null,a)):Ng(d,Lh.bind(null,a),{timeout:10*(1073741821-b)-Y()});a.callbackNode=b}}}function Lh(a,b){dd=0;if(b)return b=ka(),Ue(a,b),V(a),null;var c=fd(a);if(0!==c){b=a.callbackNode;if((p&(ca|ma))!==H)throw Error(k(327));
xb();a===U&&c===P||$a(a,c);if(null!==t){var d=p;p|=ca;var e=Mh();do try{rj();break}catch(h){Nh(a,h)}while(1);le();p=d;gd.current=e;if(F===hd)throw b=id,$a(a,c),Ya(a,c),V(a),b;if(null===t)switch(e=a.finishedWork=a.current.alternate,a.finishedExpirationTime=c,d=F,U=null,d){case Xa:case hd:throw Error(k(345));case Oh:Ue(a,2<c?2:c);break;case ad:Ya(a,c);d=a.lastSuspendedTime;c===d&&(a.nextKnownPendingLevel=Ve(e));if(1073741823===ta&&(e=Re+Ph-Y(),10<e)){if(jd){var f=a.lastPingedTime;if(0===f||f>=c){a.lastPingedTime=
c;$a(a,c);break}}f=fd(a);if(0!==f&&f!==c)break;if(0!==d&&d!==c){a.lastPingedTime=d;break}a.timeoutHandle=We(ab.bind(null,a),e);break}ab(a);break;case bd:Ya(a,c);d=a.lastSuspendedTime;c===d&&(a.nextKnownPendingLevel=Ve(e));if(jd&&(e=a.lastPingedTime,0===e||e>=c)){a.lastPingedTime=c;$a(a,c);break}e=fd(a);if(0!==e&&e!==c)break;if(0!==d&&d!==c){a.lastPingedTime=d;break}1073741823!==Yb?d=10*(1073741821-Yb)-Y():1073741823===ta?d=0:(d=10*(1073741821-ta)-5E3,e=Y(),c=10*(1073741821-c)-e,d=e-d,0>d&&(d=0),d=
(120>d?120:480>d?480:1080>d?1080:1920>d?1920:3E3>d?3E3:4320>d?4320:1960*sj(d/1960))-d,c<d&&(d=c));if(10<d){a.timeoutHandle=We(ab.bind(null,a),d);break}ab(a);break;case Xe:if(1073741823!==ta&&null!==kd){f=ta;var g=kd;d=g.busyMinDurationMs|0;0>=d?d=0:(e=g.busyDelayMs|0,f=Y()-(10*(1073741821-f)-(g.timeoutMs|0||5E3)),d=f<=e?0:e+d-f);if(10<d){Ya(a,c);a.timeoutHandle=We(ab.bind(null,a),d);break}}ab(a);break;default:throw Error(k(329));}V(a);if(a.callbackNode===b)return Lh.bind(null,a)}}return null}function Te(a){var b=
a.lastExpiredTime;b=0!==b?b:1073741823;if((p&(ca|ma))!==H)throw Error(k(327));xb();a===U&&b===P||$a(a,b);if(null!==t){var c=p;p|=ca;var d=Mh();do try{tj();break}catch(e){Nh(a,e)}while(1);le();p=c;gd.current=d;if(F===hd)throw c=id,$a(a,b),Ya(a,b),V(a),c;if(null!==t)throw Error(k(261));a.finishedWork=a.current.alternate;a.finishedExpirationTime=b;U=null;ab(a);V(a)}return null}function uj(){if(null!==bb){var a=bb;bb=null;a.forEach(function(a,c){Ue(c,a);V(c)});ha()}}function Qh(a,b){var c=p;p|=1;try{return a(b)}finally{p=
c,p===H&&ha()}}function Rh(a,b){var c=p;p&=-2;p|=Ye;try{return a(b)}finally{p=c,p===H&&ha()}}function $a(a,b){a.finishedWork=null;a.finishedExpirationTime=0;var c=a.timeoutHandle;-1!==c&&(a.timeoutHandle=-1,vj(c));if(null!==t)for(c=t.return;null!==c;){var d=c;switch(d.tag){case 1:d=d.type.childContextTypes;null!==d&&void 0!==d&&(q(G),q(B));break;case 3:tb();q(G);q(B);break;case 5:te(d);break;case 4:tb();break;case 13:q(D);break;case 19:q(D);break;case 10:me(d)}c=c.return}U=a;t=Sa(a.current,null);
P=b;F=Xa;id=null;Yb=ta=1073741823;kd=null;Xb=0;jd=!1}function Nh(a,b){do{try{le();Sc.current=Tc;if(Uc)for(var c=z.memoizedState;null!==c;){var d=c.queue;null!==d&&(d.pending=null);c=c.next}Ia=0;J=K=z=null;Uc=!1;if(null===t||null===t.return)return F=hd,id=b,t=null;a:{var e=a,f=t.return,g=t,h=b;b=P;g.effectTag|=2048;g.firstEffect=g.lastEffect=null;if(null!==h&&"object"===typeof h&&"function"===typeof h.then){var m=h;if(0===(g.mode&2)){var n=g.alternate;n?(g.updateQueue=n.updateQueue,g.memoizedState=
n.memoizedState,g.expirationTime=n.expirationTime):(g.updateQueue=null,g.memoizedState=null)}var l=0!==(D.current&1),k=f;do{var p;if(p=13===k.tag){var q=k.memoizedState;if(null!==q)p=null!==q.dehydrated?!0:!1;else{var w=k.memoizedProps;p=void 0===w.fallback?!1:!0!==w.unstable_avoidThisFallback?!0:l?!1:!0}}if(p){var y=k.updateQueue;if(null===y){var r=new Set;r.add(m);k.updateQueue=r}else y.add(m);if(0===(k.mode&2)){k.effectTag|=64;g.effectTag&=-2981;if(1===g.tag)if(null===g.alternate)g.tag=17;else{var O=
Ea(1073741823,null);O.tag=Jc;Fa(g,O)}g.expirationTime=1073741823;break a}h=void 0;g=b;var v=e.pingCache;null===v?(v=e.pingCache=new wj,h=new Set,v.set(m,h)):(h=v.get(m),void 0===h&&(h=new Set,v.set(m,h)));if(!h.has(g)){h.add(g);var x=xj.bind(null,e,m,g);m.then(x,x)}k.effectTag|=4096;k.expirationTime=b;break a}k=k.return}while(null!==k);h=Error((na(g.type)||"A React component")+" suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display."+
Bd(g))}F!==Xe&&(F=Oh);h=Le(h,g);k=f;do{switch(k.tag){case 3:m=h;k.effectTag|=4096;k.expirationTime=b;var A=Ih(k,m,b);Ug(k,A);break a;case 1:m=h;var u=k.type,B=k.stateNode;if(0===(k.effectTag&64)&&("function"===typeof u.getDerivedStateFromError||null!==B&&"function"===typeof B.componentDidCatch&&(null===La||!La.has(B)))){k.effectTag|=4096;k.expirationTime=b;var H=Jh(k,m,b);Ug(k,H);break a}}k=k.return}while(null!==k)}t=Sh(t)}catch(cj){b=cj;continue}break}while(1)}function Mh(a){a=gd.current;gd.current=
Tc;return null===a?Tc:a}function Vg(a,b){a<ta&&2<a&&(ta=a);null!==b&&a<Yb&&2<a&&(Yb=a,kd=b)}function Kc(a){a>Xb&&(Xb=a)}function tj(){for(;null!==t;)t=Th(t)}function rj(){for(;null!==t&&!yj();)t=Th(t)}function Th(a){var b=zj(a.alternate,a,P);a.memoizedProps=a.pendingProps;null===b&&(b=Sh(a));Uh.current=null;return b}function Sh(a){t=a;do{var b=t.alternate;a=t.return;if(0===(t.effectTag&2048)){b=hj(b,t,P);if(1===P||1!==t.childExpirationTime){for(var c=0,d=t.child;null!==d;){var e=d.expirationTime,
f=d.childExpirationTime;e>c&&(c=e);f>c&&(c=f);d=d.sibling}t.childExpirationTime=c}if(null!==b)return b;null!==a&&0===(a.effectTag&2048)&&(null===a.firstEffect&&(a.firstEffect=t.firstEffect),null!==t.lastEffect&&(null!==a.lastEffect&&(a.lastEffect.nextEffect=t.firstEffect),a.lastEffect=t.lastEffect),1<t.effectTag&&(null!==a.lastEffect?a.lastEffect.nextEffect=t:a.firstEffect=t,a.lastEffect=t))}else{b=lj(t);if(null!==b)return b.effectTag&=2047,b;null!==a&&(a.firstEffect=a.lastEffect=null,a.effectTag|=
2048)}b=t.sibling;if(null!==b)return b;t=a}while(null!==t);F===Xa&&(F=Xe);return null}function Ve(a){var b=a.expirationTime;a=a.childExpirationTime;return b>a?b:a}function ab(a){var b=Cc();Da(99,Aj.bind(null,a,b));return null}function Aj(a,b){do xb();while(null!==Zb);if((p&(ca|ma))!==H)throw Error(k(327));var c=a.finishedWork,d=a.finishedExpirationTime;if(null===c)return null;a.finishedWork=null;a.finishedExpirationTime=0;if(c===a.current)throw Error(k(177));a.callbackNode=null;a.callbackExpirationTime=
0;a.callbackPriority=90;a.nextKnownPendingLevel=0;var e=Ve(c);a.firstPendingTime=e;d<=a.lastSuspendedTime?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=0:d<=a.firstSuspendedTime&&(a.firstSuspendedTime=d-1);d<=a.lastPingedTime&&(a.lastPingedTime=0);d<=a.lastExpiredTime&&(a.lastExpiredTime=0);a===U&&(t=U=null,P=0);1<c.effectTag?null!==c.lastEffect?(c.lastEffect.nextEffect=c,e=c.firstEffect):e=c:e=c.firstEffect;if(null!==e){var f=p;p|=ma;Uh.current=null;Ze=tc;var g=kg();if(Xd(g)){if("selectionStart"in
g)var h={start:g.selectionStart,end:g.selectionEnd};else a:{h=(h=g.ownerDocument)&&h.defaultView||window;var m=h.getSelection&&h.getSelection();if(m&&0!==m.rangeCount){h=m.anchorNode;var n=m.anchorOffset,q=m.focusNode;m=m.focusOffset;try{h.nodeType,q.nodeType}catch(sb){h=null;break a}var ba=0,w=-1,y=-1,B=0,D=0,r=g,z=null;b:for(;;){for(var v;;){r!==h||0!==n&&3!==r.nodeType||(w=ba+n);r!==q||0!==m&&3!==r.nodeType||(y=ba+m);3===r.nodeType&&(ba+=r.nodeValue.length);if(null===(v=r.firstChild))break;z=r;
r=v}for(;;){if(r===g)break b;z===h&&++B===n&&(w=ba);z===q&&++D===m&&(y=ba);if(null!==(v=r.nextSibling))break;r=z;z=r.parentNode}r=v}h=-1===w||-1===y?null:{start:w,end:y}}else h=null}h=h||{start:0,end:0}}else h=null;$e={activeElementDetached:null,focusedElem:g,selectionRange:h};tc=!1;l=e;do try{Bj()}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);l=e;do try{for(g=a,h=b;null!==l;){var x=l.effectTag;x&16&&Wb(l.stateNode,"");if(x&128){var A=l.alternate;if(null!==A){var u=
A.ref;null!==u&&("function"===typeof u?u(null):u.current=null)}}switch(x&1038){case 2:Gh(l);l.effectTag&=-3;break;case 6:Gh(l);l.effectTag&=-3;Qe(l.alternate,l);break;case 1024:l.effectTag&=-1025;break;case 1028:l.effectTag&=-1025;Qe(l.alternate,l);break;case 4:Qe(l.alternate,l);break;case 8:n=l,Dh(g,n,h),Eh(n)}l=l.nextEffect}}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);u=$e;A=kg();x=u.focusedElem;h=u.selectionRange;if(A!==x&&x&&x.ownerDocument&&jg(x.ownerDocument.documentElement,
x)){null!==h&&Xd(x)&&(A=h.start,u=h.end,void 0===u&&(u=A),"selectionStart"in x?(x.selectionStart=A,x.selectionEnd=Math.min(u,x.value.length)):(u=(A=x.ownerDocument||document)&&A.defaultView||window,u.getSelection&&(u=u.getSelection(),n=x.textContent.length,g=Math.min(h.start,n),h=void 0===h.end?g:Math.min(h.end,n),!u.extend&&g>h&&(n=h,h=g,g=n),n=ig(x,g),q=ig(x,h),n&&q&&(1!==u.rangeCount||u.anchorNode!==n.node||u.anchorOffset!==n.offset||u.focusNode!==q.node||u.focusOffset!==q.offset)&&(A=A.createRange(),
A.setStart(n.node,n.offset),u.removeAllRanges(),g>h?(u.addRange(A),u.extend(q.node,q.offset)):(A.setEnd(q.node,q.offset),u.addRange(A))))));A=[];for(u=x;u=u.parentNode;)1===u.nodeType&&A.push({element:u,left:u.scrollLeft,top:u.scrollTop});"function"===typeof x.focus&&x.focus();for(x=0;x<A.length;x++)u=A[x],u.element.scrollLeft=u.left,u.element.scrollTop=u.top}tc=!!Ze;$e=Ze=null;a.current=c;l=e;do try{for(x=a;null!==l;){var F=l.effectTag;F&36&&oj(x,l.alternate,l);if(F&128){A=void 0;var E=l.ref;if(null!==
E){var G=l.stateNode;switch(l.tag){case 5:A=G;break;default:A=G}"function"===typeof E?E(A):E.current=A}}l=l.nextEffect}}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);l=null;Cj();p=f}else a.current=c;if(ld)ld=!1,Zb=a,$b=b;else for(l=e;null!==l;)b=l.nextEffect,l.nextEffect=null,l=b;b=a.firstPendingTime;0===b&&(La=null);1073741823===b?a===af?ac++:(ac=0,af=a):ac=0;"function"===typeof bf&&bf(c.stateNode,d);V(a);if(cd)throw cd=!1,a=Se,Se=null,a;if((p&Ye)!==H)return null;
ha();return null}function Bj(){for(;null!==l;){var a=l.effectTag;0!==(a&256)&&nj(l.alternate,l);0===(a&512)||ld||(ld=!0,Ng(97,function(){xb();return null}));l=l.nextEffect}}function xb(){if(90!==$b){var a=97<$b?97:$b;$b=90;return Da(a,Dj)}}function Dj(){if(null===Zb)return!1;var a=Zb;Zb=null;if((p&(ca|ma))!==H)throw Error(k(331));var b=p;p|=ma;for(a=a.current.firstEffect;null!==a;){try{var c=a;if(0!==(c.effectTag&512))switch(c.tag){case 0:case 11:case 15:case 22:Ah(5,c),Bh(5,c)}}catch(d){if(null===
a)throw Error(k(330));Za(a,d)}c=a.nextEffect;a.nextEffect=null;a=c}p=b;ha();return!0}function Vh(a,b,c){b=Le(c,b);b=Ih(a,b,1073741823);Fa(a,b);a=ed(a,1073741823);null!==a&&V(a)}function Za(a,b){if(3===a.tag)Vh(a,a,b);else for(var c=a.return;null!==c;){if(3===c.tag){Vh(c,a,b);break}else if(1===c.tag){var d=c.stateNode;if("function"===typeof c.type.getDerivedStateFromError||"function"===typeof d.componentDidCatch&&(null===La||!La.has(d))){a=Le(b,a);a=Jh(c,a,1073741823);Fa(c,a);c=ed(c,1073741823);null!==
c&&V(c);break}}c=c.return}}function xj(a,b,c){var d=a.pingCache;null!==d&&d.delete(b);U===a&&P===c?F===bd||F===ad&&1073741823===ta&&Y()-Re<Ph?$a(a,P):jd=!0:Kh(a,c)&&(b=a.lastPingedTime,0!==b&&b<c||(a.lastPingedTime=c,V(a)))}function qj(a,b){var c=a.stateNode;null!==c&&c.delete(b);b=0;0===b&&(b=ka(),b=Va(b,a,null));a=ed(a,b);null!==a&&V(a)}function Ej(a){if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)return!1;var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)return!0;try{var c=
b.inject(a);bf=function(a,e){try{b.onCommitFiberRoot(c,a,void 0,64===(a.current.effectTag&64))}catch(f){}};Ne=function(a){try{b.onCommitFiberUnmount(c,a)}catch(e){}}}catch(d){}return!0}function Fj(a,b,c,d){this.tag=a;this.key=c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.effectTag=0;this.lastEffect=this.firstEffect=this.nextEffect=
null;this.childExpirationTime=this.expirationTime=0;this.alternate=null}function Ge(a){a=a.prototype;return!(!a||!a.isReactComponent)}function Gj(a){if("function"===typeof a)return Ge(a)?1:0;if(void 0!==a&&null!==a){a=a.$$typeof;if(a===zd)return 11;if(a===Ad)return 14}return 2}function Sa(a,b){var c=a.alternate;null===c?(c=la(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.effectTag=0,c.nextEffect=null,c.firstEffect=
null,c.lastEffect=null);c.childExpirationTime=a.childExpirationTime;c.expirationTime=a.expirationTime;c.child=a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;b=a.dependencies;c.dependencies=null===b?null:{expirationTime:b.expirationTime,firstContext:b.firstContext,responders:b.responders};c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function Oc(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)Ge(a)&&(g=1);else if("string"===typeof a)g=
5;else a:switch(a){case Ma:return Ha(c.children,e,f,b);case Hj:g=8;e|=7;break;case Af:g=8;e|=1;break;case kc:return a=la(12,c,b,e|8),a.elementType=kc,a.type=kc,a.expirationTime=f,a;case lc:return a=la(13,c,b,e),a.type=lc,a.elementType=lc,a.expirationTime=f,a;case yd:return a=la(19,c,b,e),a.elementType=yd,a.expirationTime=f,a;default:if("object"===typeof a&&null!==a)switch(a.$$typeof){case Cf:g=10;break a;case Bf:g=9;break a;case zd:g=11;break a;case Ad:g=14;break a;case Ef:g=16;d=null;break a;case Df:g=
22;break a}throw Error(k(130,null==a?a:typeof a,""));}b=la(g,c,b,e);b.elementType=a;b.type=d;b.expirationTime=f;return b}function Ha(a,b,c,d){a=la(7,a,d,b);a.expirationTime=c;return a}function qe(a,b,c){a=la(6,a,null,b);a.expirationTime=c;return a}function re(a,b,c){b=la(4,null!==a.children?a.children:[],a.key,b);b.expirationTime=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Ij(a,b,c){this.tag=b;this.current=null;this.containerInfo=
a;this.pingCache=this.pendingChildren=null;this.finishedExpirationTime=0;this.finishedWork=null;this.timeoutHandle=-1;this.pendingContext=this.context=null;this.hydrate=c;this.callbackNode=null;this.callbackPriority=90;this.lastExpiredTime=this.lastPingedTime=this.nextKnownPendingLevel=this.lastSuspendedTime=this.firstSuspendedTime=this.firstPendingTime=0}function Kh(a,b){var c=a.firstSuspendedTime;a=a.lastSuspendedTime;return 0!==c&&c>=b&&a<=b}function Ya(a,b){var c=a.firstSuspendedTime,d=a.lastSuspendedTime;
c<b&&(a.firstSuspendedTime=b);if(d>b||0===c)a.lastSuspendedTime=b;b<=a.lastPingedTime&&(a.lastPingedTime=0);b<=a.lastExpiredTime&&(a.lastExpiredTime=0)}function yh(a,b){b>a.firstPendingTime&&(a.firstPendingTime=b);var c=a.firstSuspendedTime;0!==c&&(b>=c?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=0:b>=a.lastSuspendedTime&&(a.lastSuspendedTime=b+1),b>a.nextKnownPendingLevel&&(a.nextKnownPendingLevel=b))}function Ue(a,b){var c=a.lastExpiredTime;if(0===c||c>b)a.lastExpiredTime=b}
function md(a,b,c,d){var e=b.current,f=ka(),g=Vb.suspense;f=Va(f,e,g);a:if(c){c=c._reactInternalFiber;b:{if(Na(c)!==c||1!==c.tag)throw Error(k(170));var h=c;do{switch(h.tag){case 3:h=h.stateNode.context;break b;case 1:if(N(h.type)){h=h.stateNode.__reactInternalMemoizedMergedChildContext;break b}}h=h.return}while(null!==h);throw Error(k(171));}if(1===c.tag){var m=c.type;if(N(m)){c=Gg(c,m,h);break a}}c=h}else c=Ca;null===b.context?b.context=c:b.pendingContext=c;b=Ea(f,g);b.payload={element:a};d=void 0===
d?null:d;null!==d&&(b.callback=d);Fa(e,b);Ja(e,f);return f}function cf(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function Wh(a,b){a=a.memoizedState;null!==a&&null!==a.dehydrated&&a.retryTime<b&&(a.retryTime=b)}function df(a,b){Wh(a,b);(a=a.alternate)&&Wh(a,b)}function ef(a,b,c){c=null!=c&&!0===c.hydrate;var d=new Ij(a,b,c),e=la(3,null,null,2===b?7:1===b?3:0);d.current=e;e.stateNode=d;ne(e);a[Lb]=d.current;c&&0!==b&&
xi(a,9===a.nodeType?a:a.ownerDocument);this._internalRoot=d}function bc(a){return!(!a||1!==a.nodeType&&9!==a.nodeType&&11!==a.nodeType&&(8!==a.nodeType||" react-mount-point-unstable "!==a.nodeValue))}function Jj(a,b){b||(b=a?9===a.nodeType?a.documentElement:a.firstChild:null,b=!(!b||1!==b.nodeType||!b.hasAttribute("data-reactroot")));if(!b)for(var c;c=a.lastChild;)a.removeChild(c);return new ef(a,0,b?{hydrate:!0}:void 0)}function nd(a,b,c,d,e){var f=c._reactRootContainer;if(f){var g=f._internalRoot;
if("function"===typeof e){var h=e;e=function(){var a=cf(g);h.call(a)}}md(b,g,a,e)}else{f=c._reactRootContainer=Jj(c,d);g=f._internalRoot;if("function"===typeof e){var m=e;e=function(){var a=cf(g);m.call(a)}}Rh(function(){md(b,g,a,e)})}return cf(g)}function Kj(a,b,c){var d=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:gb,key:null==d?null:""+d,children:a,containerInfo:b,implementation:c}}function Xh(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;
if(!bc(b))throw Error(k(200));return Kj(a,b,null,c)}if(!ea)throw Error(k(227));var ki=function(a,b,c,d,e,f,g,h,m){var n=Array.prototype.slice.call(arguments,3);try{b.apply(c,n)}catch(C){this.onError(C)}},yb=!1,gc=null,hc=!1,pd=null,li={onError:function(a){yb=!0;gc=a}},td=null,rf=null,mf=null,ic=null,cb={},jc=[],qd={},db={},rd={},wa=!("undefined"===typeof window||"undefined"===typeof window.document||"undefined"===typeof window.document.createElement),M=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.assign,
sd=null,eb=null,fb=null,ee=function(a,b){return a(b)},eg=function(a,b,c,d,e){return a(b,c,d,e)},vd=function(){},vf=ee,Oa=!1,wd=!1,Z=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler,Lj=Z.unstable_cancelCallback,ff=Z.unstable_now,$f=Z.unstable_scheduleCallback,Mj=Z.unstable_shouldYield,Yh=Z.unstable_requestPaint,Pd=Z.unstable_runWithPriority,Nj=Z.unstable_getCurrentPriorityLevel,Oj=Z.unstable_ImmediatePriority,Zh=Z.unstable_UserBlockingPriority,ag=Z.unstable_NormalPriority,Pj=Z.unstable_LowPriority,
Qj=Z.unstable_IdlePriority,oi=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,wf=Object.prototype.hasOwnProperty,yf={},xf={},E={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){E[a]=
new L(a,0,!1,a,null,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var b=a[0];E[b]=new L(b,1,!1,a[1],null,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(a){E[a]=new L(a,2,!1,a.toLowerCase(),null,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){E[a]=new L(a,2,!1,a,null,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){E[a]=
new L(a,3,!1,a.toLowerCase(),null,!1)});["checked","multiple","muted","selected"].forEach(function(a){E[a]=new L(a,3,!0,a,null,!1)});["capture","download"].forEach(function(a){E[a]=new L(a,4,!1,a,null,!1)});["cols","rows","size","span"].forEach(function(a){E[a]=new L(a,6,!1,a,null,!1)});["rowSpan","start"].forEach(function(a){E[a]=new L(a,5,!1,a.toLowerCase(),null,!1)});var gf=/[\-:]([a-z])/g,hf=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var b=
a.replace(gf,hf);E[b]=new L(b,1,!1,a,null,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var b=a.replace(gf,hf);E[b]=new L(b,1,!1,a,"http://www.w3.org/1999/xlink",!1)});["xml:base","xml:lang","xml:space"].forEach(function(a){var b=a.replace(gf,hf);E[b]=new L(b,1,!1,a,"http://www.w3.org/XML/1998/namespace",!1)});["tabIndex","crossOrigin"].forEach(function(a){E[a]=new L(a,1,!1,a.toLowerCase(),null,!1)});E.xlinkHref=new L("xlinkHref",1,
!1,"xlink:href","http://www.w3.org/1999/xlink",!0);["src","href","action","formAction"].forEach(function(a){E[a]=new L(a,1,!1,a.toLowerCase(),null,!0)});var da=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;da.hasOwnProperty("ReactCurrentDispatcher")||(da.ReactCurrentDispatcher={current:null});da.hasOwnProperty("ReactCurrentBatchConfig")||(da.ReactCurrentBatchConfig={suspense:null});var si=/^(.*)[\\\/]/,Q="function"===typeof Symbol&&Symbol.for,Pc=Q?Symbol.for("react.element"):60103,gb=Q?Symbol.for("react.portal"):
60106,Ma=Q?Symbol.for("react.fragment"):60107,Af=Q?Symbol.for("react.strict_mode"):60108,kc=Q?Symbol.for("react.profiler"):60114,Cf=Q?Symbol.for("react.provider"):60109,Bf=Q?Symbol.for("react.context"):60110,Hj=Q?Symbol.for("react.concurrent_mode"):60111,zd=Q?Symbol.for("react.forward_ref"):60112,lc=Q?Symbol.for("react.suspense"):60113,yd=Q?Symbol.for("react.suspense_list"):60120,Ad=Q?Symbol.for("react.memo"):60115,Ef=Q?Symbol.for("react.lazy"):60116,Df=Q?Symbol.for("react.block"):60121,zf="function"===
typeof Symbol&&Symbol.iterator,od,xh=function(a){return"undefined"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,c,d,e)})}:a}(function(a,b){if("http://www.w3.org/2000/svg"!==a.namespaceURI||"innerHTML"in a)a.innerHTML=b;else{od=od||document.createElement("div");od.innerHTML="<svg>"+b.valueOf().toString()+"</svg>";for(b=od.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}}),Wb=function(a,
b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b},ib={animationend:nc("Animation","AnimationEnd"),animationiteration:nc("Animation","AnimationIteration"),animationstart:nc("Animation","AnimationStart"),transitionend:nc("Transition","TransitionEnd")},Id={},Of={};wa&&(Of=document.createElement("div").style,"AnimationEvent"in window||(delete ib.animationend.animation,delete ib.animationiteration.animation,delete ib.animationstart.animation),"TransitionEvent"in
window||delete ib.transitionend.transition);var $h=oc("animationend"),ai=oc("animationiteration"),bi=oc("animationstart"),ci=oc("transitionend"),Db="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),Pf=new ("function"===typeof WeakMap?WeakMap:Map),Ab=null,wi=function(a){if(a){var b=a._dispatchListeners,c=a._dispatchInstances;
if(Array.isArray(b))for(var d=0;d<b.length&&!a.isPropagationStopped();d++)lf(a,b[d],c[d]);else b&&lf(a,b,c);a._dispatchListeners=null;a._dispatchInstances=null;a.isPersistent()||a.constructor.release(a)}},qc=[],Rd=!1,fa=[],xa=null,ya=null,za=null,Eb=new Map,Fb=new Map,Jb=[],Nd="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput close cancel copy cut paste click change contextmenu reset submit".split(" "),
yi="focus blur dragenter dragleave mouseover mouseout pointerover pointerout gotpointercapture lostpointercapture".split(" "),dg={},cg=new Map,Td=new Map,Rj=["abort","abort",$h,"animationEnd",ai,"animationIteration",bi,"animationStart","canplay","canPlay","canplaythrough","canPlayThrough","durationchange","durationChange","emptied","emptied","encrypted","encrypted","ended","ended","error","error","gotpointercapture","gotPointerCapture","load","load","loadeddata","loadedData","loadedmetadata","loadedMetadata",
"loadstart","loadStart","lostpointercapture","lostPointerCapture","playing","playing","progress","progress","seeking","seeking","stalled","stalled","suspend","suspend","timeupdate","timeUpdate",ci,"transitionEnd","waiting","waiting"];Sd("blur blur cancel cancel click click close close contextmenu contextMenu copy copy cut cut auxclick auxClick dblclick doubleClick dragend dragEnd dragstart dragStart drop drop focus focus input input invalid invalid keydown keyDown keypress keyPress keyup keyUp mousedown mouseDown mouseup mouseUp paste paste pause pause play play pointercancel pointerCancel pointerdown pointerDown pointerup pointerUp ratechange rateChange reset reset seeked seeked submit submit touchcancel touchCancel touchend touchEnd touchstart touchStart volumechange volumeChange".split(" "),
0);Sd("drag drag dragenter dragEnter dragexit dragExit dragleave dragLeave dragover dragOver mousemove mouseMove mouseout mouseOut mouseover mouseOver pointermove pointerMove pointerout pointerOut pointerover pointerOver scroll scroll toggle toggle touchmove touchMove wheel wheel".split(" "),1);Sd(Rj,2);(function(a,b){for(var c=0;c<a.length;c++)Td.set(a[c],b)})("change selectionchange textInput compositionstart compositionend compositionupdate".split(" "),0);var Hi=Zh,Gi=Pd,tc=!0,Kb={animationIterationCount:!0,
borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,
strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Sj=["Webkit","ms","Moz","O"];Object.keys(Kb).forEach(function(a){Sj.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);Kb[b]=Kb[a]})});var Ii=M({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),ng="$",og="/$",$d="$?",Zd="$!",Ze=null,$e=null,We="function"===typeof setTimeout?setTimeout:void 0,vj="function"===
typeof clearTimeout?clearTimeout:void 0,jf=Math.random().toString(36).slice(2),Aa="__reactInternalInstance$"+jf,vc="__reactEventHandlers$"+jf,Lb="__reactContainere$"+jf,Ba=null,ce=null,wc=null;M(R.prototype,{preventDefault:function(){this.defaultPrevented=!0;var a=this.nativeEvent;a&&(a.preventDefault?a.preventDefault():"unknown"!==typeof a.returnValue&&(a.returnValue=!1),this.isDefaultPrevented=xc)},stopPropagation:function(){var a=this.nativeEvent;a&&(a.stopPropagation?a.stopPropagation():"unknown"!==
typeof a.cancelBubble&&(a.cancelBubble=!0),this.isPropagationStopped=xc)},persist:function(){this.isPersistent=xc},isPersistent:yc,destructor:function(){var a=this.constructor.Interface,b;for(b in a)this[b]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null;this.isPropagationStopped=this.isDefaultPrevented=yc;this._dispatchInstances=this._dispatchListeners=null}});R.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(a){return a.timeStamp||
Date.now()},defaultPrevented:null,isTrusted:null};R.extend=function(a){function b(){return c.apply(this,arguments)}var c=this,d=function(){};d.prototype=c.prototype;d=new d;M(d,b.prototype);b.prototype=d;b.prototype.constructor=b;b.Interface=M({},c.Interface,a);b.extend=c.extend;sg(b);return b};sg(R);var Tj=R.extend({data:null}),Uj=R.extend({data:null}),Ni=[9,13,27,32],de=wa&&"CompositionEvent"in window,cc=null;wa&&"documentMode"in document&&(cc=document.documentMode);var Vj=wa&&"TextEvent"in window&&
!cc,xg=wa&&(!de||cc&&8<cc&&11>=cc),wg=String.fromCharCode(32),ua={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},
dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},vg=!1,mb=!1,Wj={eventTypes:ua,extractEvents:function(a,b,c,d,e){var f;if(de)b:{switch(a){case "compositionstart":var g=ua.compositionStart;break b;case "compositionend":g=ua.compositionEnd;break b;case "compositionupdate":g=
ua.compositionUpdate;break b}g=void 0}else mb?tg(a,c)&&(g=ua.compositionEnd):"keydown"===a&&229===c.keyCode&&(g=ua.compositionStart);g?(xg&&"ko"!==c.locale&&(mb||g!==ua.compositionStart?g===ua.compositionEnd&&mb&&(f=rg()):(Ba=d,ce="value"in Ba?Ba.value:Ba.textContent,mb=!0)),e=Tj.getPooled(g,b,c,d),f?e.data=f:(f=ug(c),null!==f&&(e.data=f)),lb(e),f=e):f=null;(a=Vj?Oi(a,c):Pi(a,c))?(b=Uj.getPooled(ua.beforeInput,b,c,d),b.data=a,lb(b)):b=null;return null===f?b:null===b?f:[f,b]}},Qi={color:!0,date:!0,
datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},Ag={change:{phasedRegistrationNames:{bubbled:"onChange",captured:"onChangeCapture"},dependencies:"blur change click focus input keydown keyup selectionchange".split(" ")}},Mb=null,Nb=null,kf=!1;wa&&(kf=Tf("input")&&(!document.documentMode||9<document.documentMode));var Xj={eventTypes:Ag,_isInputEventSupported:kf,extractEvents:function(a,b,c,d,e){e=b?Pa(b):window;var f=
e.nodeName&&e.nodeName.toLowerCase();if("select"===f||"input"===f&&"file"===e.type)var g=Si;else if(yg(e))if(kf)g=Wi;else{g=Ui;var h=Ti}else(f=e.nodeName)&&"input"===f.toLowerCase()&&("checkbox"===e.type||"radio"===e.type)&&(g=Vi);if(g&&(g=g(a,b)))return zg(g,c,d);h&&h(a,e,b);"blur"===a&&(a=e._wrapperState)&&a.controlled&&"number"===e.type&&Ed(e,"number",e.value)}},dc=R.extend({view:null,detail:null}),Yi={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"},di=0,ei=0,fi=!1,gi=!1,ec=dc.extend({screenX:null,
screenY:null,clientX:null,clientY:null,pageX:null,pageY:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,getModifierState:fe,button:null,buttons:null,relatedTarget:function(a){return a.relatedTarget||(a.fromElement===a.srcElement?a.toElement:a.fromElement)},movementX:function(a){if("movementX"in a)return a.movementX;var b=di;di=a.screenX;return fi?"mousemove"===a.type?a.screenX-b:0:(fi=!0,0)},movementY:function(a){if("movementY"in a)return a.movementY;var b=ei;ei=a.screenY;return gi?"mousemove"===
a.type?a.screenY-b:0:(gi=!0,0)}}),hi=ec.extend({pointerId:null,width:null,height:null,pressure:null,tangentialPressure:null,tiltX:null,tiltY:null,twist:null,pointerType:null,isPrimary:null}),fc={mouseEnter:{registrationName:"onMouseEnter",dependencies:["mouseout","mouseover"]},mouseLeave:{registrationName:"onMouseLeave",dependencies:["mouseout","mouseover"]},pointerEnter:{registrationName:"onPointerEnter",dependencies:["pointerout","pointerover"]},pointerLeave:{registrationName:"onPointerLeave",dependencies:["pointerout",
"pointerover"]}},Yj={eventTypes:fc,extractEvents:function(a,b,c,d,e){var f="mouseover"===a||"pointerover"===a,g="mouseout"===a||"pointerout"===a;if(f&&0===(e&32)&&(c.relatedTarget||c.fromElement)||!g&&!f)return null;f=d.window===d?d:(f=d.ownerDocument)?f.defaultView||f.parentWindow:window;if(g){if(g=b,b=(b=c.relatedTarget||c.toElement)?Bb(b):null,null!==b){var h=Na(b);if(b!==h||5!==b.tag&&6!==b.tag)b=null}}else g=null;if(g===b)return null;if("mouseout"===a||"mouseover"===a){var m=ec;var n=fc.mouseLeave;
var l=fc.mouseEnter;var k="mouse"}else if("pointerout"===a||"pointerover"===a)m=hi,n=fc.pointerLeave,l=fc.pointerEnter,k="pointer";a=null==g?f:Pa(g);f=null==b?f:Pa(b);n=m.getPooled(n,g,c,d);n.type=k+"leave";n.target=a;n.relatedTarget=f;c=m.getPooled(l,b,c,d);c.type=k+"enter";c.target=f;c.relatedTarget=a;d=g;k=b;if(d&&k)a:{m=d;l=k;g=0;for(a=m;a;a=pa(a))g++;a=0;for(b=l;b;b=pa(b))a++;for(;0<g-a;)m=pa(m),g--;for(;0<a-g;)l=pa(l),a--;for(;g--;){if(m===l||m===l.alternate)break a;m=pa(m);l=pa(l)}m=null}else m=
null;l=m;for(m=[];d&&d!==l;){g=d.alternate;if(null!==g&&g===l)break;m.push(d);d=pa(d)}for(d=[];k&&k!==l;){g=k.alternate;if(null!==g&&g===l)break;d.push(k);k=pa(k)}for(k=0;k<m.length;k++)be(m[k],"bubbled",n);for(k=d.length;0<k--;)be(d[k],"captured",c);return 0===(e&64)?[n]:[n,c]}},Qa="function"===typeof Object.is?Object.is:Zi,$i=Object.prototype.hasOwnProperty,Zj=wa&&"documentMode"in document&&11>=document.documentMode,Eg={select:{phasedRegistrationNames:{bubbled:"onSelect",captured:"onSelectCapture"},
dependencies:"blur contextmenu dragend focus keydown keyup mousedown mouseup selectionchange".split(" ")}},nb=null,he=null,Pb=null,ge=!1,ak={eventTypes:Eg,extractEvents:function(a,b,c,d,e,f){e=f||(d.window===d?d.document:9===d.nodeType?d:d.ownerDocument);if(!(f=!e)){a:{e=Jd(e);f=rd.onSelect;for(var g=0;g<f.length;g++)if(!e.has(f[g])){e=!1;break a}e=!0}f=!e}if(f)return null;e=b?Pa(b):window;switch(a){case "focus":if(yg(e)||"true"===e.contentEditable)nb=e,he=b,Pb=null;break;case "blur":Pb=he=nb=null;
break;case "mousedown":ge=!0;break;case "contextmenu":case "mouseup":case "dragend":return ge=!1,Dg(c,d);case "selectionchange":if(Zj)break;case "keydown":case "keyup":return Dg(c,d)}return null}},bk=R.extend({animationName:null,elapsedTime:null,pseudoElement:null}),ck=R.extend({clipboardData:function(a){return"clipboardData"in a?a.clipboardData:window.clipboardData}}),dk=dc.extend({relatedTarget:null}),ek={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",
Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},fk={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",
224:"Meta"},gk=dc.extend({key:function(a){if(a.key){var b=ek[a.key]||a.key;if("Unidentified"!==b)return b}return"keypress"===a.type?(a=Ac(a),13===a?"Enter":String.fromCharCode(a)):"keydown"===a.type||"keyup"===a.type?fk[a.keyCode]||"Unidentified":""},location:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,repeat:null,locale:null,getModifierState:fe,charCode:function(a){return"keypress"===a.type?Ac(a):0},keyCode:function(a){return"keydown"===a.type||"keyup"===a.type?a.keyCode:0},which:function(a){return"keypress"===
a.type?Ac(a):"keydown"===a.type||"keyup"===a.type?a.keyCode:0}}),hk=ec.extend({dataTransfer:null}),ik=dc.extend({touches:null,targetTouches:null,changedTouches:null,altKey:null,metaKey:null,ctrlKey:null,shiftKey:null,getModifierState:fe}),jk=R.extend({propertyName:null,elapsedTime:null,pseudoElement:null}),kk=ec.extend({deltaX:function(a){return"deltaX"in a?a.deltaX:"wheelDeltaX"in a?-a.wheelDeltaX:0},deltaY:function(a){return"deltaY"in a?a.deltaY:"wheelDeltaY"in a?-a.wheelDeltaY:"wheelDelta"in a?
-a.wheelDelta:0},deltaZ:null,deltaMode:null}),lk={eventTypes:dg,extractEvents:function(a,b,c,d,e){e=cg.get(a);if(!e)return null;switch(a){case "keypress":if(0===Ac(c))return null;case "keydown":case "keyup":a=gk;break;case "blur":case "focus":a=dk;break;case "click":if(2===c.button)return null;case "auxclick":case "dblclick":case "mousedown":case "mousemove":case "mouseup":case "mouseout":case "mouseover":case "contextmenu":a=ec;break;case "drag":case "dragend":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "dragstart":case "drop":a=
hk;break;case "touchcancel":case "touchend":case "touchmove":case "touchstart":a=ik;break;case $h:case ai:case bi:a=bk;break;case ci:a=jk;break;case "scroll":a=dc;break;case "wheel":a=kk;break;case "copy":case "cut":case "paste":a=ck;break;case "gotpointercapture":case "lostpointercapture":case "pointercancel":case "pointerdown":case "pointermove":case "pointerout":case "pointerover":case "pointerup":a=hi;break;default:a=R}b=a.getPooled(e,b,c,d);lb(b);return b}};(function(a){if(ic)throw Error(k(101));
ic=Array.prototype.slice.call(a);nf()})("ResponderEventPlugin SimpleEventPlugin EnterLeaveEventPlugin ChangeEventPlugin SelectEventPlugin BeforeInputEventPlugin".split(" "));(function(a,b,c){td=a;rf=b;mf=c})(ae,Hb,Pa);pf({SimpleEventPlugin:lk,EnterLeaveEventPlugin:Yj,ChangeEventPlugin:Xj,SelectEventPlugin:ak,BeforeInputEventPlugin:Wj});var ie=[],ob=-1,Ca={},B={current:Ca},G={current:!1},Ra=Ca,bj=Pd,je=$f,Rg=Lj,aj=Nj,Dc=Oj,Ig=Zh,Jg=ag,Kg=Pj,Lg=Qj,Qg={},yj=Mj,Cj=void 0!==Yh?Yh:function(){},qa=null,
Ec=null,ke=!1,ii=ff(),Y=1E4>ii?ff:function(){return ff()-ii},Ic={current:null},Hc=null,qb=null,Gc=null,Tg=0,Jc=2,Ga=!1,Vb=da.ReactCurrentBatchConfig,$g=(new ea.Component).refs,Mc={isMounted:function(a){return(a=a._reactInternalFiber)?Na(a)===a:!1},enqueueSetState:function(a,b,c){a=a._reactInternalFiber;var d=ka(),e=Vb.suspense;d=Va(d,a,e);e=Ea(d,e);e.payload=b;void 0!==c&&null!==c&&(e.callback=c);Fa(a,e);Ja(a,d)},enqueueReplaceState:function(a,b,c){a=a._reactInternalFiber;var d=ka(),e=Vb.suspense;
d=Va(d,a,e);e=Ea(d,e);e.tag=1;e.payload=b;void 0!==c&&null!==c&&(e.callback=c);Fa(a,e);Ja(a,d)},enqueueForceUpdate:function(a,b){a=a._reactInternalFiber;var c=ka(),d=Vb.suspense;c=Va(c,a,d);d=Ea(c,d);d.tag=Jc;void 0!==b&&null!==b&&(d.callback=b);Fa(a,d);Ja(a,c)}},Qc=Array.isArray,wb=ah(!0),Fe=ah(!1),Sb={},ja={current:Sb},Ub={current:Sb},Tb={current:Sb},D={current:0},Sc=da.ReactCurrentDispatcher,X=da.ReactCurrentBatchConfig,Ia=0,z=null,K=null,J=null,Uc=!1,Tc={readContext:W,useCallback:S,useContext:S,
useEffect:S,useImperativeHandle:S,useLayoutEffect:S,useMemo:S,useReducer:S,useRef:S,useState:S,useDebugValue:S,useResponder:S,useDeferredValue:S,useTransition:S},dj={readContext:W,useCallback:ih,useContext:W,useEffect:eh,useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return ze(4,2,gh.bind(null,b,a),c)},useLayoutEffect:function(a,b){return ze(4,2,a,b)},useMemo:function(a,b){var c=ub();b=void 0===b?null:b;a=a();c.memoizedState=[a,b];return a},useReducer:function(a,b,c){var d=
ub();b=void 0!==c?c(b):b;d.memoizedState=d.baseState=b;a=d.queue={pending:null,dispatch:null,lastRenderedReducer:a,lastRenderedState:b};a=a.dispatch=ch.bind(null,z,a);return[d.memoizedState,a]},useRef:function(a){var b=ub();a={current:a};return b.memoizedState=a},useState:xe,useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=xe(a),d=c[0],e=c[1];eh(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=
xe(!1),c=b[0];b=b[1];return[ih(Ce.bind(null,b,a),[b,a]),c]}},ej={readContext:W,useCallback:Yc,useContext:W,useEffect:Xc,useImperativeHandle:hh,useLayoutEffect:fh,useMemo:jh,useReducer:Vc,useRef:dh,useState:function(a){return Vc(Ua)},useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=Vc(Ua),d=c[0],e=c[1];Xc(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=Vc(Ua),c=b[0];b=b[1];return[Yc(Ce.bind(null,
b,a),[b,a]),c]}},fj={readContext:W,useCallback:Yc,useContext:W,useEffect:Xc,useImperativeHandle:hh,useLayoutEffect:fh,useMemo:jh,useReducer:Wc,useRef:dh,useState:function(a){return Wc(Ua)},useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=Wc(Ua),d=c[0],e=c[1];Xc(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=Wc(Ua),c=b[0];b=b[1];return[Yc(Ce.bind(null,b,a),[b,a]),c]}},ra=null,Ka=null,Wa=
!1,gj=da.ReactCurrentOwner,ia=!1,Je={dehydrated:null,retryTime:0};var jj=function(a,b,c,d){for(c=b.child;null!==c;){if(5===c.tag||6===c.tag)a.appendChild(c.stateNode);else if(4!==c.tag&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return}c.sibling.return=c.return;c=c.sibling}};var wh=function(a){};var ij=function(a,b,c,d,e){var f=a.memoizedProps;if(f!==d){var g=b.stateNode;Ta(ja.current);a=null;switch(c){case "input":f=
Cd(g,f);d=Cd(g,d);a=[];break;case "option":f=Fd(g,f);d=Fd(g,d);a=[];break;case "select":f=M({},f,{value:void 0});d=M({},d,{value:void 0});a=[];break;case "textarea":f=Gd(g,f);d=Gd(g,d);a=[];break;default:"function"!==typeof f.onClick&&"function"===typeof d.onClick&&(g.onclick=uc)}Ud(c,d);var h,m;c=null;for(h in f)if(!d.hasOwnProperty(h)&&f.hasOwnProperty(h)&&null!=f[h])if("style"===h)for(m in g=f[h],g)g.hasOwnProperty(m)&&(c||(c={}),c[m]="");else"dangerouslySetInnerHTML"!==h&&"children"!==h&&"suppressContentEditableWarning"!==
h&&"suppressHydrationWarning"!==h&&"autoFocus"!==h&&(db.hasOwnProperty(h)?a||(a=[]):(a=a||[]).push(h,null));for(h in d){var k=d[h];g=null!=f?f[h]:void 0;if(d.hasOwnProperty(h)&&k!==g&&(null!=k||null!=g))if("style"===h)if(g){for(m in g)!g.hasOwnProperty(m)||k&&k.hasOwnProperty(m)||(c||(c={}),c[m]="");for(m in k)k.hasOwnProperty(m)&&g[m]!==k[m]&&(c||(c={}),c[m]=k[m])}else c||(a||(a=[]),a.push(h,c)),c=k;else"dangerouslySetInnerHTML"===h?(k=k?k.__html:void 0,g=g?g.__html:void 0,null!=k&&g!==k&&(a=a||
[]).push(h,k)):"children"===h?g===k||"string"!==typeof k&&"number"!==typeof k||(a=a||[]).push(h,""+k):"suppressContentEditableWarning"!==h&&"suppressHydrationWarning"!==h&&(db.hasOwnProperty(h)?(null!=k&&oa(e,h),a||g===k||(a=[])):(a=a||[]).push(h,k))}c&&(a=a||[]).push("style",c);e=a;if(b.updateQueue=e)b.effectTag|=4}};var kj=function(a,b,c,d){c!==d&&(b.effectTag|=4)};var pj="function"===typeof WeakSet?WeakSet:Set,wj="function"===typeof WeakMap?WeakMap:Map,sj=Math.ceil,gd=da.ReactCurrentDispatcher,
Uh=da.ReactCurrentOwner,H=0,Ye=8,ca=16,ma=32,Xa=0,hd=1,Oh=2,ad=3,bd=4,Xe=5,p=H,U=null,t=null,P=0,F=Xa,id=null,ta=1073741823,Yb=1073741823,kd=null,Xb=0,jd=!1,Re=0,Ph=500,l=null,cd=!1,Se=null,La=null,ld=!1,Zb=null,$b=90,bb=null,ac=0,af=null,dd=0,Ja=function(a,b){if(50<ac)throw ac=0,af=null,Error(k(185));a=ed(a,b);if(null!==a){var c=Cc();1073741823===b?(p&Ye)!==H&&(p&(ca|ma))===H?Te(a):(V(a),p===H&&ha()):V(a);(p&4)===H||98!==c&&99!==c||(null===bb?bb=new Map([[a,b]]):(c=bb.get(a),(void 0===c||c>b)&&bb.set(a,
b)))}};var zj=function(a,b,c){var d=b.expirationTime;if(null!==a){var e=b.pendingProps;if(a.memoizedProps!==e||G.current)ia=!0;else{if(d<c){ia=!1;switch(b.tag){case 3:sh(b);Ee();break;case 5:bh(b);if(b.mode&4&&1!==c&&e.hidden)return b.expirationTime=b.childExpirationTime=1,null;break;case 1:N(b.type)&&Bc(b);break;case 4:se(b,b.stateNode.containerInfo);break;case 10:d=b.memoizedProps.value;e=b.type._context;y(Ic,e._currentValue);e._currentValue=d;break;case 13:if(null!==b.memoizedState){d=b.child.childExpirationTime;
if(0!==d&&d>=c)return th(a,b,c);y(D,D.current&1);b=sa(a,b,c);return null!==b?b.sibling:null}y(D,D.current&1);break;case 19:d=b.childExpirationTime>=c;if(0!==(a.effectTag&64)){if(d)return vh(a,b,c);b.effectTag|=64}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null);y(D,D.current);if(!d)return null}return sa(a,b,c)}ia=!1}}else ia=!1;b.expirationTime=0;switch(b.tag){case 2:d=b.type;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;e=pb(b,B.current);rb(b,c);e=we(null,
b,d,a,e,c);b.effectTag|=1;if("object"===typeof e&&null!==e&&"function"===typeof e.render&&void 0===e.$$typeof){b.tag=1;b.memoizedState=null;b.updateQueue=null;if(N(d)){var f=!0;Bc(b)}else f=!1;b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null;ne(b);var g=d.getDerivedStateFromProps;"function"===typeof g&&Lc(b,d,g,a);e.updater=Mc;b.stateNode=e;e._reactInternalFiber=b;pe(b,d,a,c);b=Ie(null,b,d,!0,f,c)}else b.tag=0,T(null,b,e,c),b=b.child;return b;case 16:a:{e=b.elementType;null!==a&&(a.alternate=
null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;ri(e);if(1!==e._status)throw e._result;e=e._result;b.type=e;f=b.tag=Gj(e);a=aa(e,a);switch(f){case 0:b=He(null,b,e,a,c);break a;case 1:b=rh(null,b,e,a,c);break a;case 11:b=nh(null,b,e,a,c);break a;case 14:b=oh(null,b,e,aa(e.type,a),d,c);break a}throw Error(k(306,e,""));}return b;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),He(a,b,d,e,c);case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),rh(a,b,d,e,c);
case 3:sh(b);d=b.updateQueue;if(null===a||null===d)throw Error(k(282));d=b.pendingProps;e=b.memoizedState;e=null!==e?e.element:null;oe(a,b);Qb(b,d,null,c);d=b.memoizedState.element;if(d===e)Ee(),b=sa(a,b,c);else{if(e=b.stateNode.hydrate)Ka=kb(b.stateNode.containerInfo.firstChild),ra=b,e=Wa=!0;if(e)for(c=Fe(b,null,d,c),b.child=c;c;)c.effectTag=c.effectTag&-3|1024,c=c.sibling;else T(a,b,d,c),Ee();b=b.child}return b;case 5:return bh(b),null===a&&De(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps:
null,g=e.children,Yd(d,e)?g=null:null!==f&&Yd(d,f)&&(b.effectTag|=16),qh(a,b),b.mode&4&&1!==c&&e.hidden?(b.expirationTime=b.childExpirationTime=1,b=null):(T(a,b,g,c),b=b.child),b;case 6:return null===a&&De(b),null;case 13:return th(a,b,c);case 4:return se(b,b.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=wb(b,null,d,c):T(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),nh(a,b,d,e,c);case 7:return T(a,b,b.pendingProps,c),b.child;case 8:return T(a,
b,b.pendingProps.children,c),b.child;case 12:return T(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;g=b.memoizedProps;f=e.value;var h=b.type._context;y(Ic,h._currentValue);h._currentValue=f;if(null!==g)if(h=g.value,f=Qa(h,f)?0:("function"===typeof d._calculateChangedBits?d._calculateChangedBits(h,f):1073741823)|0,0===f){if(g.children===e.children&&!G.current){b=sa(a,b,c);break a}}else for(h=b.child,null!==h&&(h.return=b);null!==h;){var m=h.dependencies;if(null!==
m){g=h.child;for(var l=m.firstContext;null!==l;){if(l.context===d&&0!==(l.observedBits&f)){1===h.tag&&(l=Ea(c,null),l.tag=Jc,Fa(h,l));h.expirationTime<c&&(h.expirationTime=c);l=h.alternate;null!==l&&l.expirationTime<c&&(l.expirationTime=c);Sg(h.return,c);m.expirationTime<c&&(m.expirationTime=c);break}l=l.next}}else g=10===h.tag?h.type===b.type?null:h.child:h.child;if(null!==g)g.return=h;else for(g=h;null!==g;){if(g===b){g=null;break}h=g.sibling;if(null!==h){h.return=g.return;g=h;break}g=g.return}h=
g}T(a,b,e.children,c);b=b.child}return b;case 9:return e=b.type,f=b.pendingProps,d=f.children,rb(b,c),e=W(e,f.unstable_observedBits),d=d(e),b.effectTag|=1,T(a,b,d,c),b.child;case 14:return e=b.type,f=aa(e,b.pendingProps),f=aa(e.type,f),oh(a,b,e,f,d,c);case 15:return ph(a,b,b.type,b.pendingProps,d,c);case 17:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),b.tag=1,N(d)?(a=!0,Bc(b)):a=!1,rb(b,c),Yg(b,d,e),pe(b,d,e,c),Ie(null,
b,d,!0,a,c);case 19:return vh(a,b,c)}throw Error(k(156,b.tag));};var bf=null,Ne=null,la=function(a,b,c,d){return new Fj(a,b,c,d)};ef.prototype.render=function(a){md(a,this._internalRoot,null,null)};ef.prototype.unmount=function(){var a=this._internalRoot,b=a.containerInfo;md(null,a,null,function(){b[Lb]=null})};var Di=function(a){if(13===a.tag){var b=Fc(ka(),150,100);Ja(a,b);df(a,b)}};var Yf=function(a){13===a.tag&&(Ja(a,3),df(a,3))};var Bi=function(a){if(13===a.tag){var b=ka();b=Va(b,a,null);Ja(a,
b);df(a,b)}};sd=function(a,b,c){switch(b){case "input":Dd(a,c);b=c.name;if("radio"===c.type&&null!=b){for(c=a;c.parentNode;)c=c.parentNode;c=c.querySelectorAll("input[name="+JSON.stringify(""+b)+'][type="radio"]');for(b=0;b<c.length;b++){var d=c[b];if(d!==a&&d.form===a.form){var e=ae(d);if(!e)throw Error(k(90));Gf(d);Dd(d,e)}}}break;case "textarea":Lf(a,c);break;case "select":b=c.value,null!=b&&hb(a,!!c.multiple,b,!1)}};(function(a,b,c,d){ee=a;eg=b;vd=c;vf=d})(Qh,function(a,b,c,d,e){var f=p;p|=4;
try{return Da(98,a.bind(null,b,c,d,e))}finally{p=f,p===H&&ha()}},function(){(p&(1|ca|ma))===H&&(uj(),xb())},function(a,b){var c=p;p|=2;try{return a(b)}finally{p=c,p===H&&ha()}});var mk={Events:[Hb,Pa,ae,pf,qd,lb,function(a){Kd(a,Ki)},sf,tf,sc,pc,xb,{current:!1}]};(function(a){var b=a.findFiberByHostInstance;return Ej(M({},a,{overrideHookState:null,overrideProps:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:da.ReactCurrentDispatcher,findHostInstanceByFiber:function(a){a=Sf(a);
return null===a?null:a.stateNode},findFiberByHostInstance:function(a){return b?b(a):null},findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null}))})({findFiberByHostInstance:Bb,bundleType:0,version:"16.13.1",rendererPackageName:"react-dom"});I.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=mk;I.createPortal=Xh;I.findDOMNode=function(a){if(null==a)return null;if(1===a.nodeType)return a;var b=a._reactInternalFiber;if(void 0===
b){if("function"===typeof a.render)throw Error(k(188));throw Error(k(268,Object.keys(a)));}a=Sf(b);a=null===a?null:a.stateNode;return a};I.flushSync=function(a,b){if((p&(ca|ma))!==H)throw Error(k(187));var c=p;p|=1;try{return Da(99,a.bind(null,b))}finally{p=c,ha()}};I.hydrate=function(a,b,c){if(!bc(b))throw Error(k(200));return nd(null,a,b,!0,c)};I.render=function(a,b,c){if(!bc(b))throw Error(k(200));return nd(null,a,b,!1,c)};I.unmountComponentAtNode=function(a){if(!bc(a))throw Error(k(40));return a._reactRootContainer?
(Rh(function(){nd(null,null,a,!1,function(){a._reactRootContainer=null;a[Lb]=null})}),!0):!1};I.unstable_batchedUpdates=Qh;I.unstable_createPortal=function(a,b){return Xh(a,b,2<arguments.length&&void 0!==arguments[2]?arguments[2]:null)};I.unstable_renderSubtreeIntoContainer=function(a,b,c,d){if(!bc(c))throw Error(k(200));if(null==a||void 0===a._reactInternalFiber)throw Error(k(38));return nd(a,b,c,!1,d)};I.version="16.13.1"});
</script>
    <script>const e = React.createElement;

function pathToString(path) {
  if (path[0] === '/') {
    return '/' + path.slice(1).join('/');
  } else {
    return path.join('/');
  }
}

function findCommonPath(files) {
  if (!files || !files.length) {
    return [];
  }

  function isPrefix(arr, prefix) {
    if (arr.length < prefix.length) {
      return false;
    }
    for (let i = prefix.length - 1; i >= 0; --i) {
      if (arr[i] !== prefix[i]) {
        return false;
      }
    }
    return true;
  }

  let commonPath = files[0].path.slice(0, -1);
  while (commonPath.length) {
    if (files.every(file => isPrefix(file.path, commonPath))) {
      break;
    }
    commonPath.pop();
  }
  return commonPath;
}

function findFolders(files) {
  if (!files || !files.length) {
    return [];
  }

  let folders = files.filter(file => file.path.length > 1).map(file => file.path[0]);
  folders = [...new Set(folders)]; // unique
  folders.sort();

  folders = folders.map(folder => {
    let filesInFolder = files
      .filter(file => file.path[0] === folder)
      .map(file => ({
        ...file,
        path: file.path.slice(1),
        parent: [...file.parent, file.path[0]],
      }));

    const children = findFolders(filesInFolder); // recursion

    return {
      is_folder: true,
      path: [folder],
      parent: files[0].parent,
      children,
      covered: children.reduce((sum, file) => sum + file.covered, 0),
      coverable: children.reduce((sum, file) => sum + file.coverable, 0),
      prevRun: {
        covered: children.reduce((sum, file) => sum + file.prevRun.covered, 0),
        coverable: children.reduce((sum, file) => sum + file.prevRun.coverable, 0),
      }
    };
  });

  return [
    ...folders,
    ...files.filter(file => file.path.length === 1),
  ];
}

class App extends React.Component {
  constructor(...args) {
    super(...args);

    this.state = {
      current: [],
    };
  }

  componentDidMount() {
    this.updateStateFromLocation();
    window.addEventListener("hashchange", () => this.updateStateFromLocation(), false);
  }

  updateStateFromLocation() {
    if (window.location.hash.length > 1) {
      const current = window.location.hash.substr(1).split('/');
      this.setState({current});
    } else {
      this.setState({current: []});
    }
  }

  getCurrentPath() {
    let file = this.props.root;
    let path = [file];
    for (let p of this.state.current) {
      file = file.children.find(file => file.path[0] === p);
      if (!file) {
        return path;
      }
      path.push(file);
    }
    return path;
  }

  render() {
    const path = this.getCurrentPath();
    const file = path[path.length - 1];

    let w = null;
    if (file.is_folder) {
      w = e(FilesList, {
        folder: file,
        onSelectFile: this.selectFile.bind(this),
        onBack: path.length > 1 ? this.back.bind(this) : null,
      });
    } else {
      w = e(DisplayFile, {
        file,
        onBack: this.back.bind(this),
      });
    }

    return e('div', {className: 'app'}, w);
  }

  selectFile(file) {
    this.setState(({current}) => {
      return {current: [...current, file.path[0]]};
    }, () => this.updateHash());
  }

  back(file) {
    this.setState(({current}) => {
      return {current: current.slice(0, current.length - 1)};
    }, () => this.updateHash());
  }

  updateHash() {
    if (!this.state.current || !this.state.current.length) {
      window.location = '#';
    } else {
      window.location = '#' + this.state.current.join('/');
    }
  }
}

function FilesList({folder, onSelectFile, onBack}) {
  let files = folder.children;
  return e('div', {className: 'display-folder'},
    e(FileHeader, {file: folder, onBack}),
    e('table', {className: 'files-list'},
      e('thead', {className: 'files-list__head'},
        e('tr', null,
          e('th', null, "Path"),
          e('th', null, "Coverage")
        )
      ),
      e('tbody', {className: 'files-list__body'},
        files.map(file => e(File, {file, onClick: onSelectFile}))
      )
    )
  );
}

function File({file, onClick}) {
  const coverage = file.coverable ? file.covered / file.coverable * 100 : -1;
  const coverageDelta = file.prevRun &&
    (file.covered / file.coverable * 100 - file.prevRun.covered / file.prevRun.coverable * 100);

  return e('tr', {
      className: 'files-list__file'
        + (coverage >= 0 && coverage < 50 ? ' files-list__file_low': '')
        + (coverage >= 50 && coverage < 80 ? ' files-list__file_medium': '')
        + (coverage >= 80 ? ' files-list__file_high': '')
        + (file.is_folder ? ' files-list__file_folder': ''),
      onClick: () => onClick(file),
    },
    e('td', null, e('a', null, pathToString(file.path))),
    e('td', null,
      file.covered + ' / ' + file.coverable +
      (coverage >= 0 ? ' (' + coverage.toFixed(2) + '%)' : ''),
      e('span', {title: 'Change from the previous run'},
        (coverageDelta ? ` (${coverageDelta > 0 ? '+' : ''}${coverageDelta.toFixed(2)}%)` : ''))
    )
  );
}

function DisplayFile({file, onBack}) {
  return e('div', {className: 'display-file'},
    e(FileHeader, {file, onBack}),
    e(FileContent, {file})
  );
}

function FileHeader({file, onBack}) {
  const coverage = file.covered / file.coverable * 100;
  const coverageDelta = file.prevRun && (coverage - file.prevRun.covered / file.prevRun.coverable * 100);

  return e('div', {className: 'file-header'},
    onBack ? e('a', {className: 'file-header__back', onClick: onBack}, 'Back') : null,
    e('div', {className: 'file-header__name'}, pathToString([...file.parent, ...file.path])),
    e('div', {className: 'file-header__stat'},
      'Covered: ' + file.covered + ' of ' + file.coverable +
      (file.coverable ? ' (' + coverage.toFixed(2) + '%)' : ''),
      e('span', {title: 'Change from the previous run'},
        (coverageDelta ? ` (${coverageDelta > 0 ? '+' : ''}${coverageDelta.toFixed(2)}%)` : ''))
    )
  );
}

function FileContent({file}) {
  return e('pre', {className: 'file-content'},
    file.content.split(/\r?\n/).map((line, index) => {
      const trace = file.traces.find(trace => trace.line === index + 1);
      const covered = trace && trace.stats.Line;
      const uncovered = trace && !trace.stats.Line;
      return e('code', {
          className: 'code-line'
            + (covered ? ' code-line_covered' : '')
            + (uncovered ? ' code-line_uncovered' : ''),
          title: trace ? JSON.stringify(trace.stats, null, 2) : null,
        }, line);
    })
  );
}

(function(){
  const commonPath = findCommonPath(data.files);
  const prevFilesMap = new Map();

  previousData && previousData.files.forEach((file) => {
    const path = file.path.slice(commonPath.length).join('/');
    prevFilesMap.set(path, file);
  });

  const files = data.files.map((file) => {
    const path = file.path.slice(commonPath.length);
    const { covered = 0, coverable = 0 } = prevFilesMap.get(path.join('/')) || {};
    return {
      ...file,
      path,
      parent: commonPath,
      prevRun: { covered, coverable },
    };
  });

  const children = findFolders(files);

  const root = {
    is_folder: true,
    children,
    path: commonPath,
    parent: [],
    covered: children.reduce((sum, file) => sum + file.covered, 0),
    coverable: children.reduce((sum, file) => sum + file.coverable, 0),
    prevRun: {
      covered: children.reduce((sum, file) => sum + file.prevRun.covered, 0),
      coverable: children.reduce((sum, file) => sum + file.prevRun.coverable, 0),
    }
  };

  ReactDOM.render(e(App, {root, prevFilesMap}), document.getElementById('root'));
}());
</script>
</body>
</html>