Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@ use crate::config;
use crate::util;
use crossterm::style::Stylize;
use serde::de::DeserializeOwned;
use std::time::Duration;

/// Cap on any single HTTP request. Connection create + synchronous schema
/// discovery against a slow remote catalog can take well over a minute, so
/// this needs to be generous; 5 minutes leaves headroom while still bounding
/// the worst case if the server genuinely hangs.
const HTTP_REQUEST_TIMEOUT: Duration = Duration::from_secs(300);

/// TCP keepalive cadence. Without this, macOS will drop a TCP connection
/// that has been quiet (e.g. while the server is doing slow synchronous
/// work) and reqwest surfaces it as "error sending request" even though the
/// request itself completed server-side.
const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30);

fn build_http_client() -> reqwest::blocking::Client {
reqwest::blocking::Client::builder()
.timeout(HTTP_REQUEST_TIMEOUT)
.tcp_keepalive(TCP_KEEPALIVE_INTERVAL)
.build()
.expect("reqwest blocking client should always build with these defaults")
}

#[derive(Clone)]
pub struct ApiClient {
Expand Down Expand Up @@ -48,7 +69,7 @@ impl ApiClient {
};

Self {
client: reqwest::blocking::Client::new(),
client: build_http_client(),
api_key: access_token,
api_url: profile_config.api_url.to_string(),
workspace_id: workspace_id.map(String::from),
Expand All @@ -66,7 +87,7 @@ impl ApiClient {
#[cfg(test)]
pub(crate) fn test_new(api_url: &str, api_key: &str, workspace_id: Option<&str>) -> Self {
Self {
client: reqwest::blocking::Client::new(),
client: build_http_client(),
api_key: api_key.to_string(),
api_url: api_url.to_string(),
workspace_id: workspace_id.map(String::from),
Expand Down
66 changes: 45 additions & 21 deletions src/connections_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,31 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option<Value> {

let field_type = field["type"].as_str().unwrap_or("string");
let format = field["format"].as_str().unwrap_or("");
let description = field["description"].as_str();
// Accept both string and integer examples — `as_str` alone would silently
// miss schemas like `"examples": [8080]` on integer fields.
let example: Option<String> = field["examples"]
.as_array()
.and_then(|a| a.first())
.and_then(|v| {
v.as_str()
.map(str::to_owned)
.or_else(|| v.as_i64().map(|n| n.to_string()))
});
let opt_hint = "optional — press Enter to skip";
let help_message: Option<String> = match (description, is_required) {
(Some(d), true) => Some(d.to_string()),
(Some(d), false) => Some(format!("{d} ({opt_hint})")),
(None, true) => None,
(None, false) => Some(opt_hint.to_string()),
};

match (field_type, format) {
("string", "password") => {
let label = format!("{key}:");
let mut p = Password::new(&label).without_confirmation();
if !is_required {
p = p.with_help_message(opt_hint);
if let Some(h) = &help_message {
p = p.with_help_message(h);
}
let val = p.prompt().unwrap_or_else(|_| std::process::exit(0));
if val.is_empty() && !is_required {
Expand All @@ -133,11 +150,14 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option<Value> {
("string", _) => {
let label = format!("{key}:");
let mut t = Text::new(&label);
if let Some(default) = field["default"].as_str() {
t = t.with_default(default);
let default = field["default"].as_str();
if let Some(d) = default {
t = t.with_default(d);
} else if let Some(e) = example.as_deref() {
t = t.with_placeholder(e);
}
if !is_required {
t = t.with_help_message(opt_hint);
if let Some(h) = &help_message {
t = t.with_help_message(h);
}
let val = t.prompt().unwrap_or_else(|_| std::process::exit(0));
if val.is_empty() && !is_required {
Expand All @@ -149,7 +169,7 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option<Value> {

("integer", _) => {
let label = format!("{key}:");
let t = Text::new(&label).with_validator(move |input: &str| {
let mut t = Text::new(&label).with_validator(move |input: &str| {
if input.is_empty() {
if is_required {
return Ok(Validation::Invalid("This field is required".into()));
Expand All @@ -162,13 +182,12 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option<Value> {
Ok(Validation::Invalid("Must be a whole number".into()))
}
});
let help_t;
let t = if !is_required {
help_t = t.with_help_message(opt_hint);
help_t
} else {
t
};
if let Some(e) = example.as_deref() {
t = t.with_placeholder(e);
}
if let Some(h) = &help_message {
t = t.with_help_message(h);
}
let val = t.prompt().unwrap_or_else(|_| std::process::exit(0));
if val.is_empty() && !is_required {
None
Expand All @@ -182,23 +201,28 @@ fn prompt_field(key: &str, field: &Value, is_required: bool) -> Option<Value> {
("boolean", _) => {
let label = format!("{key}:");
let default = field["default"].as_bool().unwrap_or(false);
let val = Confirm::new(&label)
.with_default(default)
.prompt()
.unwrap_or_else(|_| std::process::exit(0));
let mut c = Confirm::new(&label).with_default(default);
if let Some(h) = &help_message {
c = c.with_help_message(h);
}
let val = c.prompt().unwrap_or_else(|_| std::process::exit(0));
Some(Value::Bool(val))
}

("array", _) => {
let label = format!("{key}:");
let help = if is_required {
let array_hint = if is_required {
"Enter values separated by commas"
} else {
"Enter values separated by commas — optional, press Enter to skip"
};
let help = match description {
Some(d) => format!("{d} — {array_hint}"),
None => array_hint.to_string(),
};
let val = Text::new(&label)
.with_placeholder("value1, value2, ...")
.with_help_message(help)
.with_placeholder(example.as_deref().unwrap_or("value1, value2, ..."))
.with_help_message(&help)
.prompt()
.unwrap_or_else(|_| std::process::exit(0));
if val.is_empty() && !is_required {
Expand Down
Loading