use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use dataflow_rs::engine::functions::AsyncFunctionHandler;
use dataflow_rs::engine::functions::HttpCallConfig;
use dataflow_rs::engine::task_context::TaskContext;
use dataflow_rs::engine::task_outcome::TaskOutcome;
use super::http_common::{self, build_url};
use super::schema::{FieldKind, FieldSchema};
use crate::connector::ConnectorRegistry;
const NAME: &str = "http_call";
pub struct HttpCallHandler {
pub registry: Arc<ConnectorRegistry>,
pub client: reqwest::Client,
}
#[async_trait]
impl AsyncFunctionHandler for HttpCallHandler {
type Input = HttpCallConfig;
async fn execute(
&self,
ctx: &mut TaskContext<'_>,
input: &HttpCallConfig,
) -> dataflow_rs::Result<TaskOutcome> {
let channel = super::extract_channel(ctx.message()).to_string();
super::connector_helpers::guarded_handler(
NAME,
&self.registry,
&input.connector,
&channel,
async move {
let connector_config =
super::connector_helpers::resolve_connector(&self.registry, &input.connector)
.await?;
let http_config = super::connector_helpers::require_http_connector(
connector_config.as_ref(),
&input.connector,
)?;
let method = super::to_reqwest_method(&input.method);
super::connector_helpers::require_method_allowed(
&http_config.operations,
method.as_str(),
&input.connector,
)?;
let path = input.resolve_path(ctx)?;
let url = build_url(&http_config.url, path.as_deref());
let body = input.resolve_body(ctx)?;
let timeout = Duration::from_millis(input.timeout_ms);
let retryable_method =
input.method.is_idempotent() || http_config.retry_non_idempotent;
let retry_config = &http_config.retry;
let max_retries = if retryable_method {
retry_config.max_retries
} else {
0
};
let policy = super::RetryPolicy {
max_retries,
retry_delay_ms: retry_config.retry_delay_ms,
deadline: Some(timeout.saturating_mul(max_retries.saturating_add(1))),
};
let response_body = super::retry_with_policy(policy, "HTTP call", || {
http_common::execute_request(
&self.client,
&method,
&url,
Some(&input.headers),
http_config,
body.as_ref(),
timeout,
)
})
.await?;
if let Some(ref response_path) = input.response_path {
ctx.set_json(response_path, &response_body);
}
Ok(TaskOutcome::Success)
},
)
.await
}
}
pub(super) const HTTP_CALL_FIELDS: &[FieldSchema] = &[
FieldSchema {
name: "connector",
description: "Name of the HTTP connector to call.",
kind: FieldKind::String,
required: true,
resolvable: false,
alias: None,
},
FieldSchema {
name: "method",
description: "HTTP method (GET, POST, PUT, DELETE, PATCH). Defaults to GET.",
kind: FieldKind::String,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "path",
description: "Static path appended to the connector's base URL.",
kind: FieldKind::String,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "path_logic",
description: "JSONLogic expression evaluated to derive the request path.",
kind: FieldKind::Any,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "headers",
description: "Additional request headers.",
kind: FieldKind::Object,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "body",
description: "Static request body (any JSON value).",
kind: FieldKind::Any,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "body_logic",
description: "JSONLogic expression evaluated to derive the request body.",
kind: FieldKind::Any,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "output",
description: "Dotted path where the response body is written. Omit to discard it. (Was `response_path` before 1.0; still accepted, but not alongside `output`.)",
kind: FieldKind::String,
required: false,
resolvable: false,
alias: Some("response_path"),
},
FieldSchema {
name: "timeout_ms",
description: "Request timeout in milliseconds. Defaults to 30000.",
kind: FieldKind::Number,
required: false,
resolvable: false,
alias: None,
},
];