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::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 vista = vista.get_ref(&rel)?;
224 mode = Mode::List;
225 column_override = None;
229 if let Some(i) = idx {
230 let (v, m) = apply_index(vista, i).await?;
231 vista = v;
232 mode = m;
233 }
234 }
235 Token::Columns(cols, idx) => {
236 column_override = Some(cols);
237 if let Some(i) = idx {
238 let (v, m) = apply_index(vista, i).await?;
239 vista = v;
240 mode = m;
241 }
242 }
243 Token::ModelName(_, _) | Token::Arn(_) => {
244 return Err(error!(
245 "Model name or ARN may only appear as the first argument"
246 ));
247 }
248 }
249 }
250
251 match mode {
252 Mode::List => {
253 let records = vista.list_values().await?;
254 renderer.render_list(&vista, &records, column_override.as_deref());
255 }
256 Mode::Single => {
257 let (id, record) = vista
258 .get_some_value()
259 .await?
260 .ok_or_else(|| error!("No record found"))?;
261 let relations: Vec<String> = vista
262 .get_references()
263 .iter()
264 .map(|s| s.to_string())
265 .collect();
266 renderer.render_record(&vista, &id, &record, &relations);
267 }
268 }
269
270 Ok(())
271}
272
273fn string_to_cbor(value: &str) -> CborValue {
278 if let Ok(i) = value.parse::<i64>() {
279 CborValue::Integer(i.into())
280 } else if let Ok(f) = value.parse::<f64>() {
281 CborValue::Float(f)
282 } else if value == "true" {
283 CborValue::Bool(true)
284 } else if value == "false" {
285 CborValue::Bool(false)
286 } else {
287 CborValue::Text(value.to_string())
288 }
289}
290
291async fn apply_index(mut vista: Vista, index: usize) -> Result<(Vista, Mode)> {
295 let records = vista.list_values().await?;
296 let total = records.len();
297 let (id, _record) = records.into_iter().nth(index).ok_or_else(|| {
298 error!(format!(
299 "Index [{index}] out of bounds — only {total} record(s) match"
300 ))
301 })?;
302 let id_field = vista.get_id_column().map(str::to_string).ok_or_else(|| {
303 error!(format!(
304 "Cannot apply index — vista `{}` has no id column",
305 vista.name()
306 ))
307 })?;
308 vista.add_condition_eq(&id_field, string_to_cbor(&id))?;
309 Ok((vista, Mode::Single))
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn token_split_index_suffix() {
318 assert_eq!(split_index_suffix("users"), ("users", None));
319 assert_eq!(split_index_suffix("users[0]"), ("users", Some(0)));
320 assert_eq!(split_index_suffix("[3]"), ("", Some(3)));
321 assert_eq!(split_index_suffix("foo[bar]"), ("foo[bar]", None));
322 }
323
324 #[test]
325 fn token_parse_relation() {
326 match parse_token(":albums[2]").unwrap() {
327 Token::Relation(r, i) => {
328 assert_eq!(r, "albums");
329 assert_eq!(i, Some(2));
330 }
331 t => panic!("expected Relation, got {t:?}"),
332 }
333 }
334}