From 16e10e55f1efca4e842e6dd56ecf0bcb6cbb771d Mon Sep 17 00:00:00 2001 From: Will Chandler Date: Tue, 16 Sep 2025 11:25:50 -0400 Subject: [PATCH 1/2] Migrate oxide_override to its own module We're about to expand the logic around `OxideOverride`, move it into its own module now for a more understandable diff. --- cli/src/main.rs | 221 +------------------------------------ cli/src/oxide_override.rs | 223 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 218 deletions(-) create mode 100644 cli/src/oxide_override.rs diff --git a/cli/src/main.rs b/cli/src/main.rs index 99762ac2..cbe1d718 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -8,22 +8,12 @@ #![cfg_attr(not(test), deny(clippy::print_stdout, clippy::print_stderr))] use std::io; -use std::net::IpAddr; -use std::path::PathBuf; -use std::sync::atomic::AtomicBool; -use anyhow::{Context as _, Result}; +use anyhow::Result; use async_trait::async_trait; -use base64::Engine; use cli_builder::NewCli; use context::Context; -use generated_cli::CliConfig; -use oxide::{ - types::{ - AllowedSourceIps, DerEncodedKeyPair, IdpMetadataSource, IpRange, Ipv4Range, Ipv6Range, - }, - Client, -}; +use oxide::Client; use url::Url; mod cmd_api; @@ -40,6 +30,7 @@ mod cmd_version; mod cli_builder; mod context; +mod oxide_override; #[macro_use] mod print; mod util; @@ -135,212 +126,6 @@ async fn main() { } } -#[derive(Default)] -struct OxideOverride { - needs_comma: AtomicBool, -} - -impl OxideOverride { - fn ip_range(matches: &clap::ArgMatches) -> anyhow::Result { - let first = matches.get_one::("first").unwrap(); - let last = matches.get_one::("last").unwrap(); - - match (first, last) { - (IpAddr::V4(first), IpAddr::V4(last)) => { - let range = Ipv4Range::try_from(Ipv4Range::builder().first(*first).last(*last))?; - Ok(range.into()) - } - (IpAddr::V6(first), IpAddr::V6(last)) => { - let range = Ipv6Range::try_from(Ipv6Range::builder().first(*first).last(*last))?; - Ok(range.into()) - } - _ => anyhow::bail!( - "first and last must either both be ipv4 or ipv6 addresses".to_string() - ), - } - } -} - -impl CliConfig for OxideOverride { - fn success_item(&self, value: &oxide::ResponseValue) - where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, - { - let s = serde_json::to_string_pretty(std::ops::Deref::deref(value)) - .expect("failed to serialize return to json"); - println_nopipe!("{}", s); - } - - fn success_no_item(&self, _: &oxide::ResponseValue<()>) {} - - fn error(&self, _value: &oxide::Error) - where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, - { - eprintln_nopipe!("error"); - } - - fn list_start(&self) - where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, - { - self.needs_comma - .store(false, std::sync::atomic::Ordering::Relaxed); - print_nopipe!("["); - } - - fn list_item(&self, value: &T) - where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, - { - let s = serde_json::to_string_pretty(&[value]).expect("failed to serialize result to json"); - if self.needs_comma.load(std::sync::atomic::Ordering::Relaxed) { - print_nopipe!(", {}", &s[4..s.len() - 2]); - } else { - print_nopipe!("\n{}", &s[2..s.len() - 2]); - }; - self.needs_comma - .store(true, std::sync::atomic::Ordering::Relaxed); - } - - fn list_end_success(&self) - where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, - { - if self.needs_comma.load(std::sync::atomic::Ordering::Relaxed) { - println_nopipe!("\n]"); - } else { - println_nopipe!("]"); - } - } - - fn list_end_error(&self, _value: &oxide::Error) - where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, - { - self.list_end_success::() - } - - // Deal with all the operations that require an `IpPool` as input - fn execute_ip_pool_range_add( - &self, - matches: &clap::ArgMatches, - request: &mut oxide::builder::IpPoolRangeAdd, - ) -> anyhow::Result<()> { - *request = request.to_owned().body(Self::ip_range(matches)?); - Ok(()) - } - fn execute_ip_pool_range_remove( - &self, - matches: &clap::ArgMatches, - request: &mut oxide::builder::IpPoolRangeRemove, - ) -> anyhow::Result<()> { - *request = request.to_owned().body(Self::ip_range(matches)?); - Ok(()) - } - fn execute_ip_pool_service_range_add( - &self, - matches: &clap::ArgMatches, - request: &mut oxide::builder::IpPoolServiceRangeAdd, - ) -> anyhow::Result<()> { - *request = request.to_owned().body(Self::ip_range(matches)?); - Ok(()) - } - fn execute_ip_pool_service_range_remove( - &self, - matches: &clap::ArgMatches, - request: &mut oxide::builder::IpPoolServiceRangeRemove, - ) -> anyhow::Result<()> { - *request = request.to_owned().body(Self::ip_range(matches)?); - Ok(()) - } - - fn execute_saml_identity_provider_create( - &self, - matches: &clap::ArgMatches, - request: &mut oxide::builder::SamlIdentityProviderCreate, - ) -> anyhow::Result<()> { - match matches - .get_one::("idp_metadata_source") - .map(clap::Id::as_str) - { - Some("metadata-url") => { - let value = matches.get_one::("metadata-url").unwrap(); - *request = request.to_owned().body_map(|body| { - body.idp_metadata_source(IdpMetadataSource::Url { url: value.clone() }) - }); - Ok::<_, anyhow::Error>(()) - } - Some("metadata-value") => { - let xml_path = matches.get_one::("metadata-value").unwrap(); - let xml_bytes = std::fs::read(xml_path).with_context(|| { - format!("failed to read metadata XML file {}", xml_path.display()) - })?; - let encoded_xml = base64::engine::general_purpose::STANDARD.encode(xml_bytes); - *request = request.to_owned().body_map(|body| { - body.idp_metadata_source(IdpMetadataSource::Base64EncodedXml { - data: encoded_xml, - }) - }); - Ok(()) - } - _ => unreachable!("invalid value for idp_metadata_source group"), - }?; - - if matches.get_one::("signing_keypair").is_some() { - let privkey_path = matches.get_one::("private-key").unwrap(); - let privkey_bytes = std::fs::read(privkey_path).with_context(|| { - format!("failed to read private key file {}", privkey_path.display()) - })?; - let encoded_privkey = base64::engine::general_purpose::STANDARD.encode(&privkey_bytes); - - let cert_path = matches.get_one::("public-cert").unwrap(); - let cert_bytes = std::fs::read(cert_path).with_context(|| { - format!("failed to read public cert file {}", cert_path.display()) - })?; - let encoded_cert = base64::engine::general_purpose::STANDARD.encode(&cert_bytes); - - *request = request.to_owned().body_map(|body| { - body.signing_keypair(DerEncodedKeyPair { - private_key: encoded_privkey, - public_cert: encoded_cert, - }) - }); - } - Ok(()) - } - - fn execute_networking_allow_list_update( - &self, - matches: &clap::ArgMatches, - request: &mut oxide::builder::NetworkingAllowListUpdate, - ) -> anyhow::Result<()> { - match matches - .get_one::("allow-list") - .map(clap::Id::as_str) - { - Some("any") => { - let value = matches.get_one::("any").unwrap(); - assert!(value); - *request = request - .to_owned() - .body_map(|body| body.allowed_ips(AllowedSourceIps::Any)); - } - Some("ips") => { - let values: Vec = matches.get_many("ips").unwrap().cloned().collect(); - *request = request.to_owned().body_map(|body| { - body.allowed_ips(AllowedSourceIps::List( - values.into_iter().map(IpOrNet::into_ip_net).collect(), - )) - }); - } - _ => unreachable!("invalid value for allow-list group"), - } - - Ok(()) - } -} - #[cfg(test)] mod tests { use clap::Command; diff --git a/cli/src/oxide_override.rs b/cli/src/oxide_override.rs new file mode 100644 index 00000000..dbed2e07 --- /dev/null +++ b/cli/src/oxide_override.rs @@ -0,0 +1,223 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2025 Oxide Computer Company + +use std::net::IpAddr; +use std::path::PathBuf; +use std::sync::atomic::AtomicBool; + +use crate::generated_cli::CliConfig; +use crate::{eprintln_nopipe, print_nopipe, println_nopipe, IpOrNet}; +use anyhow::Context as _; +use base64::Engine; +use oxide::types::{ + AllowedSourceIps, DerEncodedKeyPair, IdpMetadataSource, IpRange, Ipv4Range, Ipv6Range, +}; + +#[derive(Default)] +pub struct OxideOverride { + needs_comma: AtomicBool, +} + +impl OxideOverride { + fn ip_range(matches: &clap::ArgMatches) -> anyhow::Result { + let first = matches.get_one::("first").unwrap(); + let last = matches.get_one::("last").unwrap(); + + match (first, last) { + (IpAddr::V4(first), IpAddr::V4(last)) => { + let range = Ipv4Range::try_from(Ipv4Range::builder().first(*first).last(*last))?; + Ok(range.into()) + } + (IpAddr::V6(first), IpAddr::V6(last)) => { + let range = Ipv6Range::try_from(Ipv6Range::builder().first(*first).last(*last))?; + Ok(range.into()) + } + _ => anyhow::bail!( + "first and last must either both be ipv4 or ipv6 addresses".to_string() + ), + } + } +} + +impl CliConfig for OxideOverride { + fn success_item(&self, value: &oxide::ResponseValue) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + { + let s = serde_json::to_string_pretty(std::ops::Deref::deref(value)) + .expect("failed to serialize return to json"); + println_nopipe!("{}", s); + } + + fn success_no_item(&self, _: &oxide::ResponseValue<()>) {} + + fn error(&self, _value: &oxide::Error) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + { + eprintln_nopipe!("error"); + } + + fn list_start(&self) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + { + self.needs_comma + .store(false, std::sync::atomic::Ordering::Relaxed); + print_nopipe!("["); + } + + fn list_item(&self, value: &T) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + { + let s = serde_json::to_string_pretty(&[value]).expect("failed to serialize result to json"); + if self.needs_comma.load(std::sync::atomic::Ordering::Relaxed) { + print_nopipe!(", {}", &s[4..s.len() - 2]); + } else { + print_nopipe!("\n{}", &s[2..s.len() - 2]); + }; + self.needs_comma + .store(true, std::sync::atomic::Ordering::Relaxed); + } + + fn list_end_success(&self) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + { + if self.needs_comma.load(std::sync::atomic::Ordering::Relaxed) { + println_nopipe!("\n]"); + } else { + println_nopipe!("]"); + } + } + + fn list_end_error(&self, _value: &oxide::Error) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + { + self.list_end_success::() + } + + // Deal with all the operations that require an `IpPool` as input + fn execute_ip_pool_range_add( + &self, + matches: &clap::ArgMatches, + request: &mut oxide::builder::IpPoolRangeAdd, + ) -> anyhow::Result<()> { + *request = request.to_owned().body(Self::ip_range(matches)?); + Ok(()) + } + fn execute_ip_pool_range_remove( + &self, + matches: &clap::ArgMatches, + request: &mut oxide::builder::IpPoolRangeRemove, + ) -> anyhow::Result<()> { + *request = request.to_owned().body(Self::ip_range(matches)?); + Ok(()) + } + fn execute_ip_pool_service_range_add( + &self, + matches: &clap::ArgMatches, + request: &mut oxide::builder::IpPoolServiceRangeAdd, + ) -> anyhow::Result<()> { + *request = request.to_owned().body(Self::ip_range(matches)?); + Ok(()) + } + fn execute_ip_pool_service_range_remove( + &self, + matches: &clap::ArgMatches, + request: &mut oxide::builder::IpPoolServiceRangeRemove, + ) -> anyhow::Result<()> { + *request = request.to_owned().body(Self::ip_range(matches)?); + Ok(()) + } + + fn execute_saml_identity_provider_create( + &self, + matches: &clap::ArgMatches, + request: &mut oxide::builder::SamlIdentityProviderCreate, + ) -> anyhow::Result<()> { + match matches + .get_one::("idp_metadata_source") + .map(clap::Id::as_str) + { + Some("metadata-url") => { + let value = matches.get_one::("metadata-url").unwrap(); + *request = request.to_owned().body_map(|body| { + body.idp_metadata_source(IdpMetadataSource::Url { url: value.clone() }) + }); + Ok::<_, anyhow::Error>(()) + } + Some("metadata-value") => { + let xml_path = matches.get_one::("metadata-value").unwrap(); + let xml_bytes = std::fs::read(xml_path).with_context(|| { + format!("failed to read metadata XML file {}", xml_path.display()) + })?; + let encoded_xml = base64::engine::general_purpose::STANDARD.encode(xml_bytes); + *request = request.to_owned().body_map(|body| { + body.idp_metadata_source(IdpMetadataSource::Base64EncodedXml { + data: encoded_xml, + }) + }); + Ok(()) + } + _ => unreachable!("invalid value for idp_metadata_source group"), + }?; + + if matches.get_one::("signing_keypair").is_some() { + let privkey_path = matches.get_one::("private-key").unwrap(); + let privkey_bytes = std::fs::read(privkey_path).with_context(|| { + format!("failed to read private key file {}", privkey_path.display()) + })?; + let encoded_privkey = base64::engine::general_purpose::STANDARD.encode(&privkey_bytes); + + let cert_path = matches.get_one::("public-cert").unwrap(); + let cert_bytes = std::fs::read(cert_path).with_context(|| { + format!("failed to read public cert file {}", cert_path.display()) + })?; + let encoded_cert = base64::engine::general_purpose::STANDARD.encode(&cert_bytes); + + *request = request.to_owned().body_map(|body| { + body.signing_keypair(DerEncodedKeyPair { + private_key: encoded_privkey, + public_cert: encoded_cert, + }) + }); + } + Ok(()) + } + + fn execute_networking_allow_list_update( + &self, + matches: &clap::ArgMatches, + request: &mut oxide::builder::NetworkingAllowListUpdate, + ) -> anyhow::Result<()> { + match matches + .get_one::("allow-list") + .map(clap::Id::as_str) + { + Some("any") => { + let value = matches.get_one::("any").unwrap(); + assert!(value); + *request = request + .to_owned() + .body_map(|body| body.allowed_ips(AllowedSourceIps::Any)); + } + Some("ips") => { + let values: Vec = matches.get_many("ips").unwrap().cloned().collect(); + *request = request.to_owned().body_map(|body| { + body.allowed_ips(AllowedSourceIps::List( + values.into_iter().map(IpOrNet::into_ip_net).collect(), + )) + }); + } + _ => unreachable!("invalid value for allow-list group"), + } + + Ok(()) + } +} From 2fda6dac5f74a8b756708602f3383233d33f72ec Mon Sep 17 00:00:00 2001 From: Will Chandler Date: Tue, 16 Sep 2025 11:27:17 -0400 Subject: [PATCH 2/2] Add tabular output with --format flag Tabular output is much easier to quickly scan for value, as well as being more amenable to traditional UNIX scripting. For example, compare finding the block_size for an image in the following output: $ oxide image list --format=json [ { "block_size": 512, "description": "Debian 13 generic cloud image", "id": "3672f476-51ff-49ab-bd2c-723151a921c6", "name": "debian-13", "os": "Debian", "size": 3221225472, "time_created": "2025-08-11T16:48:17.827861Z", "time_modified": "2025-08-11T16:52:23.411189Z", "version": "13" }, { "block_size": 4096, "description": "Silo image for Packer acceptance testing.", "id": "f13b140e-4d34-4060-b898-6316cdcc2f1e", "name": "packer-acc-test-silo-image", "os": "", "size": 1073741824, "time_created": "2025-05-15T23:11:14.015948Z", "time_modified": "2025-05-15T23:12:02.098119Z", "version": "" } ] Versus: $ oxide image list --format=table:name,block_size NAME BLOCK_SIZE debian-13 512 packer-acc-test-silo-image 4096 For query operations with a response type that is amenable to tabular formatting, we add a `--format` flag to the subcommand. This takes an optional comma-separated list of field names to print, as larger response items can easily overflow typical terminal widths. The available field names are listed in the `--help` output for the subcommand. The existing JSON format remains the default. Future changes may add the ability to set a default format on a per-command basis. Not all API return values can be reasonably formatted as a table. Endpoints returning `()`, byte streams, and unstructured `serde_json::Value` objects are deliberately excluded. Internally, this is implemented using the newly added `ResponseFields` trait, which xtask creates as part of the generated CLI. This enables getting the field names of a type and accessing them as a `serde_json::Value` via their name. --- Cargo.lock | 20 +- Cargo.toml | 5 + cli/Cargo.toml | 1 + cli/docs/cli.json | 516 +++ cli/src/cli_builder.rs | 93 +- cli/src/cmd_update.rs | 5 +- cli/src/context.rs | 2 +- cli/src/generated_cli.rs | 3735 ++++++++++++++++- cli/src/oxide_override.rs | 239 +- ...t_table_anti_affinity_group_members.stdout | 5 + cli/tests/data/test_table_bgp_routes.stdout | 3 + ...test_table_project_list_basic_table.stdout | 5 + .../data/test_table_project_list_json.stdout | 27 + ...able_project_list_table_with_fields.stdout | 5 + ...list_table_with_no_requested_fields.stderr | 9 + cli/tests/test_table.rs | 256 ++ xtask/Cargo.toml | 4 + xtask/src/cli_extras.rs | 723 ++++ xtask/src/main.rs | 23 +- 19 files changed, 5615 insertions(+), 61 deletions(-) create mode 100644 cli/tests/data/test_table_anti_affinity_group_members.stdout create mode 100644 cli/tests/data/test_table_bgp_routes.stdout create mode 100644 cli/tests/data/test_table_project_list_basic_table.stdout create mode 100644 cli/tests/data/test_table_project_list_json.stdout create mode 100644 cli/tests/data/test_table_project_list_table_with_fields.stdout create mode 100644 cli/tests/data/test_table_project_list_table_with_no_requested_fields.stderr create mode 100644 cli/tests/test_table.rs create mode 100644 xtask/src/cli_extras.rs diff --git a/Cargo.lock b/Cargo.lock index 151a1731..c2993f36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,6 +633,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "comfy-table" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" +dependencies = [ + "crossterm 0.29.0", + "unicode-segmentation", + "unicode-width 0.2.0", +] + [[package]] name = "compact_str" version = "0.8.1" @@ -2522,6 +2533,7 @@ dependencies = [ "clap", "clap_complete", "colored", + "comfy-table", "crossterm 0.29.0", "dialoguer", "dirs", @@ -4423,9 +4435,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -4996,13 +5008,17 @@ name = "xtask" version = "0.0.0" dependencies = [ "clap", + "indexmap", "newline-converter", + "proc-macro2", "progenitor", + "quote", "regex", "rustc_version", "rustfmt-wrapper", "serde_json", "similar", + "syn 2.0.106", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 767bcdc6..f5633016 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ chrono = { version = "0.4.42", features = ["serde"] } clap = { version = "4.5.47", features = ["derive", "string", "env", "wrap_help"] } clap_complete = "4.5.58" colored = "3.0.0" +comfy-table = "7.2.1" crossterm = { version = "0.29.0", features = [ "event-stream" ] } dialoguer = "0.11.0" dirs = "6.0.0" @@ -32,6 +33,7 @@ flume = "0.11.1" futures = "0.3.31" httpmock = "0.7.0" humantime = "2.3.0" +indexmap = "2.11.3" indicatif = "0.18.0" libc = "0.2.175" log = "0.4.28" @@ -44,9 +46,11 @@ oxide-httpmock = { path = "sdk-httpmock", version = "0.13.0" } oxnet = "0.1.3" predicates = "3.1.3" pretty_assertions = "1.4.1" +proc-macro2 = "1.0.101" progenitor = { git = "https://github.com/oxidecomputer/progenitor", default-features = false } progenitor-client = "0.11.1" rand = "0.9.2" +quote = "1.0.40" ratatui = "0.29.0" rcgen = { version = "0.14.4", features = ["pem"] } regex = "1.11.2" @@ -59,6 +63,7 @@ serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.145" similar = "2.7.0" support-bundle-viewer = "0.1.2" +syn = "2.0.106" tabwriter = "1.4.1" thiserror = "2.0.16" tempfile = "3.22.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7e993fa7..20fbe3c4 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -27,6 +27,7 @@ chrono = { workspace = true } clap = { workspace = true } clap_complete = { workspace = true } colored = { workspace = true } +comfy-table = { workspace = true } crossterm = { workspace = true } dialoguer = { workspace = true } dirs = { workspace = true } diff --git a/cli/docs/cli.json b/cli/docs/cli.json index 422c0a28..82a40f7c 100644 --- a/cli/docs/cli.json +++ b/cli/docs/cli.json @@ -62,6 +62,10 @@ "long": "filter", "help": "An optional glob pattern for filtering alert class names.\n\nIf provided, only alert classes which match this glob pattern will be included in the response." }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -104,6 +108,10 @@ "name": "list", "about": "List alert receivers", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -144,6 +152,10 @@ ], "help": "If true, include deliveries which have failed permanently.\n\nIf any of the \"pending\", \"failed\", or \"delivered\" query parameters are set to true, only deliveries matching those state(s) will be included in the response. If NO state filter parameters are set, then all deliveries are included.\n\nA delivery fails permanently when the retry limit of three total attempts is reached without a successful delivery." }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -267,6 +279,10 @@ "name": "view", "about": "Fetch alert receiver", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -373,6 +389,10 @@ "name": "list", "about": "List webhook receiver secret IDs", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -498,6 +518,10 @@ "long": "end-time", "help": "Exclusive" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -636,6 +660,10 @@ "name": "view", "about": "Fetch current silo's auth settings", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -744,6 +772,10 @@ "name": "list", "about": "List all support bundles", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -797,6 +829,10 @@ "long": "bundle-id", "help": "ID of the support bundle" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -878,6 +914,10 @@ "about": "List certificates for external endpoints", "long_about": "Returns a list of TLS certificates used for the external API (for the current Silo). These are sorted by creation date, with the most recent certificates appearing first.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -906,6 +946,10 @@ "long": "certificate", "help": "Name or ID of the certificate" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -980,6 +1024,10 @@ "about": "List access tokens", "long_about": "List device access tokens for the currently authenticated user.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -1003,6 +1051,10 @@ "name": "groups", "about": "Fetch current user's groups", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -1081,6 +1133,10 @@ "about": "List SSH public keys", "long_about": "Lists SSH public keys for the currently authenticated user.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -1105,6 +1161,10 @@ "about": "Fetch SSH public key", "long_about": "Fetch SSH public key associated with the currently authenticated user.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -1122,6 +1182,10 @@ "name": "view", "about": "Fetch user for current session", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -1363,6 +1427,10 @@ "name": "list", "about": "List disks", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -1394,6 +1462,10 @@ "long": "disk", "help": "Name or ID of the disk" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -1513,6 +1585,10 @@ "name": "list", "about": "List affinity groups", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -1575,6 +1651,10 @@ "long": "affinity-group", "help": "Name or ID of the affinity group" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -1626,6 +1706,10 @@ { "long": "affinity-group" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "instance" }, @@ -1683,6 +1767,10 @@ "long": "affinity-group", "help": "Name or ID of the affinity group" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -1776,6 +1864,10 @@ "name": "list", "about": "List instrumentation probes", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -1803,6 +1895,10 @@ "name": "view", "about": "View instrumentation probe", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "probe", "help": "Name or ID of the probe" @@ -1835,6 +1931,10 @@ "about": "Run timeseries query", "long_about": "Queries are written in OxQL.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "json-body", "help": "Path to a file that contains the full json body." @@ -1868,6 +1968,10 @@ "name": "list", "about": "List timeseries schemas", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -1932,6 +2036,10 @@ "about": "Get the current target release of the rack's system software", "long_about": "This may not correspond to the actual software running on the rack at the time of request; it is instead the release that the rack reconfigurator should be moving towards as a goal state. After some number of planning and execution phases, the software running on the rack should eventually correspond to the release described here.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -1991,6 +2099,10 @@ "about": "List root roles in the updates trust store", "long_about": "A root role is a JSON document describing the cryptographic keys that are trusted to sign system release repositories, as described by The Update Framework. Uploading a repository requires its metadata to be signed by keys trusted by the trust store.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -2012,6 +2124,10 @@ "name": "view", "about": "Fetch trusted root role", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -2064,6 +2180,10 @@ "about": "Run project-scoped timeseries query", "long_about": "Queries are written in OxQL. Project must be specified by name or ID in URL query parameter. The OxQL query will only return timeseries data from the specified project.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "json-body", "help": "Path to a file that contains the full json body." @@ -2219,6 +2339,10 @@ "name": "list", "about": "List floating IPs", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -2283,6 +2407,10 @@ "long": "floating-ip", "help": "Name or ID of the floating IP" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -2310,6 +2438,10 @@ "name": "list", "about": "List groups", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -2422,6 +2554,10 @@ "about": "List images", "long_about": "List images which are global or scoped to the specified project. The images are returned sorted by creation date, with the most recent images appearing first.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -2470,6 +2606,10 @@ "about": "Fetch image", "long_about": "Fetch the details for a specific image in a project.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "image", "help": "Name or ID of the image" @@ -2572,6 +2712,10 @@ "name": "list", "about": "List anti-affinity groups", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -2634,6 +2778,10 @@ "long": "anti-affinity-group", "help": "Name or ID of the anti affinity group" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -2685,6 +2833,10 @@ { "long": "anti-affinity-group" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "instance" }, @@ -2742,6 +2894,10 @@ "long": "anti-affinity-group", "help": "Name or ID of the anti affinity group" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -2919,6 +3075,10 @@ "name": "list", "about": "List disks for instance", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "instance", "help": "Name or ID of the instance" @@ -3012,6 +3172,10 @@ "name": "list", "about": "List external IP addresses", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "instance", "help": "Name or ID of the instance" @@ -3080,6 +3244,10 @@ "name": "list", "about": "List instances", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -3186,6 +3354,10 @@ "name": "list", "about": "List network interfaces", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "instance", "help": "Name or ID of the instance" @@ -3262,6 +3434,10 @@ "name": "view", "about": "Fetch network interface", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "instance", "help": "Name or ID of the instance" @@ -3297,6 +3473,10 @@ "name": "affinity", "about": "List affinity groups containing instance", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "instance", "help": "Name or ID of the instance" @@ -3328,6 +3508,10 @@ "name": "anti-affinity", "about": "List anti-affinity groups containing instance", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "instance", "help": "Name or ID of the instance" @@ -3360,6 +3544,10 @@ "about": "List SSH public keys for instance", "long_about": "List SSH public keys injected via cloud-init during instance creation. Note that this list is a snapshot in time and will not reflect updates made after the instance is created.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "instance", "help": "Name or ID of the instance" @@ -3596,6 +3784,10 @@ "name": "view", "about": "Fetch instance", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "instance", "help": "Name or ID of the instance" @@ -3712,6 +3904,10 @@ "name": "list", "about": "List IP addresses attached to internet gateway", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "gateway", "help": "Name or ID of the internet gateway" @@ -3898,6 +4094,10 @@ "name": "list", "about": "List IP pools attached to internet gateway", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "gateway", "help": "Name or ID of the internet gateway" @@ -3935,6 +4135,10 @@ "name": "list", "about": "List internet gateways", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -3966,6 +4170,10 @@ "name": "view", "about": "Fetch internet gateway", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "gateway", "help": "Name or ID of the gateway" @@ -4049,6 +4257,10 @@ "name": "list", "about": "List IP pools", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -4113,6 +4325,10 @@ "about": "List ranges for IP pool", "long_about": "Ranges are ordered by their first address.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -4210,6 +4426,10 @@ "about": "List IP ranges for the Oxide service pool", "long_about": "Ranges are ordered by their first address.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -4252,6 +4472,10 @@ "name": "view", "about": "Fetch Oxide service IP pool", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -4276,6 +4500,10 @@ "about": "Link IP pool to silo", "long_about": "Users in linked silos can allocate external IPs from this pool for their instances. A silo can have at most one default pool. IPs are allocated from the default pool when users ask for one without specifying a pool.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "is-default", "values": [ @@ -4310,6 +4538,10 @@ "name": "list", "about": "List IP pool's linked silos", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -4418,6 +4650,10 @@ "name": "utilization", "about": "Fetch IP pool utilization", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "pool", "help": "Name or ID of the IP pool" @@ -4433,6 +4669,10 @@ "name": "view", "about": "Fetch IP pool", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "pool", "help": "Name or ID of the IP pool" @@ -4451,6 +4691,10 @@ "about": "Ping API", "long_about": "Always responds with Ok if it responds at all.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -4491,6 +4735,10 @@ "name": "view", "about": "Fetch current silo's IAM policy", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -4564,6 +4812,10 @@ "name": "list", "about": "List IP pools", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -4587,6 +4839,10 @@ "name": "view", "about": "Fetch IP pool", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "pool", "help": "Name or ID of the IP pool" @@ -4604,6 +4860,10 @@ "name": "list", "about": "List projects", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -4660,6 +4920,10 @@ "name": "view", "about": "Fetch project's IAM policy", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -4706,6 +4970,10 @@ "name": "view", "about": "Fetch project", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -4803,6 +5071,10 @@ "about": "List identity providers for silo", "long_about": "List identity providers for silo by silo name or ID.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5008,6 +5280,10 @@ "name": "view", "about": "Fetch SAML identity provider", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5042,6 +5318,10 @@ "about": "List IP pools linked to silo", "long_about": "Linked IP pools are available to users in the specified silo. A silo can have at most one default pool. IPs are allocated from the default pool when users ask for one without specifying a pool.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5072,6 +5352,10 @@ "about": "List silos", "long_about": "Lists silos that are discoverable based on the current permissions.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5128,6 +5412,10 @@ "name": "view", "about": "Fetch silo IAM policy", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5155,6 +5443,10 @@ "name": "list", "about": "Lists resource quotas for all silos", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5212,6 +5504,10 @@ "name": "view", "about": "Fetch resource quotas for silo", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5239,6 +5535,10 @@ "name": "list", "about": "List built-in (system) users in silo", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5264,6 +5564,10 @@ "name": "view", "about": "Fetch built-in (system) user", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5295,6 +5599,10 @@ "name": "list", "about": "List current utilization state for all silos", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5318,6 +5626,10 @@ "name": "view", "about": "Fetch current utilization for given silo", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5336,6 +5648,10 @@ "about": "Fetch silo", "long_about": "Fetch silo by name or ID.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5416,6 +5732,10 @@ "name": "list", "about": "List snapshots", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5443,6 +5763,10 @@ "name": "view", "about": "Fetch snapshot", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5494,6 +5818,10 @@ "name": "list", "about": "List physical disks", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5519,6 +5847,10 @@ "long": "disk-id", "help": "ID of the physical disk" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5542,6 +5874,10 @@ "name": "list", "about": "List racks", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5563,6 +5899,10 @@ "name": "view", "about": "Fetch rack", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5615,6 +5955,10 @@ "name": "disk-led", "about": "List physical disks attached to sleds", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5640,6 +5984,10 @@ "name": "instance-list", "about": "List instances running on given sled", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5665,6 +6013,10 @@ "name": "list", "about": "List sleds", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5686,6 +6038,10 @@ "name": "list-uninitialized", "about": "List uninitialized sleds", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5701,6 +6057,10 @@ "name": "set-provision-policy", "about": "Set sled provision policy", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "json-body", "help": "Path to a file that contains the full json body." @@ -5732,6 +6092,10 @@ "name": "view", "about": "Fetch sled", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5759,6 +6123,10 @@ "name": "list", "about": "List switches", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5780,6 +6148,10 @@ "name": "view", "about": "Fetch switch", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -5865,6 +6237,10 @@ "name": "list", "about": "List switch ports", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -5901,6 +6277,10 @@ "name": "status", "about": "Get switch port status", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "port", "help": "A name to use when selecting switch ports." @@ -6113,6 +6493,10 @@ "long": "address-lot", "help": "Name or ID of the address lot" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -6184,6 +6568,10 @@ "name": "list", "about": "List address lots", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -6211,6 +6599,10 @@ "long": "address-lot", "help": "Name or ID of the address lot" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -6259,6 +6651,10 @@ "name": "view", "about": "Get user-facing services IP allowlist", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -6356,6 +6752,10 @@ "name": "status", "about": "Get BFD status", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -6431,6 +6831,10 @@ "name": "list", "about": "List BGP announce sets", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -6500,6 +6904,10 @@ "long": "announce-set", "help": "Name or ID of the announce set" }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -6644,6 +7052,10 @@ "name": "list", "about": "List BGP configurations", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -6679,6 +7091,10 @@ "name": "ipv4", "about": "Get BGP exported routes", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -6778,6 +7194,10 @@ "long": "asn", "help": "The ASN to filter on. Required." }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -6803,6 +7223,10 @@ "long": "asn", "help": "The ASN to filter on. Required." }, + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -7106,6 +7530,10 @@ "name": "status", "about": "Get BGP peer status", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -7176,6 +7604,10 @@ "name": "view", "about": "Return whether API services can receive limited ICMP traffic", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -7367,6 +7799,10 @@ "name": "neighbors", "about": "Fetch the LLDP neighbors seen on a switch port", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -7467,6 +7903,10 @@ "name": "view", "about": "Fetch the LLDP configuration for a switch port", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "port", "help": "A name to use when selecting switch ports." @@ -7576,6 +8016,10 @@ "name": "list", "about": "List loopback addresses", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -7812,6 +8256,10 @@ "name": "list", "about": "List switch port settings", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -7850,6 +8298,10 @@ "name": "view", "about": "Get information about switch port", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "port", "help": "A name or id to use when selecting switch port settings info objects." @@ -7898,6 +8350,10 @@ "name": "view", "about": "Fetch top-level IAM policy", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -7962,6 +8418,10 @@ "name": "list", "about": "List users", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "group" }, @@ -7986,6 +8446,10 @@ "name": "list-sessions", "about": "List user's console sessions", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -8011,6 +8475,10 @@ "name": "list-tokens", "about": "List user's access tokens", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -8052,6 +8520,10 @@ "name": "view", "about": "Fetch user", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -8069,6 +8541,10 @@ "name": "utilization", "about": "Fetch resource utilization for user's current silo", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -8194,6 +8670,10 @@ "name": "view", "about": "List firewall rules", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -8215,6 +8695,10 @@ "name": "list", "about": "List VPCs", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -8308,6 +8792,10 @@ "name": "list", "about": "List routers", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -8414,6 +8902,10 @@ "about": "List routes", "long_about": "List the routes associated with a router in a particular VPC.", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -8490,6 +8982,10 @@ "name": "view", "about": "Fetch route", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -8556,6 +9052,10 @@ "name": "view", "about": "Fetch router", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -8659,6 +9159,10 @@ "name": "list", "about": "List subnets", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -8700,6 +9204,10 @@ "name": "list", "about": "List network interfaces", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "limit", "help": "Maximum number of items returned by a single call" @@ -8778,6 +9286,10 @@ "name": "view", "about": "Fetch subnet", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", @@ -8839,6 +9351,10 @@ "name": "view", "about": "Fetch VPC", "args": [ + { + "long": "format", + "help": "Format in which to print output: 'json' or 'table'" + }, { "long": "profile", "help": "Configuration profile to use for commands", diff --git a/cli/src/cli_builder.rs b/cli/src/cli_builder.rs index 47e08c28..b41f3a20 100644 --- a/cli/src/cli_builder.rs +++ b/cli/src/cli_builder.rs @@ -4,7 +4,10 @@ // Copyright 2025 Oxide Computer Company -use std::{any::TypeId, collections::BTreeMap, marker::PhantomData, net::IpAddr, path::PathBuf}; +use std::{ + any::TypeId, collections::BTreeMap, marker::PhantomData, net::IpAddr, path::PathBuf, + str::FromStr, +}; use anyhow::{bail, Result}; use async_trait::async_trait; @@ -14,10 +17,48 @@ use tracing_subscriber::EnvFilter; use crate::{ context::Context, generated_cli::{Cli, CliCommand}, - OxideOverride, RunnableCmd, + oxide_override::OxideOverride, + RunnableCmd, }; use oxide::{types::ByteCount, ClientConfig}; +#[derive(Default, Clone, Debug)] +pub enum Format { + /// Output as JSON + #[default] + Json, + /// Output as table with optional columns + Table { + /// Fields to display in the table + fields: Vec, + }, +} + +impl FromStr for Format { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s == "json" { + return Ok(Format::Json); + } + + if let Some(fields_str) = s.strip_prefix("table:") { + let fields: Vec = fields_str + .split(',') + // Allow users to pass a quoted string with spaces between column names, + // e.g. `--format "table: name, id"` + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + Ok(Format::Table { fields }) + } else if s == "table" { + Ok(Format::Table { fields: vec![] }) + } else { + Err("available formats are 'json' and 'table'") + } + } +} + /// Control an Oxide environment #[derive(clap::Parser, Debug, Clone)] #[command(name = "oxide", verbatim_doc_comment)] @@ -113,7 +154,46 @@ impl Default for NewCli<'_> { let Some(path) = xxx(op) else { continue }; runner.add_cmd(path, GeneratedCmd(op)); - let cmd = Cli::::get_command(op); + let mut cmd = Cli::::get_command(op); + if !op.field_names().is_empty() { + // Find the position to display `--format` to retain ordering. + let mut arg_names = vec!["format"]; + arg_names.extend(cmd.get_arguments().filter_map(|a| a.get_long())); + arg_names.sort_unstable(); + + let format_position = arg_names + .iter() + .enumerate() + .find(|(_, &arg)| arg == "format") + .map(|(i, _)| i) + .unwrap(); + + cmd = cmd.arg( + clap::Arg::new("format") + .long("format") + .required(false) + .value_parser(Format::from_str) + .display_order(format_position) + .help("Format in which to print output: 'json' or 'table'") + .long_help(format!( + "Format in which to print output + +Possible values: + - json Output as pretty-printed JSON + - table Output as table with all columns displayed + - table:field1,field2,... Output as table, specifying which columns to display + +Examples: + --format json + --format table + --format table:name,id,description + +Available fields: + - {}", + op.field_names().join("\n - "), + )), + ); + } let cmd = match op { CliCommand::IpPoolRangeAdd | CliCommand::IpPoolRangeRemove @@ -357,7 +437,12 @@ struct GeneratedCmd(CliCommand); impl RunIt for GeneratedCmd { async fn run_cmd(&self, matches: &ArgMatches, ctx: &Context) -> Result<()> { let client = oxide::Client::new_authenticated_config(ctx.client_config())?; - let cli = Cli::new(client, OxideOverride::default()); + + let ox_override = match matches.try_get_one::("format") { + Ok(Some(Format::Table { fields })) => OxideOverride::new_table(fields), + _ => OxideOverride::new_json(), + }; + let cli = Cli::new(client, ox_override); cli.execute(self.0, matches).await } diff --git a/cli/src/cmd_update.rs b/cli/src/cmd_update.rs index e354f26b..41caf53a 100644 --- a/cli/src/cmd_update.rs +++ b/cli/src/cmd_update.rs @@ -14,7 +14,10 @@ use oxide::{Client, ClientSystemUpdateExt}; use tokio::{fs::File, sync::watch}; use tokio_util::io::ReaderStream; -use crate::{generated_cli::CliConfig, util::start_progress_bar, AuthenticatedCmd, OxideOverride}; +use crate::{ + generated_cli::CliConfig, oxide_override::OxideOverride, util::start_progress_bar, + AuthenticatedCmd, +}; #[derive(Parser, Debug, Clone)] #[command(verbatim_doc_comment)] diff --git a/cli/src/context.rs b/cli/src/context.rs index e1e16d4d..98a1b76a 100644 --- a/cli/src/context.rs +++ b/cli/src/context.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -// Copyright 2024 Oxide Computer Company +// Copyright 2025 Oxide Computer Company use std::path::{Path, PathBuf}; diff --git a/cli/src/generated_cli.rs b/cli/src/generated_cli.rs index 1e002e91..22b0f489 100644 --- a/cli/src/generated_cli.rs +++ b/cli/src/generated_cli.rs @@ -18285,23 +18285,23 @@ impl Cli { pub trait CliConfig { fn success_item(&self, value: &ResponseValue) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; fn success_no_item(&self, value: &ResponseValue<()>); fn error(&self, value: &Error) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; fn list_start(&self) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; fn list_item(&self, value: &T) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; fn list_end_success(&self) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; fn list_end_error(&self, value: &Error) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; fn execute_device_auth_request( &self, matches: &::clap::ArgMatches, @@ -21044,3 +21044,3726 @@ impl CliCommand { .into_iter() } } + +/// A trait for flexibly accessing objects returned by the Oxide API. +pub trait ResponseFields { + /// The individual object type returned from the API. + /// + /// Generally this is the type wrapped by a `ResponseValue`, but in some + /// cases + /// this is a collection of items. For example, + /// `/v1/system/networking/bgp-routes-ipv4` returns a + /// `ResponseValue>`. + type Item: ResponseFields; + /// Get the field names of the object. + /// + /// For enums, the variant is included as "type". + /// Unnamed fields are accessed as "value" for a tuple of arity 1, or + /// "value_N", for + /// larger arities. + fn field_names() -> &'static [&'static str]; + /// Attempt to retrieve the specified field of an object as a JSON value. + /// + /// We convert to JSON instead of a string to provide callers with more + /// flexibility + /// in how they format the object. + fn get_field(&self, field_name: &str) -> Option; + /// Get an iterator over all `Item`s contained in the response. + fn items(&self) -> Box + '_>; +} + +impl ResponseFields for Vec { + type Item = T; + fn field_names() -> &'static [&'static str] { + T::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.iter()) + } +} + +impl ResponseFields for types::Error { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[] + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::empty()) + } +} + +impl ResponseFields for types::ProbeInfo { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["external_ips", "id", "interface", "name", "sled"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "external_ips" => serde_json::to_value(&self.external_ips).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "interface" => serde_json::to_value(&self.interface).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "sled" => serde_json::to_value(&self.sled).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::ProbeInfoResultsPage { + type Item = types::ProbeInfo; + fn field_names() -> &'static [&'static str] { + types::ProbeInfo::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Probe { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "name", + "sled", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "sled" => serde_json::to_value(&self.sled).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SupportBundleInfo { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "id", + "reason_for_creation", + "reason_for_failure", + "state", + "time_created", + "user_comment", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "id" => serde_json::to_value(&self.id).ok(), + "reason_for_creation" => serde_json::to_value(&self.reason_for_creation).ok(), + "reason_for_failure" => serde_json::to_value(&self.reason_for_failure).ok(), + "state" => serde_json::to_value(&self.state).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "user_comment" => serde_json::to_value(&self.user_comment).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SupportBundleInfoResultsPage { + type Item = types::SupportBundleInfo; + fn field_names() -> &'static [&'static str] { + types::SupportBundleInfo::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::AffinityGroup { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "failure_domain", + "id", + "name", + "policy", + "project_id", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "failure_domain" => serde_json::to_value(&self.failure_domain).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "policy" => serde_json::to_value(&self.policy).ok(), + "project_id" => serde_json::to_value(&self.project_id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AffinityGroupResultsPage { + type Item = types::AffinityGroup; + fn field_names() -> &'static [&'static str] { + types::AffinityGroup::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::AffinityGroupMember { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["type", "id", "name", "run_state"] + } + + fn get_field(&self, field_name: &str) -> Option { + match self { + types::AffinityGroupMember::Instance { + id, + name, + run_state, + } => match field_name { + "type" => Some(serde_json::Value::String(String::from("Instance"))), + "id" => serde_json::to_value(id).ok(), + "name" => serde_json::to_value(name).ok(), + "run_state" => serde_json::to_value(run_state).ok(), + _ => None, + }, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AffinityGroupMemberResultsPage { + type Item = types::AffinityGroupMember; + fn field_names() -> &'static [&'static str] { + types::AffinityGroupMember::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::AlertClass { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["description", "name"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "name" => serde_json::to_value(&self.name).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AlertClassResultsPage { + type Item = types::AlertClass; + fn field_names() -> &'static [&'static str] { + types::AlertClass::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::AlertReceiver { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "kind", + "name", + "subscriptions", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "kind" => serde_json::to_value(&self.kind).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "subscriptions" => serde_json::to_value(&self.subscriptions).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AlertReceiverResultsPage { + type Item = types::AlertReceiver; + fn field_names() -> &'static [&'static str] { + types::AlertReceiver::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::AlertDelivery { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "alert_class", + "alert_id", + "attempts", + "id", + "receiver_id", + "state", + "time_started", + "trigger", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "alert_class" => serde_json::to_value(&self.alert_class).ok(), + "alert_id" => serde_json::to_value(&self.alert_id).ok(), + "attempts" => serde_json::to_value(&self.attempts).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "receiver_id" => serde_json::to_value(&self.receiver_id).ok(), + "state" => serde_json::to_value(&self.state).ok(), + "time_started" => serde_json::to_value(&self.time_started).ok(), + "trigger" => serde_json::to_value(&self.trigger).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AlertDeliveryResultsPage { + type Item = types::AlertDelivery; + fn field_names() -> &'static [&'static str] { + types::AlertDelivery::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::AlertProbeResult { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["probe", "resends_started"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "probe" => serde_json::to_value(&self.probe).ok(), + "resends_started" => serde_json::to_value(&self.resends_started).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AlertSubscriptionCreated { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["subscription"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "subscription" => serde_json::to_value(&self.subscription).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AlertDeliveryId { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["delivery_id"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "delivery_id" => serde_json::to_value(&self.delivery_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AntiAffinityGroup { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "failure_domain", + "id", + "name", + "policy", + "project_id", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "failure_domain" => serde_json::to_value(&self.failure_domain).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "policy" => serde_json::to_value(&self.policy).ok(), + "project_id" => serde_json::to_value(&self.project_id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AntiAffinityGroupResultsPage { + type Item = types::AntiAffinityGroup; + fn field_names() -> &'static [&'static str] { + types::AntiAffinityGroup::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::AntiAffinityGroupMember { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["type", "id", "name", "run_state"] + } + + fn get_field(&self, field_name: &str) -> Option { + match self { + types::AntiAffinityGroupMember::Instance { + id, + name, + run_state, + } => match field_name { + "type" => Some(serde_json::Value::String(String::from("Instance"))), + "id" => serde_json::to_value(id).ok(), + "name" => serde_json::to_value(name).ok(), + "run_state" => serde_json::to_value(run_state).ok(), + _ => None, + }, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AntiAffinityGroupMemberResultsPage { + type Item = types::AntiAffinityGroupMember; + fn field_names() -> &'static [&'static str] { + types::AntiAffinityGroupMember::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::SiloAuthSettings { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["device_token_max_ttl_seconds", "silo_id"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "device_token_max_ttl_seconds" => { + serde_json::to_value(&self.device_token_max_ttl_seconds).ok() + } + "silo_id" => serde_json::to_value(&self.silo_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::Certificate { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "cert", + "description", + "id", + "name", + "service", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "cert" => serde_json::to_value(&self.cert).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "service" => serde_json::to_value(&self.service).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::CertificateResultsPage { + type Item = types::Certificate; + fn field_names() -> &'static [&'static str] { + types::Certificate::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Disk { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "block_size", + "description", + "device_path", + "id", + "image_id", + "name", + "project_id", + "size", + "snapshot_id", + "state", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "block_size" => serde_json::to_value(&self.block_size).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "device_path" => serde_json::to_value(&self.device_path).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "image_id" => serde_json::to_value(&self.image_id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "project_id" => serde_json::to_value(&self.project_id).ok(), + "size" => serde_json::to_value(&self.size).ok(), + "snapshot_id" => serde_json::to_value(&self.snapshot_id).ok(), + "state" => serde_json::to_value(&self.state).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::DiskResultsPage { + type Item = types::Disk; + fn field_names() -> &'static [&'static str] { + types::Disk::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::FloatingIp { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "instance_id", + "ip", + "ip_pool_id", + "name", + "project_id", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "instance_id" => serde_json::to_value(&self.instance_id).ok(), + "ip" => serde_json::to_value(&self.ip).ok(), + "ip_pool_id" => serde_json::to_value(&self.ip_pool_id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "project_id" => serde_json::to_value(&self.project_id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::FloatingIpResultsPage { + type Item = types::FloatingIp; + fn field_names() -> &'static [&'static str] { + types::FloatingIp::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Group { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["display_name", "id", "silo_id"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "display_name" => serde_json::to_value(&self.display_name).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "silo_id" => serde_json::to_value(&self.silo_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::GroupResultsPage { + type Item = types::Group; + fn field_names() -> &'static [&'static str] { + types::Group::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Image { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "block_size", + "description", + "digest", + "id", + "name", + "os", + "project_id", + "size", + "time_created", + "time_modified", + "version", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "block_size" => serde_json::to_value(&self.block_size).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "digest" => serde_json::to_value(&self.digest).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "os" => serde_json::to_value(&self.os).ok(), + "project_id" => serde_json::to_value(&self.project_id).ok(), + "size" => serde_json::to_value(&self.size).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "version" => serde_json::to_value(&self.version).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::ImageResultsPage { + type Item = types::Image; + fn field_names() -> &'static [&'static str] { + types::Image::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Instance { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "auto_restart_cooldown_expiration", + "auto_restart_enabled", + "auto_restart_policy", + "boot_disk_id", + "cpu_platform", + "description", + "hostname", + "id", + "memory", + "name", + "ncpus", + "project_id", + "run_state", + "time_created", + "time_last_auto_restarted", + "time_modified", + "time_run_state_updated", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "auto_restart_cooldown_expiration" => { + serde_json::to_value(&self.auto_restart_cooldown_expiration).ok() + } + "auto_restart_enabled" => serde_json::to_value(&self.auto_restart_enabled).ok(), + "auto_restart_policy" => serde_json::to_value(&self.auto_restart_policy).ok(), + "boot_disk_id" => serde_json::to_value(&self.boot_disk_id).ok(), + "cpu_platform" => serde_json::to_value(&self.cpu_platform).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "hostname" => serde_json::to_value(&self.hostname).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "memory" => serde_json::to_value(&self.memory).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "ncpus" => serde_json::to_value(&self.ncpus).ok(), + "project_id" => serde_json::to_value(&self.project_id).ok(), + "run_state" => serde_json::to_value(&self.run_state).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_last_auto_restarted" => serde_json::to_value(&self.time_last_auto_restarted).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "time_run_state_updated" => serde_json::to_value(&self.time_run_state_updated).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::InstanceResultsPage { + type Item = types::Instance; + fn field_names() -> &'static [&'static str] { + types::Instance::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::ExternalIp { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "type", + "first_port", + "ip", + "ip_pool_id", + "last_port", + "description", + "id", + "instance_id", + "name", + "project_id", + "time_created", + "time_modified", + ] + } + + fn get_field(&self, field_name: &str) -> Option { + match self { + types::ExternalIp::Snat { + first_port, + ip, + ip_pool_id, + last_port, + } => match field_name { + "type" => Some(serde_json::Value::String(String::from("Snat"))), + "first_port" => serde_json::to_value(first_port).ok(), + "ip" => serde_json::to_value(ip).ok(), + "ip_pool_id" => serde_json::to_value(ip_pool_id).ok(), + "last_port" => serde_json::to_value(last_port).ok(), + _ => None, + }, + types::ExternalIp::Ephemeral { ip, ip_pool_id } => match field_name { + "type" => Some(serde_json::Value::String(String::from("Ephemeral"))), + "ip" => serde_json::to_value(ip).ok(), + "ip_pool_id" => serde_json::to_value(ip_pool_id).ok(), + _ => None, + }, + types::ExternalIp::Floating { + description, + id, + instance_id, + ip, + ip_pool_id, + name, + project_id, + time_created, + time_modified, + } => match field_name { + "type" => Some(serde_json::Value::String(String::from("Floating"))), + "description" => serde_json::to_value(description).ok(), + "id" => serde_json::to_value(id).ok(), + "instance_id" => serde_json::to_value(instance_id).ok(), + "ip" => serde_json::to_value(ip).ok(), + "ip_pool_id" => serde_json::to_value(ip_pool_id).ok(), + "name" => serde_json::to_value(name).ok(), + "project_id" => serde_json::to_value(project_id).ok(), + "time_created" => serde_json::to_value(time_created).ok(), + "time_modified" => serde_json::to_value(time_modified).ok(), + _ => None, + }, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::ExternalIpResultsPage { + type Item = types::ExternalIp; + fn field_names() -> &'static [&'static str] { + types::ExternalIp::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::InstanceSerialConsoleData { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["data", "last_byte_offset"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "data" => serde_json::to_value(&self.data).ok(), + "last_byte_offset" => serde_json::to_value(&self.last_byte_offset).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SshKey { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "name", + "public_key", + "silo_user_id", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "public_key" => serde_json::to_value(&self.public_key).ok(), + "silo_user_id" => serde_json::to_value(&self.silo_user_id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SshKeyResultsPage { + type Item = types::SshKey; + fn field_names() -> &'static [&'static str] { + types::SshKey::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::InternetGatewayIpAddress { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "address", + "description", + "id", + "internet_gateway_id", + "name", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "address" => serde_json::to_value(&self.address).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "internet_gateway_id" => serde_json::to_value(&self.internet_gateway_id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::InternetGatewayIpAddressResultsPage { + type Item = types::InternetGatewayIpAddress; + fn field_names() -> &'static [&'static str] { + types::InternetGatewayIpAddress::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::InternetGatewayIpPool { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "internet_gateway_id", + "ip_pool_id", + "name", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "internet_gateway_id" => serde_json::to_value(&self.internet_gateway_id).ok(), + "ip_pool_id" => serde_json::to_value(&self.ip_pool_id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::InternetGatewayIpPoolResultsPage { + type Item = types::InternetGatewayIpPool; + fn field_names() -> &'static [&'static str] { + types::InternetGatewayIpPool::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::InternetGateway { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "name", + "time_created", + "time_modified", + "vpc_id", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "vpc_id" => serde_json::to_value(&self.vpc_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::InternetGatewayResultsPage { + type Item = types::InternetGateway; + fn field_names() -> &'static [&'static str] { + types::InternetGateway::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::SiloIpPool { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "is_default", + "name", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "is_default" => serde_json::to_value(&self.is_default).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SiloIpPoolResultsPage { + type Item = types::SiloIpPool; + fn field_names() -> &'static [&'static str] { + types::SiloIpPool::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::CurrentUser { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "display_name", + "fleet_viewer", + "id", + "silo_admin", + "silo_id", + "silo_name", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "display_name" => serde_json::to_value(&self.display_name).ok(), + "fleet_viewer" => serde_json::to_value(&self.fleet_viewer).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "silo_admin" => serde_json::to_value(&self.silo_admin).ok(), + "silo_id" => serde_json::to_value(&self.silo_id).ok(), + "silo_name" => serde_json::to_value(&self.silo_name).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::DeviceAccessToken { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["id", "time_created", "time_expires"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "id" => serde_json::to_value(&self.id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_expires" => serde_json::to_value(&self.time_expires).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::DeviceAccessTokenResultsPage { + type Item = types::DeviceAccessToken; + fn field_names() -> &'static [&'static str] { + types::DeviceAccessToken::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Measurement { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["datum", "timestamp"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "datum" => serde_json::to_value(&self.datum).ok(), + "timestamp" => serde_json::to_value(&self.timestamp).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::MeasurementResultsPage { + type Item = types::Measurement; + fn field_names() -> &'static [&'static str] { + types::Measurement::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::InstanceNetworkInterface { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "instance_id", + "ip", + "mac", + "name", + "primary", + "subnet_id", + "time_created", + "time_modified", + "transit_ips", + "vpc_id", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "instance_id" => serde_json::to_value(&self.instance_id).ok(), + "ip" => serde_json::to_value(&self.ip).ok(), + "mac" => serde_json::to_value(&self.mac).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "primary" => serde_json::to_value(&self.primary).ok(), + "subnet_id" => serde_json::to_value(&self.subnet_id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "transit_ips" => serde_json::to_value(&self.transit_ips).ok(), + "vpc_id" => serde_json::to_value(&self.vpc_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::InstanceNetworkInterfaceResultsPage { + type Item = types::InstanceNetworkInterface; + fn field_names() -> &'static [&'static str] { + types::InstanceNetworkInterface::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Ping { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["status"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "status" => serde_json::to_value(&self.status).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SiloRolePolicy { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["role_assignments"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "role_assignments" => serde_json::to_value(&self.role_assignments).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::Project { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["description", "id", "name", "time_created", "time_modified"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::ProjectResultsPage { + type Item = types::Project; + fn field_names() -> &'static [&'static str] { + types::Project::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::ProjectRolePolicy { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["role_assignments"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "role_assignments" => serde_json::to_value(&self.role_assignments).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::Snapshot { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "disk_id", + "id", + "name", + "project_id", + "size", + "state", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "disk_id" => serde_json::to_value(&self.disk_id).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "project_id" => serde_json::to_value(&self.project_id).ok(), + "size" => serde_json::to_value(&self.size).ok(), + "state" => serde_json::to_value(&self.state).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SnapshotResultsPage { + type Item = types::Snapshot; + fn field_names() -> &'static [&'static str] { + types::Snapshot::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::AuditLogEntry { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "actor", + "auth_method", + "id", + "operation_id", + "request_id", + "request_uri", + "result", + "source_ip", + "time_completed", + "time_started", + "user_agent", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "actor" => serde_json::to_value(&self.actor).ok(), + "auth_method" => serde_json::to_value(&self.auth_method).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "operation_id" => serde_json::to_value(&self.operation_id).ok(), + "request_id" => serde_json::to_value(&self.request_id).ok(), + "request_uri" => serde_json::to_value(&self.request_uri).ok(), + "result" => serde_json::to_value(&self.result).ok(), + "source_ip" => serde_json::to_value(&self.source_ip).ok(), + "time_completed" => serde_json::to_value(&self.time_completed).ok(), + "time_started" => serde_json::to_value(&self.time_started).ok(), + "user_agent" => serde_json::to_value(&self.user_agent).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AuditLogEntryResultsPage { + type Item = types::AuditLogEntry; + fn field_names() -> &'static [&'static str] { + types::AuditLogEntry::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::PhysicalDisk { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "form_factor", + "id", + "model", + "policy", + "serial", + "sled_id", + "state", + "time_created", + "time_modified", + "vendor", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "form_factor" => serde_json::to_value(&self.form_factor).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "model" => serde_json::to_value(&self.model).ok(), + "policy" => serde_json::to_value(&self.policy).ok(), + "serial" => serde_json::to_value(&self.serial).ok(), + "sled_id" => serde_json::to_value(&self.sled_id).ok(), + "state" => serde_json::to_value(&self.state).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "vendor" => serde_json::to_value(&self.vendor).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::PhysicalDiskResultsPage { + type Item = types::PhysicalDisk; + fn field_names() -> &'static [&'static str] { + types::PhysicalDisk::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::LldpNeighbor { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "chassis_id", + "first_seen", + "last_seen", + "link_description", + "link_name", + "local_port", + "management_ip", + "system_description", + "system_name", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "chassis_id" => serde_json::to_value(&self.chassis_id).ok(), + "first_seen" => serde_json::to_value(&self.first_seen).ok(), + "last_seen" => serde_json::to_value(&self.last_seen).ok(), + "link_description" => serde_json::to_value(&self.link_description).ok(), + "link_name" => serde_json::to_value(&self.link_name).ok(), + "local_port" => serde_json::to_value(&self.local_port).ok(), + "management_ip" => serde_json::to_value(&self.management_ip).ok(), + "system_description" => serde_json::to_value(&self.system_description).ok(), + "system_name" => serde_json::to_value(&self.system_name).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::LldpNeighborResultsPage { + type Item = types::LldpNeighbor; + fn field_names() -> &'static [&'static str] { + types::LldpNeighbor::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Rack { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["id", "time_created", "time_modified"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "id" => serde_json::to_value(&self.id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::RackResultsPage { + type Item = types::Rack; + fn field_names() -> &'static [&'static str] { + types::Rack::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Sled { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "baseboard", + "id", + "policy", + "rack_id", + "state", + "time_created", + "time_modified", + "usable_hardware_threads", + "usable_physical_ram", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "baseboard" => serde_json::to_value(&self.baseboard).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "policy" => serde_json::to_value(&self.policy).ok(), + "rack_id" => serde_json::to_value(&self.rack_id).ok(), + "state" => serde_json::to_value(&self.state).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "usable_hardware_threads" => serde_json::to_value(&self.usable_hardware_threads).ok(), + "usable_physical_ram" => serde_json::to_value(&self.usable_physical_ram).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SledResultsPage { + type Item = types::Sled; + fn field_names() -> &'static [&'static str] { + types::Sled::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::SledId { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["id"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "id" => serde_json::to_value(&self.id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SledInstance { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "active_sled_id", + "id", + "memory", + "migration_id", + "name", + "ncpus", + "project_name", + "silo_name", + "state", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "active_sled_id" => serde_json::to_value(&self.active_sled_id).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "memory" => serde_json::to_value(&self.memory).ok(), + "migration_id" => serde_json::to_value(&self.migration_id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "ncpus" => serde_json::to_value(&self.ncpus).ok(), + "project_name" => serde_json::to_value(&self.project_name).ok(), + "silo_name" => serde_json::to_value(&self.silo_name).ok(), + "state" => serde_json::to_value(&self.state).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SledInstanceResultsPage { + type Item = types::SledInstance; + fn field_names() -> &'static [&'static str] { + types::SledInstance::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::SledProvisionPolicyResponse { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["new_state", "old_state"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "new_state" => serde_json::to_value(&self.new_state).ok(), + "old_state" => serde_json::to_value(&self.old_state).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::UninitializedSled { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["baseboard", "cubby", "rack_id"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "baseboard" => serde_json::to_value(&self.baseboard).ok(), + "cubby" => serde_json::to_value(&self.cubby).ok(), + "rack_id" => serde_json::to_value(&self.rack_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::UninitializedSledResultsPage { + type Item = types::UninitializedSled; + fn field_names() -> &'static [&'static str] { + types::UninitializedSled::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::SwitchPort { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "id", + "port_name", + "port_settings_id", + "rack_id", + "switch_location", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "id" => serde_json::to_value(&self.id).ok(), + "port_name" => serde_json::to_value(&self.port_name).ok(), + "port_settings_id" => serde_json::to_value(&self.port_settings_id).ok(), + "rack_id" => serde_json::to_value(&self.rack_id).ok(), + "switch_location" => serde_json::to_value(&self.switch_location).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SwitchPortResultsPage { + type Item = types::SwitchPort; + fn field_names() -> &'static [&'static str] { + types::SwitchPort::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::LldpLinkConfig { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "chassis_id", + "enabled", + "id", + "link_description", + "link_name", + "management_ip", + "system_description", + "system_name", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "chassis_id" => serde_json::to_value(&self.chassis_id).ok(), + "enabled" => serde_json::to_value(&self.enabled).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "link_description" => serde_json::to_value(&self.link_description).ok(), + "link_name" => serde_json::to_value(&self.link_name).ok(), + "management_ip" => serde_json::to_value(&self.management_ip).ok(), + "system_description" => serde_json::to_value(&self.system_description).ok(), + "system_name" => serde_json::to_value(&self.system_name).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SwitchLinkState { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["value"] + } + + #[allow(clippy::borrow_deref_ref)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "value" => serde_json::to_value(&*self).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::Switch { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "baseboard", + "id", + "rack_id", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "baseboard" => serde_json::to_value(&self.baseboard).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "rack_id" => serde_json::to_value(&self.rack_id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SwitchResultsPage { + type Item = types::Switch; + fn field_names() -> &'static [&'static str] { + types::Switch::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::IdentityProvider { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "name", + "provider_type", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "provider_type" => serde_json::to_value(&self.provider_type).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::IdentityProviderResultsPage { + type Item = types::IdentityProvider; + fn field_names() -> &'static [&'static str] { + types::IdentityProvider::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::User { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["display_name", "id", "silo_id"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "display_name" => serde_json::to_value(&self.display_name).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "silo_id" => serde_json::to_value(&self.silo_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SamlIdentityProvider { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "acs_url", + "description", + "group_attribute_name", + "id", + "idp_entity_id", + "name", + "public_cert", + "slo_url", + "sp_client_id", + "technical_contact_email", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "acs_url" => serde_json::to_value(&self.acs_url).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "group_attribute_name" => serde_json::to_value(&self.group_attribute_name).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "idp_entity_id" => serde_json::to_value(&self.idp_entity_id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "public_cert" => serde_json::to_value(&self.public_cert).ok(), + "slo_url" => serde_json::to_value(&self.slo_url).ok(), + "sp_client_id" => serde_json::to_value(&self.sp_client_id).ok(), + "technical_contact_email" => serde_json::to_value(&self.technical_contact_email).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::IpPool { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "ip_version", + "name", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "ip_version" => serde_json::to_value(&self.ip_version).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::IpPoolResultsPage { + type Item = types::IpPool; + fn field_names() -> &'static [&'static str] { + types::IpPool::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::IpPoolRange { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["id", "ip_pool_id", "range", "time_created"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "id" => serde_json::to_value(&self.id).ok(), + "ip_pool_id" => serde_json::to_value(&self.ip_pool_id).ok(), + "range" => serde_json::to_value(&self.range).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::IpPoolRangeResultsPage { + type Item = types::IpPoolRange; + fn field_names() -> &'static [&'static str] { + types::IpPoolRange::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::IpPoolSiloLink { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["ip_pool_id", "is_default", "silo_id"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "ip_pool_id" => serde_json::to_value(&self.ip_pool_id).ok(), + "is_default" => serde_json::to_value(&self.is_default).ok(), + "silo_id" => serde_json::to_value(&self.silo_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::IpPoolSiloLinkResultsPage { + type Item = types::IpPoolSiloLink; + fn field_names() -> &'static [&'static str] { + types::IpPoolSiloLink::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::IpPoolUtilization { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["capacity", "remaining"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "capacity" => serde_json::to_value(&self.capacity).ok(), + "remaining" => serde_json::to_value(&self.remaining).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AddressLot { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "kind", + "name", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "kind" => serde_json::to_value(&self.kind).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AddressLotResultsPage { + type Item = types::AddressLot; + fn field_names() -> &'static [&'static str] { + types::AddressLot::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::AddressLotCreateResponse { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["blocks", "lot"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "blocks" => serde_json::to_value(&self.blocks).ok(), + "lot" => serde_json::to_value(&self.lot).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AddressLotViewResponse { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["blocks", "lot"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "blocks" => serde_json::to_value(&self.blocks).ok(), + "lot" => serde_json::to_value(&self.lot).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AddressLotBlock { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["first_address", "id", "last_address"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "first_address" => serde_json::to_value(&self.first_address).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "last_address" => serde_json::to_value(&self.last_address).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AddressLotBlockResultsPage { + type Item = types::AddressLotBlock; + fn field_names() -> &'static [&'static str] { + types::AddressLotBlock::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::AllowList { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["allowed_ips", "time_created", "time_modified"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "allowed_ips" => serde_json::to_value(&self.allowed_ips).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::BfdStatus { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "detection_threshold", + "local", + "mode", + "peer", + "required_rx", + "state", + "switch", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "detection_threshold" => serde_json::to_value(&self.detection_threshold).ok(), + "local" => serde_json::to_value(&self.local).ok(), + "mode" => serde_json::to_value(&self.mode).ok(), + "peer" => serde_json::to_value(&self.peer).ok(), + "required_rx" => serde_json::to_value(&self.required_rx).ok(), + "state" => serde_json::to_value(&self.state).ok(), + "switch" => serde_json::to_value(&self.switch).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::BgpConfig { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "asn", + "description", + "id", + "name", + "time_created", + "time_modified", + "vrf", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "asn" => serde_json::to_value(&self.asn).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "vrf" => serde_json::to_value(&self.vrf).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::BgpConfigResultsPage { + type Item = types::BgpConfig; + fn field_names() -> &'static [&'static str] { + types::BgpConfig::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::BgpAnnounceSet { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["description", "id", "name", "time_created", "time_modified"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::BgpAnnouncement { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["address_lot_block_id", "announce_set_id", "network"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "address_lot_block_id" => serde_json::to_value(&self.address_lot_block_id).ok(), + "announce_set_id" => serde_json::to_value(&self.announce_set_id).ok(), + "network" => serde_json::to_value(&self.network).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::BgpExported { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["exports"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "exports" => serde_json::to_value(&self.exports).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::AggregateBgpMessageHistory { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["switch_histories"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "switch_histories" => serde_json::to_value(&self.switch_histories).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::BgpImportedRouteIpv4 { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["id", "nexthop", "prefix", "switch"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "id" => serde_json::to_value(&self.id).ok(), + "nexthop" => serde_json::to_value(&self.nexthop).ok(), + "prefix" => serde_json::to_value(&self.prefix).ok(), + "switch" => serde_json::to_value(&self.switch).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::BgpPeerStatus { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "addr", + "local_asn", + "remote_asn", + "state", + "state_duration_millis", + "switch", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "addr" => serde_json::to_value(&self.addr).ok(), + "local_asn" => serde_json::to_value(&self.local_asn).ok(), + "remote_asn" => serde_json::to_value(&self.remote_asn).ok(), + "state" => serde_json::to_value(&self.state).ok(), + "state_duration_millis" => serde_json::to_value(&self.state_duration_millis).ok(), + "switch" => serde_json::to_value(&self.switch).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::ServiceIcmpConfig { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["enabled"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "enabled" => serde_json::to_value(&self.enabled).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::LoopbackAddress { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "address", + "address_lot_block_id", + "id", + "rack_id", + "switch_location", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "address" => serde_json::to_value(&self.address).ok(), + "address_lot_block_id" => serde_json::to_value(&self.address_lot_block_id).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "rack_id" => serde_json::to_value(&self.rack_id).ok(), + "switch_location" => serde_json::to_value(&self.switch_location).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::LoopbackAddressResultsPage { + type Item = types::LoopbackAddress; + fn field_names() -> &'static [&'static str] { + types::LoopbackAddress::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::SwitchPortSettingsIdentity { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["description", "id", "name", "time_created", "time_modified"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SwitchPortSettingsIdentityResultsPage { + type Item = types::SwitchPortSettingsIdentity; + fn field_names() -> &'static [&'static str] { + types::SwitchPortSettingsIdentity::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::SwitchPortSettings { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "addresses", + "bgp_peers", + "description", + "groups", + "id", + "interfaces", + "links", + "name", + "port", + "routes", + "time_created", + "time_modified", + "vlan_interfaces", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "addresses" => serde_json::to_value(&self.addresses).ok(), + "bgp_peers" => serde_json::to_value(&self.bgp_peers).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "groups" => serde_json::to_value(&self.groups).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "interfaces" => serde_json::to_value(&self.interfaces).ok(), + "links" => serde_json::to_value(&self.links).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "port" => serde_json::to_value(&self.port).ok(), + "routes" => serde_json::to_value(&self.routes).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "vlan_interfaces" => serde_json::to_value(&self.vlan_interfaces).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::FleetRolePolicy { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["role_assignments"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "role_assignments" => serde_json::to_value(&self.role_assignments).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SiloQuotas { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["cpus", "memory", "silo_id", "storage"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "cpus" => serde_json::to_value(&self.cpus).ok(), + "memory" => serde_json::to_value(&self.memory).ok(), + "silo_id" => serde_json::to_value(&self.silo_id).ok(), + "storage" => serde_json::to_value(&self.storage).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SiloQuotasResultsPage { + type Item = types::SiloQuotas; + fn field_names() -> &'static [&'static str] { + types::SiloQuotas::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Silo { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "admin_group_name", + "description", + "discoverable", + "id", + "identity_mode", + "mapped_fleet_roles", + "name", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "admin_group_name" => serde_json::to_value(&self.admin_group_name).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "discoverable" => serde_json::to_value(&self.discoverable).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "identity_mode" => serde_json::to_value(&self.identity_mode).ok(), + "mapped_fleet_roles" => serde_json::to_value(&self.mapped_fleet_roles).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SiloResultsPage { + type Item = types::Silo; + fn field_names() -> &'static [&'static str] { + types::Silo::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::OxqlQueryResult { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["tables"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "tables" => serde_json::to_value(&self.tables).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::TimeseriesSchema { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "authz_scope", + "created", + "datum_type", + "description", + "field_schema", + "timeseries_name", + "units", + "version", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "authz_scope" => serde_json::to_value(&self.authz_scope).ok(), + "created" => serde_json::to_value(&self.created).ok(), + "datum_type" => serde_json::to_value(&self.datum_type).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "field_schema" => serde_json::to_value(&self.field_schema).ok(), + "timeseries_name" => serde_json::to_value(&self.timeseries_name).ok(), + "units" => serde_json::to_value(&self.units).ok(), + "version" => serde_json::to_value(&self.version).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::TimeseriesSchemaResultsPage { + type Item = types::TimeseriesSchema; + fn field_names() -> &'static [&'static str] { + types::TimeseriesSchema::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::TufRepoInsertResponse { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["recorded", "status"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "recorded" => serde_json::to_value(&self.recorded).ok(), + "status" => serde_json::to_value(&self.status).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::TufRepoGetResponse { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["description"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::TargetRelease { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["generation", "release_source", "time_requested"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "generation" => serde_json::to_value(&self.generation).ok(), + "release_source" => serde_json::to_value(&self.release_source).ok(), + "time_requested" => serde_json::to_value(&self.time_requested).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::UpdatesTrustRoot { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["id", "root_role", "time_created"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "id" => serde_json::to_value(&self.id).ok(), + "root_role" => serde_json::to_value(&self.root_role).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::UpdatesTrustRootResultsPage { + type Item = types::UpdatesTrustRoot; + fn field_names() -> &'static [&'static str] { + types::UpdatesTrustRoot::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::UserResultsPage { + type Item = types::User; + fn field_names() -> &'static [&'static str] { + types::User::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::UserBuiltin { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["description", "id", "name", "time_created", "time_modified"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::UserBuiltinResultsPage { + type Item = types::UserBuiltin; + fn field_names() -> &'static [&'static str] { + types::UserBuiltin::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::SiloUtilization { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["allocated", "provisioned", "silo_id", "silo_name"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "allocated" => serde_json::to_value(&self.allocated).ok(), + "provisioned" => serde_json::to_value(&self.provisioned).ok(), + "silo_id" => serde_json::to_value(&self.silo_id).ok(), + "silo_name" => serde_json::to_value(&self.silo_name).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::SiloUtilizationResultsPage { + type Item = types::SiloUtilization; + fn field_names() -> &'static [&'static str] { + types::SiloUtilization::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::ConsoleSession { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["id", "time_created", "time_last_used"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "id" => serde_json::to_value(&self.id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_last_used" => serde_json::to_value(&self.time_last_used).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::ConsoleSessionResultsPage { + type Item = types::ConsoleSession; + fn field_names() -> &'static [&'static str] { + types::ConsoleSession::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Utilization { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["capacity", "provisioned"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "capacity" => serde_json::to_value(&self.capacity).ok(), + "provisioned" => serde_json::to_value(&self.provisioned).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::VpcFirewallRules { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["rules"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "rules" => serde_json::to_value(&self.rules).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::RouterRoute { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "destination", + "id", + "kind", + "name", + "target", + "time_created", + "time_modified", + "vpc_router_id", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "destination" => serde_json::to_value(&self.destination).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "kind" => serde_json::to_value(&self.kind).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "target" => serde_json::to_value(&self.target).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "vpc_router_id" => serde_json::to_value(&self.vpc_router_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::RouterRouteResultsPage { + type Item = types::RouterRoute; + fn field_names() -> &'static [&'static str] { + types::RouterRoute::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::VpcRouter { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "id", + "kind", + "name", + "time_created", + "time_modified", + "vpc_id", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "kind" => serde_json::to_value(&self.kind).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "vpc_id" => serde_json::to_value(&self.vpc_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::VpcRouterResultsPage { + type Item = types::VpcRouter; + fn field_names() -> &'static [&'static str] { + types::VpcRouter::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::VpcSubnet { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "custom_router_id", + "description", + "id", + "ipv4_block", + "ipv6_block", + "name", + "time_created", + "time_modified", + "vpc_id", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "custom_router_id" => serde_json::to_value(&self.custom_router_id).ok(), + "description" => serde_json::to_value(&self.description).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "ipv4_block" => serde_json::to_value(&self.ipv4_block).ok(), + "ipv6_block" => serde_json::to_value(&self.ipv6_block).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + "vpc_id" => serde_json::to_value(&self.vpc_id).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::VpcSubnetResultsPage { + type Item = types::VpcSubnet; + fn field_names() -> &'static [&'static str] { + types::VpcSubnet::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::Vpc { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "dns_name", + "id", + "ipv6_prefix", + "name", + "project_id", + "system_router_id", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "dns_name" => serde_json::to_value(&self.dns_name).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "ipv6_prefix" => serde_json::to_value(&self.ipv6_prefix).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "project_id" => serde_json::to_value(&self.project_id).ok(), + "system_router_id" => serde_json::to_value(&self.system_router_id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::VpcResultsPage { + type Item = types::Vpc; + fn field_names() -> &'static [&'static str] { + types::Vpc::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } +} + +impl ResponseFields for types::WebhookReceiver { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &[ + "description", + "endpoint", + "id", + "name", + "secrets", + "subscriptions", + "time_created", + "time_modified", + ] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "description" => serde_json::to_value(&self.description).ok(), + "endpoint" => serde_json::to_value(&self.endpoint).ok(), + "id" => serde_json::to_value(&self.id).ok(), + "name" => serde_json::to_value(&self.name).ok(), + "secrets" => serde_json::to_value(&self.secrets).ok(), + "subscriptions" => serde_json::to_value(&self.subscriptions).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + "time_modified" => serde_json::to_value(&self.time_modified).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::WebhookSecrets { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["secrets"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "secrets" => serde_json::to_value(&self.secrets).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl ResponseFields for types::WebhookSecret { + type Item = Self; + fn field_names() -> &'static [&'static str] { + &["id", "time_created"] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + "id" => serde_json::to_value(&self.id).ok(), + "time_created" => serde_json::to_value(&self.time_created).ok(), + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } +} + +impl CliCommand { + pub fn field_names(&self) -> &'static [&'static str] { + match self { + CliCommand::ProbeList => types::ProbeInfoResultsPage::field_names(), + CliCommand::ProbeView => types::ProbeInfo::field_names(), + CliCommand::SupportBundleList => types::SupportBundleInfoResultsPage::field_names(), + CliCommand::SupportBundleView => types::SupportBundleInfo::field_names(), + CliCommand::AffinityGroupList => types::AffinityGroupResultsPage::field_names(), + CliCommand::AffinityGroupView => types::AffinityGroup::field_names(), + CliCommand::AffinityGroupMemberList => { + types::AffinityGroupMemberResultsPage::field_names() + } + CliCommand::AffinityGroupMemberInstanceView => { + types::AffinityGroupMember::field_names() + } + CliCommand::AlertClassList => types::AlertClassResultsPage::field_names(), + CliCommand::AlertReceiverList => types::AlertReceiverResultsPage::field_names(), + CliCommand::AlertReceiverView => types::AlertReceiver::field_names(), + CliCommand::AlertDeliveryList => types::AlertDeliveryResultsPage::field_names(), + CliCommand::AntiAffinityGroupList => types::AntiAffinityGroupResultsPage::field_names(), + CliCommand::AntiAffinityGroupView => types::AntiAffinityGroup::field_names(), + CliCommand::AntiAffinityGroupMemberList => { + types::AntiAffinityGroupMemberResultsPage::field_names() + } + CliCommand::AntiAffinityGroupMemberInstanceView => { + types::AntiAffinityGroupMember::field_names() + } + CliCommand::AuthSettingsView => types::SiloAuthSettings::field_names(), + CliCommand::CertificateList => types::CertificateResultsPage::field_names(), + CliCommand::CertificateView => types::Certificate::field_names(), + CliCommand::DiskList => types::DiskResultsPage::field_names(), + CliCommand::DiskView => types::Disk::field_names(), + CliCommand::FloatingIpList => types::FloatingIpResultsPage::field_names(), + CliCommand::FloatingIpView => types::FloatingIp::field_names(), + CliCommand::GroupList => types::GroupResultsPage::field_names(), + CliCommand::GroupView => types::Group::field_names(), + CliCommand::ImageList => types::ImageResultsPage::field_names(), + CliCommand::ImageView => types::Image::field_names(), + CliCommand::InstanceList => types::InstanceResultsPage::field_names(), + CliCommand::InstanceView => types::Instance::field_names(), + CliCommand::InstanceAffinityGroupList => types::AffinityGroupResultsPage::field_names(), + CliCommand::InstanceAntiAffinityGroupList => { + types::AntiAffinityGroupResultsPage::field_names() + } + CliCommand::InstanceDiskList => types::DiskResultsPage::field_names(), + CliCommand::InstanceExternalIpList => types::ExternalIpResultsPage::field_names(), + CliCommand::InstanceSshPublicKeyList => types::SshKeyResultsPage::field_names(), + CliCommand::InternetGatewayIpAddressList => { + types::InternetGatewayIpAddressResultsPage::field_names() + } + CliCommand::InternetGatewayIpPoolList => { + types::InternetGatewayIpPoolResultsPage::field_names() + } + CliCommand::InternetGatewayList => types::InternetGatewayResultsPage::field_names(), + CliCommand::InternetGatewayView => types::InternetGateway::field_names(), + CliCommand::ProjectIpPoolList => types::SiloIpPoolResultsPage::field_names(), + CliCommand::ProjectIpPoolView => types::SiloIpPool::field_names(), + CliCommand::CurrentUserView => types::CurrentUser::field_names(), + CliCommand::CurrentUserAccessTokenList => { + types::DeviceAccessTokenResultsPage::field_names() + } + CliCommand::CurrentUserGroups => types::GroupResultsPage::field_names(), + CliCommand::CurrentUserSshKeyList => types::SshKeyResultsPage::field_names(), + CliCommand::CurrentUserSshKeyView => types::SshKey::field_names(), + CliCommand::SiloMetric => types::MeasurementResultsPage::field_names(), + CliCommand::InstanceNetworkInterfaceList => { + types::InstanceNetworkInterfaceResultsPage::field_names() + } + CliCommand::InstanceNetworkInterfaceView => { + types::InstanceNetworkInterface::field_names() + } + CliCommand::Ping => types::Ping::field_names(), + CliCommand::PolicyView => types::SiloRolePolicy::field_names(), + CliCommand::ProjectList => types::ProjectResultsPage::field_names(), + CliCommand::ProjectView => types::Project::field_names(), + CliCommand::ProjectPolicyView => types::ProjectRolePolicy::field_names(), + CliCommand::SnapshotList => types::SnapshotResultsPage::field_names(), + CliCommand::SnapshotView => types::Snapshot::field_names(), + CliCommand::AuditLogList => types::AuditLogEntryResultsPage::field_names(), + CliCommand::PhysicalDiskList => types::PhysicalDiskResultsPage::field_names(), + CliCommand::PhysicalDiskView => types::PhysicalDisk::field_names(), + CliCommand::NetworkingSwitchPortLldpNeighbors => { + types::LldpNeighborResultsPage::field_names() + } + CliCommand::RackList => types::RackResultsPage::field_names(), + CliCommand::RackView => types::Rack::field_names(), + CliCommand::SledList => types::SledResultsPage::field_names(), + CliCommand::SledView => types::Sled::field_names(), + CliCommand::SledPhysicalDiskList => types::PhysicalDiskResultsPage::field_names(), + CliCommand::SledInstanceList => types::SledInstanceResultsPage::field_names(), + CliCommand::SledSetProvisionPolicy => types::SledProvisionPolicyResponse::field_names(), + CliCommand::SledListUninitialized => types::UninitializedSledResultsPage::field_names(), + CliCommand::NetworkingSwitchPortList => types::SwitchPortResultsPage::field_names(), + CliCommand::NetworkingSwitchPortLldpConfigView => types::LldpLinkConfig::field_names(), + CliCommand::NetworkingSwitchPortStatus => types::SwitchLinkState::field_names(), + CliCommand::SwitchList => types::SwitchResultsPage::field_names(), + CliCommand::SwitchView => types::Switch::field_names(), + CliCommand::SiloIdentityProviderList => { + types::IdentityProviderResultsPage::field_names() + } + CliCommand::SamlIdentityProviderView => types::SamlIdentityProvider::field_names(), + CliCommand::IpPoolList => types::IpPoolResultsPage::field_names(), + CliCommand::IpPoolView => types::IpPool::field_names(), + CliCommand::IpPoolRangeList => types::IpPoolRangeResultsPage::field_names(), + CliCommand::IpPoolSiloList => types::IpPoolSiloLinkResultsPage::field_names(), + CliCommand::IpPoolSiloLink => types::IpPoolSiloLink::field_names(), + CliCommand::IpPoolUtilizationView => types::IpPoolUtilization::field_names(), + CliCommand::IpPoolServiceView => types::IpPool::field_names(), + CliCommand::IpPoolServiceRangeList => types::IpPoolRangeResultsPage::field_names(), + CliCommand::SystemMetric => types::MeasurementResultsPage::field_names(), + CliCommand::NetworkingAddressLotList => types::AddressLotResultsPage::field_names(), + CliCommand::NetworkingAddressLotView => types::AddressLotViewResponse::field_names(), + CliCommand::NetworkingAddressLotBlockList => { + types::AddressLotBlockResultsPage::field_names() + } + CliCommand::NetworkingAllowListView => types::AllowList::field_names(), + CliCommand::NetworkingBfdStatus => ::std::vec::Vec::::field_names(), + CliCommand::NetworkingBgpConfigList => types::BgpConfigResultsPage::field_names(), + CliCommand::NetworkingBgpAnnounceSetList => { + ::std::vec::Vec::::field_names() + } + CliCommand::NetworkingBgpAnnouncementList => { + ::std::vec::Vec::::field_names() + } + CliCommand::NetworkingBgpExported => types::BgpExported::field_names(), + CliCommand::NetworkingBgpMessageHistory => { + types::AggregateBgpMessageHistory::field_names() + } + CliCommand::NetworkingBgpImportedRoutesIpv4 => { + ::std::vec::Vec::::field_names() + } + CliCommand::NetworkingBgpStatus => { + ::std::vec::Vec::::field_names() + } + CliCommand::NetworkingInboundIcmpView => types::ServiceIcmpConfig::field_names(), + CliCommand::NetworkingLoopbackAddressList => { + types::LoopbackAddressResultsPage::field_names() + } + CliCommand::NetworkingSwitchPortSettingsList => { + types::SwitchPortSettingsIdentityResultsPage::field_names() + } + CliCommand::NetworkingSwitchPortSettingsView => { + types::SwitchPortSettings::field_names() + } + CliCommand::SystemPolicyView => types::FleetRolePolicy::field_names(), + CliCommand::SystemQuotasList => types::SiloQuotasResultsPage::field_names(), + CliCommand::SiloList => types::SiloResultsPage::field_names(), + CliCommand::SiloView => types::Silo::field_names(), + CliCommand::SiloIpPoolList => types::SiloIpPoolResultsPage::field_names(), + CliCommand::SiloPolicyView => types::SiloRolePolicy::field_names(), + CliCommand::SiloQuotasView => types::SiloQuotas::field_names(), + CliCommand::SystemTimeseriesQuery => types::OxqlQueryResult::field_names(), + CliCommand::SystemTimeseriesSchemaList => { + types::TimeseriesSchemaResultsPage::field_names() + } + CliCommand::SystemUpdatePutRepository => types::TufRepoInsertResponse::field_names(), + CliCommand::SystemUpdateGetRepository => types::TufRepoGetResponse::field_names(), + CliCommand::TargetReleaseView => types::TargetRelease::field_names(), + CliCommand::SystemUpdateTrustRootList => { + types::UpdatesTrustRootResultsPage::field_names() + } + CliCommand::SystemUpdateTrustRootView => types::UpdatesTrustRoot::field_names(), + CliCommand::SiloUserList => types::UserResultsPage::field_names(), + CliCommand::SiloUserView => types::User::field_names(), + CliCommand::UserBuiltinList => types::UserBuiltinResultsPage::field_names(), + CliCommand::UserBuiltinView => types::UserBuiltin::field_names(), + CliCommand::SiloUtilizationList => types::SiloUtilizationResultsPage::field_names(), + CliCommand::SiloUtilizationView => types::SiloUtilization::field_names(), + CliCommand::TimeseriesQuery => types::OxqlQueryResult::field_names(), + CliCommand::UserList => types::UserResultsPage::field_names(), + CliCommand::UserView => types::User::field_names(), + CliCommand::UserTokenList => types::DeviceAccessTokenResultsPage::field_names(), + CliCommand::UserSessionList => types::ConsoleSessionResultsPage::field_names(), + CliCommand::UtilizationView => types::Utilization::field_names(), + CliCommand::VpcFirewallRulesView => types::VpcFirewallRules::field_names(), + CliCommand::VpcRouterRouteList => types::RouterRouteResultsPage::field_names(), + CliCommand::VpcRouterRouteView => types::RouterRoute::field_names(), + CliCommand::VpcRouterList => types::VpcRouterResultsPage::field_names(), + CliCommand::VpcRouterView => types::VpcRouter::field_names(), + CliCommand::VpcSubnetList => types::VpcSubnetResultsPage::field_names(), + CliCommand::VpcSubnetView => types::VpcSubnet::field_names(), + CliCommand::VpcSubnetListNetworkInterfaces => { + types::InstanceNetworkInterfaceResultsPage::field_names() + } + CliCommand::VpcList => types::VpcResultsPage::field_names(), + CliCommand::VpcView => types::Vpc::field_names(), + CliCommand::WebhookSecretsList => types::WebhookSecrets::field_names(), + _ => &[], + } + } +} diff --git a/cli/src/oxide_override.rs b/cli/src/oxide_override.rs index dbed2e07..b98f64e2 100644 --- a/cli/src/oxide_override.rs +++ b/cli/src/oxide_override.rs @@ -4,100 +4,220 @@ // Copyright 2025 Oxide Computer Company +use std::collections::HashSet; use std::net::IpAddr; use std::path::PathBuf; use std::sync::atomic::AtomicBool; +use std::sync::Mutex; -use crate::generated_cli::CliConfig; +use crate::generated_cli::{CliConfig, ResponseFields}; use crate::{eprintln_nopipe, print_nopipe, println_nopipe, IpOrNet}; use anyhow::Context as _; use base64::Engine; +use comfy_table::{ContentArrangement, Table}; use oxide::types::{ AllowedSourceIps, DerEncodedKeyPair, IdpMetadataSource, IpRange, Ipv4Range, Ipv6Range, }; -#[derive(Default)] -pub struct OxideOverride { - needs_comma: AtomicBool, +const NO_USER_REQUESTED_FIELDS: &str = + "ERROR: None of the requested '--format' fields are present in this command's output"; + +pub enum OxideOverride { + Json { needs_comma: AtomicBool }, + Table { table: Box> }, } -impl OxideOverride { - fn ip_range(matches: &clap::ArgMatches) -> anyhow::Result { - let first = matches.get_one::("first").unwrap(); - let last = matches.get_one::("last").unwrap(); +/// Format response values into a table. +pub struct TableFormatter { + requested_fields: Vec, + fields_to_print: Vec, + table: Table, +} - match (first, last) { - (IpAddr::V4(first), IpAddr::V4(last)) => { - let range = Ipv4Range::try_from(Ipv4Range::builder().first(*first).last(*last))?; - Ok(range.into()) +impl TableFormatter { + fn new(requested_fields: &[String]) -> Self { + let mut table = Table::new(); + + table + .load_preset(comfy_table::presets::NOTHING) + .set_content_arrangement(ContentArrangement::Disabled); + + Self { + // Downcase user-requested fields to better match the return type. + requested_fields: requested_fields.iter().map(|f| f.to_lowercase()).collect(), + fields_to_print: Vec::new(), + table, + } + } + + fn set_header_fields(&mut self, available_fields: &[&str]) -> Result<(), String> { + let fields_to_print = if !self.requested_fields.is_empty() { + let requested: HashSet<_> = self.requested_fields.iter().map(|s| s.as_str()).collect(); + let available: HashSet<_> = available_fields.iter().copied().collect(); + let invalid = requested.difference(&available); + + for field in invalid { + eprintln_nopipe!("WARNING: '{field}' is not a valid field"); } - (IpAddr::V6(first), IpAddr::V6(last)) => { - let range = Ipv6Range::try_from(Ipv6Range::builder().first(*first).last(*last))?; - Ok(range.into()) + + let mut fields = self.requested_fields.to_vec(); + fields.retain(|f| available.contains(f.as_str())); + + if fields.is_empty() { + // Show list of the available fields to the user. + let field_list = available_fields + .iter() + .map(|f| f.to_string()) + .collect::>() + .join("\n "); + + return Err(format!( + "{NO_USER_REQUESTED_FIELDS}\n\nAvailable fields:\n {field_list}" + )); + } + + fields + } else { + available_fields.iter().map(|s| s.to_string()).collect() + }; + + let upcased: Vec<_> = fields_to_print.iter().map(|f| f.to_uppercase()).collect(); + + self.table.set_header(upcased); + self.fields_to_print = fields_to_print; + + Ok(()) + } + + fn add_row(&mut self, obj: &T) { + let mut row = Vec::with_capacity(self.fields_to_print.len()); + + for field in &self.fields_to_print { + let s = obj + .get_field(field) + .filter(|v| !v.is_null()) // Convert Null to None + .map(|v| v.to_string()) + .unwrap_or_default(); + + // `to_string` encloses values in double quotes. Remove these unless we're + // writing a multi-word field. + if s.contains(' ') { + row.push(s); + } else { + row.push(s.trim_matches('"').to_string()); } - _ => anyhow::bail!( - "first and last must either both be ipv4 or ipv6 addresses".to_string() - ), } + self.table.add_row(row); + } +} + +impl Default for OxideOverride { + fn default() -> Self { + Self::new_json() } } impl CliConfig for OxideOverride { fn success_item(&self, value: &oxide::ResponseValue) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug, { - let s = serde_json::to_string_pretty(std::ops::Deref::deref(value)) - .expect("failed to serialize return to json"); - println_nopipe!("{}", s); + match &self { + OxideOverride::Json { needs_comma: _ } => { + let s = serde_json::to_string_pretty(std::ops::Deref::deref(value)) + .expect("failed to serialize return to json"); + println_nopipe!("{}", s); + } + OxideOverride::Table { table: t } => { + let mut t = t.lock().unwrap(); + + if let Err(e) = t.set_header_fields(T::field_names()) { + eprintln_nopipe!("{e}"); + std::process::exit(1); + } + + for item in value.items() { + t.add_row(item); + } + + println_nopipe!("{}", t.table); + } + } } fn success_no_item(&self, _: &oxide::ResponseValue<()>) {} fn error(&self, _value: &oxide::Error) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug, { eprintln_nopipe!("error"); } fn list_start(&self) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug, { - self.needs_comma - .store(false, std::sync::atomic::Ordering::Relaxed); - print_nopipe!("["); + match &self { + OxideOverride::Json { needs_comma } => { + needs_comma.store(false, std::sync::atomic::Ordering::Relaxed); + print_nopipe!("["); + } + OxideOverride::Table { table: t } => { + let mut t = t.lock().unwrap(); + + if let Err(e) = t.set_header_fields(T::field_names()) { + eprintln_nopipe!("{e}"); + std::process::exit(1); + } + } + } } fn list_item(&self, value: &T) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug, { - let s = serde_json::to_string_pretty(&[value]).expect("failed to serialize result to json"); - if self.needs_comma.load(std::sync::atomic::Ordering::Relaxed) { - print_nopipe!(", {}", &s[4..s.len() - 2]); - } else { - print_nopipe!("\n{}", &s[2..s.len() - 2]); - }; - self.needs_comma - .store(true, std::sync::atomic::Ordering::Relaxed); + match &self { + OxideOverride::Json { needs_comma } => { + let s = serde_json::to_string_pretty(&[value]) + .expect("failed to serialize result to json"); + if needs_comma.load(std::sync::atomic::Ordering::Relaxed) { + print_nopipe!(", {}", &s[4..s.len() - 2]); + } else { + print_nopipe!("\n{}", &s[2..s.len() - 2]); + }; + needs_comma.store(true, std::sync::atomic::Ordering::Relaxed); + } + OxideOverride::Table { table: t } => { + let mut t = t.lock().unwrap(); + t.add_row(value); + } + } } fn list_end_success(&self) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug, { - if self.needs_comma.load(std::sync::atomic::Ordering::Relaxed) { - println_nopipe!("\n]"); - } else { - println_nopipe!("]"); + match &self { + OxideOverride::Json { needs_comma } => { + if needs_comma.load(std::sync::atomic::Ordering::Relaxed) { + println_nopipe!("\n]"); + } else { + println_nopipe!("]"); + } + } + OxideOverride::Table { table: t } => { + let t = t.lock().unwrap(); + println_nopipe!("{}", t.table); + } } } fn list_end_error(&self, _value: &oxide::Error) where - T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug, + T: ResponseFields + schemars::JsonSchema + serde::Serialize + std::fmt::Debug, { self.list_end_success::() } @@ -221,3 +341,38 @@ impl CliConfig for OxideOverride { Ok(()) } } + +impl OxideOverride { + /// Construct a new OxideOverride for JSON output. + pub fn new_json() -> Self { + OxideOverride::Json { + needs_comma: AtomicBool::new(false), + } + } + + /// Construct a new OxideOverride for tabular output. + pub fn new_table(fields: &[String]) -> Self { + OxideOverride::Table { + table: Box::new(Mutex::new(TableFormatter::new(fields))), + } + } + + fn ip_range(matches: &clap::ArgMatches) -> anyhow::Result { + let first = matches.get_one::("first").unwrap(); + let last = matches.get_one::("last").unwrap(); + + match (first, last) { + (IpAddr::V4(first), IpAddr::V4(last)) => { + let range = Ipv4Range::try_from(Ipv4Range::builder().first(*first).last(*last))?; + Ok(range.into()) + } + (IpAddr::V6(first), IpAddr::V6(last)) => { + let range = Ipv6Range::try_from(Ipv6Range::builder().first(*first).last(*last))?; + Ok(range.into()) + } + _ => anyhow::bail!( + "first and last must either both be ipv4 or ipv6 addresses".to_string() + ), + } + } +} diff --git a/cli/tests/data/test_table_anti_affinity_group_members.stdout b/cli/tests/data/test_table_anti_affinity_group_members.stdout new file mode 100644 index 00000000..52953b3d --- /dev/null +++ b/cli/tests/data/test_table_anti_affinity_group_members.stdout @@ -0,0 +1,5 @@ + TYPE ID NAME RUN_STATE + Instance 603800ff-07da-3298-0070-bbd45865eabb qintar starting + Instance e272d3bf-c637-7d5d-3a7d-ab4481968f52 suqs starting + Instance 26254a50-c57c-8083-692d-37f41d8ab55a umiaq failed + Instance a880ac27-4e97-15bf-8098-8119595a2344 qaid running diff --git a/cli/tests/data/test_table_bgp_routes.stdout b/cli/tests/data/test_table_bgp_routes.stdout new file mode 100644 index 00000000..e2e26cba --- /dev/null +++ b/cli/tests/data/test_table_bgp_routes.stdout @@ -0,0 +1,3 @@ + ADDR LOCAL_ASN REMOTE_ASN STATE STATE_DURATION_MILLIS SWITCH + 10.0.0.1 65001 65002 open_confirm 1000000 switch0 + 10.0.0.2 65003 65004 established 1000000 switch1 diff --git a/cli/tests/data/test_table_project_list_basic_table.stdout b/cli/tests/data/test_table_project_list_basic_table.stdout new file mode 100644 index 00000000..cb7c5d2b --- /dev/null +++ b/cli/tests/data/test_table_project_list_basic_table.stdout @@ -0,0 +1,5 @@ + DESCRIPTION ID NAME TIME_CREATED TIME_MODIFIED + qats fb603800-ff07-da32-9800-70bbd45865ea qiviuts 1983-04-06T03:52:51Z 2015-10-10T15:39:40Z + qintars e272d3bf-c637-7d5d-3a7d-ab4481968f52 suqs 1958-10-29T11:12:56Z 1993-07-07T01:08:41Z + qindarkas 254a50c5-7c80-8369-2d37-f41d8ab55a50 suqs 2023-12-07T23:07:16Z 1997-01-29T14:17:53Z + suq ac274e97-15bf-8098-8119-595a2344c36d buqsha 1976-06-06T06:59:52Z 1973-08-11T14:45:56Z diff --git a/cli/tests/data/test_table_project_list_json.stdout b/cli/tests/data/test_table_project_list_json.stdout new file mode 100644 index 00000000..10194583 --- /dev/null +++ b/cli/tests/data/test_table_project_list_json.stdout @@ -0,0 +1,27 @@ +[ + { + "description": "qats", + "id": "fb603800-ff07-da32-9800-70bbd45865ea", + "name": "qiviuts", + "time_created": "1983-04-06T03:52:51Z", + "time_modified": "2015-10-10T15:39:40Z" + }, { + "description": "qintars", + "id": "e272d3bf-c637-7d5d-3a7d-ab4481968f52", + "name": "suqs", + "time_created": "1958-10-29T11:12:56Z", + "time_modified": "1993-07-07T01:08:41Z" + }, { + "description": "qindarkas", + "id": "254a50c5-7c80-8369-2d37-f41d8ab55a50", + "name": "suqs", + "time_created": "2023-12-07T23:07:16Z", + "time_modified": "1997-01-29T14:17:53Z" + }, { + "description": "suq", + "id": "ac274e97-15bf-8098-8119-595a2344c36d", + "name": "buqsha", + "time_created": "1976-06-06T06:59:52Z", + "time_modified": "1973-08-11T14:45:56Z" + } +] diff --git a/cli/tests/data/test_table_project_list_table_with_fields.stdout b/cli/tests/data/test_table_project_list_table_with_fields.stdout new file mode 100644 index 00000000..da19a018 --- /dev/null +++ b/cli/tests/data/test_table_project_list_table_with_fields.stdout @@ -0,0 +1,5 @@ + NAME ID + qiviuts fb603800-ff07-da32-9800-70bbd45865ea + suqs e272d3bf-c637-7d5d-3a7d-ab4481968f52 + suqs 254a50c5-7c80-8369-2d37-f41d8ab55a50 + buqsha ac274e97-15bf-8098-8119-595a2344c36d diff --git a/cli/tests/data/test_table_project_list_table_with_no_requested_fields.stderr b/cli/tests/data/test_table_project_list_table_with_no_requested_fields.stderr new file mode 100644 index 00000000..25a804f9 --- /dev/null +++ b/cli/tests/data/test_table_project_list_table_with_no_requested_fields.stderr @@ -0,0 +1,9 @@ +WARNING: 'not_a_field' is not a valid field +ERROR: None of the requested '--format' fields are present in this command's output + +Available fields: + description + id + name + time_created + time_modified diff --git a/cli/tests/test_table.rs b/cli/tests/test_table.rs new file mode 100644 index 00000000..145e9e5d --- /dev/null +++ b/cli/tests/test_table.rs @@ -0,0 +1,256 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2025 Oxide Computer Company + +use assert_cmd::Command; +use httpmock::MockServer; +use oxide::types::{ + AntiAffinityGroupMember, AntiAffinityGroupMemberResultsPage, BgpPeerState, BgpPeerStatus, + Project, ProjectResultsPage, SwitchLocation, +}; +use oxide_httpmock::MockServerExt; +use rand::SeedableRng; +use test_common::JsonMock; + +#[test] +fn test_table_arg_parse() { + let mut src = rand::rngs::SmallRng::seed_from_u64(42); + let server = MockServer::start(); + + let results = ProjectResultsPage { + items: Vec::::mock_value(&mut src).unwrap(), + next_page: None, + }; + + let mock = server.project_list(|when, then| { + when.into_inner().any_request(); + then.ok(&results); + }); + + let format_args = [ + // JSON + ["--format", "json"], + // Simple table + ["--format", "table"], + // Table with column names specified + ["--format", "table:name,id"], + // Table with spaces between column names + ["--format", "table: name, id "], + ]; + + for args in format_args { + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("project") + .arg("list") + .arg("--sort-by") + .arg("name_ascending") + .args(args) + .assert() + .success(); + } + + let bad_args = [ + // Unknown format + ["--format", "foo"], + // JSON with field args + ["--format", "json:name,id"], + ]; + + for args in bad_args { + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("project") + .arg("list") + .arg("--sort-by") + .arg("name_ascending") + .args(args) + .assert() + .failure(); + } + + mock.assert_hits(format_args.len()); +} + +#[test] +fn test_table_project_list() { + let mut src = rand::rngs::SmallRng::seed_from_u64(42); + let server = MockServer::start(); + + let results = ProjectResultsPage { + items: Vec::::mock_value(&mut src).unwrap(), + next_page: None, + }; + + let mock = server.project_list(|when, then| { + when.into_inner().any_request(); + then.ok(&results); + }); + + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("project") + .arg("list") + .arg("--sort-by") + .arg("name_ascending") + .arg("--format") + .arg("json") + .assert() + .success() + .stdout(expectorate::eq_file_or_panic( + "tests/data/test_table_project_list_json.stdout", + )); + + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("project") + .arg("list") + .arg("--sort-by") + .arg("name_ascending") + .arg("--format") + .arg("table") + .assert() + .success() + .stdout(expectorate::eq_file_or_panic( + "tests/data/test_table_project_list_basic_table.stdout", + )); + + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("project") + .arg("list") + .arg("--sort-by") + .arg("name_ascending") + .arg("--format") + .arg("table:name,id") + .assert() + .success() + .stdout(expectorate::eq_file_or_panic( + "tests/data/test_table_project_list_table_with_fields.stdout", + )); + + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("project") + .arg("list") + .arg("--sort-by") + .arg("name_ascending") + .arg("--format") + .arg("table:not_a_field") + .assert() + .failure() + .stderr(expectorate::eq_file_or_panic( + "tests/data/test_table_project_list_table_with_no_requested_fields.stderr", + )); + + mock.assert_hits(3); +} + +/// Validate an endpoint returning `Vec`. +#[test] +fn test_table_bgp_routes() { + let server = MockServer::start(); + + // Manually construct the response as `Vec` is not compatible with + // `serde_json::from_value()`. + let results = vec![ + BgpPeerStatus { + addr: "10.0.0.1".parse().unwrap(), + local_asn: 65001, + remote_asn: 65002, + state: BgpPeerState::OpenConfirm, + state_duration_millis: 1_000_000, + switch: SwitchLocation::Switch0, + }, + BgpPeerStatus { + addr: "10.0.0.2".parse().unwrap(), + local_asn: 65003, + remote_asn: 65004, + state: BgpPeerState::Established, + state_duration_millis: 1_000_000, + switch: SwitchLocation::Switch1, + }, + ]; + + let mock = server.networking_bgp_status(|when, then| { + when.into_inner().any_request(); + then.ok(&results); + }); + + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("system") + .arg("networking") + .arg("bgp") + .arg("status") + .arg("--format") + .arg("table") + .assert() + .success() + .stdout(expectorate::eq_file_or_panic( + "tests/data/test_table_bgp_routes.stdout", + )); + + mock.assert(); +} + +/// Validate an endpoint returning an enum. +#[test] +fn test_table_anti_affinity_group_members() { + let mut src = rand::rngs::SmallRng::seed_from_u64(42); + let server = MockServer::start(); + + let results = AntiAffinityGroupMemberResultsPage { + items: Vec::::mock_value(&mut src).unwrap(), + next_page: None, + }; + + let mock = server.anti_affinity_group_member_list(|when, then| { + when.into_inner().any_request(); + then.ok(&results); + }); + + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("instance") + .arg("anti-affinity") + .arg("member") + .arg("list") + .arg("--anti-affinity-group") + .arg("42e56270-889a-e74d-fa7c-849b22449cd6") + .arg("--format") + .arg("table") + .assert() + .success() + .stdout(expectorate::eq_file_or_panic( + "tests/data/test_table_anti_affinity_group_members.stdout", + )); + + mock.assert(); +} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 81b4e64f..21f372ae 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -6,10 +6,14 @@ publish = false [dependencies] clap = { workspace = true } +indexmap = { workspace = true } newline-converter = { workspace = true } +proc-macro2 = { workspace = true } progenitor = { workspace = true, default-features = false } +quote = { workspace = true } regex = { workspace = true } rustc_version = { workspace = true } rustfmt-wrapper = { workspace = true } serde_json = { workspace = true } similar = { workspace = true } +syn = { workspace = true } diff --git a/xtask/src/cli_extras.rs b/xtask/src/cli_extras.rs new file mode 100644 index 00000000..ca0ba33e --- /dev/null +++ b/xtask/src/cli_extras.rs @@ -0,0 +1,723 @@ +use indexmap::IndexSet; +use proc_macro2::TokenStream; +use quote::quote; +use std::collections::HashSet; +use std::sync::OnceLock; +use syn::{ + Fields, GenericArgument, Generics, Ident, ImplItem, ImplItemFn, Index, Item, ItemEnum, + ItemImpl, ItemStruct, Path, PathArguments, ReturnType, Signature, Type, TypePath, Variant, +}; + +static UNPRESENTABLE_TYPES: OnceLock> = OnceLock::new(); + +fn is_unpresentable(type_name: &str) -> bool { + UNPRESENTABLE_TYPES + .get_or_init(|| { + HashSet::from([ + "ByteStream", // Unstructured bytes + "reqwest::Upgraded", // HTTP connection + "serde_json::Value", // No defined field names + "types::InstanceSerialConsoleData", // Unstructured bytes + ]) + }) + .contains(type_name) +} + +/// Generate a `ResponseFields` implementation for all types in the `oxide::types` module and +/// inject it into `CliCommand`. +pub fn gen_response_fields(sdk_tokens: TokenStream, cli_tokens: TokenStream) -> TokenStream { + let sdk_file: syn::File = syn::parse2(sdk_tokens).unwrap(); + let cli_file: syn::File = syn::parse2(cli_tokens).unwrap(); + + let response_field_tokens = gen_response_fields_impls(&sdk_file); + let cli_cmd_tokens = gen_cli_command(&sdk_file, &cli_file); + + quote! { + + #response_field_tokens + + #cli_cmd_tokens + } +} + +/// Generate `ResponseFields` impls for all types returned by a `send` fn. +pub fn gen_response_fields_impls(sdk_file: &syn::File) -> TokenStream { + let builder_content = mod_content(sdk_file, "builder"); + let types_content = mod_content(sdk_file, "types"); + + let mut implemented_types = HashSet::new(); + let mut impls = TokenStream::new(); + + for impl_items in builder_content.iter().filter_map(|i| match i { + Item::Impl(ItemImpl { + trait_: None, + items, + .. + }) => Some(items.as_slice()), + _ => None, + }) { + let ret_type = send_return_type(impl_items); + let inner_type = response_value_inner_type(ret_type); + let Some(ident) = filter_innermost_type_ident(inner_type) else { + continue; + }; + if let Some(tokens) = gen_for_ident(types_content, &mut implemented_types, &ident) { + impls.extend(tokens); + } + } + + quote! { + /// A trait for flexibly accessing objects returned by the Oxide API. + pub trait ResponseFields { + /// The individual object type returned from the API. + /// + /// Generally this is the type wrapped by a `ResponseValue`, but in some cases + /// this is a collection of items. For example, + /// `/v1/system/networking/bgp-routes-ipv4` returns a + /// `ResponseValue>`. + type Item: ResponseFields; + + /// Get the field names of the object. + /// + /// For enums, the variant is included as "type". + /// Unnamed fields are accessed as "value" for a tuple of arity 1, or "value_N", for + /// larger arities. + fn field_names() -> &'static [&'static str]; + + /// Attempt to retrieve the specified field of an object as a JSON value. + /// + /// We convert to JSON instead of a string to provide callers with more flexibility + /// in how they format the object. + fn get_field(&self, field_name: &str) -> Option; + + /// Get an iterator over all `Item`s contained in the response. + fn items(&self) -> Box + '_>; + } + + impl ResponseFields for Vec { + type Item = T; + + fn field_names() -> &'static [&'static str] { + T::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.iter()) + } + } + + impl ResponseFields for types::Error { + type Item = Self; + + fn field_names() -> &'static [&'static str] { + &[] + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::empty()) + } + } + + #impls + } +} + +/// Extract the content of a module. +fn mod_content<'a>(file: &'a syn::File, name: &str) -> &'a Vec { + let module = file + .items + .iter() + .filter_map(|i| match i { + Item::Mod(module) => Some(module), + _ => None, + }) + .find(|m| m.ident == name) + .unwrap(); + + let Some((_, content)) = &module.content else { + unreachable!("no module content"); + }; + + content +} + +/// Get the return type for `send()`. +fn send_return_type(items: &[ImplItem]) -> Type { + items + .iter() + .filter_map(|i| match i { + ImplItem::Fn(ImplItemFn { + sig: + Signature { + ident: fn_ident, + output: ReturnType::Type(_, ret), + .. + }, + .. + }) if fn_ident == "send" => Some(ret), + _ => None, + }) + .next() + .map(|ret_type| *ret_type.clone()) + .expect("no send fn in block") +} + +/// Extract the inner `T` from `Result, E>`. +fn response_value_inner_type(ret_type: Type) -> Type { + let Type::Path(TypePath { path, .. }) = ret_type else { + unreachable!("type was not a path"); + }; + + // Get the generic arguments to `Result`. + let last_segment = path.segments.last().unwrap(); + let PathArguments::AngleBracketed(args) = &last_segment.arguments else { + unreachable!("no angle bracket on path"); + }; + + // Get the first type argument in `Result`. + let Some(GenericArgument::Type(Type::Path(type_path))) = args.args.first() else { + unreachable!("no generic args in path"); + }; + + // This type must be a `ResponseValue`. + let segment = type_path.path.segments.last().unwrap(); + if segment.ident != "ResponseValue" { + unreachable!("return type was not a ResponseValue"); + } + + // Get its generic argument. + let PathArguments::AngleBracketed(inner_args) = &segment.arguments else { + unreachable!("no angle bracket on ResponseValue"); + }; + let Some(GenericArgument::Type(inner_type)) = inner_args.args.first() else { + unreachable!("no generic args in ResponseValue"); + }; + + inner_type.clone() +} + +/// Get the `Ident` of the type being returned, filtering for types that need an +/// implementation of `ResponseFields`. +fn filter_innermost_type_ident(ty: Type) -> Option { + // This filters out `()`. + let Type::Path(TypePath { path, .. }) = ty else { + return None; + }; + + let mut inner_ident = path.segments.last()?.ident.clone(); + let mut path_str = path_to_string(&path); + + if inner_ident == "Vec" { + let PathArguments::AngleBracketed(args) = &path.segments.last()?.arguments else { + return None; + }; + + // Take the inner `T`. + let GenericArgument::Type(vec_ty) = args.args.first()? else { + return None; + }; + + let Type::Path(TypePath { path: vec_path, .. }) = vec_ty else { + return None; + }; + inner_ident = vec_path.segments.last()?.ident.clone(); + path_str = path_to_string(vec_path); + } + + // Only handle `oxide::types` types, we don't need impls for the others. + if path_str.starts_with("types::") { + Some(inner_ident) + } else { + None + } +} + +/// Generate a `ResponseFields` impl for an `Ident`. +fn gen_for_ident( + types_content: &[Item], + implemented_types: &mut HashSet, + ty_ident: &Ident, +) -> Option { + if !implemented_types.insert(ty_ident.to_string()) { + return None; + } + + match types_content + .iter() + .find(|i| + matches!(i, Item::Enum(ItemEnum { ident, ..} ) | Item::Struct(ItemStruct { ident, .. }) if ident == ty_ident) + ) { + Some(Item::Enum(e)) => { + let (impl_generics, ty_generics, where_clause) = e.generics.split_for_impl(); + let impls = gen_enum_impl(e); + let ident = &e.ident; + + Some(quote! { + impl #impl_generics ResponseFields for types::#ident #ty_generics #where_clause { + #impls + } + }) + } + Some(Item::Struct(ItemStruct { + fields, + ident, + generics, + .. + })) => { + if ident.to_string().ends_with("ResultsPage") { + gen_result_page_impl(ident, fields, types_content, generics, implemented_types) + } else { + let impls = gen_struct_impl(fields); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + Some(quote! { + impl #impl_generics ResponseFields for types::#ident #ty_generics #where_clause { + #impls + } + }) + } + } + _ => unreachable!("return type was neither a struct nor enum"), + } +} + +/// Generate a `ResponseFields` impl for a `ResultPage` type and its wrapped `items` type. +fn gen_result_page_impl( + ident: &Ident, + fields: &Fields, + types_content: &[Item], + generics: &Generics, + implemented_types: &mut HashSet, +) -> Option { + let ty = results_page_child_type(fields); + let Type::Path(TypePath { path, .. }) = &ty else { + return None; + }; + + let child_ident = path.segments.last().cloned().map(|s| s.ident)?; + let child_impl = gen_for_ident(types_content, implemented_types, &child_ident); + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + Some(quote! { + #child_impl + + impl #impl_generics ResponseFields for types::#ident #ty_generics #where_clause { + type Item = types::#ty; + + fn field_names() -> &'static [&'static str] { + types::#ty::field_names() + } + + fn get_field(&self, _field_name: &str) -> Option { + None + } + + fn items(&self) -> Box + '_> { + Box::new(self.items.iter()) + } + } + }) +} + +/// Extract the inner `T` from a `ResultsPage`. +fn results_page_child_type(fields: &Fields) -> Type { + let Fields::Named(named) = fields else { + unreachable!("no named fields on ResultsPage"); + }; + + // Find the `items` field on the `ResultsPage`. + let items = named + .named + .iter() + .find(|&f| f.ident.as_ref().map(|ident| ident.to_string()) == Some(String::from("items"))) + .unwrap(); + + match &items.ty { + Type::Path(TypePath { path, .. }) if path.segments.last().unwrap().ident == "Vec" => { + // Access the generic arguments to `Vec`. + let PathArguments::AngleBracketed(args) = &path.segments.last().unwrap().arguments + else { + unreachable!("no angle bracket path on Vec"); + }; + + // Take the inner `T`. + let GenericArgument::Type(inner_type) = args.args.first().unwrap() else { + unreachable!("no generic argument to Vec"); + }; + inner_type.clone() + } + _ => unreachable!(), + } +} + +/// Generate a `ResponseFields` impl for a struct. +fn gen_struct_impl(fields: &Fields) -> TokenStream { + match fields { + Fields::Named(fields) => { + let field_names: Vec<_> = fields + .named + .iter() + .map(|f| f.ident.as_ref().unwrap()) + .collect(); + + let field_accessors: Vec<_> = field_names + .iter() + .map(|field_name| { + let field_str = field_name.to_string(); + quote! { + #field_str => serde_json::to_value(&self.#field_name).ok() + } + }) + .collect(); + + let field_strings: Vec<_> = field_names.iter().map(|ident| ident.to_string()).collect(); + + quote! { + type Item = Self; + + fn field_names() -> &'static [&'static str] { + &[#(#field_strings),*] + } + + #[allow(clippy::needless_borrows_for_generic_args)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + #(#field_accessors,)* + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } + } + } + Fields::Unnamed(fields) => { + let count = fields.unnamed.len(); + let (field_strings, field_accessors) = if count == 1 { + let field_names = vec!["value".to_string()]; + + // All single-value tuple structs implement `Deref` to access their payload. + let field_accessors = vec![quote! { + "value" => serde_json::to_value(&*self).ok() + }]; + (field_names, field_accessors) + } else { + let field_strings: Vec<_> = (0..count).map(|i| format!("value_{}", i)).collect(); + + let field_accessors: Vec<_> = (0..count) + .map(|i| { + let index = Index::from(i); + let field_str = format!("value_{}", i); + quote! { + #field_str => serde_json::to_value(&self.#index).ok() + } + }) + .collect(); + + (field_strings, field_accessors) + }; + + quote! { + type Item = Self; + + fn field_names() -> &'static [&'static str] { + &[#(#field_strings),*] + } + + #[allow(clippy::borrow_deref_ref)] + fn get_field(&self, field_name: &str) -> Option { + match field_name { + #(#field_accessors,)* + _ => None, + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } + } + } + // Do not generate a `ResponseFields` impl for Unit structs, nothing to show. + Fields::Unit => { + quote! {} + } + } +} + +/// Generate a `ResponseFields` impl for an enum. +fn gen_enum_impl(data: &ItemEnum) -> TokenStream { + let enum_ident = &data.ident; + + // Collect all unique field names across all variants, including the variant name as "type". + let mut field_strings = IndexSet::from(["type".to_string()]); + + for variant in &data.variants { + match &variant.fields { + Fields::Named(fields) => { + for field in &fields.named { + field_strings.insert(field.ident.as_ref().unwrap().to_string()); + } + } + Fields::Unnamed(fields) => { + let count = fields.unnamed.len(); + if count == 1 { + field_strings.insert("value".to_string()); + } else { + for i in 0..count { + field_strings.insert(format!("value_{}", i)); + } + } + } + Fields::Unit => {} + } + } + + let field_strings: Vec<_> = field_strings.into_iter().collect(); + + let variant_arms: Vec<_> = data + .variants + .iter() + .map(|variant| { + let variant_ident = &variant.ident; + let variant_string = variant_ident.to_string(); + + match &variant.fields { + Fields::Named(fields) => { + let field_idents: Vec<_> = fields + .named + .iter() + .map(|f| f.ident.as_ref().unwrap()).collect(); + + let field_matches: Vec<_> = field_idents.iter().map(|field_ident| { + let field_string = field_ident.to_string(); + quote! { + #field_string => serde_json::to_value(#field_ident).ok() + } + }) + .collect(); + + quote! { + types::#enum_ident::#variant_ident { #(#field_idents),* } => { + match field_name { + "type" => Some(serde_json::Value::String(String::from(#variant_string))), + #(#field_matches,)* + _ => None, + } + } + } + } + Fields::Unnamed(fields) => { + let count = fields.unnamed.len(); + + if count == 1 { + quote! { + types::#enum_ident::#variant_ident(value) => { + match field_name { + "value" => serde_json::to_value(&value).ok(), + _ => None, + } + } + } + } else { + let field_bindings: Vec<_> = (0..count) + .map(|i| { + syn::Ident::new( + &format!("field_{}", i), + proc_macro2::Span::call_site(), + ) + }) + .collect(); + + let field_matches: Vec<_> = field_bindings.iter().enumerate() + .map(|(i, ident)| { + let field_string = format!("value_{}", i); + quote! { + #field_string => serde_json::to_value(#ident).ok() + } + }) + .collect(); + + quote! { + #enum_ident::#variant_ident(#(#field_bindings),*) => { + match field_name { + "type" => Some(serde_json::Value::String(String::from(#variant_string))), + #(#field_matches,)* + _ => None, + } + } + } + } + } + Fields::Unit => { + quote! { + types::#enum_ident::#variant_ident => { + match field_name { + "type" => Some(serde_json::Value::String(String::from(#variant_string))), + _ => None, + } + } + } + } + } + }) + .collect(); + + quote! { + type Item = Self; + + fn field_names() -> &'static [&'static str] { + &[#(#field_strings),*] + } + + fn get_field(&self, field_name: &str) -> Option { + match self { + #(#variant_arms,)* + } + } + + fn items(&self) -> Box + '_> { + Box::new(std::iter::once(self)) + } + } +} + +/// Generate a `field_names` method for `CliCommand` use to determine which subcommands +/// are eligible for the `--format` flag, and adding the list of available fields `--help` +/// output. +fn gen_cli_command(sdk_file: &syn::File, cli_file: &syn::File) -> TokenStream { + let builder_content = mod_content(sdk_file, "builder"); + + let cli_variants = cli_file + .items + .iter() + .filter_map(|i| match i { + Item::Enum(ItemEnum { + ident, variants, .. + }) if ident == "CliCommand" => Some(variants), + _ => None, + }) + .next() + .unwrap(); + + // We expect tabular formatting to be useful when querying. Creation APIs in particular already + // have many arguments, adding the bulky `--format` help text on top of that will make them + // harder to understand. However, there is no 100% reliable way to determine which subcommand is + // performing a query. The return type for create and view endpoints are frequently the same, so + // we can't discriminate by type. Trying to limit to `List` and `View` will miss a fair number + // of query endpoints, e.g. `NetworkingBgpImportedRoutesIpv4` and `CurrentUserGroups`. The + // heuristic below should catch most new write operations. + let action_words = HashSet::from([ + "Add", "Attach", "Create", "Demote", "Detach", "Probe", "Promote", "Reboot", "Resend", + "Start", "Stop", "Update", + ]); + + let mut match_arms = Vec::new(); + for Variant { + ident: variant_ident, + .. + } in cli_variants + { + let variant_str = variant_ident.to_string(); + let last_word = variant_str + .rfind(char::is_uppercase) + .map(|i| &variant_str[i..]) + .unwrap_or_default(); + + if action_words.contains(last_word) { + continue; + } + + let impl_items = builder_type_for_variant(variant_ident, builder_content); + let ret_type = send_return_type(impl_items); + let inner_type = response_value_inner_type(ret_type); + if let Some(return_path) = extract_type_path_expression(inner_type) { + match_arms.push(quote! { + CliCommand::#variant_ident => #return_path::field_names(), + }); + } + } + + quote! { + impl CliCommand { + pub fn field_names(&self) -> &'static [&'static str] { + match self { + #(#match_arms)* + _ => &[], + } + } + } + } +} + +/// Find the corresponding `impl` block in the `builder` mod for a `CliCommand` variant. +fn builder_type_for_variant<'a>( + variant_ident: &Ident, + builder_content: &'a [Item], +) -> &'a [ImplItem] { + builder_content + .iter() + .filter_map(|i| match i { + Item::Impl(ItemImpl { + self_ty, + trait_: None, + items, + .. + }) => { + let Type::Path(TypePath { path, .. }) = &**self_ty else { + return None; + }; + let struct_ident = path.segments.first()?; + if &struct_ident.ident != variant_ident { + return None; + } + Some(items) + } + _ => None, + }) + .map(|i| i.as_slice()) + .next() + .expect("no corresponding builder type for CliCommand variant") +} + +/// Convert a `Type` to a `Path` in expression format. +fn extract_type_path_expression(mut inner_ty: Type) -> Option { + let Type::Path(TypePath { path, .. }) = &mut inner_ty else { + return None; + }; + let path_str = path_to_string(path); + + // Skip any variant with a return type that cannot be reasonably presented in table + // format. + if is_unpresentable(&path_str) { + return None; + } + + // We need to convert the type name from declaration syntax to expression, e.g., + // `Vec` to `Vec::`. + for segment in &mut path.segments { + if let PathArguments::AngleBracketed(args) = &mut segment.arguments { + // Add the `::` token for turbofish syntax. + args.colon2_token = Some(syn::token::PathSep::default()); + } + } + + Some(path.clone()) +} + +fn path_to_string(path: &Path) -> String { + path.segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join("::") +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 882af172..f47fec70 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -// Copyright 2023 Oxide Computer Company +// Copyright 2025 Oxide Computer Company #![forbid(unsafe_code)] @@ -13,6 +13,8 @@ use newline_converter::dos2unix; use progenitor::{GenerationSettings, Generator, TagStyle}; use similar::{Algorithm, ChangeTag, TextDiff}; +mod cli_extras; + #[derive(Parser)] #[command(name = "xtask")] #[command(about = "build tasks")] @@ -72,21 +74,23 @@ fn generate( GenerationSettings::default() .with_interface(progenitor::InterfaceStyle::Builder) .with_tag(TagStyle::Separate) - .with_derive("schemars::JsonSchema"), + .with_derive("schemars::JsonSchema") + .with_cli_bounds("ResponseFields"), ); let mut error = false; let mut loc = 0; + let mut sdk_tokens = None; // TODO I'd like to generate a hash as well to have a way to check if the // spec has changed since the last generation. - // SDK if sdk { print!("generating sdk ... "); std::io::stdout().flush().unwrap(); let code = generator.generate_tokens(&spec).unwrap(); + sdk_tokens = Some(code.clone()); let contents = format_code(code.to_string()); loc += contents.matches('\n').count(); @@ -118,11 +122,20 @@ fn generate( if cli { print!("generating cli ... "); std::io::stdout().flush().unwrap(); - let code = generator.cli(&spec, "oxide").unwrap().to_string(); + + // Running `generator.generate_tokens()` a second time will create duplicate types. + let sdk_tokens = sdk_tokens.unwrap_or_else(|| generator.generate_tokens(&spec).unwrap()); + + let cli_tokens = generator.cli(&spec, "oxide").unwrap(); + let mut code = cli_tokens.to_string(); + + let response_fields = cli_extras::gen_response_fields(sdk_tokens, cli_tokens).to_string(); + code.push_str(&response_fields); + let contents = format_code(code); loc += contents.matches('\n').count(); - let mut out_path = root_path; + let mut out_path = root_path.clone(); out_path.push("cli"); out_path.push("src"); out_path.push("generated_cli.rs");