use std::sync::Arc;
use camel_api::{BoxProcessor, CamelError};
use camel_component_api::{Consumer, Endpoint, ProducerContext, RuntimeObservability};
use tokio::sync::Semaphore;
use crate::config::ExecGlobalConfig;
use crate::producer::ExecProducer;
pub struct ExecEndpoint {
uri: String,
profile_name: String,
global: Arc<ExecGlobalConfig>,
}
impl ExecEndpoint {
pub fn new(uri: String, profile_name: String, global: Arc<ExecGlobalConfig>) -> Self {
Self {
uri,
profile_name,
global,
}
}
}
impl Endpoint for ExecEndpoint {
fn uri(&self) -> &str {
&self.uri
}
fn create_consumer(
&self,
_rt: Arc<dyn RuntimeObservability>,
) -> Result<Box<dyn Consumer>, CamelError> {
Err(CamelError::InvalidUri(
"exec component is producer-only".into(),
))
}
fn create_producer(
&self,
rt: Arc<dyn RuntimeObservability>,
ctx: &ProducerContext,
) -> Result<BoxProcessor, CamelError> {
let profile = self
.global
.profile(&self.profile_name)
.ok_or_else(|| {
CamelError::EndpointCreationFailed(format!(
"exec profile {:?} not found",
self.profile_name
))
})?
.clone();
let route_id = ctx.route_id().unwrap_or("unknown").to_string();
let semaphore = Arc::new(Semaphore::new(profile.concurrency));
let host_env: std::collections::HashMap<String, String> = std::env::vars().collect();
let producer = ExecProducer {
profile: Arc::new(profile),
global: Arc::clone(&self.global),
route_id,
host_env,
semaphore,
rt: Some(rt),
};
Ok(BoxProcessor::new(producer))
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use camel_api::CamelError;
use camel_component_api::{Component, NoOpComponentContext, RuntimeObservability};
use crate::ExecComponent;
use crate::config::ExecGlobalConfig;
fn component_with_echo() -> ExecComponent {
let cfg: ExecGlobalConfig = toml::from_str(
r#"
[[profiles]]
name = "echo"
executable = "echo"
"#,
)
.unwrap();
ExecComponent::new(cfg)
}
fn extract_err<T>(result: Result<T, CamelError>) -> CamelError {
match result {
Err(e) => e,
Ok(_) => panic!("expected Err, got Ok"),
}
}
#[test]
fn create_endpoint_rejects_missing_prefix() {
let c = component_with_echo();
let ctx = NoOpComponentContext;
let err = extract_err(c.create_endpoint("notecho:echo", &ctx));
assert!(
matches!(&err, CamelError::InvalidUri(_)),
"expected InvalidUri, got {err:?}"
);
assert!(
err.to_string().contains("exec:"),
"should mention 'exec:': {err}"
);
}
#[test]
fn create_endpoint_rejects_empty_profile() {
let c = component_with_echo();
let ctx = NoOpComponentContext;
let err = extract_err(c.create_endpoint("exec:", &ctx));
assert!(
matches!(&err, CamelError::InvalidUri(_)),
"expected InvalidUri, got {err:?}"
);
assert!(
err.to_string().contains("profile name"),
"should mention 'profile name': {err}"
);
}
#[test]
fn create_endpoint_strips_query_string() {
let c = component_with_echo();
let ctx = NoOpComponentContext;
let ep = c
.create_endpoint("exec:echo?foo=bar", &ctx)
.expect("should succeed with query string");
assert_eq!(ep.uri(), "exec:echo?foo=bar");
}
#[test]
fn create_endpoint_rejects_unknown_profile() {
let c = component_with_echo();
let ctx = NoOpComponentContext;
let err = extract_err(c.create_endpoint("exec:nope", &ctx));
assert!(
matches!(&err, CamelError::EndpointCreationFailed(_)),
"expected EndpointCreationFailed, got {err:?}"
);
assert!(
err.to_string().contains("not configured"),
"should mention 'not configured': {err}"
);
}
#[test]
fn create_endpoint_valid_profile_ok() {
let c = component_with_echo();
let ctx = NoOpComponentContext;
let ep = c
.create_endpoint("exec:echo", &ctx)
.expect("should succeed");
assert_eq!(ep.uri(), "exec:echo");
}
#[test]
fn endpoint_is_producer_only() {
let c = component_with_echo();
let ctx = NoOpComponentContext;
let ep = c
.create_endpoint("exec:echo", &ctx)
.expect("should succeed");
let rt: Arc<dyn RuntimeObservability> = Arc::new(NoOpComponentContext);
let err = extract_err(ep.create_consumer(rt));
assert!(
matches!(&err, CamelError::InvalidUri(_)),
"expected InvalidUri, got {err:?}"
);
assert!(
err.to_string().contains("producer-only"),
"should mention 'producer-only': {err}"
);
}
}