use anyhow::{Context, Result};
use ciborium::Value as CborValue;
use clap::Parser;
use indexmap::IndexMap;
use vantage_aws::AwsAccount;
use vantage_aws::models::{Factory, FactoryMode};
use vantage_aws::types::{AnyAwsType, typed_records};
use vantage_cli_util::model_cli::{self, Mode, ModelFactory, Renderer};
use vantage_cli_util::{render_records_columns, render_records_typed};
use vantage_table::any::AnyTable;
use vantage_table::traits::table_like::TableLike;
use vantage_types::{Record, TerminalRender};
#[derive(Parser)]
#[command(
name = "vantage-aws-cli",
about = "Generic CLI for vantage-aws models",
long_about = "Use a dotted model name (e.g. iam.users, log.group) or an ARN as the first arg, \
then chain filters (field=value), index selectors ([0]), and relation \
traversals (:relation). Example: aws-cli iam.user UserName=alice :groups."
)]
struct Cli {
#[arg(long, global = true)]
region: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = false)]
args: Vec<String>,
}
struct AwsFactoryAdapter(Factory);
impl ModelFactory for AwsFactoryAdapter {
fn for_name(&self, name: &str) -> Option<(AnyTable, Mode)> {
self.0.for_name(name).map(|(t, m)| {
(
t,
match m {
FactoryMode::List => Mode::List,
FactoryMode::Single => Mode::Single,
},
)
})
}
fn for_arn(&self, arn: &str) -> Option<AnyTable> {
self.0.from_arn(arn)
}
}
struct AwsRenderer;
impl Renderer for AwsRenderer {
fn render_list(
&self,
table: &AnyTable,
records: &IndexMap<String, Record<CborValue>>,
column_override: Option<&[String]>,
) {
let id_field = table.id_field_name();
let column_types = table.column_types();
let title_fields = table.title_field_names();
let typed = typed_records(records.clone(), &column_types);
if let Some(cols) = column_override {
let resolved: Vec<String> = cols
.iter()
.map(|raw| {
if raw == "id" {
id_field.clone().unwrap_or_else(|| raw.clone())
} else {
raw.clone()
}
})
.collect();
render_records_columns(&typed, &resolved, &column_types);
return;
}
let visible: IndexMap<String, &'static str> = if title_fields.is_empty() {
column_types.clone()
} else {
let mut v = IndexMap::new();
for f in &title_fields {
if let Some(t) = column_types.get(f) {
v.insert(f.clone(), *t);
}
}
v
};
render_records_typed(&typed, id_field.as_deref(), &visible);
}
fn render_record(
&self,
table: &AnyTable,
id: &str,
record: &Record<CborValue>,
relations: &[String],
) {
let id_field = table.id_field_name();
let title_fields = table.title_field_names();
let column_types = table.column_types();
let typed_rec: Record<AnyAwsType> = record
.iter()
.map(|(k, v)| {
let declared = column_types.get(k).copied().unwrap_or("");
(k.clone(), AnyAwsType::from_cbor_typed(v.clone(), declared))
})
.collect();
if let Some(ref name) = id_field {
println!(
"{}: {}",
name,
format_field(&typed_rec, name).unwrap_or_else(|| id.to_string())
);
} else {
println!("id: {id}");
}
for tf in &title_fields {
if Some(tf.as_str()) == id_field.as_deref() {
continue;
}
if let Some(s) = format_field(&typed_rec, tf) {
println!("{tf}: {s}");
}
}
println!("--------");
for k in column_types.keys() {
if Some(k.as_str()) == id_field.as_deref() || title_fields.contains(k) {
continue;
}
if let Some(s) = format_field(&typed_rec, k) {
println!("{k}: {s}");
}
}
if !relations.is_empty() {
println!();
println!("Relations:");
for r in relations {
println!(" :{r}");
}
}
}
}
fn format_field(record: &Record<AnyAwsType>, key: &str) -> Option<String> {
record.get(key).map(|v| v.render().to_string())
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
if let Some(region) = &cli.region {
unsafe { std::env::set_var("AWS_REGION", region) };
}
let aws = AwsAccount::from_default().context(
"Set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_REGION, or configure ~/.aws/credentials [default]",
)?;
if cli.args.is_empty() {
eprintln!("usage: aws-cli <model | arn> [field=value ...] [[N]] [:relation ...]");
eprintln!("\nKnown models:");
for name in Factory::known_names() {
eprintln!(" {name}");
}
std::process::exit(2);
}
let factory = AwsFactoryAdapter(Factory::new(aws));
let renderer = AwsRenderer;
model_cli::run(&factory, &renderer, &cli.args).await?;
Ok(())
}