diff --git a/src/auth/callback.rs b/src/auth/callback.rs index d55cef01..59c88e27 100644 --- a/src/auth/callback.rs +++ b/src/auth/callback.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + #[cfg(not(target_arch = "wasm32"))] use anyhow::bail; use anyhow::Result; diff --git a/src/auth/dcr.rs b/src/auth/dcr.rs index 22a6ad8e..56fa02c4 100644 --- a/src/auth/dcr.rs +++ b/src/auth/dcr.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + #[cfg(not(target_arch = "wasm32"))] use anyhow::{bail, Context, Result}; #[cfg(not(target_arch = "wasm32"))] diff --git a/src/auth/mod.rs b/src/auth/mod.rs index f86404f9..90519af2 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + pub mod callback; pub mod dcr; pub mod pkce; diff --git a/src/auth/pkce.rs b/src/auth/pkce.rs index 737f7b3e..5832cee5 100644 --- a/src/auth/pkce.rs +++ b/src/auth/pkce.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + #[cfg(not(target_arch = "wasm32"))] use anyhow::Result; #[cfg(not(target_arch = "wasm32"))] diff --git a/src/auth/storage.rs b/src/auth/storage.rs index ce48dca0..8d69d6a5 100644 --- a/src/auth/storage.rs +++ b/src/auth/storage.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + use anyhow::{Context, Result}; use std::path::PathBuf; diff --git a/src/auth/types.rs b/src/auth/types.rs index e4d74a44..98fdcaab 100644 --- a/src/auth/types.rs +++ b/src/auth/types.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + use chrono::Utc; use serde::{Deserialize, Serialize}; @@ -260,7 +262,7 @@ pub fn default_scopes() -> Vec<&'static str> { "synthetics_read", "synthetics_write", "synthetics_private_location_read", - // Tag Rules + // Tag Policies "telemetry_rules_create", "telemetry_rules_read", // Teams diff --git a/src/client.rs b/src/client.rs index e7253d00..8dc4c57e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; #[cfg(not(target_arch = "wasm32"))] @@ -31,24 +33,6 @@ 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 { - 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. @@ -148,9 +132,7 @@ pub fn make_dd_client(cfg: &Config, send_bearer: bool) -> Option, + }, + /// Get all downtimes + /// + /// Get all scheduled downtimes. + List { + /// Only return downtimes that are active when the request is made. + #[arg(long)] + current_only: Option, + /// Comma-separated list of resource paths for related resources to include in the response. Supported resource + /// paths are `created_by` and `monitor`. + #[arg(long)] + include: Option, + /// Specific offset to use as the beginning of the returned page. + #[arg(long)] + page_offset: Option, + /// Maximum number of downtimes in the response. + #[arg(long)] + page_limit: Option, + }, + /// Get active downtimes for a monitor + /// + /// Get all active downtimes for the specified monitor. + ListMonitor { + /// The id of the monitor. + monitor_id: i64, + /// Specific offset to use as the beginning of the returned page. + #[arg(long)] + page_offset: Option, + /// Maximum number of downtimes in the response. + #[arg(long)] + page_limit: Option, + }, + /// Update a downtime + /// + /// Update a downtime by `downtime_id`. + Update { + /// ID of the downtime to update. + downtime_id: String, + #[arg(long)] + file: String, + }, +} + +pub async fn run(cfg: &Config, command: Command) -> Result<()> { + match command { + Command::Cancel { downtime_id } => cancel(cfg, downtime_id).await, + Command::Create { file } => create(cfg, &file).await, + Command::Get { + downtime_id, + include, + } => get(cfg, downtime_id, include).await, + Command::List { + current_only, + include, + page_offset, + page_limit, + } => list(cfg, current_only, include, page_offset, page_limit).await, + Command::ListMonitor { + monitor_id, + page_offset, + page_limit, + } => list_monitor(cfg, monitor_id, page_offset, page_limit).await, + Command::Update { downtime_id, file } => update(cfg, downtime_id, &file).await, + } +} + +/// Cancel a downtime +/// +/// Cancel a downtime. +/// +/// **Note**: Downtimes canceled through the API are no longer active, but are retained for approximately two days before being permanently removed. The downtime may still appear in search results until it is permanently removed. +pub async fn cancel(cfg: &Config, downtime_id: String) -> Result<()> { + let api = crate::make_api!(DowntimesAPI, cfg); + api.cancel_downtime(downtime_id) + .await + .map_err(|e| anyhow::anyhow!("failed to cancel_downtime: {:?}", e))?; + println!("downtimes cancel: ok"); + Ok(()) +} + +/// Schedule a downtime +/// +/// Schedule a downtime. +pub async fn create(cfg: &Config, file: &str) -> Result<()> { + let body: DowntimeCreateRequest = util::read_json_file(file)?; + let api = crate::make_api!(DowntimesAPI, cfg); + let resp = api + .create_downtime(body) + .await + .map_err(|e| anyhow::anyhow!("failed to create_downtime: {:?}", e))?; + formatter::output(cfg, &resp) +} + +/// Get a downtime +/// +/// Get downtime detail by `downtime_id`. +pub async fn get(cfg: &Config, downtime_id: String, include: Option) -> Result<()> { + let api = crate::make_api!(DowntimesAPI, cfg); + let mut params = GetDowntimeOptionalParams::default(); + if let Some(v) = include { + params = params.include(v); + } + let resp = api + .get_downtime(downtime_id, params) + .await + .map_err(|e| anyhow::anyhow!("failed to get_downtime: {:?}", e))?; + formatter::output(cfg, &resp) +} + +/// Get all downtimes +/// +/// Get all scheduled downtimes. +pub async fn list( + cfg: &Config, + current_only: Option, + include: Option, + page_offset: Option, + page_limit: Option, +) -> Result<()> { + let api = crate::make_api!(DowntimesAPI, cfg); + let mut params = ListDowntimesOptionalParams::default(); + if let Some(v) = current_only { + params = params.current_only(v); + } + if let Some(v) = include { + params = params.include(v); + } + if let Some(v) = page_offset { + params = params.page_offset(v); + } + if let Some(v) = page_limit { + params = params.page_limit(v); + } + let resp = api + .list_downtimes(params) + .await + .map_err(|e| anyhow::anyhow!("failed to list_downtimes: {:?}", e))?; + let count = resp.data.as_ref().map_or(0, |d| d.len()); + let truncated = false; + let next_action: Option = None; + let meta = formatter::Metadata { + count: Some(count), + truncated, + command: Some("downtimes list".to_string()), + next_action, + }; + formatter::format_and_print(&resp, &cfg.output_format, cfg.agent_mode, Some(&meta)) +} + +/// Get active downtimes for a monitor +/// +/// Get all active downtimes for the specified monitor. +pub async fn list_monitor( + cfg: &Config, + monitor_id: i64, + page_offset: Option, + page_limit: Option, +) -> Result<()> { + let api = crate::make_api!(DowntimesAPI, cfg); + let mut params = ListMonitorDowntimesOptionalParams::default(); + if let Some(v) = page_offset { + params = params.page_offset(v); + } + if let Some(v) = page_limit { + params = params.page_limit(v); + } + let resp = api + .list_monitor_downtimes(monitor_id, params) + .await + .map_err(|e| anyhow::anyhow!("failed to list_monitor_downtimes: {:?}", e))?; + let count = resp.data.as_ref().map_or(0, |d| d.len()); + let truncated = false; + let next_action: Option = None; + let meta = formatter::Metadata { + count: Some(count), + truncated, + command: Some("downtimes list_monitor".to_string()), + next_action, + }; + formatter::format_and_print(&resp, &cfg.output_format, cfg.agent_mode, Some(&meta)) +} + +/// Update a downtime +/// +/// Update a downtime by `downtime_id`. +pub async fn update(cfg: &Config, downtime_id: String, file: &str) -> Result<()> { + let body: DowntimeUpdateRequest = util::read_json_file(file)?; + let api = crate::make_api!(DowntimesAPI, cfg); + let resp = api + .update_downtime(downtime_id, body) + .await + .map_err(|e| anyhow::anyhow!("failed to update_downtime: {:?}", e))?; + formatter::output(cfg, &resp) +} + +#[cfg(test)] +mod tests { + use crate::test_support::*; + + #[tokio::test] + async fn test_get_ok() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any(&mut server, "GET", r##"{"data": {"attributes": {"created": "2024-01-01T00:00:00+00:00", "display_timezone": "America/New_York", "message": "Message about the downtime", "modified": "2024-01-01T00:00:00+00:00", "monitor_identifier": {"monitor_tags": ["*"]}, "mute_first_recovery_notification": false, "notify_end_states": ["alert", "warn"], "notify_end_types": ["canceled", "expired"], "scope": "env:(staging OR prod) AND datacenter:us-east-1", "status": "active"}, "id": "00000000-0000-1234-0000-000000000000", "type": "downtime"}}"##).await; + let result = super::get(&cfg, "test".to_string(), None).await; + assert!(result.is_ok(), "get failed: {:?}", result.err()); + cleanup_env(); + } + + #[tokio::test] + async fn test_list_ok() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any(&mut server, "GET", r##"{"data": [{"attributes": {"created": "2024-01-01T00:00:00+00:00", "display_timezone": "America/New_York", "message": "Message about the downtime", "modified": "2024-01-01T00:00:00+00:00", "monitor_identifier": {"monitor_tags": ["*"]}, "mute_first_recovery_notification": false, "notify_end_states": ["alert", "warn"], "notify_end_types": ["canceled", "expired"], "scope": "env:(staging OR prod) AND datacenter:us-east-1", "status": "active"}, "id": "00000000-0000-1234-0000-000000000000", "type": "downtime"}], "meta": {"page": {"total_filtered_count": 1}}}"##).await; + let result = super::list(&cfg, None, None, None, None).await; + assert!(result.is_ok(), "list failed: {:?}", result.err()); + cleanup_env(); + } + + #[tokio::test] + async fn test_list_monitor_ok() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any(&mut server, "GET", r##"{"data": [{"attributes": {"end": "2024-01-01T01:00:00+00:00", "groups": ["service:postgres"], "scope": "env:(staging OR prod) AND datacenter:us-east-1", "start": "2024-01-01T00:00:00+00:00"}, "id": "00000000-0000-1234-0000-000000000000", "type": "downtime_match"}], "meta": {"page": {"total_filtered_count": 1}}}"##).await; + let result = super::list_monitor(&cfg, 1, None, None).await; + assert!(result.is_ok(), "list_monitor failed: {:?}", result.err()); + cleanup_env(); + } +} diff --git a/src/filter.rs b/src/filter.rs index a75d84eb..01b03e7e 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + //! jq filter support via the [`jaq`](https://github.com/01mf02/jaq) engine. //! //! [`apply_jq`] is the sole public entry point; the jaq API is encapsulated diff --git a/src/formatter.rs b/src/formatter.rs index fb18846f..c1c26314 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + use anyhow::Result; use serde::Serialize; @@ -157,10 +159,6 @@ pub fn format_and_print( } 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(()); } @@ -170,14 +168,7 @@ pub fn format_and_print( 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). @@ -198,50 +189,48 @@ pub fn print_json(data: &serde_json::Value) -> Result<()> { 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 { - if agent_mode && *format == OutputFormat::Json { - let envelope = build_agent_envelope(data, None)?; - return Ok(go_html_escape(&serde_json::to_string_pretty(&envelope)?)); - } +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(()) +} - match format { - OutputFormat::Json => { - let sorted_data = sort_json_value(data.clone()); - Ok(go_html_escape(&serde_json::to_string_pretty(&sorted_data)?)) - } - OutputFormat::Yaml => { - let sorted_data = sort_json_value(data.clone()); - Ok(serde_norway::to_string(&sorted_data)?) +/// 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()); + } } - OutputFormat::Table => format_table_to_string(data), - OutputFormat::Csv => format_csv_to_string(data), - OutputFormat::Tsv => format_tsv_to_string(data), + serde_json::Value::Object(flat) + } else { + value.clone() } } -/// 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 { +fn print_table(data: &serde_json::Value) -> Result<()> { let raw_rows = extract_rows(data); let owned_rows: Vec = raw_rows.iter().map(|r| flatten_row(r)).collect(); let rows: Vec<&serde_json::Value> = owned_rows.iter().collect(); if rows.is_empty() { - return Ok("No results found".to_string()); + println!("No results found"); + return Ok(()); } // Collect headers from all rows @@ -308,45 +297,7 @@ fn format_table_to_string(data: &serde_json::Value) -> Result { table.add_row(cells); } - 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)?); + println!("{table}"); Ok(()) } @@ -395,13 +346,14 @@ fn csv_cell(value: Option<&serde_json::Value>) -> String { } } -fn format_csv_to_string(data: &serde_json::Value) -> Result { +fn print_csv(data: &serde_json::Value) -> Result<()> { let raw_rows = extract_rows(data); if raw_rows.is_empty() { - return Ok(String::new()); + return Ok(()); } + // Deep-flatten every row so all nested sub-fields become columns. let flat_rows: Vec> = raw_rows .iter() .map(|r| { @@ -411,6 +363,7 @@ fn format_csv_to_string(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 = Vec::new(); for row in &flat_rows { @@ -422,28 +375,26 @@ fn format_csv_to_string(data: &serde_json::Value) -> Result { } headers.sort(); - let mut lines = vec![headers - .iter() - .map(|h| csv_escape(h)) - .collect::>() - .join(",")]; + // Print header row. + println!( + "{}", + headers + .iter() + .map(|h| csv_escape(h)) + .collect::>() + .join(",") + ); + + // Print data rows. for row in &flat_rows { - lines.push( - headers - .iter() - .map(|h| csv_escape(&csv_cell(row.get(h.as_str())))) - .collect::>() - .join(","), - ); + let line = headers + .iter() + .map(|h| csv_escape(&csv_cell(row.get(h.as_str())))) + .collect::>() + .join(","); + println!("{line}"); } - 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(()) } @@ -453,13 +404,14 @@ fn tsv_escape(s: &str) -> String { s.replace('\t', "\\t") } -fn format_tsv_to_string(data: &serde_json::Value) -> Result { +fn print_tsv(data: &serde_json::Value) -> Result<()> { let raw_rows = extract_rows(data); if raw_rows.is_empty() { - return Ok(String::new()); + return Ok(()); } + // Deep-flatten every row so all nested sub-fields become columns. let flat_rows: Vec> = raw_rows .iter() .map(|r| { @@ -469,6 +421,7 @@ fn format_tsv_to_string(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 = Vec::new(); for row in &flat_rows { @@ -480,28 +433,26 @@ fn format_tsv_to_string(data: &serde_json::Value) -> Result { } headers.sort(); - let mut lines = vec![headers - .iter() - .map(|h| tsv_escape(h)) - .collect::>() - .join("\t")]; + // Print header row. + println!( + "{}", + headers + .iter() + .map(|h| tsv_escape(h)) + .collect::>() + .join("\t") + ); + + // Print data rows. for row in &flat_rows { - lines.push( - headers - .iter() - .map(|h| tsv_escape(&csv_cell(row.get(h.as_str())))) - .collect::>() - .join("\t"), - ); + let line = headers + .iter() + .map(|h| tsv_escape(&csv_cell(row.get(h.as_str())))) + .collect::>() + .join("\t"); + println!("{line}"); } - 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(()) } diff --git a/src/generated.rs b/src/generated.rs index 4fe4a5cd..2d59fcea 100644 --- a/src/generated.rs +++ b/src/generated.rs @@ -1,15 +1,27 @@ -// Stable integration point for openapi-transformer-generated pup commands. -// -// Future regenerations add variants to `GeneratedCommand` (and new modules -// alongside this file) in place; main.rs never needs to change again. +// Code generated by openapi-transformer. DO NOT EDIT. + +#[path = "downtimes.rs"] +pub mod downtimes; use anyhow::Result; use crate::config::Config; +/// All spec-generated pup commands. +/// +/// Re-run the generator to add or remove tags; pup never needs manual changes +/// for new commands. #[derive(clap::Subcommand)] -pub enum GeneratedCommand {} +pub enum GeneratedCommand { + /// Manage downtimes resources + Downtimes { + #[command(subcommand)] + action: downtimes::Command, + }, +} -pub async fn run(_cfg: &Config, command: GeneratedCommand) -> Result<()> { - match command {} +pub async fn run(cfg: &Config, command: GeneratedCommand) -> Result<()> { + match command { + GeneratedCommand::Downtimes { action } => downtimes::run(cfg, action).await, + } } diff --git a/src/useragent.rs b/src/useragent.rs index 6016e381..2c6b19cc 100644 --- a/src/useragent.rs +++ b/src/useragent.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + use crate::version; #[allow(dead_code)] diff --git a/src/util.rs b/src/util.rs index 785be68c..29f9c954 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + use anyhow::Result; /// Read a JSON file and deserialize into the specified type. diff --git a/src/version.rs b/src/version.rs index 52db4dce..eed3b368 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,3 +1,5 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + /// Version is set at build time via env var or defaults to Cargo package version. pub const VERSION: &str = env!("CARGO_PKG_VERSION");