use txtx_addon_kit::reqwest::header::CONTENT_TYPE;
use txtx_addon_kit::reqwest::{self, Method};
use txtx_addon_kit::types::commands::{CommandExecutionFutureResult, PreCommandSpecification};
use txtx_addon_kit::types::frontend::{Actions, BlockEvent};
use txtx_addon_kit::types::stores::ValueStore;
use txtx_addon_kit::types::types::ObjectProperty;
use txtx_addon_kit::types::types::RunbookSupervisionContext;
use txtx_addon_kit::types::ConstructDid;
use txtx_addon_kit::types::{
commands::{CommandExecutionResult, CommandImplementation, CommandSpecification},
diagnostics::Diagnostic,
types::{Type, Value},
};
use txtx_addon_kit::{define_command, indoc};
lazy_static! {
pub static ref SEND_HTTP_REQUEST: PreCommandSpecification = define_command! {
SendHttpRequest => {
name: "Send an HTTP request",
matcher: "send_http_request",
documentation: "`send_http_request` command makes an HTTP request to the given URL and exports the response.",
implements_signing_capability: false,
implements_background_task_capability: false,
inputs: [
url: {
documentation: "The URL for the request. Supported schemes are http and https.",
typing: Type::string(),
optional: false,
tainting: true,
internal: false
},
request_body: {
documentation: "The request body as a string.",
typing: Type::string(),
optional: true,
tainting: true,
internal: false
},
method: {
documentation: indoc!{r#"
The HTTP Method for the request.
Allowed methods are a subset of methods defined in RFC7231: GET, HEAD, and POST.
POST support is only intended for read-only URLs, such as submitting a search."#},
typing: Type::string(),
optional: true,
tainting: true,
internal: false
},
request_timeout_ms: {
documentation: "The request timeout in milliseconds.",
typing: Type::integer(),
optional: true,
tainting: true,
internal: false
},
request_headers: {
documentation: "A map of request header field names and values.",
typing: Type::object(vec![ObjectProperty {
name: "Content-Type".into(),
documentation: "Content-Type".into(),
typing: Type::string(),
optional: true,
tainting: true,
internal: false,
}]),
optional: true,
tainting: true,
internal: false
}
],
outputs: [
response_body: {
documentation: "The response body returned as a string.",
typing: Type::string()
},
status_code: {
documentation: "The HTTP response status code.",
typing: Type::integer()
}
],
example: indoc!{r#"
action "example" "std::send_http_request" {
url = "https://example.com"
}
output "status" {
value = action.example.status_code
}
// > status: 200
"#},
}
};
}
pub struct SendHttpRequest;
impl CommandImplementation for SendHttpRequest {
fn check_instantiability(
_ctx: &CommandSpecification,
_args: Vec<Type>,
) -> Result<Type, Diagnostic> {
unimplemented!()
}
fn check_executability(
_construct_id: &ConstructDid,
_instance_name: &str,
_spec: &CommandSpecification,
_values: &ValueStore,
_supervision_context: &RunbookSupervisionContext,
) -> Result<Actions, Diagnostic> {
Ok(Actions::none())
}
fn run_execution(
_construct_id: &ConstructDid,
_spec: &CommandSpecification,
values: &ValueStore,
_progress_tx: &txtx_addon_kit::channel::Sender<BlockEvent>,
) -> CommandExecutionFutureResult {
let mut result = CommandExecutionResult::new();
let values = values.clone();
let url = values.get_expected_string("url")?.to_string();
let request_body = values.get_string("request_body").map(|v| v.to_string());
let method = {
let value = values.get_string("method").unwrap_or("GET");
Method::try_from(value).unwrap()
};
let request_headers = values
.get_value("request_headers")
.and_then(|value| Some(value.expect_object().clone()));
#[cfg(not(feature = "wasm"))]
let future = async move {
let client = reqwest::Client::new();
let mut req_builder = client.request(method, url);
req_builder = req_builder.header(CONTENT_TYPE, "application/json");
if let Some(request_headers) = request_headers {
for (k, v) in request_headers.iter() {
req_builder = req_builder.header(k, v.expect_string());
}
}
if let Some(request_body) = request_body {
req_builder = req_builder.body(request_body);
}
let res = req_builder.send().await.map_err(|e| {
Diagnostic::error_from_string(format!(
"unable to broadcast Stacks transaction - {e}"
))
})?;
let status_code = res.status();
let response_body = res.text().await.map_err(|e| {
Diagnostic::error_from_string(format!(
"Failed to parse broadcasted Stacks transaction result: {e}"
))
})?;
result
.outputs
.insert(format!("status_code"), Value::integer(status_code.as_u16().into()));
result.outputs.insert(format!("response_body"), Value::string(response_body));
Ok(result)
};
#[cfg(feature = "wasm")]
panic!("async commands are not enabled for wasm");
#[cfg(not(feature = "wasm"))]
Ok(Box::pin(future))
}
}