use anyhow::{Context, Result};
use ciborium::Value as CborValue;
use indexmap::IndexMap;
use vantage_aws::AwsAccount;
use vantage_aws::models::{Factory, FactoryMode};
use vantage_aws::types::AnyAwsType;
use vantage_cli_util::output::{self, OutputFormat};
use vantage_cli_util::vista_cli::{self, AggregateOp, Mode, ModelFactory, Renderer};
use vantage_cli_util::{render_records_columns, render_records_typed};
use vantage_types::{Record, TerminalRender};
use vantage_vista::{ReferenceKind, Vista};
struct AwsFactoryAdapter(Factory);
impl ModelFactory for AwsFactoryAdapter {
fn for_name(&self, name: &str) -> Option<(Vista, Mode)> {
self.0.for_name(name).map(|(v, m)| {
(
v,
match m {
FactoryMode::List => Mode::List,
FactoryMode::Single => Mode::Single,
},
)
})
}
fn for_locator(&self, locator: &str) -> Option<Vista> {
self.0.from_arn(locator)
}
}
struct AwsRenderer {
format: OutputFormat,
}
impl Renderer for AwsRenderer {
fn render_list(
&self,
vista: &Vista,
records: &IndexMap<String, Record<CborValue>>,
column_override: Option<&[String]>,
) {
match self.format {
OutputFormat::Table => render_list_table(vista, records, column_override),
_ => print!("{}", output::render_list(self.format, records)),
}
}
fn render_record(
&self,
vista: &Vista,
id: &str,
record: &Record<CborValue>,
_relations: &[String],
) {
match self.format {
OutputFormat::Table => render_record_table(vista, id, record),
_ => print!("{}", output::render_record(self.format, id, record)),
}
}
fn render_scalar(
&self,
_vista: &Vista,
op: AggregateOp,
field: Option<&str>,
value: &CborValue,
) {
let label = match field {
Some(f) => format!("{}({f})", op.name()),
None => format!("{}()", op.name()),
};
match self.format {
OutputFormat::Table => println!("{label} = {}", scalar(value)),
_ => print!("{}", output::render_scalar(self.format, &label, value)),
}
}
}
fn render_list_table(
vista: &Vista,
records: &IndexMap<String, Record<CborValue>>,
column_override: Option<&[String]>,
) {
let id_field = vista.get_id_column().map(str::to_string);
let title_fields: Vec<String> = vista
.get_title_columns()
.into_iter()
.map(str::to_string)
.collect();
let column_types = column_types_static(vista);
let typed = typed_records_borrowed(records, vista);
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_table(vista: &Vista, id: &str, record: &Record<CborValue>) {
let id_field = vista.get_id_column().map(str::to_string);
let title_fields: Vec<String> = vista
.get_title_columns()
.into_iter()
.map(str::to_string)
.collect();
let typed_rec: Record<AnyAwsType> = record
.iter()
.map(|(k, v)| {
let declared = vista
.get_column(k)
.map(|c| c.original_type.as_str())
.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 name in vista.get_column_names() {
if Some(name) == id_field.as_deref() || title_fields.iter().any(|t| t == name) {
continue;
}
if let Some(s) = format_field(&typed_rec, name) {
println!("{name}: {s}");
}
}
let refs = vista.list_references();
if !refs.is_empty() {
println!();
println!("Relations:");
for (name, kind) in refs {
let marker = match kind {
ReferenceKind::HasOne => "→ one",
ReferenceKind::HasMany => "↠ many",
};
println!(" :{name} {marker}");
}
}
}
fn column_types_static(vista: &Vista) -> IndexMap<String, &'static str> {
let mut out = IndexMap::new();
for name in vista.get_column_names() {
let original = vista
.get_column(name)
.map(|c| c.original_type.as_str())
.unwrap_or("");
let leaked: &'static str = Box::leak(original.to_owned().into_boxed_str());
out.insert(name.to_string(), leaked);
}
out
}
fn typed_records_borrowed(
records: &IndexMap<String, Record<CborValue>>,
vista: &Vista,
) -> IndexMap<String, Record<AnyAwsType>> {
records
.iter()
.map(|(id, record)| {
let typed: Record<AnyAwsType> = record
.iter()
.map(|(k, v)| {
let declared = vista
.get_column(k)
.map(|c| c.original_type.as_str())
.unwrap_or("");
(k.clone(), AnyAwsType::from_cbor_typed(v.clone(), declared))
})
.collect();
(id.clone(), typed)
})
.collect()
}
fn format_field(record: &Record<AnyAwsType>, key: &str) -> Option<String> {
record.get(key).map(|v| v.render().to_string())
}
fn scalar(v: &CborValue) -> String {
match v {
CborValue::Text(s) => s.clone(),
CborValue::Integer(i) => i128::from(*i).to_string(),
CborValue::Float(f) => f.to_string(),
CborValue::Bool(b) => b.to_string(),
CborValue::Null => "—".to_string(),
CborValue::Bytes(b) => format!("<{} bytes>", b.len()),
other => format!("{other:?}"),
}
}
fn print_usage() {
eprintln!(
"usage: aws-cli [--region <r>] [--format=<f>] <model | arn> [field=value ...] [[N]] [:relation ...] [@op[:field]]"
);
eprintln!("\nFormats: table (default), json, ndjson, cbor-diag");
eprintln!("\nKnown models:");
for name in Factory::known_names() {
eprintln!(" {name}");
}
}
#[tokio::main]
async fn main() -> Result<()> {
let raw: Vec<String> = std::env::args().skip(1).collect();
let mut region: Option<String> = None;
let mut format = OutputFormat::Table;
let mut positional: Vec<String> = Vec::with_capacity(raw.len());
let mut it = raw.into_iter();
while let Some(arg) = it.next() {
if let Some(value) = arg.strip_prefix("--format=") {
format = OutputFormat::parse(value)
.with_context(|| format!("unknown --format `{value}`"))?;
} else if arg == "--region" {
region = it.next();
} else if let Some(value) = arg.strip_prefix("--region=") {
region = Some(value.to_string());
} else {
positional.push(arg);
}
}
if let Some(r) = ®ion {
unsafe { std::env::set_var("AWS_REGION", r) };
}
if positional.is_empty() {
print_usage();
std::process::exit(2);
}
let aws = AwsAccount::from_default().context(
"Set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_REGION, or configure ~/.aws/credentials [default]",
)?;
let factory = AwsFactoryAdapter(Factory::new(aws));
let renderer = AwsRenderer { format };
vista_cli::run(&factory, &renderer, &positional).await?;
Ok(())
}