use ciborium::Value as CborValue;
use vantage_core::{Result, error};
use vantage_dataset::traits::ReadableValueSet;
use vantage_vista::{ReferenceKind, Vista};
use super::factory::{ModelFactory, Renderer};
use super::parse::parse_token;
use super::token::{AggregateOp, Mode, Op, Selector, Slice, Token};
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. `users`) or a locator"
));
}
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 aggregate: Option<(AggregateOp, Option<String>)> = None;
let (mut vista, mut mode) = match first {
Token::ModelName(name, sel) => {
let (v, m) = factory
.for_name(&name)
.ok_or_else(|| error!(format!("Unknown model `{name}`")))?;
apply_selector_opt(v, m, sel, renderer).await?
}
Token::Locator(s) => {
let v = factory
.for_locator(&s)
.ok_or_else(|| error!(format!("Cannot resolve locator `{s}`")))?;
(v, Mode::Single)
}
Token::OpCondition { .. }
| Token::Relation(_, _)
| Token::Bracket(_)
| Token::Columns(_, _)
| Token::Search(_)
| Token::Aggregate { .. } => {
return Err(error!(format!(
"First argument must be a model name or locator, got `{}`",
args[0]
)));
}
};
for token in tokens {
if aggregate.is_some() {
return Err(error!(
"Aggregate token (`@op:field`) must be the last argument"
));
}
match token {
Token::ModelName(_, _) | Token::Locator(_) => {
return Err(error!(
"Model name or locator may only appear as the first argument"
));
}
Token::OpCondition {
field,
op,
value,
selector,
} => {
if let Some(new_mode) = apply_condition(&mut vista, &field, op, value, renderer)? {
mode = new_mode;
}
if let Some(sel) = selector {
let (v, m) = apply_selector(vista, mode, sel, renderer).await?;
vista = v;
mode = m;
}
}
Token::Relation(rel, sel) => {
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]`)"
)));
}
let child_kind = vista
.list_references()
.into_iter()
.find(|(name, _)| name == &rel)
.map(|(_, k)| k);
let (_id, parent_row) = vista.get_some_value().await?.ok_or_else(|| {
error!(format!(
"Cannot traverse `:{rel}` — narrowed vista has no matching record"
))
})?;
vista = vista.get_ref(&rel, &parent_row)?;
mode = match child_kind {
Some(ReferenceKind::HasOne) => Mode::Single,
_ => Mode::List,
};
column_override = None;
if let Some(sel) = sel {
let (v, m) = apply_selector(vista, mode, sel, renderer).await?;
vista = v;
mode = m;
}
}
Token::Bracket(sel) => {
let (v, m) = apply_selector(vista, mode, sel, renderer).await?;
vista = v;
mode = m;
}
Token::Columns(cols, sel) => {
column_override = Some(cols);
if let Some(sel) = sel {
let (v, m) = apply_selector(vista, mode, sel, renderer).await?;
vista = v;
mode = m;
}
}
Token::Search(query) => {
renderer.note_stub(&format!("add_search({query:?})"));
}
Token::Aggregate { op, field } => {
aggregate = Some((op, field));
}
}
}
if let Some((op, field)) = aggregate {
renderer.note_stub(&format!(
"{}({})",
op.name(),
field.as_deref().unwrap_or("*")
));
renderer.render_scalar(&vista, op, field.as_deref(), &CborValue::Null);
return Ok(());
}
match mode {
Mode::List => {
let records = vista.list_values().await?;
renderer.render_list(&vista, &records, column_override.as_deref());
}
Mode::Single => {
let (id, record) = vista
.get_some_value()
.await?
.ok_or_else(|| error!("No record found"))?;
let relations: Vec<String> = vista
.get_references()
.iter()
.map(|s| s.to_string())
.collect();
renderer.render_record(&vista, &id, &record, &relations);
}
}
Ok(())
}
async fn apply_selector<R: Renderer>(
vista: Vista,
mode: Mode,
sel: Selector,
renderer: &R,
) -> Result<(Vista, Mode)> {
let mut vista = vista;
let mut mode = mode;
if let Some((field, dir)) = &sel.sort {
renderer.note_stub(&format!("add_order({field:?}, {dir:?})"));
}
if let Some(slice) = sel.slice {
match slice {
Slice::Index(n) => {
let (v, m) = apply_index(vista, n).await?;
vista = v;
mode = m;
}
Slice::Range { start, end } => {
renderer.note_stub(&format!("set_pagination({start}, {end:?})"));
}
}
}
Ok((vista, mode))
}
async fn apply_selector_opt<R: Renderer>(
vista: Vista,
mode: Mode,
opt_sel: Option<Selector>,
renderer: &R,
) -> Result<(Vista, Mode)> {
match opt_sel {
Some(sel) => apply_selector(vista, mode, sel, renderer).await,
None => Ok((vista, mode)),
}
}
fn apply_condition<R: Renderer>(
vista: &mut Vista,
field: &str,
op: Op,
value: Option<CborValue>,
renderer: &R,
) -> Result<Option<Mode>> {
if op.is_nullary() {
renderer.note_stub(&format!("add_condition({field:?}, {})", op.name()));
return Ok(None);
}
let v = value.ok_or_else(|| error!("internal: value-bearing operator missing value"))?;
match op {
Op::Eq => {
let is_id_alias = field == "id";
let resolved_field = if is_id_alias {
vista.get_id_column().map(str::to_string).ok_or_else(|| {
error!(format!(
"`id=` used but vista `{}` has no id column",
vista.name()
))
})?
} else {
field.to_string()
};
vista.add_condition_eq(&resolved_field, v)?;
Ok(if is_id_alias {
Some(Mode::Single)
} else {
None
})
}
_ => {
renderer.note_stub(&format!("add_condition({field:?}, {}, {v:?})", op.name()));
Ok(None)
}
}
}
async fn apply_index(mut vista: Vista, index: usize) -> Result<(Vista, Mode)> {
let records = vista.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 = vista.get_id_column().map(str::to_string).ok_or_else(|| {
error!(format!(
"Cannot apply index — vista `{}` has no id column",
vista.name()
))
})?;
vista.add_condition_eq(&id_field, super::value::auto_detect(&id))?;
Ok((vista, Mode::Single))
}