Skip to content

Commit 5c9451b

Browse files
committed
fix: make target_details available early in the request
1 parent 3bc0f15 commit 5c9451b

2 files changed

Lines changed: 229 additions & 10 deletions

File tree

src/models/connection/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ pub struct ConnectionConfig {
1111
pub base_path: Option<String>,
1212
}
1313

14+
impl ConnectionConfig {
15+
/// Constructs a base URL from the connection configuration
16+
pub fn to_base_url(&self) -> String {
17+
let protocol = self.protocol.as_deref().unwrap_or("http");
18+
let port = self.port.map(|p| format!(":{}", p)).unwrap_or_default();
19+
let path = self.base_path.as_deref().unwrap_or("");
20+
let path = if !path.is_empty() && !path.starts_with('/') {
21+
format!("/{}", path)
22+
} else {
23+
path.to_string()
24+
};
25+
format!("{}://{}{}{}", protocol, self.host, port, path)
26+
}
27+
}
28+
1429
/// Global authentication definition (DSL v1.9.0+)
1530
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1631
pub struct AuthenticationDefinition {

src/pipeline/executor.rs

Lines changed: 214 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::config::config::Config;
2-
use crate::models::envelope::envelope::{RequestEnvelope, ResponseEnvelope};
2+
use crate::models::envelope::envelope::{RequestEnvelope, ResponseEnvelope, TargetDetails};
33
use crate::models::middleware::chain::MiddlewareChain;
44
use crate::models::middleware::middleware::build_middleware_instances_for_pipeline;
55
use crate::models::pipelines::config::Pipeline;
@@ -51,11 +51,12 @@ impl PipelineExecutor {
5151
///
5252
/// # Flow
5353
/// 1. Endpoint service preprocessing
54-
/// 2. Incoming middleware chain (left)
55-
/// 3. Backend invocation
56-
/// 4. Outgoing middleware chain (right)
57-
/// 5. Endpoint service post-processing (protocol-aware)
58-
/// 6. Return ResponseEnvelope
54+
/// 2. Resolve target_details from backend config
55+
/// 3. Incoming middleware chain (left)
56+
/// 4. Backend invocation
57+
/// 5. Outgoing middleware chain (right)
58+
/// 6. Endpoint service post-processing (protocol-aware)
59+
/// 7. Return ResponseEnvelope
5960
///
6061
/// # Arguments
6162
/// * `envelope` - The request envelope to process
@@ -80,22 +81,88 @@ impl PipelineExecutor {
8081
// 1. Endpoint service preprocessing
8182
let envelope = Self::process_endpoint_incoming(envelope, pipeline, config).await?;
8283

83-
// 2. Incoming middleware chain (left)
84+
// 2. Resolve target_details from backend config (so middleware has access)
85+
let envelope = Self::resolve_target_details(envelope, pipeline, config).await?;
86+
87+
// 3. Incoming middleware chain (left)
8488
let envelope = Self::process_incoming_middleware(envelope, pipeline, config).await?;
8589

86-
// 3. Backend invocation
90+
// 4. Backend invocation
8791
let response = Self::process_backends(envelope, pipeline, config).await?;
8892

89-
// 4. Outgoing middleware chain (right)
93+
// 5. Outgoing middleware chain (right)
9094
let mut response = Self::process_outgoing_middleware(response, pipeline, config).await?;
9195

92-
// 5. Endpoint service post-processing (protocol-aware)
96+
// 6. Endpoint service post-processing (protocol-aware)
9397
Self::process_endpoint_outgoing(&mut response, pipeline, config, ctx).await?;
9498

9599
tracing::info!("Pipeline execution completed successfully");
96100
Ok(response)
97101
}
98102

103+
/// Resolve target_details from backend configuration
104+
///
105+
/// This populates target_details early so middleware has access to target info.
106+
/// The backend will use these details if present, or create its own if not.
107+
async fn resolve_target_details(
108+
mut envelope: RequestEnvelope<Vec<u8>>,
109+
pipeline: &Pipeline,
110+
config: &Config,
111+
) -> Result<RequestEnvelope<Vec<u8>>, PipelineError> {
112+
// Skip if target_details already set (e.g., by endpoint)
113+
if envelope.target_details.is_some() {
114+
return Ok(envelope);
115+
}
116+
117+
// Get first backend from pipeline
118+
let backend_name = match pipeline.backends.first() {
119+
Some(name) => name,
120+
None => return Ok(envelope), // No backends, leave target_details as None
121+
};
122+
123+
let backend = match config.backends.get(backend_name) {
124+
Some(b) => b,
125+
None => return Ok(envelope), // Backend not found, leave as None
126+
};
127+
128+
// Build base_url from backend's resolved connection
129+
let base_url = backend
130+
.connection
131+
.as_ref()
132+
.map(|c| c.to_base_url())
133+
.or_else(|| {
134+
backend
135+
.options
136+
.as_ref()
137+
.and_then(|opts| opts.get("base_url"))
138+
.and_then(|v| v.as_str())
139+
.map(|s| s.to_string())
140+
})
141+
.unwrap_or_default();
142+
143+
if base_url.is_empty() {
144+
tracing::debug!("No base_url available for target_details resolution");
145+
return Ok(envelope);
146+
}
147+
148+
// Extract path without query string
149+
let path = crate::models::services::path_utils::extract_path(&envelope);
150+
151+
// Create TargetDetails
152+
let mut target =
153+
TargetDetails::from_request_details(base_url, &envelope.request_details);
154+
target.uri = path;
155+
156+
tracing::debug!(
157+
"Resolved target_details: {} {}",
158+
target.method,
159+
target.full_url().unwrap_or_else(|_| "<invalid>".to_string())
160+
);
161+
162+
envelope.target_details = Some(target);
163+
Ok(envelope)
164+
}
165+
99166
/// Process endpoint incoming request
100167
async fn process_endpoint_incoming(
101168
envelope: RequestEnvelope<Vec<u8>>,
@@ -405,6 +472,10 @@ impl PipelineExecutor {
405472
#[cfg(test)]
406473
mod tests {
407474
use super::*;
475+
use crate::models::backends::backends::Backend;
476+
use crate::models::connection::ConnectionConfig;
477+
use crate::models::envelope::envelope::RequestEnvelope;
478+
use crate::models::pipelines::config::PipelineMiddleware;
408479

409480
#[test]
410481
fn test_pipeline_error_display() {
@@ -420,4 +491,137 @@ mod tests {
420491
let err: PipelineError = "test".into();
421492
assert_eq!(err.to_string(), "Service error: test");
422493
}
494+
495+
#[tokio::test]
496+
async fn test_resolve_target_details_from_backend_connection() {
497+
let mut config = Config::default();
498+
499+
// Add a backend with connection config
500+
config.backends.insert(
501+
"test_backend".to_string(),
502+
Backend {
503+
service: "http".to_string(),
504+
target_ref: None,
505+
connection: Some(ConnectionConfig {
506+
host: "api.example.com".to_string(),
507+
port: Some(443),
508+
protocol: Some("https".to_string()),
509+
base_path: Some("/v1".to_string()),
510+
}),
511+
authentication: None,
512+
timeout_secs: None,
513+
max_retries: None,
514+
options: None,
515+
},
516+
);
517+
518+
let pipeline = Pipeline {
519+
description: "test pipeline".to_string(),
520+
networks: vec![],
521+
endpoints: vec![],
522+
backends: vec!["test_backend".to_string()],
523+
middleware: PipelineMiddleware::default(),
524+
};
525+
526+
// Create envelope with request details
527+
let envelope = RequestEnvelope::builder()
528+
.method("GET")
529+
.uri("/users?id=123")
530+
.query_params(HashMap::from([(
531+
"id".to_string(),
532+
vec!["123".to_string()],
533+
)]))
534+
.original_data(vec![])
535+
.build()
536+
.unwrap();
537+
538+
// Should have no target_details initially
539+
assert!(envelope.target_details.is_none());
540+
541+
// Resolve target details
542+
let result = PipelineExecutor::resolve_target_details(envelope, &pipeline, &config).await;
543+
assert!(result.is_ok());
544+
545+
let envelope = result.unwrap();
546+
assert!(envelope.target_details.is_some());
547+
548+
let target = envelope.target_details.unwrap();
549+
assert_eq!(target.base_url, "https://api.example.com:443/v1");
550+
assert_eq!(target.method, "GET");
551+
assert_eq!(target.uri, "/users"); // Path without query string
552+
assert_eq!(
553+
target.query_params.get("id"),
554+
Some(&vec!["123".to_string()])
555+
);
556+
}
557+
558+
#[tokio::test]
559+
async fn test_resolve_target_details_no_backend() {
560+
let config = Config::default();
561+
562+
let pipeline = Pipeline {
563+
description: "test pipeline".to_string(),
564+
networks: vec![],
565+
endpoints: vec![],
566+
backends: vec![], // No backends
567+
middleware: PipelineMiddleware::default(),
568+
};
569+
570+
let envelope = RequestEnvelope::builder()
571+
.method("GET")
572+
.uri("/test")
573+
.original_data(vec![])
574+
.build()
575+
.unwrap();
576+
577+
let result = PipelineExecutor::resolve_target_details(envelope, &pipeline, &config).await;
578+
assert!(result.is_ok());
579+
580+
// Should return envelope unchanged (no target_details)
581+
let envelope = result.unwrap();
582+
assert!(envelope.target_details.is_none());
583+
}
584+
585+
#[tokio::test]
586+
async fn test_resolve_target_details_preserves_existing() {
587+
let config = Config::default();
588+
589+
let pipeline = Pipeline {
590+
description: "test pipeline".to_string(),
591+
networks: vec![],
592+
endpoints: vec![],
593+
backends: vec![],
594+
middleware: PipelineMiddleware::default(),
595+
};
596+
597+
// Create envelope with existing target_details
598+
let existing_target = TargetDetails {
599+
base_url: "https://existing.com".to_string(),
600+
method: "POST".to_string(),
601+
uri: "/existing".to_string(),
602+
headers: HashMap::new(),
603+
cookies: HashMap::new(),
604+
query_params: HashMap::new(),
605+
metadata: HashMap::new(),
606+
};
607+
608+
let envelope = RequestEnvelope::builder()
609+
.method("GET")
610+
.uri("/test")
611+
.target_details(Some(existing_target.clone()))
612+
.original_data(vec![])
613+
.build()
614+
.unwrap();
615+
616+
let result = PipelineExecutor::resolve_target_details(envelope, &pipeline, &config).await;
617+
assert!(result.is_ok());
618+
619+
let envelope = result.unwrap();
620+
let target = envelope.target_details.unwrap();
621+
622+
// Should preserve existing target_details
623+
assert_eq!(target.base_url, "https://existing.com");
624+
assert_eq!(target.method, "POST");
625+
assert_eq!(target.uri, "/existing");
626+
}
423627
}

0 commit comments

Comments
 (0)