camel_component_exec/
endpoint.rs1use std::sync::Arc;
8
9use camel_api::{BoxProcessor, CamelError};
10use camel_component_api::{Consumer, Endpoint, ProducerContext, RuntimeObservability};
11use tokio::sync::Semaphore;
12
13use crate::config::ExecGlobalConfig;
14use crate::producer::ExecProducer;
15
16pub struct ExecEndpoint {
22 uri: String,
23 profile_name: String,
24 global: Arc<ExecGlobalConfig>,
25}
26
27impl ExecEndpoint {
28 pub fn new(uri: String, profile_name: String, global: Arc<ExecGlobalConfig>) -> Self {
29 Self {
30 uri,
31 profile_name,
32 global,
33 }
34 }
35}
36
37impl Endpoint for ExecEndpoint {
38 fn uri(&self) -> &str {
39 &self.uri
40 }
41
42 fn create_consumer(
43 &self,
44 _rt: Arc<dyn RuntimeObservability>,
45 ) -> Result<Box<dyn Consumer>, CamelError> {
46 Err(CamelError::InvalidUri(
50 "exec component is producer-only".into(),
51 ))
52 }
53
54 fn create_producer(
55 &self,
56 rt: Arc<dyn RuntimeObservability>,
57 ctx: &ProducerContext,
58 ) -> Result<BoxProcessor, CamelError> {
59 let profile = self
60 .global
61 .profile(&self.profile_name)
62 .ok_or_else(|| {
63 CamelError::EndpointCreationFailed(format!(
64 "exec profile {:?} not found",
65 self.profile_name
66 ))
67 })?
68 .clone();
69
70 let route_id = ctx.route_id().unwrap_or("unknown").to_string();
71 let semaphore = Arc::new(Semaphore::new(profile.concurrency));
72 let host_env =
73 Arc::new(std::env::vars().collect::<std::collections::HashMap<String, String>>());
74
75 let producer = ExecProducer {
76 profile: Arc::new(profile),
77 global: Arc::clone(&self.global),
78 route_id,
79 host_env,
80 semaphore,
81 rt: Some(rt),
82 };
83
84 Ok(BoxProcessor::new(producer))
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use std::sync::Arc;
91
92 use camel_api::CamelError;
93 use camel_component_api::{Component, NoOpComponentContext, RuntimeObservability};
94
95 use crate::ExecComponent;
96 use crate::config::ExecGlobalConfig;
97
98 fn component_with_echo() -> ExecComponent {
99 let cfg: ExecGlobalConfig = toml::from_str(
100 r#"
101[[profiles]]
102name = "echo"
103executable = "echo"
104"#,
105 )
106 .unwrap();
107 ExecComponent::new(cfg)
108 }
109
110 fn extract_err<T>(result: Result<T, CamelError>) -> CamelError {
111 match result {
112 Err(e) => e,
113 Ok(_) => panic!("expected Err, got Ok"),
114 }
115 }
116
117 #[test]
118 fn create_endpoint_rejects_missing_prefix() {
119 let c = component_with_echo();
120 let ctx = NoOpComponentContext;
121 let err = extract_err(c.create_endpoint("notecho:echo", &ctx));
122 assert!(
123 matches!(&err, CamelError::InvalidUri(_)),
124 "expected InvalidUri, got {err:?}"
125 );
126 assert!(
127 err.to_string().contains("exec:"),
128 "should mention 'exec:': {err}"
129 );
130 }
131
132 #[test]
133 fn create_endpoint_rejects_empty_profile() {
134 let c = component_with_echo();
135 let ctx = NoOpComponentContext;
136 let err = extract_err(c.create_endpoint("exec:", &ctx));
137 assert!(
138 matches!(&err, CamelError::InvalidUri(_)),
139 "expected InvalidUri, got {err:?}"
140 );
141 assert!(
142 err.to_string().contains("profile name"),
143 "should mention 'profile name': {err}"
144 );
145 }
146
147 #[test]
148 fn create_endpoint_strips_query_string() {
149 let c = component_with_echo();
150 let ctx = NoOpComponentContext;
151 let ep = c
152 .create_endpoint("exec:echo?foo=bar", &ctx)
153 .expect("should succeed with query string");
154 assert_eq!(ep.uri(), "exec:echo?foo=bar");
155 }
156
157 #[test]
158 fn create_endpoint_rejects_unknown_profile() {
159 let c = component_with_echo();
160 let ctx = NoOpComponentContext;
161 let err = extract_err(c.create_endpoint("exec:nope", &ctx));
162 assert!(
163 matches!(&err, CamelError::EndpointCreationFailed(_)),
164 "expected EndpointCreationFailed, got {err:?}"
165 );
166 assert!(
167 err.to_string().contains("not configured"),
168 "should mention 'not configured': {err}"
169 );
170 }
171
172 #[test]
173 fn create_endpoint_valid_profile_ok() {
174 let c = component_with_echo();
175 let ctx = NoOpComponentContext;
176 let ep = c
177 .create_endpoint("exec:echo", &ctx)
178 .expect("should succeed");
179 assert_eq!(ep.uri(), "exec:echo");
180 }
181
182 #[test]
183 fn endpoint_is_producer_only() {
184 let c = component_with_echo();
185 let ctx = NoOpComponentContext;
186 let ep = c
187 .create_endpoint("exec:echo", &ctx)
188 .expect("should succeed");
189 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoOpComponentContext);
190 let err = extract_err(ep.create_consumer(rt));
191 assert!(
192 matches!(&err, CamelError::InvalidUri(_)),
193 "expected InvalidUri, got {err:?}"
194 );
195 assert!(
196 err.to_string().contains("producer-only"),
197 "should mention 'producer-only': {err}"
198 );
199 }
200}