mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-25 01:58:39 +02:00
extract shared yaak send flow and wire CLI
This commit is contained in:
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -8239,6 +8239,19 @@ dependencies = [
|
|||||||
"rustix 1.0.7",
|
"rustix 1.0.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"log 0.4.29",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
"yaak-http",
|
||||||
|
"yaak-models",
|
||||||
|
"yaak-templates",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaak-api"
|
name = "yaak-api"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -8290,6 +8303,7 @@ dependencies = [
|
|||||||
"ts-rs",
|
"ts-rs",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"yaak",
|
||||||
"yaak-api",
|
"yaak-api",
|
||||||
"yaak-common",
|
"yaak-common",
|
||||||
"yaak-core",
|
"yaak-core",
|
||||||
@@ -8324,6 +8338,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"yaak",
|
||||||
"yaak-crypto",
|
"yaak-crypto",
|
||||||
"yaak-http",
|
"yaak-http",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
|
"crates/yaak",
|
||||||
# Shared crates (no Tauri dependency)
|
# Shared crates (no Tauri dependency)
|
||||||
"crates/yaak-core",
|
"crates/yaak-core",
|
||||||
"crates/yaak-common",
|
"crates/yaak-common",
|
||||||
@@ -47,6 +48,7 @@ ts-rs = "11.1.0"
|
|||||||
|
|
||||||
# Internal crates - shared
|
# Internal crates - shared
|
||||||
yaak-core = { path = "crates/yaak-core" }
|
yaak-core = { path = "crates/yaak-core" }
|
||||||
|
yaak = { path = "crates/yaak" }
|
||||||
yaak-common = { path = "crates/yaak-common" }
|
yaak-common = { path = "crates/yaak-common" }
|
||||||
yaak-crypto = { path = "crates/yaak-crypto" }
|
yaak-crypto = { path = "crates/yaak-crypto" }
|
||||||
yaak-git = { path = "crates/yaak-git" }
|
yaak-git = { path = "crates/yaak-git" }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ log = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
|
yaak = { workspace = true }
|
||||||
yaak-crypto = { workspace = true }
|
yaak-crypto = { workspace = true }
|
||||||
yaak-http = { workspace = true }
|
yaak-http = { workspace = true }
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
|
|||||||
@@ -5,19 +5,13 @@ use crate::commands::json::{
|
|||||||
validate_create_id,
|
validate_create_id,
|
||||||
};
|
};
|
||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use log::info;
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use yaak_http::path_placeholders::apply_path_placeholders;
|
use yaak::send::{SendHttpRequestByIdParams, send_http_request_by_id};
|
||||||
use yaak_http::sender::{HttpSender, ReqwestSender};
|
use yaak_http::types::SendableHttpRequestOptions;
|
||||||
use yaak_http::types::{SendableHttpRequest, SendableHttpRequestOptions};
|
use yaak_models::models::HttpRequest;
|
||||||
use yaak_models::models::{Environment, HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
|
||||||
use yaak_models::render::make_vars_hashmap;
|
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
use yaak_plugins::events::{PluginContext, RenderPurpose};
|
use yaak_plugins::events::{PluginContext, RenderPurpose};
|
||||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||||
use yaak_templates::{RenderOptions, parse_and_render, render_json_value_raw};
|
|
||||||
|
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
ctx: &CliContext,
|
ctx: &CliContext,
|
||||||
@@ -179,11 +173,6 @@ pub async fn send_request_by_id(
|
|||||||
let request =
|
let request =
|
||||||
ctx.db().get_http_request(request_id).map_err(|e| format!("Failed to get request: {e}"))?;
|
ctx.db().get_http_request(request_id).map_err(|e| format!("Failed to get request: {e}"))?;
|
||||||
|
|
||||||
let environment_chain = ctx
|
|
||||||
.db()
|
|
||||||
.resolve_environments(&request.workspace_id, request.folder_id.as_deref(), environment)
|
|
||||||
.map_err(|e| format!("Failed to resolve environments: {e}"))?;
|
|
||||||
|
|
||||||
let plugin_context = PluginContext::new(None, Some(request.workspace_id.clone()));
|
let plugin_context = PluginContext::new(None, Some(request.workspace_id.clone()));
|
||||||
let template_callback = PluginTemplateCallback::new(
|
let template_callback = PluginTemplateCallback::new(
|
||||||
ctx.plugin_manager(),
|
ctx.plugin_manager(),
|
||||||
@@ -192,147 +181,49 @@ pub async fn send_request_by_id(
|
|||||||
RenderPurpose::Send,
|
RenderPurpose::Send,
|
||||||
);
|
);
|
||||||
|
|
||||||
let rendered_request = render_http_request(
|
|
||||||
&request,
|
|
||||||
environment_chain,
|
|
||||||
&template_callback,
|
|
||||||
&RenderOptions::throw(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to render request templates: {e}"))?;
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
println!("> {} {}", rendered_request.method, rendered_request.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
let sendable = SendableHttpRequest::from_http_request(
|
|
||||||
&rendered_request,
|
|
||||||
SendableHttpRequestOptions::default(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to build request: {e}"))?;
|
|
||||||
|
|
||||||
let (event_tx, mut event_rx) = mpsc::channel(100);
|
let (event_tx, mut event_rx) = mpsc::channel(100);
|
||||||
|
let event_handle = tokio::spawn(async move {
|
||||||
let verbose_handle = if verbose {
|
while let Some(event) = event_rx.recv().await {
|
||||||
Some(tokio::spawn(async move {
|
if verbose {
|
||||||
while let Some(event) = event_rx.recv().await {
|
|
||||||
println!("{}", event);
|
println!("{}", event);
|
||||||
}
|
}
|
||||||
}))
|
}
|
||||||
} else {
|
});
|
||||||
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
let response_dir = ctx.data_dir().join("responses");
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let sender = ReqwestSender::new().map_err(|e| format!("Failed to create HTTP client: {e}"))?;
|
let result = send_http_request_by_id(SendHttpRequestByIdParams {
|
||||||
let response = sender
|
query_manager: ctx.query_manager(),
|
||||||
.send(sendable, event_tx)
|
blob_manager: ctx.blob_manager(),
|
||||||
.await
|
request_id,
|
||||||
.map_err(|e| format!("Failed to send request: {e}"))?;
|
environment_id: environment,
|
||||||
|
template_callback: &template_callback,
|
||||||
|
send_options: SendableHttpRequestOptions::default(),
|
||||||
|
update_source: UpdateSource::Sync,
|
||||||
|
response_dir: &response_dir,
|
||||||
|
persist_events: true,
|
||||||
|
emit_events_to: Some(event_tx),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
if let Some(handle) = verbose_handle {
|
let _ = event_handle.await;
|
||||||
let _ = handle.await;
|
let result = result.map_err(|e| e.to_string())?;
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
if verbose {
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
println!("HTTP {} {}", response.status, response.status_reason.as_deref().unwrap_or(""));
|
println!(
|
||||||
|
"HTTP {} {}",
|
||||||
|
result.response.status,
|
||||||
|
result.response.status_reason.as_deref().unwrap_or("")
|
||||||
|
);
|
||||||
if verbose {
|
if verbose {
|
||||||
for (name, value) in &response.headers {
|
for header in &result.response.headers {
|
||||||
println!("{}: {}", name, value);
|
println!("{}: {}", header.name, header.value);
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
let body = String::from_utf8(result.response_body)
|
||||||
let (body, _stats) =
|
.map_err(|e| format!("Failed to read response body: {e}"))?;
|
||||||
response.text().await.map_err(|e| format!("Failed to read response body: {e}"))?;
|
|
||||||
println!("{}", body);
|
println!("{}", body);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render an HTTP request with template variables and plugin functions.
|
|
||||||
async fn render_http_request(
|
|
||||||
request: &HttpRequest,
|
|
||||||
environment_chain: Vec<Environment>,
|
|
||||||
callback: &PluginTemplateCallback,
|
|
||||||
options: &RenderOptions,
|
|
||||||
) -> yaak_templates::error::Result<HttpRequest> {
|
|
||||||
let vars = &make_vars_hashmap(environment_chain);
|
|
||||||
|
|
||||||
let mut url_parameters = Vec::new();
|
|
||||||
for parameter in request.url_parameters.clone() {
|
|
||||||
if !parameter.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
url_parameters.push(HttpUrlParameter {
|
|
||||||
enabled: parameter.enabled,
|
|
||||||
name: parse_and_render(parameter.name.as_str(), vars, callback, options).await?,
|
|
||||||
value: parse_and_render(parameter.value.as_str(), vars, callback, options).await?,
|
|
||||||
id: parameter.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut headers = Vec::new();
|
|
||||||
for header in request.headers.clone() {
|
|
||||||
if !header.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.push(HttpRequestHeader {
|
|
||||||
enabled: header.enabled,
|
|
||||||
name: parse_and_render(header.name.as_str(), vars, callback, options).await?,
|
|
||||||
value: parse_and_render(header.value.as_str(), vars, callback, options).await?,
|
|
||||||
id: header.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut body = BTreeMap::new();
|
|
||||||
for (key, value) in request.body.clone() {
|
|
||||||
body.insert(key, render_json_value_raw(value, vars, callback, options).await?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let authentication = {
|
|
||||||
let mut disabled = false;
|
|
||||||
let mut auth = BTreeMap::new();
|
|
||||||
|
|
||||||
match request.authentication.get("disabled") {
|
|
||||||
Some(Value::Bool(true)) => {
|
|
||||||
disabled = true;
|
|
||||||
}
|
|
||||||
Some(Value::String(template)) => {
|
|
||||||
disabled = parse_and_render(template.as_str(), vars, callback, options)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
.is_empty();
|
|
||||||
info!(
|
|
||||||
"Rendering authentication.disabled as a template: {disabled} from \"{template}\""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if disabled {
|
|
||||||
auth.insert("disabled".to_string(), Value::Bool(true));
|
|
||||||
} else {
|
|
||||||
for (key, value) in request.authentication.clone() {
|
|
||||||
if key == "disabled" {
|
|
||||||
auth.insert(key, Value::Bool(false));
|
|
||||||
} else {
|
|
||||||
auth.insert(key, render_json_value_raw(value, vars, callback, options).await?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auth
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = parse_and_render(request.url.clone().as_str(), vars, callback, options).await?;
|
|
||||||
|
|
||||||
let (url, url_parameters) = apply_path_placeholders(&url, &url_parameters);
|
|
||||||
|
|
||||||
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..request.to_owned() })
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
|
use yaak_models::blob_manager::BlobManager;
|
||||||
use yaak_models::db_context::DbContext;
|
use yaak_models::db_context::DbContext;
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_plugins::events::PluginContext;
|
use yaak_plugins::events::PluginContext;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
pub struct CliContext {
|
pub struct CliContext {
|
||||||
|
data_dir: PathBuf,
|
||||||
query_manager: QueryManager,
|
query_manager: QueryManager,
|
||||||
|
blob_manager: BlobManager,
|
||||||
pub encryption_manager: Arc<EncryptionManager>,
|
pub encryption_manager: Arc<EncryptionManager>,
|
||||||
plugin_manager: Option<Arc<PluginManager>>,
|
plugin_manager: Option<Arc<PluginManager>>,
|
||||||
}
|
}
|
||||||
@@ -17,9 +20,8 @@ impl CliContext {
|
|||||||
let db_path = data_dir.join("db.sqlite");
|
let db_path = data_dir.join("db.sqlite");
|
||||||
let blob_path = data_dir.join("blobs.sqlite");
|
let blob_path = data_dir.join("blobs.sqlite");
|
||||||
|
|
||||||
let (query_manager, _blob_manager, _rx) =
|
let (query_manager, blob_manager, _rx) = yaak_models::init_standalone(&db_path, &blob_path)
|
||||||
yaak_models::init_standalone(&db_path, &blob_path)
|
.expect("Failed to initialize database");
|
||||||
.expect("Failed to initialize database");
|
|
||||||
|
|
||||||
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||||
|
|
||||||
@@ -63,13 +65,25 @@ impl CliContext {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
Self { query_manager, encryption_manager, plugin_manager }
|
Self { data_dir, query_manager, blob_manager, encryption_manager, plugin_manager }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_dir(&self) -> &Path {
|
||||||
|
&self.data_dir
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn db(&self) -> DbContext<'_> {
|
pub fn db(&self) -> DbContext<'_> {
|
||||||
self.query_manager.connect()
|
self.query_manager.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn query_manager(&self) -> &QueryManager {
|
||||||
|
&self.query_manager
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blob_manager(&self) -> &BlobManager {
|
||||||
|
&self.blob_manager
|
||||||
|
}
|
||||||
|
|
||||||
pub fn plugin_manager(&self) -> Arc<PluginManager> {
|
pub fn plugin_manager(&self) -> Arc<PluginManager> {
|
||||||
self.plugin_manager.clone().expect("Plugin manager was not initialized for this command")
|
self.plugin_manager.clone().expect("Plugin manager was not initialized for this command")
|
||||||
}
|
}
|
||||||
|
|||||||
42
crates-cli/yaak-cli/tests/common/http_server.rs
Normal file
42
crates-cli/yaak-cli/tests/common/http_server.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpListener;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
pub struct TestHttpServer {
|
||||||
|
pub url: String,
|
||||||
|
handle: Option<thread::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestHttpServer {
|
||||||
|
pub fn spawn_ok(body: &'static str) -> Self {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind test HTTP server");
|
||||||
|
let addr = listener.local_addr().expect("Failed to get local addr");
|
||||||
|
let url = format!("http://{addr}/test");
|
||||||
|
let body_bytes = body.as_bytes().to_vec();
|
||||||
|
|
||||||
|
let handle = thread::spawn(move || {
|
||||||
|
if let Ok((mut stream, _)) = listener.accept() {
|
||||||
|
let mut request_buf = [0u8; 4096];
|
||||||
|
let _ = stream.read(&mut request_buf);
|
||||||
|
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||||
|
body_bytes.len()
|
||||||
|
);
|
||||||
|
let _ = stream.write_all(response.as_bytes());
|
||||||
|
let _ = stream.write_all(&body_bytes);
|
||||||
|
let _ = stream.flush();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self { url, handle: Some(handle) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TestHttpServer {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(handle) = self.handle.take() {
|
||||||
|
let _ = handle.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
pub mod http_server;
|
||||||
|
|
||||||
use assert_cmd::Command;
|
use assert_cmd::Command;
|
||||||
use assert_cmd::cargo::cargo_bin_cmd;
|
use assert_cmd::cargo::cargo_bin_cmd;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
|
use common::http_server::TestHttpServer;
|
||||||
use common::{cli_cmd, parse_created_id, query_manager, seed_request, seed_workspace};
|
use common::{cli_cmd, parse_created_id, query_manager, seed_request, seed_workspace};
|
||||||
use predicates::str::contains;
|
use predicates::str::contains;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
use yaak_models::models::HttpResponseState;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn show_and_delete_yes_round_trip() {
|
fn show_and_delete_yes_round_trip() {
|
||||||
@@ -105,3 +107,53 @@ fn update_requires_id_in_json_payload() {
|
|||||||
.failure()
|
.failure()
|
||||||
.stderr(contains("request update requires a non-empty \"id\" field"));
|
.stderr(contains("request update requires a non-empty \"id\" field"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_send_persists_response_body_and_events() {
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
|
let data_dir = temp_dir.path();
|
||||||
|
seed_workspace(data_dir, "wk_test");
|
||||||
|
|
||||||
|
let server = TestHttpServer::spawn_ok("hello from integration test");
|
||||||
|
|
||||||
|
let create_assert = cli_cmd(data_dir)
|
||||||
|
.args([
|
||||||
|
"request",
|
||||||
|
"create",
|
||||||
|
"wk_test",
|
||||||
|
"--name",
|
||||||
|
"Send Test",
|
||||||
|
"--url",
|
||||||
|
&server.url,
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
let request_id = parse_created_id(&create_assert.get_output().stdout, "request create");
|
||||||
|
|
||||||
|
cli_cmd(data_dir)
|
||||||
|
.args(["request", "send", &request_id])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("HTTP 200 OK"))
|
||||||
|
.stdout(contains("hello from integration test"));
|
||||||
|
|
||||||
|
let qm = query_manager(data_dir);
|
||||||
|
let db = qm.connect();
|
||||||
|
let responses =
|
||||||
|
db.list_http_responses_for_request(&request_id, None).expect("Failed to load responses");
|
||||||
|
assert_eq!(responses.len(), 1, "expected exactly one persisted response");
|
||||||
|
|
||||||
|
let response = &responses[0];
|
||||||
|
assert_eq!(response.status, 200);
|
||||||
|
assert!(matches!(response.state, HttpResponseState::Closed));
|
||||||
|
assert!(response.error.is_none());
|
||||||
|
|
||||||
|
let body_path =
|
||||||
|
response.body_path.as_ref().expect("expected persisted response body path").to_string();
|
||||||
|
let body = std::fs::read_to_string(&body_path).expect("Failed to read response body file");
|
||||||
|
assert_eq!(body, "hello from integration test");
|
||||||
|
|
||||||
|
let events =
|
||||||
|
db.list_http_response_events(&response.id).expect("Failed to load response events");
|
||||||
|
assert!(!events.is_empty(), "expected at least one persisted response event");
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ yaak-api = { workspace = true }
|
|||||||
yaak-common = { workspace = true }
|
yaak-common = { workspace = true }
|
||||||
yaak-tauri-utils = { workspace = true }
|
yaak-tauri-utils = { workspace = true }
|
||||||
yaak-core = { workspace = true }
|
yaak-core = { workspace = true }
|
||||||
|
yaak = { workspace = true }
|
||||||
yaak-crypto = { workspace = true }
|
yaak-crypto = { workspace = true }
|
||||||
yaak-fonts = { workspace = true }
|
yaak-fonts = { workspace = true }
|
||||||
yaak-git = { workspace = true }
|
yaak-git = { workspace = true }
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
use log::info;
|
use log::info;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use yaak_http::path_placeholders::apply_path_placeholders;
|
pub use yaak::render::render_http_request;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader};
|
||||||
Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
|
|
||||||
};
|
|
||||||
use yaak_models::render::make_vars_hashmap;
|
use yaak_models::render::make_vars_hashmap;
|
||||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
@@ -85,151 +83,3 @@ pub async fn render_grpc_request<T: TemplateCallback>(
|
|||||||
|
|
||||||
Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() })
|
Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_http_request<T: TemplateCallback>(
|
|
||||||
r: &HttpRequest,
|
|
||||||
environment_chain: Vec<Environment>,
|
|
||||||
cb: &T,
|
|
||||||
opt: &RenderOptions,
|
|
||||||
) -> yaak_templates::error::Result<HttpRequest> {
|
|
||||||
let vars = &make_vars_hashmap(environment_chain);
|
|
||||||
|
|
||||||
let mut url_parameters = Vec::new();
|
|
||||||
for p in r.url_parameters.clone() {
|
|
||||||
if !p.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
url_parameters.push(HttpUrlParameter {
|
|
||||||
enabled: p.enabled,
|
|
||||||
name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?,
|
|
||||||
value: parse_and_render(p.value.as_str(), vars, cb, &opt).await?,
|
|
||||||
id: p.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut headers = Vec::new();
|
|
||||||
for p in r.headers.clone() {
|
|
||||||
if !p.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
headers.push(HttpRequestHeader {
|
|
||||||
enabled: p.enabled,
|
|
||||||
name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?,
|
|
||||||
value: parse_and_render(p.value.as_str(), vars, cb, &opt).await?,
|
|
||||||
id: p.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut body = BTreeMap::new();
|
|
||||||
for (k, v) in r.body.clone() {
|
|
||||||
let v = if k == "form" { strip_disabled_form_entries(v) } else { v };
|
|
||||||
body.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let authentication = {
|
|
||||||
let mut disabled = false;
|
|
||||||
let mut auth = BTreeMap::new();
|
|
||||||
match r.authentication.get("disabled") {
|
|
||||||
Some(Value::Bool(true)) => {
|
|
||||||
disabled = true;
|
|
||||||
}
|
|
||||||
Some(Value::String(tmpl)) => {
|
|
||||||
disabled = parse_and_render(tmpl.as_str(), vars, cb, &opt)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
.is_empty();
|
|
||||||
info!(
|
|
||||||
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if disabled {
|
|
||||||
auth.insert("disabled".to_string(), Value::Bool(true));
|
|
||||||
} else {
|
|
||||||
for (k, v) in r.authentication.clone() {
|
|
||||||
if k == "disabled" {
|
|
||||||
auth.insert(k, Value::Bool(false));
|
|
||||||
} else {
|
|
||||||
auth.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auth
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = parse_and_render(r.url.clone().as_str(), vars, cb, &opt).await?;
|
|
||||||
|
|
||||||
// This doesn't fit perfectly with the concept of "rendering" but it kind of does
|
|
||||||
let (url, url_parameters) = apply_path_placeholders(&url, &url_parameters);
|
|
||||||
|
|
||||||
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..r.to_owned() })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Strip disabled entries from a JSON array of form objects.
|
|
||||||
fn strip_disabled_form_entries(v: Value) -> Value {
|
|
||||||
match v {
|
|
||||||
Value::Array(items) => Value::Array(
|
|
||||||
items
|
|
||||||
.into_iter()
|
|
||||||
.filter(|item| item.get("enabled").and_then(|e| e.as_bool()).unwrap_or(true))
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
v => v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_strip_disabled_form_entries() {
|
|
||||||
let input = json!([
|
|
||||||
{"enabled": true, "name": "foo", "value": "bar"},
|
|
||||||
{"enabled": false, "name": "disabled", "value": "gone"},
|
|
||||||
{"enabled": true, "name": "baz", "value": "qux"},
|
|
||||||
]);
|
|
||||||
let result = strip_disabled_form_entries(input);
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
json!([
|
|
||||||
{"enabled": true, "name": "foo", "value": "bar"},
|
|
||||||
{"enabled": true, "name": "baz", "value": "qux"},
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_strip_disabled_form_entries_all_disabled() {
|
|
||||||
let input = json!([
|
|
||||||
{"enabled": false, "name": "a", "value": "b"},
|
|
||||||
{"enabled": false, "name": "c", "value": "d"},
|
|
||||||
]);
|
|
||||||
let result = strip_disabled_form_entries(input);
|
|
||||||
assert_eq!(result, json!([]));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_strip_disabled_form_entries_missing_enabled_defaults_to_kept() {
|
|
||||||
let input = json!([
|
|
||||||
{"name": "no_enabled_field", "value": "kept"},
|
|
||||||
{"enabled": false, "name": "disabled", "value": "gone"},
|
|
||||||
]);
|
|
||||||
let result = strip_disabled_form_entries(input);
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
json!([
|
|
||||||
{"name": "no_enabled_field", "value": "kept"},
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_strip_disabled_form_entries_non_array_passthrough() {
|
|
||||||
let input = json!("just a string");
|
|
||||||
let result = strip_disabled_form_entries(input.clone());
|
|
||||||
assert_eq!(result, input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
14
crates/yaak/Cargo.toml
Normal file
14
crates/yaak/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "yaak"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["sync", "rt"] }
|
||||||
|
yaak-http = { workspace = true }
|
||||||
|
yaak-models = { workspace = true }
|
||||||
|
yaak-templates = { workspace = true }
|
||||||
9
crates/yaak/src/error.rs
Normal file
9
crates/yaak/src/error.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
Send(#[from] crate::send::SendHttpRequestError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
6
crates/yaak/src/lib.rs
Normal file
6
crates/yaak/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod error;
|
||||||
|
pub mod render;
|
||||||
|
pub mod send;
|
||||||
|
|
||||||
|
pub use error::Error;
|
||||||
|
pub type Result<T> = error::Result<T>;
|
||||||
157
crates/yaak/src/render.rs
Normal file
157
crates/yaak/src/render.rs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
use log::info;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use yaak_http::path_placeholders::apply_path_placeholders;
|
||||||
|
use yaak_models::models::{Environment, HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
||||||
|
use yaak_models::render::make_vars_hashmap;
|
||||||
|
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
|
pub async fn render_http_request<T: TemplateCallback>(
|
||||||
|
request: &HttpRequest,
|
||||||
|
environment_chain: Vec<Environment>,
|
||||||
|
callback: &T,
|
||||||
|
options: &RenderOptions,
|
||||||
|
) -> yaak_templates::error::Result<HttpRequest> {
|
||||||
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
|
|
||||||
|
let mut url_parameters = Vec::new();
|
||||||
|
for parameter in request.url_parameters.clone() {
|
||||||
|
if !parameter.enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
url_parameters.push(HttpUrlParameter {
|
||||||
|
enabled: parameter.enabled,
|
||||||
|
name: parse_and_render(parameter.name.as_str(), vars, callback, options).await?,
|
||||||
|
value: parse_and_render(parameter.value.as_str(), vars, callback, options).await?,
|
||||||
|
id: parameter.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut headers = Vec::new();
|
||||||
|
for header in request.headers.clone() {
|
||||||
|
if !header.enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.push(HttpRequestHeader {
|
||||||
|
enabled: header.enabled,
|
||||||
|
name: parse_and_render(header.name.as_str(), vars, callback, options).await?,
|
||||||
|
value: parse_and_render(header.value.as_str(), vars, callback, options).await?,
|
||||||
|
id: header.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = BTreeMap::new();
|
||||||
|
for (key, value) in request.body.clone() {
|
||||||
|
let value = if key == "form" { strip_disabled_form_entries(value) } else { value };
|
||||||
|
body.insert(key, render_json_value_raw(value, vars, callback, options).await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let authentication = {
|
||||||
|
let mut disabled = false;
|
||||||
|
let mut auth = BTreeMap::new();
|
||||||
|
|
||||||
|
match request.authentication.get("disabled") {
|
||||||
|
Some(Value::Bool(true)) => {
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
Some(Value::String(template)) => {
|
||||||
|
disabled = parse_and_render(template.as_str(), vars, callback, options)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.is_empty();
|
||||||
|
info!(
|
||||||
|
"Rendering authentication.disabled as a template: {disabled} from \"{template}\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if disabled {
|
||||||
|
auth.insert("disabled".to_string(), Value::Bool(true));
|
||||||
|
} else {
|
||||||
|
for (key, value) in request.authentication.clone() {
|
||||||
|
if key == "disabled" {
|
||||||
|
auth.insert(key, Value::Bool(false));
|
||||||
|
} else {
|
||||||
|
auth.insert(key, render_json_value_raw(value, vars, callback, options).await?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = parse_and_render(request.url.clone().as_str(), vars, callback, options).await?;
|
||||||
|
let (url, url_parameters) = apply_path_placeholders(&url, &url_parameters);
|
||||||
|
|
||||||
|
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..request.to_owned() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_disabled_form_entries(v: Value) -> Value {
|
||||||
|
match v {
|
||||||
|
Value::Array(items) => Value::Array(
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.filter(|item| item.get("enabled").and_then(|e| e.as_bool()).unwrap_or(true))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
v => v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_strip_disabled_form_entries() {
|
||||||
|
let input = json!([
|
||||||
|
{"enabled": true, "name": "foo", "value": "bar"},
|
||||||
|
{"enabled": false, "name": "disabled", "value": "gone"},
|
||||||
|
{"enabled": true, "name": "baz", "value": "qux"},
|
||||||
|
]);
|
||||||
|
let result = strip_disabled_form_entries(input);
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
json!([
|
||||||
|
{"enabled": true, "name": "foo", "value": "bar"},
|
||||||
|
{"enabled": true, "name": "baz", "value": "qux"},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_strip_disabled_form_entries_all_disabled() {
|
||||||
|
let input = json!([
|
||||||
|
{"enabled": false, "name": "a", "value": "b"},
|
||||||
|
{"enabled": false, "name": "c", "value": "d"},
|
||||||
|
]);
|
||||||
|
let result = strip_disabled_form_entries(input);
|
||||||
|
assert_eq!(result, json!([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_strip_disabled_form_entries_missing_enabled_defaults_to_kept() {
|
||||||
|
let input = json!([
|
||||||
|
{"name": "no_enabled_field", "value": "kept"},
|
||||||
|
{"enabled": false, "name": "disabled", "value": "gone"},
|
||||||
|
]);
|
||||||
|
let result = strip_disabled_form_entries(input);
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
json!([
|
||||||
|
{"name": "no_enabled_field", "value": "kept"},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_strip_disabled_form_entries_non_array_passthrough() {
|
||||||
|
let input = json!("just a string");
|
||||||
|
let result = strip_disabled_form_entries(input.clone());
|
||||||
|
assert_eq!(result, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
310
crates/yaak/src/send.rs
Normal file
310
crates/yaak/src/send.rs
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
use crate::render::render_http_request;
|
||||||
|
use log::warn;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Instant;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use yaak_http::sender::{HttpResponseEvent as SenderHttpResponseEvent, HttpSender, ReqwestSender};
|
||||||
|
use yaak_http::types::{SendableBody, SendableHttpRequest, SendableHttpRequestOptions};
|
||||||
|
use yaak_models::blob_manager::BlobManager;
|
||||||
|
use yaak_models::models::{HttpResponse, HttpResponseEvent, HttpResponseHeader, HttpResponseState};
|
||||||
|
use yaak_models::query_manager::QueryManager;
|
||||||
|
use yaak_models::util::UpdateSource;
|
||||||
|
use yaak_templates::{RenderOptions, TemplateCallback};
|
||||||
|
|
||||||
|
const HTTP_EVENT_CHANNEL_CAPACITY: usize = 100;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SendHttpRequestError {
|
||||||
|
#[error("Failed to load request: {0}")]
|
||||||
|
LoadRequest(#[source] yaak_models::error::Error),
|
||||||
|
|
||||||
|
#[error("Failed to resolve environments: {0}")]
|
||||||
|
ResolveEnvironments(#[source] yaak_models::error::Error),
|
||||||
|
|
||||||
|
#[error("Failed to resolve inherited request settings: {0}")]
|
||||||
|
ResolveRequestInheritance(#[source] yaak_models::error::Error),
|
||||||
|
|
||||||
|
#[error("Failed to render request templates: {0}")]
|
||||||
|
RenderRequest(#[source] yaak_templates::error::Error),
|
||||||
|
|
||||||
|
#[error("Failed to persist response metadata: {0}")]
|
||||||
|
PersistResponse(#[source] yaak_models::error::Error),
|
||||||
|
|
||||||
|
#[error("Failed to create HTTP client: {0}")]
|
||||||
|
CreateHttpClient(#[source] yaak_http::error::Error),
|
||||||
|
|
||||||
|
#[error("Failed to build sendable request: {0}")]
|
||||||
|
BuildSendableRequest(#[source] yaak_http::error::Error),
|
||||||
|
|
||||||
|
#[error("Failed to send request: {0}")]
|
||||||
|
SendRequest(#[source] yaak_http::error::Error),
|
||||||
|
|
||||||
|
#[error("Failed to read response body: {0}")]
|
||||||
|
ReadResponseBody(#[source] yaak_http::error::Error),
|
||||||
|
|
||||||
|
#[error("Failed to create response directory {path:?}: {source}")]
|
||||||
|
CreateResponseDirectory {
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("Failed to write response body to {path:?}: {source}")]
|
||||||
|
WriteResponseBody {
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, SendHttpRequestError>;
|
||||||
|
|
||||||
|
pub struct SendHttpRequestByIdParams<'a, T: TemplateCallback> {
|
||||||
|
pub query_manager: &'a QueryManager,
|
||||||
|
pub blob_manager: &'a BlobManager,
|
||||||
|
pub request_id: &'a str,
|
||||||
|
pub environment_id: Option<&'a str>,
|
||||||
|
pub template_callback: &'a T,
|
||||||
|
pub send_options: SendableHttpRequestOptions,
|
||||||
|
pub update_source: UpdateSource,
|
||||||
|
pub response_dir: &'a Path,
|
||||||
|
pub persist_events: bool,
|
||||||
|
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SendHttpRequestResult {
|
||||||
|
pub rendered_request: yaak_models::models::HttpRequest,
|
||||||
|
pub response: HttpResponse,
|
||||||
|
pub response_body: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_http_request_by_id<T: TemplateCallback>(
|
||||||
|
params: SendHttpRequestByIdParams<'_, T>,
|
||||||
|
) -> Result<SendHttpRequestResult> {
|
||||||
|
let db = params.query_manager.connect();
|
||||||
|
let request =
|
||||||
|
db.get_http_request(params.request_id).map_err(SendHttpRequestError::LoadRequest)?;
|
||||||
|
let environment_chain = db
|
||||||
|
.resolve_environments(
|
||||||
|
&request.workspace_id,
|
||||||
|
request.folder_id.as_deref(),
|
||||||
|
params.environment_id,
|
||||||
|
)
|
||||||
|
.map_err(SendHttpRequestError::ResolveEnvironments)?;
|
||||||
|
|
||||||
|
let (authentication_type, authentication, _auth_context_id) = db
|
||||||
|
.resolve_auth_for_http_request(&request)
|
||||||
|
.map_err(SendHttpRequestError::ResolveRequestInheritance)?;
|
||||||
|
let resolved_headers = db
|
||||||
|
.resolve_headers_for_http_request(&request)
|
||||||
|
.map_err(SendHttpRequestError::ResolveRequestInheritance)?;
|
||||||
|
drop(db);
|
||||||
|
|
||||||
|
let mut resolved_request = request.clone();
|
||||||
|
resolved_request.authentication_type = authentication_type;
|
||||||
|
resolved_request.authentication = authentication;
|
||||||
|
resolved_request.headers = resolved_headers;
|
||||||
|
|
||||||
|
let rendered_request = render_http_request(
|
||||||
|
&resolved_request,
|
||||||
|
environment_chain,
|
||||||
|
params.template_callback,
|
||||||
|
&RenderOptions::throw(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(SendHttpRequestError::RenderRequest)?;
|
||||||
|
|
||||||
|
let sendable_request =
|
||||||
|
SendableHttpRequest::from_http_request(&rendered_request, params.send_options)
|
||||||
|
.await
|
||||||
|
.map_err(SendHttpRequestError::BuildSendableRequest)?;
|
||||||
|
let request_content_length = sendable_body_length(sendable_request.body.as_ref());
|
||||||
|
|
||||||
|
let mut response = params
|
||||||
|
.query_manager
|
||||||
|
.connect()
|
||||||
|
.upsert_http_response(
|
||||||
|
&HttpResponse {
|
||||||
|
request_id: request.id.clone(),
|
||||||
|
workspace_id: request.workspace_id.clone(),
|
||||||
|
request_content_length,
|
||||||
|
request_headers: sendable_request
|
||||||
|
.headers
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| HttpResponseHeader {
|
||||||
|
name: name.clone(),
|
||||||
|
value: value.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
url: sendable_request.url.clone(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
¶ms.update_source,
|
||||||
|
params.blob_manager,
|
||||||
|
)
|
||||||
|
.map_err(SendHttpRequestError::PersistResponse)?;
|
||||||
|
|
||||||
|
let (event_tx, mut event_rx) =
|
||||||
|
mpsc::channel::<SenderHttpResponseEvent>(HTTP_EVENT_CHANNEL_CAPACITY);
|
||||||
|
let event_query_manager = params.query_manager.clone();
|
||||||
|
let event_response_id = response.id.clone();
|
||||||
|
let event_workspace_id = request.workspace_id.clone();
|
||||||
|
let event_update_source = params.update_source.clone();
|
||||||
|
let emit_events_to = params.emit_events_to.clone();
|
||||||
|
let persist_events = params.persist_events;
|
||||||
|
let event_handle = tokio::spawn(async move {
|
||||||
|
while let Some(event) = event_rx.recv().await {
|
||||||
|
if persist_events {
|
||||||
|
let db_event = HttpResponseEvent::new(
|
||||||
|
&event_response_id,
|
||||||
|
&event_workspace_id,
|
||||||
|
event.clone().into(),
|
||||||
|
);
|
||||||
|
if let Err(err) = event_query_manager
|
||||||
|
.connect()
|
||||||
|
.upsert_http_response_event(&db_event, &event_update_source)
|
||||||
|
{
|
||||||
|
warn!("Failed to persist HTTP response event: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tx) = emit_events_to.as_ref() {
|
||||||
|
let _ = tx.try_send(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let sender = ReqwestSender::new().map_err(SendHttpRequestError::CreateHttpClient)?;
|
||||||
|
let started_at = Instant::now();
|
||||||
|
let request_started_url = sendable_request.url.clone();
|
||||||
|
|
||||||
|
let http_response = match sender.send(sendable_request, event_tx).await {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(err) => {
|
||||||
|
let _ = params
|
||||||
|
.query_manager
|
||||||
|
.connect()
|
||||||
|
.upsert_http_response(
|
||||||
|
&HttpResponse {
|
||||||
|
state: HttpResponseState::Closed,
|
||||||
|
elapsed: duration_to_i32(started_at.elapsed()),
|
||||||
|
elapsed_headers: duration_to_i32(started_at.elapsed()),
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
url: request_started_url,
|
||||||
|
..response
|
||||||
|
},
|
||||||
|
¶ms.update_source,
|
||||||
|
params.blob_manager,
|
||||||
|
)
|
||||||
|
.map_err(SendHttpRequestError::PersistResponse)?;
|
||||||
|
|
||||||
|
if let Err(join_err) = event_handle.await {
|
||||||
|
warn!("Failed to join response event task: {}", join_err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(SendHttpRequestError::SendRequest(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let headers_elapsed = duration_to_i32(started_at.elapsed());
|
||||||
|
response = params
|
||||||
|
.query_manager
|
||||||
|
.connect()
|
||||||
|
.upsert_http_response(
|
||||||
|
&HttpResponse {
|
||||||
|
state: HttpResponseState::Connected,
|
||||||
|
elapsed_headers: headers_elapsed,
|
||||||
|
status: i32::from(http_response.status),
|
||||||
|
status_reason: http_response.status_reason.clone(),
|
||||||
|
url: http_response.url.clone(),
|
||||||
|
remote_addr: http_response.remote_addr.clone(),
|
||||||
|
version: http_response.version.clone(),
|
||||||
|
headers: http_response
|
||||||
|
.headers
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| HttpResponseHeader {
|
||||||
|
name: name.clone(),
|
||||||
|
value: value.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
request_headers: http_response
|
||||||
|
.request_headers
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| HttpResponseHeader {
|
||||||
|
name: name.clone(),
|
||||||
|
value: value.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
..response
|
||||||
|
},
|
||||||
|
¶ms.update_source,
|
||||||
|
params.blob_manager,
|
||||||
|
)
|
||||||
|
.map_err(SendHttpRequestError::PersistResponse)?;
|
||||||
|
|
||||||
|
let (response_body, body_stats) =
|
||||||
|
http_response.bytes().await.map_err(SendHttpRequestError::ReadResponseBody)?;
|
||||||
|
|
||||||
|
std::fs::create_dir_all(params.response_dir).map_err(|source| {
|
||||||
|
SendHttpRequestError::CreateResponseDirectory {
|
||||||
|
path: params.response_dir.to_path_buf(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let body_path = params.response_dir.join(&response.id);
|
||||||
|
std::fs::write(&body_path, &response_body).map_err(|source| {
|
||||||
|
SendHttpRequestError::WriteResponseBody { path: body_path.clone(), source }
|
||||||
|
})?;
|
||||||
|
|
||||||
|
response = params
|
||||||
|
.query_manager
|
||||||
|
.connect()
|
||||||
|
.upsert_http_response(
|
||||||
|
&HttpResponse {
|
||||||
|
body_path: Some(body_path.to_string_lossy().to_string()),
|
||||||
|
content_length: Some(usize_to_i32(response_body.len())),
|
||||||
|
content_length_compressed: Some(u64_to_i32(body_stats.size_compressed)),
|
||||||
|
elapsed: duration_to_i32(started_at.elapsed()),
|
||||||
|
elapsed_headers: headers_elapsed,
|
||||||
|
state: HttpResponseState::Closed,
|
||||||
|
..response
|
||||||
|
},
|
||||||
|
¶ms.update_source,
|
||||||
|
params.blob_manager,
|
||||||
|
)
|
||||||
|
.map_err(SendHttpRequestError::PersistResponse)?;
|
||||||
|
|
||||||
|
if let Err(join_err) = event_handle.await {
|
||||||
|
warn!("Failed to join response event task: {}", join_err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SendHttpRequestResult { rendered_request, response, response_body })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sendable_body_length(body: Option<&SendableBody>) -> Option<i32> {
|
||||||
|
match body {
|
||||||
|
Some(SendableBody::Bytes(bytes)) => Some(usize_to_i32(bytes.len())),
|
||||||
|
Some(SendableBody::Stream { content_length: Some(length), .. }) => {
|
||||||
|
Some(u64_to_i32(*length))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn duration_to_i32(duration: std::time::Duration) -> i32 {
|
||||||
|
u128_to_i32(duration.as_millis())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usize_to_i32(value: usize) -> i32 {
|
||||||
|
if value > i32::MAX as usize { i32::MAX } else { value as i32 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u64_to_i32(value: u64) -> i32 {
|
||||||
|
if value > i32::MAX as u64 { i32::MAX } else { value as i32 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u128_to_i32(value: u128) -> i32 {
|
||||||
|
if value > i32::MAX as u128 { i32::MAX } else { value as i32 }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user