1use ciborium::Value as CborValue;
12use indexmap::IndexMap;
13use vantage_core::{Result, error};
14use vantage_dataset::traits::ReadableValueSet;
15use vantage_types::Record;
16use vantage_vista::{ReferenceKind, Vista};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Mode {
21 List,
22 Single,
23}
24
25pub trait ModelFactory {
28 fn for_name(&self, name: &str) -> Option<(Vista, Mode)>;
32
33 fn for_arn(&self, _arn: &str) -> Option<Vista> {
37 None
38 }
39}
40
41pub trait Renderer {
43 fn render_list(
44 &self,
45 vista: &Vista,
46 records: &IndexMap<String, Record<CborValue>>,
47 column_override: Option<&[String]>,
48 );
49 fn render_record(
50 &self,
51 vista: &Vista,
52 id: &str,
53 record: &Record<CborValue>,
54 relations: &[String],
55 );
56}
57
58#[derive(Debug)]
59enum Token {
60 ModelName(String, Option<usize>),
61 Arn(String),
62 Condition(String, String, Option<usize>),
63 Relation(String, Option<usize>),
64 Index(usize),
65 Columns(Vec<String>, Option<usize>),
66}
67
68fn split_index_suffix(s: &str) -> (&str, Option<usize>) {
69 if let Some(stripped) = s.strip_suffix(']')
70 && let Some(open) = stripped.rfind('[')
71 {
72 let inner = &stripped[open + 1..];
73 if !inner.is_empty()
74 && inner.chars().all(|c| c.is_ascii_digit())
75 && let Ok(n) = inner.parse::<usize>()
76 {
77 return (&stripped[..open], Some(n));
78 }
79 }
80 (s, None)
81}
82
83fn parse_token(arg: &str) -> Result<Token> {
84 if arg.is_empty() {
85 return Err(error!("Empty argument"));
86 }
87 if arg.starts_with("arn:") {
88 return Ok(Token::Arn(arg.to_string()));
89 }
90 if let Some(rest) = arg.strip_prefix(':') {
91 let (rel, idx) = split_index_suffix(rest);
92 if rel.is_empty() {
93 return Err(error!(format!("Empty relation name in token `{arg}`")));
94 }
95 return Ok(Token::Relation(rel.to_string(), idx));
96 }
97 if arg.starts_with('[') {
98 let (_, idx) = split_index_suffix(arg);
99 let idx = idx.ok_or_else(|| error!(format!("Invalid index token `{arg}`")))?;
100 return Ok(Token::Index(idx));
101 }
102 if let Some(rest) = arg.strip_prefix('=') {
103 let (cols_part, idx) = split_index_suffix(rest);
104 if cols_part.is_empty() {
105 return Err(error!(format!(
106 "Empty column list in token `{arg}` — write `=col1,col2`"
107 )));
108 }
109 let cols: Vec<String> = cols_part
110 .split(',')
111 .map(|s| s.trim().to_string())
112 .filter(|s| !s.is_empty())
113 .collect();
114 if cols.is_empty() {
115 return Err(error!(format!("Empty column list in token `{arg}`")));
116 }
117 return Ok(Token::Columns(cols, idx));
118 }
119 if let Some(eq_pos) = arg.find('=') {
120 let field = arg[..eq_pos].to_string();
121 if field.is_empty() {
122 return Err(error!(format!("Empty field name in token `{arg}`")));
123 }
124 let value_part = &arg[eq_pos + 1..];
125 let (value, idx) =
126 if value_part.starts_with('"') && value_part.ends_with('"') && value_part.len() >= 2 {
127 (value_part[1..value_part.len() - 1].to_string(), None)
128 } else {
129 let (v, i) = split_index_suffix(value_part);
130 (v.to_string(), i)
131 };
132 return Ok(Token::Condition(field, value, idx));
133 }
134 let (name, idx) = split_index_suffix(arg);
135 Ok(Token::ModelName(name.to_string(), idx))
136}
137
138pub async fn run<F: ModelFactory, R: Renderer>(
143 factory: &F,
144 renderer: &R,
145 args: &[String],
146) -> Result<()> {
147 if args.is_empty() {
148 return Err(error!(
149 "No model specified — pass a model name (e.g. `users`) or an ARN"
150 ));
151 }
152
153 let mut tokens: Vec<Token> = args.iter().map(|s| parse_token(s)).collect::<Result<_>>()?;
154 let first = tokens.remove(0);
155 let mut column_override: Option<Vec<String>> = None;
156
157 let (mut vista, mut mode) = match first {
158 Token::ModelName(name, idx) => {
159 let (v, m) = factory
160 .for_name(&name)
161 .ok_or_else(|| error!(format!("Unknown model `{name}`")))?;
162 if let Some(i) = idx {
163 apply_index(v, i).await?
164 } else {
165 (v, m)
166 }
167 }
168 Token::Arn(arn) => {
169 let v = factory
170 .for_arn(&arn)
171 .ok_or_else(|| error!(format!("Cannot resolve ARN `{arn}`")))?;
172 (v, Mode::Single)
173 }
174 Token::Condition(_, _, _)
175 | Token::Relation(_, _)
176 | Token::Index(_)
177 | Token::Columns(_, _) => {
178 return Err(error!(format!(
179 "First argument must be a model name or ARN, got `{}`",
180 args[0]
181 )));
182 }
183 };
184
185 for token in tokens {
186 match token {
187 Token::Condition(field, value, idx) => {
188 let is_id_alias = field == "id";
192 let resolved_field = if is_id_alias {
193 vista.get_id_column().map(str::to_string).ok_or_else(|| {
194 error!(format!(
195 "`id=` used but vista `{}` has no id column",
196 vista.name()
197 ))
198 })?
199 } else {
200 field.clone()
201 };
202 vista.add_condition_eq(&resolved_field, string_to_cbor(&value))?;
203 if is_id_alias {
204 mode = Mode::Single;
205 }
206 if let Some(i) = idx {
207 let (v, m) = apply_index(vista, i).await?;
208 vista = v;
209 mode = m;
210 }
211 }
212 Token::Index(i) => {
213 let (v, m) = apply_index(vista, i).await?;
214 vista = v;
215 mode = m;
216 }
217 Token::Relation(rel, idx) => {
218 if mode != Mode::Single {
219 return Err(error!(format!(
220 "Cannot traverse `:{rel}` from list mode — narrow to a single record first (add a filter or `[N]`)"
221 )));
222 }
223 let child_kind = vista
229 .list_references()
230 .into_iter()
231 .find(|(name, _)| name == &rel)
232 .map(|(_, k)| k);
233 let (_id, parent_row) = vista.get_some_value().await?.ok_or_else(|| {
237 error!(format!(
238 "Cannot traverse `:{rel}` — narrowed vista has no matching record"
239 ))
240 })?;
241 vista = vista.get_ref(&rel, &parent_row)?;
242 mode = match child_kind {
243 Some(ReferenceKind::HasOne) => Mode::Single,
244 _ => Mode::List,
249 };
250 column_override = None;
254 if let Some(i) = idx {
255 let (v, m) = apply_index(vista, i).await?;
256 vista = v;
257 mode = m;
258 }
259 }
260 Token::Columns(cols, idx) => {
261 column_override = Some(cols);
262 if let Some(i) = idx {
263 let (v, m) = apply_index(vista, i).await?;
264 vista = v;
265 mode = m;
266 }
267 }
268 Token::ModelName(_, _) | Token::Arn(_) => {
269 return Err(error!(
270 "Model name or ARN may only appear as the first argument"
271 ));
272 }
273 }
274 }
275
276 match mode {
277 Mode::List => {
278 let records = vista.list_values().await?;
279 renderer.render_list(&vista, &records, column_override.as_deref());
280 }
281 Mode::Single => {
282 let (id, record) = vista
283 .get_some_value()
284 .await?
285 .ok_or_else(|| error!("No record found"))?;
286 let relations: Vec<String> = vista
287 .get_references()
288 .iter()
289 .map(|s| s.to_string())
290 .collect();
291 renderer.render_record(&vista, &id, &record, &relations);
292 }
293 }
294
295 Ok(())
296}
297
298fn string_to_cbor(value: &str) -> CborValue {
303 if let Ok(i) = value.parse::<i64>() {
304 CborValue::Integer(i.into())
305 } else if let Ok(f) = value.parse::<f64>() {
306 CborValue::Float(f)
307 } else if value == "true" {
308 CborValue::Bool(true)
309 } else if value == "false" {
310 CborValue::Bool(false)
311 } else {
312 CborValue::Text(value.to_string())
313 }
314}
315
316async fn apply_index(mut vista: Vista, index: usize) -> Result<(Vista, Mode)> {
320 let records = vista.list_values().await?;
321 let total = records.len();
322 let (id, _record) = records.into_iter().nth(index).ok_or_else(|| {
323 error!(format!(
324 "Index [{index}] out of bounds — only {total} record(s) match"
325 ))
326 })?;
327 let id_field = vista.get_id_column().map(str::to_string).ok_or_else(|| {
328 error!(format!(
329 "Cannot apply index — vista `{}` has no id column",
330 vista.name()
331 ))
332 })?;
333 vista.add_condition_eq(&id_field, string_to_cbor(&id))?;
334 Ok((vista, Mode::Single))
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn token_split_index_suffix() {
343 assert_eq!(split_index_suffix("users"), ("users", None));
344 assert_eq!(split_index_suffix("users[0]"), ("users", Some(0)));
345 assert_eq!(split_index_suffix("[3]"), ("", Some(3)));
346 assert_eq!(split_index_suffix("foo[bar]"), ("foo[bar]", None));
347 }
348
349 #[test]
350 fn token_parse_relation() {
351 match parse_token(":albums[2]").unwrap() {
352 Token::Relation(r, i) => {
353 assert_eq!(r, "albums");
354 assert_eq!(i, Some(2));
355 }
356 t => panic!("expected Relation, got {t:?}"),
357 }
358 }
359}