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
22 changes: 21 additions & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ impl Middleware for BearerAuthMiddleware {
}
}

#[cfg(not(target_arch = "wasm32"))]
struct RateLimitCaptureMiddleware;

#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
impl Middleware for RateLimitCaptureMiddleware {
async fn handle(
&self,
req: reqwest_middleware::reqwest::Request,
extensions: &mut Extensions,
next: Next<'_>,
) -> reqwest_middleware::Result<reqwest_middleware::reqwest::Response> {
let resp = next.run(req, extensions).await?;
crate::rate_limit::store_last(crate::rate_limit::extract_from_headers(resp.headers()));
Ok(resp)
}
}

// The `datadog-api-client` SDK's `Configuration.user_agent` is `pub(crate)`
// with no setter, so the only way to override it from outside the crate is
// via middleware that mutates the header after the SDK builds the request.
Expand Down Expand Up @@ -130,7 +148,9 @@ pub fn make_dd_client(cfg: &Config, send_bearer: bool) -> Option<ClientWithMiddl
let reqwest_client = reqwest_middleware::reqwest::Client::builder()
.build()
.expect("failed to build reqwest client");
let mut builder = ClientBuilder::new(reqwest_client).with(UserAgentMiddleware);
let mut builder = ClientBuilder::new(reqwest_client)
.with(UserAgentMiddleware)
.with(RateLimitCaptureMiddleware);
if send_bearer {
if let Some(token) = cfg.access_token.as_ref() {
builder = builder.with(BearerAuthMiddleware {
Expand Down
2 changes: 2 additions & 0 deletions src/commands/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ pub async fn run(

let body_bytes = resp.bytes().await?;

crate::rate_limit::store_last(crate::rate_limit::extract_from_headers(&resp_headers));

if !status.is_success() {
let text = String::from_utf8_lossy(&body_bytes);
bail!("HTTP {} {}: {}", status.as_u16(), url, text);
Expand Down
20 changes: 5 additions & 15 deletions src/commands/skills_remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;

use crate::config::Config;
use crate::formatter;
use crate::raw_client::HttpError;
use crate::raw_client;
use crate::useragent;
use crate::util_ext;

Expand Down Expand Up @@ -163,15 +163,10 @@ async fn get_markdown(cfg: &Config, path: &str, query: &[(&str, &str)]) -> Resul
let resp = send_get(cfg, &url, query, "text/markdown").await?;
if !resp.status().is_success() {
let status = resp.status();
let headers = resp.headers().clone();
let url = resp.url().to_string();
let body = resp.text().await.unwrap_or_default();
return Err(HttpError {
status: status.as_u16(),
method: "GET".to_string(),
url,
body,
}
.into());
return Err(raw_client::http_error(status.as_u16(), "GET", url, body, &headers).into());
}
Ok(resp.text().await?)
}
Expand Down Expand Up @@ -213,15 +208,10 @@ async fn post(cfg: &Config, path: &str, body: serde_json::Value) -> Result<serde
async fn read_json(resp: reqwest::Response, method: &str) -> Result<serde_json::Value> {
if !resp.status().is_success() {
let status = resp.status();
let headers = resp.headers().clone();
let url = resp.url().to_string();
let body = resp.text().await.unwrap_or_default();
return Err(HttpError {
status: status.as_u16(),
method: method.to_string(),
url,
body,
}
.into());
return Err(raw_client::http_error(status.as_u16(), method, url, body, &headers).into());
}
Ok(resp.json().await?)
}
Expand Down
201 changes: 126 additions & 75 deletions src/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ pub fn format_and_print<T: Serialize>(
}
let json = go_html_escape(&serde_json::to_string_pretty(&envelope)?);
println!("{json}");
#[cfg(not(feature = "browser"))]
if crate::rate_limit::verbose_enabled() {
crate::rate_limit::eprint_verbose_response(format, agent_mode)?;
}
return Ok(());
}

Expand All @@ -166,7 +170,14 @@ pub fn format_and_print<T: Serialize>(
OutputFormat::Table => print_table(&value),
OutputFormat::Csv => print_csv(&value),
OutputFormat::Tsv => print_tsv(&value),
}?;

#[cfg(not(feature = "browser"))]
if crate::rate_limit::verbose_enabled() {
crate::rate_limit::eprint_verbose_response(format, agent_mode)?;
}

Ok(())
}

/// Convenience: format and print using config settings (respects -o flag, agent mode, and --jq).
Expand All @@ -187,48 +198,50 @@ pub fn print_json(data: &serde_json::Value) -> Result<()> {
Ok(())
}

fn print_yaml(data: &serde_json::Value) -> Result<()> {
let sorted_data = sort_json_value(data.clone());
let yaml = serde_norway::to_string(&sorted_data)?;
print!("{yaml}");
Ok(())
}
/// Render a JSON value to a string using the selected output format.
pub fn format_value_to_string(
data: &serde_json::Value,
format: &OutputFormat,
agent_mode: bool,
) -> Result<String> {
if agent_mode && *format == OutputFormat::Json {
let envelope = build_agent_envelope(data, None)?;
return Ok(go_html_escape(&serde_json::to_string_pretty(&envelope)?));
}

/// Flatten up to two levels of nested objects into dot-notation keys.
/// e.g. {"id": "x", "attributes": {"host": "foo", "tags": {"env": "prod"}}}
/// → {"id": "x", "attributes.host": "foo", "attributes.tags.env": "prod"}
fn flatten_row(value: &serde_json::Value) -> serde_json::Value {
if let serde_json::Value::Object(map) = value {
let mut flat = serde_json::Map::new();
for (k, v) in map {
if let serde_json::Value::Object(inner) = v {
for (ik, iv) in inner {
if let serde_json::Value::Object(inner2) = iv {
for (iik, iiv) in inner2 {
flat.insert(format!("{k}.{ik}.{iik}"), iiv.clone());
}
} else {
flat.insert(format!("{k}.{ik}"), iv.clone());
}
}
} else {
flat.insert(k.clone(), v.clone());
}
match format {
OutputFormat::Json => {
let sorted_data = sort_json_value(data.clone());
Ok(go_html_escape(&serde_json::to_string_pretty(&sorted_data)?))
}
serde_json::Value::Object(flat)
} else {
value.clone()
OutputFormat::Yaml => {
let sorted_data = sort_json_value(data.clone());
Ok(serde_norway::to_string(&sorted_data)?)
}
OutputFormat::Table => format_table_to_string(data),
OutputFormat::Csv => format_csv_to_string(data),
OutputFormat::Tsv => format_tsv_to_string(data),
}
}

fn print_table(data: &serde_json::Value) -> Result<()> {
/// Format and print a JSON value to stderr (same renderers as stdout).
pub fn eprint_formatted(
data: &serde_json::Value,
format: &OutputFormat,
agent_mode: bool,
) -> Result<()> {
let rendered = format_value_to_string(data, format, agent_mode)?;
eprintln!("{rendered}");
Ok(())
}

fn format_table_to_string(data: &serde_json::Value) -> Result<String> {
let raw_rows = extract_rows(data);
let owned_rows: Vec<serde_json::Value> = raw_rows.iter().map(|r| flatten_row(r)).collect();
let rows: Vec<&serde_json::Value> = owned_rows.iter().collect();

if rows.is_empty() {
println!("No results found");
return Ok(());
return Ok("No results found".to_string());
}

// Collect headers from all rows
Expand Down Expand Up @@ -295,7 +308,45 @@ fn print_table(data: &serde_json::Value) -> Result<()> {
table.add_row(cells);
}

println!("{table}");
Ok(table.to_string())
}

fn print_yaml(data: &serde_json::Value) -> Result<()> {
let sorted_data = sort_json_value(data.clone());
let yaml = serde_norway::to_string(&sorted_data)?;
print!("{yaml}");
Ok(())
}

/// Flatten up to two levels of nested objects into dot-notation keys.
/// e.g. {"id": "x", "attributes": {"host": "foo", "tags": {"env": "prod"}}}
/// → {"id": "x", "attributes.host": "foo", "attributes.tags.env": "prod"}
fn flatten_row(value: &serde_json::Value) -> serde_json::Value {
if let serde_json::Value::Object(map) = value {
let mut flat = serde_json::Map::new();
for (k, v) in map {
if let serde_json::Value::Object(inner) = v {
for (ik, iv) in inner {
if let serde_json::Value::Object(inner2) = iv {
for (iik, iiv) in inner2 {
flat.insert(format!("{k}.{ik}.{iik}"), iiv.clone());
}
} else {
flat.insert(format!("{k}.{ik}"), iv.clone());
}
}
} else {
flat.insert(k.clone(), v.clone());
}
}
serde_json::Value::Object(flat)
} else {
value.clone()
}
}

fn print_table(data: &serde_json::Value) -> Result<()> {
println!("{}", format_table_to_string(data)?);
Ok(())
}

Expand Down Expand Up @@ -344,14 +395,13 @@ fn csv_cell(value: Option<&serde_json::Value>) -> String {
}
}

fn print_csv(data: &serde_json::Value) -> Result<()> {
fn format_csv_to_string(data: &serde_json::Value) -> Result<String> {
let raw_rows = extract_rows(data);

if raw_rows.is_empty() {
return Ok(());
return Ok(String::new());
}

// Deep-flatten every row so all nested sub-fields become columns.
let flat_rows: Vec<serde_json::Map<String, serde_json::Value>> = raw_rows
.iter()
.map(|r| {
Expand All @@ -361,7 +411,6 @@ fn print_csv(data: &serde_json::Value) -> Result<()> {
})
.collect();

// Collect all headers, preserving first-seen insertion order.
let mut header_set = std::collections::HashSet::new();
let mut headers: Vec<String> = Vec::new();
for row in &flat_rows {
Expand All @@ -373,26 +422,28 @@ fn print_csv(data: &serde_json::Value) -> Result<()> {
}
headers.sort();

// Print header row.
println!(
"{}",
headers
.iter()
.map(|h| csv_escape(h))
.collect::<Vec<_>>()
.join(",")
);

// Print data rows.
let mut lines = vec![headers
.iter()
.map(|h| csv_escape(h))
.collect::<Vec<_>>()
.join(",")];
for row in &flat_rows {
let line = headers
.iter()
.map(|h| csv_escape(&csv_cell(row.get(h.as_str()))))
.collect::<Vec<_>>()
.join(",");
println!("{line}");
lines.push(
headers
.iter()
.map(|h| csv_escape(&csv_cell(row.get(h.as_str()))))
.collect::<Vec<_>>()
.join(","),
);
}
Ok(lines.join("\n"))
}

fn print_csv(data: &serde_json::Value) -> Result<()> {
let rendered = format_csv_to_string(data)?;
if !rendered.is_empty() {
println!("{rendered}");
}
Ok(())
}

Expand All @@ -402,14 +453,13 @@ fn tsv_escape(s: &str) -> String {
s.replace('\t', "\\t")
}

fn print_tsv(data: &serde_json::Value) -> Result<()> {
fn format_tsv_to_string(data: &serde_json::Value) -> Result<String> {
let raw_rows = extract_rows(data);

if raw_rows.is_empty() {
return Ok(());
return Ok(String::new());
}

// Deep-flatten every row so all nested sub-fields become columns.
let flat_rows: Vec<serde_json::Map<String, serde_json::Value>> = raw_rows
.iter()
.map(|r| {
Expand All @@ -419,7 +469,6 @@ fn print_tsv(data: &serde_json::Value) -> Result<()> {
})
.collect();

// Collect all headers, preserving first-seen insertion order.
let mut header_set = std::collections::HashSet::new();
let mut headers: Vec<String> = Vec::new();
for row in &flat_rows {
Expand All @@ -431,26 +480,28 @@ fn print_tsv(data: &serde_json::Value) -> Result<()> {
}
headers.sort();

// Print header row.
println!(
"{}",
headers
.iter()
.map(|h| tsv_escape(h))
.collect::<Vec<_>>()
.join("\t")
);

// Print data rows.
let mut lines = vec![headers
.iter()
.map(|h| tsv_escape(h))
.collect::<Vec<_>>()
.join("\t")];
for row in &flat_rows {
let line = headers
.iter()
.map(|h| tsv_escape(&csv_cell(row.get(h.as_str()))))
.collect::<Vec<_>>()
.join("\t");
println!("{line}");
lines.push(
headers
.iter()
.map(|h| tsv_escape(&csv_cell(row.get(h.as_str()))))
.collect::<Vec<_>>()
.join("\t"),
);
}
Ok(lines.join("\n"))
}

fn print_tsv(data: &serde_json::Value) -> Result<()> {
let rendered = format_tsv_to_string(data)?;
if !rendered.is_empty() {
println!("{rendered}");
}
Ok(())
}

Expand Down
Loading