use ciborium::Value as CborValue;
use indexmap::IndexMap;
use vantage_core::{Result, error};
use vantage_dataset::traits::ReadableValueSet;
use vantage_table::any::AnyTable;
use vantage_table::traits::table_like::TableLike;
use vantage_types::Record;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
List,
Single,
}
pub trait ModelFactory {
fn for_name(&self, name: &str) -> Option<(AnyTable, Mode)>;
fn for_arn(&self, arn: &str) -> Option<AnyTable>;
}
pub trait Renderer {
fn render_list(
&self,
table: &AnyTable,
records: &IndexMap<String, Record<CborValue>>,
column_override: Option<&[String]>,
);
fn render_record(
&self,
table: &AnyTable,
id: &str,
record: &Record<CborValue>,
relations: &[String],
);
}
#[derive(Debug)]
enum Token {
ModelName(String, Option<usize>),
Arn(String),
Condition(String, String, Option<usize>),
Relation(String, Option<usize>),
Index(usize),
Columns(Vec<String>, Option<usize>),
}
fn split_index_suffix(s: &str) -> (&str, Option<usize>) {
if let Some(stripped) = s.strip_suffix(']')
&& let Some(open) = stripped.rfind('[')
{
let inner = &stripped[open + 1..];
if !inner.is_empty()
&& inner.chars().all(|c| c.is_ascii_digit())
&& let Ok(n) = inner.parse::<usize>()
{
return (&stripped[..open], Some(n));
}
}
(s, None)
}
fn parse_token(arg: &str) -> Result<Token> {
if arg.is_empty() {
return Err(error!("Empty argument"));
}
if arg.starts_with("arn:") {
return Ok(Token::Arn(arg.to_string()));
}
if let Some(rest) = arg.strip_prefix(':') {
let (rel, idx) = split_index_suffix(rest);
if rel.is_empty() {
return Err(error!(format!("Empty relation name in token `{arg}`")));
}
return Ok(Token::Relation(rel.to_string(), idx));
}
if arg.starts_with('[') {
let (_, idx) = split_index_suffix(arg);
let idx = idx.ok_or_else(|| error!(format!("Invalid index token `{arg}`")))?;
return Ok(Token::Index(idx));
}
if let Some(rest) = arg.strip_prefix('=') {
let (cols_part, idx) = split_index_suffix(rest);
if cols_part.is_empty() {
return Err(error!(format!(
"Empty column list in token `{arg}` — write `=col1,col2`"
)));
}
let cols: Vec<String> = cols_part
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if cols.is_empty() {
return Err(error!(format!("Empty column list in token `{arg}`")));
}
return Ok(Token::Columns(cols, idx));
}
if let Some(eq_pos) = arg.find('=') {
let field = arg[..eq_pos].to_string();
if field.is_empty() {
return Err(error!(format!("Empty field name in token `{arg}`")));
}
let value_part = &arg[eq_pos + 1..];
let (value, idx) =
if value_part.starts_with('"') && value_part.ends_with('"') && value_part.len() >= 2 {
(value_part[1..value_part.len() - 1].to_string(), None)
} else {
let (v, i) = split_index_suffix(value_part);
(v.to_string(), i)
};
return Ok(Token::Condition(field, value, idx));
}
let (name, idx) = split_index_suffix(arg);
Ok(Token::ModelName(name.to_string(), idx))
}
pub async fn run<F: ModelFactory, R: Renderer>(
factory: &F,
renderer: &R,
args: &[String],
) -> Result<()> {
if args.is_empty() {
return Err(error!(
"No model specified — pass a model name (e.g. `iam.users`) or an ARN"
));
}
let mut tokens: Vec<Token> = args.iter().map(|s| parse_token(s)).collect::<Result<_>>()?;
let first = tokens.remove(0);
let mut column_override: Option<Vec<String>> = None;
let (mut table, mut mode) = match first {
Token::ModelName(name, idx) => {
let (t, m) = factory
.for_name(&name)
.ok_or_else(|| error!(format!("Unknown model `{name}`")))?;
if let Some(i) = idx {
apply_index(t, i).await?
} else {
(t, m)
}
}
Token::Arn(arn) => {
let t = factory
.for_arn(&arn)
.ok_or_else(|| error!(format!("Cannot resolve ARN `{arn}`")))?;
(t, Mode::Single)
}
Token::Condition(_, _, _)
| Token::Relation(_, _)
| Token::Index(_)
| Token::Columns(_, _) => {
return Err(error!(format!(
"First argument must be a model name or ARN, got `{}`",
args[0]
)));
}
};
for token in tokens {
match token {
Token::Condition(field, value, idx) => {
let is_id_alias = field == "id";
let resolved_field = if is_id_alias {
table.id_field_name().ok_or_else(|| {
error!(format!(
"`id=` used but table `{}` has no id field",
table.table_name()
))
})?
} else {
field.clone()
};
table.add_condition_eq(&resolved_field, &value)?;
if is_id_alias {
mode = Mode::Single;
}
if let Some(i) = idx {
let (t, m) = apply_index(table, i).await?;
table = t;
mode = m;
}
}
Token::Index(i) => {
let (t, m) = apply_index(table, i).await?;
table = t;
mode = m;
}
Token::Relation(rel, idx) => {
if mode != Mode::Single {
return Err(error!(format!(
"Cannot traverse `:{rel}` from list mode — narrow to a single record first (add a filter or `[N]`)"
)));
}
table = table.get_ref(&rel)?;
mode = Mode::List;
column_override = None;
if let Some(i) = idx {
let (t, m) = apply_index(table, i).await?;
table = t;
mode = m;
}
}
Token::Columns(cols, idx) => {
column_override = Some(cols);
if let Some(i) = idx {
let (t, m) = apply_index(table, i).await?;
table = t;
mode = m;
}
}
Token::ModelName(_, _) | Token::Arn(_) => {
return Err(error!(
"Model name or ARN may only appear as the first argument"
));
}
}
}
match mode {
Mode::List => {
let records = table.list_values().await?;
renderer.render_list(&table, &records, column_override.as_deref());
}
Mode::Single => {
let (id, record) = table
.get_some_value()
.await?
.ok_or_else(|| error!("No record found"))?;
let relations = table.get_ref_names();
renderer.render_record(&table, &id, &record, &relations);
}
}
Ok(())
}
async fn apply_index(mut table: AnyTable, index: usize) -> Result<(AnyTable, Mode)> {
let records = table.list_values().await?;
let total = records.len();
let (id, _record) = records.into_iter().nth(index).ok_or_else(|| {
error!(format!(
"Index [{index}] out of bounds — only {total} record(s) match"
))
})?;
let id_field = table.id_field_name().ok_or_else(|| {
error!(format!(
"Cannot apply index — table `{}` has no id field",
table.table_name()
))
})?;
table.add_condition_eq(&id_field, &id)?;
Ok((table, Mode::Single))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_split_index_suffix() {
assert_eq!(split_index_suffix("users"), ("users", None));
assert_eq!(split_index_suffix("users[0]"), ("users", Some(0)));
assert_eq!(split_index_suffix("users[42]"), ("users", Some(42)));
assert_eq!(split_index_suffix("[3]"), ("", Some(3)));
assert_eq!(split_index_suffix("foo[bar]"), ("foo[bar]", None));
assert_eq!(split_index_suffix("foo[]"), ("foo[]", None));
}
#[test]
fn token_parse_kinds() {
match parse_token("iam.users").unwrap() {
Token::ModelName(n, i) => {
assert_eq!(n, "iam.users");
assert_eq!(i, None);
}
t => panic!("expected ModelName, got {t:?}"),
}
match parse_token("iam.users[0]").unwrap() {
Token::ModelName(n, i) => {
assert_eq!(n, "iam.users");
assert_eq!(i, Some(0));
}
t => panic!("expected ModelName with index, got {t:?}"),
}
match parse_token(":members[2]").unwrap() {
Token::Relation(r, i) => {
assert_eq!(r, "members");
assert_eq!(i, Some(2));
}
t => panic!("expected Relation, got {t:?}"),
}
match parse_token("name=alice").unwrap() {
Token::Condition(f, v, i) => {
assert_eq!(f, "name");
assert_eq!(v, "alice");
assert_eq!(i, None);
}
t => panic!("expected Condition, got {t:?}"),
}
match parse_token("name=\"john doe\"").unwrap() {
Token::Condition(f, v, i) => {
assert_eq!(f, "name");
assert_eq!(v, "john doe");
assert_eq!(i, None);
}
t => panic!("expected Condition, got {t:?}"),
}
match parse_token("name=alice[0]").unwrap() {
Token::Condition(f, v, i) => {
assert_eq!(f, "name");
assert_eq!(v, "alice");
assert_eq!(i, Some(0));
}
t => panic!("expected Condition with index, got {t:?}"),
}
match parse_token("[7]").unwrap() {
Token::Index(i) => assert_eq!(i, 7),
t => panic!("expected Index, got {t:?}"),
}
match parse_token("arn:aws:iam::123:user/alice").unwrap() {
Token::Arn(s) => assert_eq!(s, "arn:aws:iam::123:user/alice"),
t => panic!("expected Arn, got {t:?}"),
}
match parse_token("=timestamp,message").unwrap() {
Token::Columns(cols, idx) => {
assert_eq!(cols, vec!["timestamp".to_string(), "message".to_string()]);
assert_eq!(idx, None);
}
t => panic!("expected Columns, got {t:?}"),
}
match parse_token("=id, name [0]").unwrap_or_else(|_| {
parse_token("=id,name[0]").unwrap()
}) {
Token::Columns(cols, idx) => {
assert_eq!(cols, vec!["id".to_string(), "name".to_string()]);
assert_eq!(idx, Some(0));
}
t => panic!("expected Columns with index, got {t:?}"),
}
}
}