itsi-scheduler 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/CODE_OF_CONDUCT.md +7 -0
  3. data/Cargo.lock +75 -14
  4. data/README.md +5 -0
  5. data/_index.md +7 -0
  6. data/ext/itsi_error/src/lib.rs +9 -0
  7. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  8. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  9. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  10. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  11. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  12. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  13. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  14. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  15. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  21. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  22. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  23. data/ext/itsi_rb_helpers/src/lib.rs +34 -7
  24. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  25. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  26. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  27. data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  28. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  29. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  30. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  31. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  32. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  33. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  34. data/ext/itsi_server/Cargo.toml +69 -30
  35. data/ext/itsi_server/src/lib.rs +79 -147
  36. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  37. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +22 -3
  38. data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
  39. data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
  40. data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
  41. data/ext/itsi_server/src/{request/itsi_request.rs → ruby_types/itsi_http_request.rs} +101 -117
  42. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +72 -41
  43. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  44. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
  45. data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
  46. data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
  47. data/ext/itsi_server/src/server/bind.rs +13 -5
  48. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  49. data/ext/itsi_server/src/server/cache_store.rs +74 -0
  50. data/ext/itsi_server/src/server/itsi_service.rs +172 -0
  51. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  52. data/ext/itsi_server/src/server/listener.rs +102 -2
  53. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
  54. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
  55. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
  56. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
  57. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +321 -0
  58. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
  59. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
  60. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
  61. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
  62. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
  63. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
  64. data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
  65. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
  66. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
  67. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  68. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
  69. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
  70. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  78. data/ext/itsi_server/src/server/middleware_stack/mod.rs +315 -0
  79. data/ext/itsi_server/src/server/mod.rs +8 -1
  80. data/ext/itsi_server/src/server/process_worker.rs +38 -12
  81. data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
  82. data/ext/itsi_server/src/server/request_job.rs +11 -0
  83. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +119 -42
  84. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  85. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +256 -111
  86. data/ext/itsi_server/src/server/signal.rs +19 -0
  87. data/ext/itsi_server/src/server/static_file_server.rs +984 -0
  88. data/ext/itsi_server/src/server/thread_worker.rs +139 -94
  89. data/ext/itsi_server/src/server/types.rs +43 -0
  90. data/ext/itsi_tracing/Cargo.toml +1 -0
  91. data/ext/itsi_tracing/src/lib.rs +216 -45
  92. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  93. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  94. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  95. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  96. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  97. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  98. data/lib/itsi/scheduler/version.rb +1 -1
  99. data/lib/itsi/scheduler.rb +2 -2
  100. metadata +77 -12
  101. data/ext/itsi_server/extconf.rb +0 -6
  102. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  103. data/ext/itsi_server/src/request/mod.rs +0 -1
  104. data/ext/itsi_server/src/response/mod.rs +0 -1
  105. data/ext/itsi_server/src/server/itsi_server.rs +0 -288
@@ -0,0 +1,93 @@
1
+ use super::MiddlewareLayer;
2
+ use crate::ruby_types::itsi_grpc_request::ItsiGrpcRequest;
3
+ use crate::server::static_file_server::ROOT_STATIC_FILE_SERVER;
4
+ use crate::{
5
+ ruby_types::itsi_http_request::ItsiHttpRequest,
6
+ server::{
7
+ itsi_service::RequestContext,
8
+ types::{HttpRequest, HttpResponse},
9
+ },
10
+ };
11
+ use async_trait::async_trait;
12
+ use derive_more::Debug;
13
+ use either::Either;
14
+ use itsi_rb_helpers::{HeapVal, HeapValue};
15
+ use magnus::{block::Proc, error::Result, value::ReprValue, Symbol};
16
+ use std::str::FromStr;
17
+ use std::sync::Arc;
18
+
19
+ #[derive(Debug)]
20
+ pub struct RubyApp {
21
+ app: Arc<HeapValue<Proc>>,
22
+ request_type: RequestType,
23
+ sendfile: bool,
24
+ }
25
+
26
+ #[derive(Debug)]
27
+ pub enum RequestType {
28
+ Http,
29
+ Grpc,
30
+ }
31
+
32
+ impl FromStr for RequestType {
33
+ type Err = &'static str;
34
+
35
+ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
36
+ match s {
37
+ "http" => Ok(RequestType::Http),
38
+ "grpc" => Ok(RequestType::Grpc),
39
+ _ => Err("Invalid request type"),
40
+ }
41
+ }
42
+ }
43
+
44
+ impl RubyApp {
45
+ pub fn from_value(params: HeapVal) -> magnus::error::Result<Self> {
46
+ let app = params.funcall::<_, _, Proc>(Symbol::new("[]"), ("app_proc",))?;
47
+ let sendfile = params
48
+ .funcall::<_, _, bool>(Symbol::new("[]"), ("sendfile",))
49
+ .unwrap_or(true);
50
+ let request_type: RequestType = params
51
+ .funcall::<_, _, String>(Symbol::new("[]"), ("request_type",))
52
+ .unwrap_or("http".to_string())
53
+ .parse()
54
+ .unwrap_or(RequestType::Http);
55
+
56
+ Ok(RubyApp {
57
+ app: Arc::new(app.into()),
58
+ sendfile,
59
+ request_type,
60
+ })
61
+ }
62
+ }
63
+
64
+ #[async_trait]
65
+ impl MiddlewareLayer for RubyApp {
66
+ async fn before(
67
+ &self,
68
+ req: HttpRequest,
69
+ context: &mut RequestContext,
70
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
71
+ match self.request_type {
72
+ RequestType::Http => ItsiHttpRequest::process_request(self.app.clone(), req, context)
73
+ .await
74
+ .map_err(|e| e.into())
75
+ .map(Either::Right),
76
+ RequestType::Grpc => ItsiGrpcRequest::process_request(self.app.clone(), req, context)
77
+ .await
78
+ .map_err(|e| e.into())
79
+ .map(Either::Right),
80
+ }
81
+ }
82
+
83
+ async fn after(&self, resp: HttpResponse, _context: &mut RequestContext) -> HttpResponse {
84
+ if self.sendfile {
85
+ if let Some(sendfile_header) = resp.headers().get("X-Sendfile") {
86
+ return ROOT_STATIC_FILE_SERVER
87
+ .serve_single(sendfile_header.to_str().unwrap())
88
+ .await;
89
+ }
90
+ }
91
+ resp
92
+ }
93
+ }
@@ -0,0 +1,162 @@
1
+ use super::{FromValue, MiddlewareLayer};
2
+ use crate::server::{
3
+ itsi_service::RequestContext,
4
+ static_file_server::{NotFoundBehavior, ServeRange, StaticFileServer, StaticFileServerConfig},
5
+ types::{HttpRequest, HttpResponse},
6
+ };
7
+ use async_trait::async_trait;
8
+ use either::Either;
9
+ use http::{
10
+ header::{IF_MODIFIED_SINCE, RANGE},
11
+ HeaderMap, Method,
12
+ };
13
+ use itsi_error::ItsiError;
14
+ use magnus::error::Result;
15
+ use serde::Deserialize;
16
+ use std::{collections::HashMap, path::PathBuf, sync::OnceLock, time::Duration};
17
+
18
+ #[derive(Debug, Deserialize)]
19
+ pub struct StaticAssets {
20
+ pub root_dir: PathBuf,
21
+ pub not_found_behavior: NotFoundBehavior,
22
+ pub auto_index: bool,
23
+ pub try_html_extension: bool,
24
+ pub max_file_size_in_memory: u64,
25
+ pub max_files_in_memory: u64,
26
+ pub file_check_interval: u64,
27
+ pub headers: Option<HashMap<String, String>>,
28
+ pub relative_path: bool,
29
+ #[serde(skip)]
30
+ file_server: OnceLock<StaticFileServer>,
31
+ }
32
+
33
+ #[async_trait]
34
+ impl MiddlewareLayer for StaticAssets {
35
+ async fn initialize(&self) -> Result<()> {
36
+ if let Ok(metadata) = tokio::fs::metadata(&self.root_dir).await {
37
+ if metadata.is_dir() {
38
+ Ok(())
39
+ } else {
40
+ Err(ItsiError::InvalidInput(
41
+ "Root directory exists but is not a directory".to_string(),
42
+ ))
43
+ }
44
+ } else {
45
+ Err(ItsiError::InvalidInput(
46
+ "Root directory exists but is not a directory".to_string(),
47
+ ))
48
+ }?;
49
+ self.file_server
50
+ .set(StaticFileServer::new(StaticFileServerConfig {
51
+ root_dir: self.root_dir.clone(),
52
+ not_found_behavior: self.not_found_behavior.clone(),
53
+ auto_index: self.auto_index,
54
+ max_entries: self.max_files_in_memory,
55
+ try_html_extension: self.try_html_extension,
56
+ max_file_size: self.max_file_size_in_memory,
57
+ recheck_interval: Duration::from_secs(self.file_check_interval),
58
+ }))
59
+ .map_err(ItsiError::default)?;
60
+ Ok(())
61
+ }
62
+
63
+ async fn before(
64
+ &self,
65
+ req: HttpRequest,
66
+ context: &mut RequestContext,
67
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
68
+ // Only handle GET and HEAD requests
69
+ if req.method() != Method::GET && req.method() != Method::HEAD {
70
+ return Ok(Either::Left(req));
71
+ }
72
+ let abs_path = req.uri().path();
73
+ let rel_path = if !self.relative_path {
74
+ abs_path
75
+ } else {
76
+ match context
77
+ .matching_pattern
78
+ .as_ref()
79
+ .and_then(|pattern| pattern.captures(req.uri().path()))
80
+ .and_then(|captures| captures.name("path_suffix"))
81
+ .map(|m| m.as_str())
82
+ {
83
+ Some(suffix) => suffix,
84
+ None => return Ok(Either::Left(req)),
85
+ }
86
+ };
87
+
88
+ // Determine if this is a HEAD request
89
+ let is_head_request = req.method() == Method::HEAD;
90
+
91
+ // Extract range and if-modified-since headers
92
+ let serve_range = parse_range_header(req.headers());
93
+ let if_modified_since = req
94
+ .headers()
95
+ .get(IF_MODIFIED_SINCE)
96
+ .and_then(|ims| ims.to_str().ok())
97
+ .and_then(|ims_str| httpdate::parse_http_date(ims_str).ok());
98
+
99
+ // Let the file server handle everything
100
+ let file_server = self.file_server.get().unwrap();
101
+ let response = file_server
102
+ .serve(
103
+ &req,
104
+ rel_path,
105
+ abs_path,
106
+ serve_range,
107
+ if_modified_since,
108
+ is_head_request,
109
+ )
110
+ .await;
111
+
112
+ if response.is_none() {
113
+ Ok(Either::Left(req))
114
+ } else {
115
+ Ok(Either::Right(response.unwrap()))
116
+ }
117
+ }
118
+ }
119
+
120
+ fn parse_range_header(headers: &HeaderMap) -> ServeRange {
121
+ let range_header = headers.get(RANGE);
122
+ if range_header.is_none() {
123
+ return ServeRange::Full;
124
+ }
125
+ let range_header = range_header.unwrap().to_str().unwrap_or("");
126
+ let bytes_prefix = "bytes=";
127
+ if !range_header.starts_with(bytes_prefix) {
128
+ return ServeRange::Full;
129
+ }
130
+
131
+ let range_str = &range_header[bytes_prefix.len()..];
132
+
133
+ let range_parts: Vec<&str> = range_str
134
+ .split(',')
135
+ .next()
136
+ .unwrap_or("")
137
+ .split('-')
138
+ .collect();
139
+ if range_parts.len() != 2 {
140
+ return ServeRange::Full;
141
+ }
142
+
143
+ let start = if range_parts[0].is_empty() {
144
+ range_parts[1].parse::<u64>().unwrap_or(0)
145
+ } else if let Ok(start) = range_parts[0].parse::<u64>() {
146
+ start
147
+ } else {
148
+ return ServeRange::Full;
149
+ };
150
+
151
+ let end = if range_parts[1].is_empty() {
152
+ u64::MAX // Use u64::MAX as sentinel for open-ended ranges
153
+ } else if let Ok(end) = range_parts[1].parse::<u64>() {
154
+ end // No conversion needed, already u64
155
+ } else {
156
+ return ServeRange::Full;
157
+ };
158
+
159
+ ServeRange::Range(start, end)
160
+ }
161
+
162
+ impl FromValue for StaticAssets {}
@@ -0,0 +1,158 @@
1
+ use crate::server::{
2
+ itsi_service::RequestContext,
3
+ types::{HttpRequest, HttpResponse},
4
+ };
5
+ use serde::Deserialize;
6
+ use std::sync::OnceLock;
7
+
8
+ #[derive(Debug, Clone, Deserialize)]
9
+ #[serde(transparent)]
10
+ pub struct StringRewrite {
11
+ pub template_string: String,
12
+ #[serde(default)]
13
+ pub segments: OnceLock<Vec<Segment>>,
14
+ }
15
+
16
+ #[derive(Debug, Clone, Deserialize)]
17
+ pub enum Segment {
18
+ Literal(String),
19
+ Placeholder(String),
20
+ }
21
+
22
+ pub fn parse_template(template: &str) -> Vec<Segment> {
23
+ let mut segments = Vec::new();
24
+ let mut last_index = 0;
25
+ while let Some(start_index) = template[last_index..].find('{') {
26
+ let start_index = last_index + start_index;
27
+ // Add the literal text before the placeholder.
28
+ if start_index > last_index {
29
+ segments.push(Segment::Literal(
30
+ template[last_index..start_index].to_string(),
31
+ ));
32
+ }
33
+ // Find the corresponding closing brace.
34
+ if let Some(end_index) = template[start_index..].find('}') {
35
+ let end_index = start_index + end_index;
36
+ let placeholder = &template[start_index + 1..end_index];
37
+ segments.push(Segment::Placeholder(placeholder.to_string()));
38
+ last_index = end_index + 1;
39
+ } else {
40
+ // No closing brace found; treat the rest as literal.
41
+ segments.push(Segment::Literal(template[start_index..].to_string()));
42
+ break;
43
+ }
44
+ }
45
+ if last_index < template.len() {
46
+ segments.push(Segment::Literal(template[last_index..].to_string()));
47
+ }
48
+ segments
49
+ }
50
+
51
+ impl StringRewrite {
52
+ pub fn rewrite_request(&self, req: &HttpRequest, context: &RequestContext) -> String {
53
+ let segments = self
54
+ .segments
55
+ .get_or_init(|| parse_template(&self.template_string));
56
+ let captures = context
57
+ .matching_pattern
58
+ .as_ref()
59
+ .and_then(|re| re.captures(req.uri().path()));
60
+
61
+ let mut result = String::with_capacity(self.template_string.len());
62
+
63
+ for segment in segments {
64
+ match segment {
65
+ Segment::Literal(text) => result.push_str(text),
66
+ Segment::Placeholder(placeholder) => {
67
+ let replacement = match placeholder.as_str() {
68
+ "request_id" => context.request_id(),
69
+ "method" => req.method().as_str().to_string(),
70
+ "path" => req.uri().path().to_string(),
71
+ "host" => req.uri().host().unwrap_or("localhost").to_string(),
72
+ "path_and_query" => req
73
+ .uri()
74
+ .path_and_query()
75
+ .map(|pq| pq.to_string())
76
+ .unwrap_or("".to_string()),
77
+ "query" => {
78
+ let query = req.uri().query().unwrap_or("").to_string();
79
+ if query.is_empty() {
80
+ query
81
+ } else {
82
+ format!("?{}", query)
83
+ }
84
+ }
85
+ "port" => req
86
+ .uri()
87
+ .port()
88
+ .map(|p| p.to_string())
89
+ .unwrap_or_else(|| "80".to_string()),
90
+ "start_time" => {
91
+ if let Some(start_time) = context.start_time() {
92
+ start_time.format("%Y-%m-%d:%H:%M:%S:%3f").to_string()
93
+ } else {
94
+ "N/A".to_string()
95
+ }
96
+ }
97
+ other => {
98
+ // Try using the context's matching regex if available.
99
+ if let Some(caps) = &captures {
100
+ if let Some(m) = caps.name(other) {
101
+ m.as_str().to_string()
102
+ } else {
103
+ // Fallback: leave the placeholder as is.
104
+ format!("{{{}}}", other)
105
+ }
106
+ } else {
107
+ format!("{{{}}}", other)
108
+ }
109
+ }
110
+ };
111
+ result.push_str(&replacement);
112
+ }
113
+ }
114
+ }
115
+
116
+ result
117
+ }
118
+
119
+ pub fn rewrite_response(&self, resp: &HttpResponse, context: &RequestContext) -> String {
120
+ let segments = self
121
+ .segments
122
+ .get_or_init(|| parse_template(&self.template_string));
123
+
124
+ let mut result = String::with_capacity(self.template_string.len());
125
+ for segment in segments {
126
+ match segment {
127
+ Segment::Literal(text) => result.push_str(text),
128
+ Segment::Placeholder(placeholder) => {
129
+ let replacement = match placeholder.as_str() {
130
+ "request_id" => context.request_id(),
131
+ "status" => resp.status().as_str().to_string(),
132
+ "response_time" => {
133
+ if let Some(response_time) = context.get_response_time() {
134
+ if let Some(microseconds) = response_time.num_microseconds() {
135
+ format!("{:.3}ms", microseconds as f64 / 1000.0)
136
+ } else {
137
+ format!("{}ms", response_time.num_milliseconds())
138
+ }
139
+ } else {
140
+ "-".to_string()
141
+ }
142
+ }
143
+ other => {
144
+ if let Some(header_value) = resp.headers().get(other) {
145
+ format!("{:?}", header_value)
146
+ } else {
147
+ format!("{{{}}}", other)
148
+ }
149
+ }
150
+ };
151
+ result.push_str(&replacement);
152
+ }
153
+ }
154
+ }
155
+
156
+ result
157
+ }
158
+ }
@@ -0,0 +1,12 @@
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ #[derive(Debug, Clone, Serialize, Deserialize)]
4
+ pub enum TokenSource {
5
+ #[serde(rename(deserialize = "header"))]
6
+ Header {
7
+ name: String,
8
+ prefix: Option<String>,
9
+ },
10
+ #[serde(rename(deserialize = "query"))]
11
+ Query(String),
12
+ }