use std::sync::Arc;
use tokio::sync::{mpsc, oneshot};
use tokio_util::sync::CancellationToken;
use wasmtime::component::{HasSelf, Linker, ResourceTable};
use camel_api::{CamelError, Exchange, Message};
use camel_component_api::consumer::ConsumerContext;
use crate::source_bindings::camel::plugin::source_host::{HttpRequest, SubmitOutcome};
use crate::source_bindings::camel::plugin::types::WasmExchange;
pub struct HttpListenerHandle;
pub const REQUEST_CHANNEL_CAPACITY: usize = 1;
pub const EXCHANGE_CHANNEL_CAPACITY: usize = 1;
pub struct SourceChannels {
pub request_tx: mpsc::Sender<HttpRequest>,
pub request_rx: mpsc::Receiver<HttpRequest>,
pub exchange_tx: mpsc::Sender<(WasmExchange, oneshot::Sender<SubmitOutcome>)>,
pub exchange_rx: mpsc::Receiver<(WasmExchange, oneshot::Sender<SubmitOutcome>)>,
}
impl SourceChannels {
pub fn new() -> Self {
let (request_tx, request_rx) = mpsc::channel(REQUEST_CHANNEL_CAPACITY);
let (exchange_tx, exchange_rx) = mpsc::channel(EXCHANGE_CHANNEL_CAPACITY);
Self {
request_tx,
request_rx,
exchange_tx,
exchange_rx,
}
}
}
impl Default for SourceChannels {
fn default() -> Self {
Self::new()
}
}
pub struct SourceHostState {
pub table: ResourceTable,
pub wasi: wasmtime_wasi::WasiCtx,
pub request_rx: mpsc::Receiver<HttpRequest>,
pub exchange_tx: mpsc::Sender<(WasmExchange, oneshot::Sender<SubmitOutcome>)>,
pub cancel_token: CancellationToken,
}
impl wasmtime_wasi::WasiView for SourceHostState {
fn ctx(&mut self) -> wasmtime_wasi::WasiCtxView<'_> {
wasmtime_wasi::WasiCtxView {
ctx: &mut self.wasi,
table: &mut self.table,
}
}
}
type SourceWasmError = crate::source_bindings::camel::plugin::types::WasmError;
impl crate::source_bindings::camel::plugin::source_host::HostHttpListener for SourceHostState {
fn drop(
&mut self,
_resource: wasmtime::component::Resource<HttpListenerHandle>,
) -> wasmtime::Result<()> {
Ok(())
}
}
impl crate::source_bindings::camel::plugin::source_host::Host for SourceHostState {
fn accept_http(
&mut self,
_listener: wasmtime::component::Resource<HttpListenerHandle>,
) -> Result<Option<HttpRequest>, SourceWasmError> {
match self.request_rx.blocking_recv() {
Some(req) => Ok(Some(req)),
None => Ok(None),
}
}
fn submit_exchange(
&mut self,
exchange: WasmExchange,
) -> Result<SubmitOutcome, SourceWasmError> {
let (reply_tx, reply_rx) = oneshot::channel();
if self
.exchange_tx
.blocking_send((exchange, reply_tx))
.is_err()
{
return Ok(SubmitOutcome::Stopped);
}
match reply_rx.blocking_recv() {
Ok(outcome) => Ok(outcome),
Err(_) => Ok(SubmitOutcome::Stopped),
}
}
fn is_cancelled(&mut self) -> bool {
self.cancel_token.is_cancelled()
}
}
pub fn add_to_linker(linker: &mut Linker<SourceHostState>) -> Result<(), wasmtime::Error> {
wasmtime_wasi::p2::add_to_linker_sync(linker)?;
crate::source_bindings::camel::plugin::source_host::add_to_linker::<_, HasSelf<_>>(
linker,
|state| state,
)?;
Ok(())
}
const MAX_BODY_BYTES: usize = 10 * 1024 * 1024;
pub async fn run_http_listener(
listener: tokio::net::TcpListener,
path_filter: Option<String>,
request_tx: mpsc::Sender<HttpRequest>,
cancel: CancellationToken,
) -> Result<(), CamelError> {
use axum::Router;
use axum::extract::State;
use axum::http::Request;
use axum::response::Response;
use axum::routing::any;
struct ListenerState {
tx: mpsc::Sender<HttpRequest>,
}
async fn handler(
State(state): State<Arc<ListenerState>>,
req: Request<axum::body::Body>,
) -> Response {
let (parts, body) = req.into_parts();
let body_bytes = match axum::body::to_bytes(body, MAX_BODY_BYTES).await {
Ok(bytes) => bytes,
Err(e) => {
use std::error::Error as _;
let oversized = e
.source()
.is_some_and(|src| src.is::<http_body_util::LengthLimitError>());
let status = if oversized {
axum::http::StatusCode::PAYLOAD_TOO_LARGE
} else {
axum::http::StatusCode::BAD_REQUEST
};
tracing::debug!(status = %status, error = %e, "source HTTP body read failed");
return Response::builder()
.status(status)
.body(axum::body::Body::empty())
.unwrap(); }
};
let http_request = HttpRequest {
method: parts.method.to_string(),
path: parts.uri.path().to_string(),
headers: parts
.headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect(),
body: body_bytes.to_vec(),
};
if state.tx.send(http_request).await.is_err() {
return Response::builder()
.status(503)
.body(axum::body::Body::from("service unavailable"))
.unwrap(); }
Response::builder()
.status(202)
.body(axum::body::Body::from("accepted"))
.unwrap() }
let state = Arc::new(ListenerState { tx: request_tx });
let route_path = path_filter
.filter(|path| !path.is_empty())
.map(|path| {
if path.starts_with('/') {
path
} else {
format!("/{path}")
}
})
.unwrap_or_else(|| "/{*path}".to_string());
let app = Router::new()
.route(&route_path, any(handler))
.with_state(state);
let local = listener.local_addr().ok();
if let Some(addr) = &local {
tracing::info!(%addr, "source HTTP listener started");
}
axum::serve(listener, app)
.with_graceful_shutdown(async move { cancel.cancelled().await })
.await
.map_err(|e| CamelError::Io(format!("HTTP listener error: {e}")))?;
if let Some(addr) = &local {
tracing::info!(%addr, "source HTTP listener stopped");
}
Ok(())
}
fn to_plugin_wasm_exchange(
src: crate::source_bindings::camel::plugin::types::WasmExchange,
) -> crate::bindings::camel::plugin::types::WasmExchange {
use crate::bindings::camel::plugin::types as plugin;
use crate::source_bindings::camel::plugin::types as source;
let convert_body = |body: source::WasmBody| -> plugin::WasmBody {
match body {
source::WasmBody::Empty => plugin::WasmBody::Empty,
source::WasmBody::Text(s) => plugin::WasmBody::Text(s),
source::WasmBody::Bytes(b) => plugin::WasmBody::Bytes(b),
source::WasmBody::Json(s) => plugin::WasmBody::Json(s),
source::WasmBody::Xml(s) => plugin::WasmBody::Xml(s),
}
};
let convert_message = |msg: source::WasmMessage| -> plugin::WasmMessage {
plugin::WasmMessage {
headers: msg.headers,
body: convert_body(msg.body),
}
};
let convert_pattern = |p: source::WasmPattern| -> plugin::WasmPattern {
match p {
source::WasmPattern::InOnly => plugin::WasmPattern::InOnly,
source::WasmPattern::InOut => plugin::WasmPattern::InOut,
}
};
plugin::WasmExchange {
input: convert_message(src.input),
output: src.output.map(convert_message),
properties: src.properties,
pattern: convert_pattern(src.pattern),
correlation_id: src.correlation_id,
route_id: src.route_id,
message_id: src.message_id,
}
}
pub async fn run_pipeline_bridge(
mut exchange_rx: mpsc::Receiver<(WasmExchange, oneshot::Sender<SubmitOutcome>)>,
ctx: ConsumerContext,
) -> Result<(), CamelError> {
while let Some((wasm_exchange, reply_tx)) = exchange_rx.recv().await {
let plugin_exchange = to_plugin_wasm_exchange(wasm_exchange);
let mut exchange = Exchange::new(Message::default());
crate::serde_bridge::wasm_to_exchange(plugin_exchange, &mut exchange);
let outcome = match ctx.send(exchange).await {
Ok(()) => SubmitOutcome::Accepted,
Err(_) => SubmitOutcome::Stopped,
};
let _ = reply_tx.send(outcome);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_channels_new() {
let channels = SourceChannels::new();
assert!(!channels.request_tx.is_closed());
assert!(!channels.exchange_tx.is_closed());
}
#[test]
fn test_source_channels_default() {
let channels = SourceChannels::default();
assert!(!channels.request_tx.is_closed());
}
#[test]
fn test_request_channel_close_returns_none() {
let (tx, mut rx) = mpsc::channel::<HttpRequest>(1);
drop(tx);
let result = std::thread::spawn(move || rx.blocking_recv())
.join()
.unwrap();
assert!(result.is_none());
}
#[test]
fn test_exchange_channel_close_detected() {
let (tx, rx) = mpsc::channel::<(WasmExchange, oneshot::Sender<SubmitOutcome>)>(1);
drop(rx);
assert!(tx.is_closed());
}
#[test]
fn test_cancel_token_is_cancelled() {
let token = CancellationToken::new();
assert!(!token.is_cancelled());
token.cancel();
assert!(token.is_cancelled());
}
#[test]
fn test_submit_outcome_variants() {
let accepted = SubmitOutcome::Accepted;
let stopped = SubmitOutcome::Stopped;
assert!(matches!(accepted, SubmitOutcome::Accepted));
assert!(matches!(stopped, SubmitOutcome::Stopped));
}
#[test]
fn test_to_plugin_wasm_exchange_converts_fields() {
use crate::source_bindings::camel::plugin::types as src;
let src_exchange = src::WasmExchange {
input: src::WasmMessage {
headers: vec![("key".to_string(), "val".to_string())],
body: src::WasmBody::Text("hello".to_string()),
},
output: None,
properties: vec![("p".to_string(), "v".to_string())],
pattern: src::WasmPattern::InOnly,
correlation_id: "corr-1".to_string(),
route_id: Some("route-1".to_string()),
message_id: Some("msg-1".to_string()),
};
let plugin = to_plugin_wasm_exchange(src_exchange);
assert_eq!(plugin.input.headers.len(), 1);
assert_eq!(plugin.correlation_id, "corr-1");
assert_eq!(plugin.route_id, Some("route-1".to_string()));
assert!(matches!(
plugin.pattern,
crate::bindings::camel::plugin::types::WasmPattern::InOnly
));
}
}