1use ciborium::Value as CborValue;
33use indexmap::IndexMap;
34use vantage_core::{Result, error};
35use vantage_dataset::traits::ReadableValueSet;
36use vantage_table::any::AnyTable;
37use vantage_table::traits::table_like::TableLike;
38use vantage_types::Record;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Mode {
43 List,
44 Single,
45}
46
47pub trait ModelFactory {
50 fn for_name(&self, name: &str) -> Option<(AnyTable, Mode)>;
54
55 fn for_arn(&self, arn: &str) -> Option<AnyTable>;
58}
59
60pub trait Renderer {
62 fn render_list(
63 &self,
64 table: &AnyTable,
65 records: &IndexMap<String, Record<CborValue>>,
66 column_override: Option<&[String]>,
67 );
68 fn render_record(
69 &self,
70 table: &AnyTable,
71 id: &str,
72 record: &Record<CborValue>,
73 relations: &[String],
74 );
75}
76
77#[derive(Debug)]
78enum Token {
79 ModelName(String, Option<usize>),
80 Arn(String),
81 Condition(String, String, Option<usize>),
82 Relation(String, Option<usize>),
83 Index(usize),
84 Columns(Vec<String>, Option<usize>),
85}
86
87fn split_index_suffix(s: &str) -> (&str, Option<usize>) {
88 if let Some(stripped) = s.strip_suffix(']')
89 && let Some(open) = stripped.rfind('[')
90 {
91 let inner = &stripped[open + 1..];
92 if !inner.is_empty()
93 && inner.chars().all(|c| c.is_ascii_digit())
94 && let Ok(n) = inner.parse::<usize>()
95 {
96 return (&stripped[..open], Some(n));
97 }
98 }
99 (s, None)
100}
101
102fn parse_token(arg: &str) -> Result<Token> {
103 if arg.is_empty() {
104 return Err(error!("Empty argument"));
105 }
106 if arg.starts_with("arn:") {
107 return Ok(Token::Arn(arg.to_string()));
108 }
109 if let Some(rest) = arg.strip_prefix(':') {
110 let (rel, idx) = split_index_suffix(rest);
111 if rel.is_empty() {
112 return Err(error!(format!("Empty relation name in token `{arg}`")));
113 }
114 return Ok(Token::Relation(rel.to_string(), idx));
115 }
116 if arg.starts_with('[') {
117 let (_, idx) = split_index_suffix(arg);
118 let idx = idx.ok_or_else(|| error!(format!("Invalid index token `{arg}`")))?;
119 return Ok(Token::Index(idx));
120 }
121 if let Some(rest) = arg.strip_prefix('=') {
122 let (cols_part, idx) = split_index_suffix(rest);
123 if cols_part.is_empty() {
124 return Err(error!(format!(
125 "Empty column list in token `{arg}` — write `=col1,col2`"
126 )));
127 }
128 let cols: Vec<String> = cols_part
129 .split(',')
130 .map(|s| s.trim().to_string())
131 .filter(|s| !s.is_empty())
132 .collect();
133 if cols.is_empty() {
134 return Err(error!(format!("Empty column list in token `{arg}`")));
135 }
136 return Ok(Token::Columns(cols, idx));
137 }
138 if let Some(eq_pos) = arg.find('=') {
139 let field = arg[..eq_pos].to_string();
140 if field.is_empty() {
141 return Err(error!(format!("Empty field name in token `{arg}`")));
142 }
143 let value_part = &arg[eq_pos + 1..];
144 let (value, idx) =
145 if value_part.starts_with('"') && value_part.ends_with('"') && value_part.len() >= 2 {
146 (value_part[1..value_part.len() - 1].to_string(), None)
147 } else {
148 let (v, i) = split_index_suffix(value_part);
149 (v.to_string(), i)
150 };
151 return Ok(Token::Condition(field, value, idx));
152 }
153 let (name, idx) = split_index_suffix(arg);
154 Ok(Token::ModelName(name.to_string(), idx))
155}
156
157pub async fn run<F: ModelFactory, R: Renderer>(
162 factory: &F,
163 renderer: &R,
164 args: &[String],
165) -> Result<()> {
166 if args.is_empty() {
167 return Err(error!(
168 "No model specified — pass a model name (e.g. `iam.users`) or an ARN"
169 ));
170 }
171
172 let mut tokens: Vec<Token> = args.iter().map(|s| parse_token(s)).collect::<Result<_>>()?;
173 let first = tokens.remove(0);
174 let mut column_override: Option<Vec<String>> = None;
175
176 let (mut table, mut mode) = match first {
177 Token::ModelName(name, idx) => {
178 let (t, m) = factory
179 .for_name(&name)
180 .ok_or_else(|| error!(format!("Unknown model `{name}`")))?;
181 if let Some(i) = idx {
182 apply_index(t, i).await?
183 } else {
184 (t, m)
185 }
186 }
187 Token::Arn(arn) => {
188 let t = factory
189 .for_arn(&arn)
190 .ok_or_else(|| error!(format!("Cannot resolve ARN `{arn}`")))?;
191 (t, Mode::Single)
192 }
193 Token::Condition(_, _, _)
194 | Token::Relation(_, _)
195 | Token::Index(_)
196 | Token::Columns(_, _) => {
197 return Err(error!(format!(
198 "First argument must be a model name or ARN, got `{}`",
199 args[0]
200 )));
201 }
202 };
203
204 for token in tokens {
205 match token {
206 Token::Condition(field, value, idx) => {
207 let is_id_alias = field == "id";
211 let resolved_field = if is_id_alias {
212 table.id_field_name().ok_or_else(|| {
213 error!(format!(
214 "`id=` used but table `{}` has no id field",
215 table.table_name()
216 ))
217 })?
218 } else {
219 field.clone()
220 };
221 table.add_condition_eq(&resolved_field, &value)?;
222 if is_id_alias {
223 mode = Mode::Single;
224 }
225 if let Some(i) = idx {
226 let (t, m) = apply_index(table, i).await?;
227 table = t;
228 mode = m;
229 }
230 }
231 Token::Index(i) => {
232 let (t, m) = apply_index(table, i).await?;
233 table = t;
234 mode = m;
235 }
236 Token::Relation(rel, idx) => {
237 if mode != Mode::Single {
238 return Err(error!(format!(
239 "Cannot traverse `:{rel}` from list mode — narrow to a single record first (add a filter or `[N]`)"
240 )));
241 }
242 table = table.get_ref(&rel)?;
243 mode = Mode::List;
244 column_override = None;
248 if let Some(i) = idx {
249 let (t, m) = apply_index(table, i).await?;
250 table = t;
251 mode = m;
252 }
253 }
254 Token::Columns(cols, idx) => {
255 column_override = Some(cols);
256 if let Some(i) = idx {
257 let (t, m) = apply_index(table, i).await?;
258 table = t;
259 mode = m;
260 }
261 }
262 Token::ModelName(_, _) | Token::Arn(_) => {
263 return Err(error!(
264 "Model name or ARN may only appear as the first argument"
265 ));
266 }
267 }
268 }
269
270 match mode {
271 Mode::List => {
272 let records = table.list_values().await?;
273 renderer.render_list(&table, &records, column_override.as_deref());
274 }
275 Mode::Single => {
276 let (id, record) = table
277 .get_some_value()
278 .await?
279 .ok_or_else(|| error!("No record found"))?;
280 let relations = table.get_ref_names();
281 renderer.render_record(&table, &id, &record, &relations);
282 }
283 }
284
285 Ok(())
286}
287
288async fn apply_index(mut table: AnyTable, index: usize) -> Result<(AnyTable, Mode)> {
292 let records = table.list_values().await?;
293 let total = records.len();
294 let (id, _record) = records.into_iter().nth(index).ok_or_else(|| {
295 error!(format!(
296 "Index [{index}] out of bounds — only {total} record(s) match"
297 ))
298 })?;
299 let id_field = table.id_field_name().ok_or_else(|| {
300 error!(format!(
301 "Cannot apply index — table `{}` has no id field",
302 table.table_name()
303 ))
304 })?;
305 table.add_condition_eq(&id_field, &id)?;
306 Ok((table, Mode::Single))
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn token_split_index_suffix() {
315 assert_eq!(split_index_suffix("users"), ("users", None));
316 assert_eq!(split_index_suffix("users[0]"), ("users", Some(0)));
317 assert_eq!(split_index_suffix("users[42]"), ("users", Some(42)));
318 assert_eq!(split_index_suffix("[3]"), ("", Some(3)));
319 assert_eq!(split_index_suffix("foo[bar]"), ("foo[bar]", None));
320 assert_eq!(split_index_suffix("foo[]"), ("foo[]", None));
321 }
322
323 #[test]
324 fn token_parse_kinds() {
325 match parse_token("iam.users").unwrap() {
326 Token::ModelName(n, i) => {
327 assert_eq!(n, "iam.users");
328 assert_eq!(i, None);
329 }
330 t => panic!("expected ModelName, got {t:?}"),
331 }
332 match parse_token("iam.users[0]").unwrap() {
333 Token::ModelName(n, i) => {
334 assert_eq!(n, "iam.users");
335 assert_eq!(i, Some(0));
336 }
337 t => panic!("expected ModelName with index, got {t:?}"),
338 }
339 match parse_token(":members[2]").unwrap() {
340 Token::Relation(r, i) => {
341 assert_eq!(r, "members");
342 assert_eq!(i, Some(2));
343 }
344 t => panic!("expected Relation, got {t:?}"),
345 }
346 match parse_token("name=alice").unwrap() {
347 Token::Condition(f, v, i) => {
348 assert_eq!(f, "name");
349 assert_eq!(v, "alice");
350 assert_eq!(i, None);
351 }
352 t => panic!("expected Condition, got {t:?}"),
353 }
354 match parse_token("name=\"john doe\"").unwrap() {
355 Token::Condition(f, v, i) => {
356 assert_eq!(f, "name");
357 assert_eq!(v, "john doe");
358 assert_eq!(i, None);
359 }
360 t => panic!("expected Condition, got {t:?}"),
361 }
362 match parse_token("name=alice[0]").unwrap() {
363 Token::Condition(f, v, i) => {
364 assert_eq!(f, "name");
365 assert_eq!(v, "alice");
366 assert_eq!(i, Some(0));
367 }
368 t => panic!("expected Condition with index, got {t:?}"),
369 }
370 match parse_token("[7]").unwrap() {
371 Token::Index(i) => assert_eq!(i, 7),
372 t => panic!("expected Index, got {t:?}"),
373 }
374 match parse_token("arn:aws:iam::123:user/alice").unwrap() {
375 Token::Arn(s) => assert_eq!(s, "arn:aws:iam::123:user/alice"),
376 t => panic!("expected Arn, got {t:?}"),
377 }
378 match parse_token("=timestamp,message").unwrap() {
379 Token::Columns(cols, idx) => {
380 assert_eq!(cols, vec!["timestamp".to_string(), "message".to_string()]);
381 assert_eq!(idx, None);
382 }
383 t => panic!("expected Columns, got {t:?}"),
384 }
385 match parse_token("=id, name [0]").unwrap_or_else(|_| {
386 parse_token("=id,name[0]").unwrap()
389 }) {
390 Token::Columns(cols, idx) => {
391 assert_eq!(cols, vec!["id".to_string(), "name".to_string()]);
392 assert_eq!(idx, Some(0));
393 }
394 t => panic!("expected Columns with index, got {t:?}"),
395 }
396 }
397}