use indexmap::IndexMap;
use vantage_core::{Context, Result, VantageError, error};
use vantage_expressions::{Expressive, SelectableDataSource};
use vantage_types::{EmptyEntity, Entity};
use vantage_vista::{ColumnFlag, ColumnSpec, JoinKey, ReferenceKind, ReferenceSugar, VistaSpec};
use crate::{
source::SelectSource,
table::Table,
table::inject::{BuildEnv, BuildKind, BuildTrail, SpecTableBuilder},
traits::table_source::TableSource,
};
impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
pub fn add_spec_column<C>(
&mut self,
name: &str,
spec: &ColumnSpec<C>,
build: impl FnOnce(&str, &ColumnSpec<C>) -> Result<T::Column<T::AnyType>>,
) -> Result<bool> {
if self.add_lazy_spec_column(spec, name)? {
return Ok(false);
}
if !self.columns.contains_key(name) {
self.add_column(build(name, spec)?);
}
if spec.has_flag(ColumnFlag::Title) {
self.add_title_field(name);
}
Ok(true)
}
pub fn add_spec_columns<C>(
&mut self,
columns: &IndexMap<String, ColumnSpec<C>>,
build: impl Fn(&str, &ColumnSpec<C>) -> Result<T::Column<T::AnyType>>,
) -> Result<()> {
for (name, spec) in columns {
self.add_spec_column(name, spec, &build)?;
}
Ok(())
}
pub fn set_spec_id_field(&mut self, id: &str) -> Result<()> {
if !self.columns.contains_key(id) {
return Err(error!("id column not present in spec.columns", id = id));
}
self.set_id_field(id);
Ok(())
}
}
impl<T: TableSource + 'static, E: Entity<T::Value> + 'static> Table<T, E>
where
T::Value: Into<ciborium::Value> + From<ciborium::Value>,
T::Id: std::fmt::Display + From<String>,
{
pub fn with_spec_references<X, C, R, S: 'static>(
mut self,
spec: &VistaSpec<X, C, R>,
env: &BuildEnv<S, T>,
build: SpecTableBuilder<S, T>,
) -> Result<Self> {
for (relation, reference) in &spec.references {
let foreign_key = reference.foreign_key(relation);
self = self.with_spec_join(
&spec.name,
relation,
&reference.table,
reference.kind,
&foreign_key,
env,
build,
)?;
}
for (column, sugar) in column_references(spec) {
let (target, kind, foreign_key) = match sugar {
ReferenceSugar::Sugar(target) => (target, ReferenceKind::HasOne, column.clone()),
ReferenceSugar::Full(r) => (&r.table, r.kind, r.foreign_key(column)),
_ => return Err(unknown_sugar(column)),
};
self =
self.with_spec_join(&spec.name, column, target, kind, &foreign_key, env, build)?;
}
Ok(self)
}
#[allow(clippy::too_many_arguments)]
fn with_spec_join<S: 'static>(
self,
from: &str,
relation: &str,
target: &str,
kind: ReferenceKind,
foreign_key: &str,
env: &BuildEnv<S, T>,
build: SpecTableBuilder<S, T>,
) -> Result<Self> {
let target_name = target.to_string();
let relation_name = relation.to_string();
let child = BuildEnv {
resolver: env.resolver.clone(),
injector: env.injector.clone(),
trail: BuildTrail::default().enter(target)?,
kind: BuildKind::Reference {
from: from.to_string(),
relation: relation.to_string(),
},
};
let build_target = move |source: T| -> Result<Table<T, EmptyEntity>> {
let Some(r) = &child.resolver else {
let mut table = Table::new(target_name.clone(), source);
child.inject(&target_name, &mut table, build)?;
return Ok(table);
};
let spec = r.resolve(&target_name).ok_or_else(|| {
error!(
"reference target has no spec",
relation = relation_name.as_str(),
target = target_name.as_str()
)
})?;
build(&spec, source, &child).with_context(|| {
error!(
"reference target failed to build",
relation = relation_name.as_str(),
target = target_name.as_str()
)
})
};
match kind {
ReferenceKind::HasOne => {
Ok(self.try_with_one(relation, foreign_key, target, build_target))
}
ReferenceKind::HasMany => {
Ok(self.try_with_many(relation, foreign_key, target, build_target))
}
_ => Err(error!(
"references: entries must be has_one or has_many",
relation = relation
)),
}
}
}
pub fn spec_references<X, C, R>(
spec: &VistaSpec<X, C, R>,
source_id: &str,
) -> Result<Vec<vantage_vista::Reference>> {
let mut out = Vec::with_capacity(spec.references.len());
for (relation, reference) in &spec.references {
out.push(reference.to_reference(relation, source_id)?);
}
for (column, sugar) in column_references(spec) {
out.push(match sugar {
ReferenceSugar::Sugar(target) => {
vantage_vista::Reference::has_one(column, target, JoinKey::to_target_id(column))
}
ReferenceSugar::Full(r) => r.to_reference(column, source_id)?,
_ => return Err(unknown_sugar(column)),
});
}
Ok(out)
}
fn unknown_sugar(column: &str) -> VantageError {
error!(
"column references: takes a table name or a full reference",
column = column
)
}
fn column_references<X, C, R>(
spec: &VistaSpec<X, C, R>,
) -> impl Iterator<Item = (&String, &ReferenceSugar)> {
spec.columns.iter().filter_map(|(column, col)| {
let sugar = col.references.as_ref()?;
(!spec.references.contains_key(column)).then_some((column, sugar))
})
}
impl<T, E, V, Cond, S> Table<T, E>
where
T: SelectableDataSource<V, Cond, Select = S>
+ TableSource<Value = V, Condition = Cond, Source = SelectSource<S>>,
V: Clone + Send + Sync + 'static + From<String>,
Cond: Clone + Send + Sync + 'static,
S: Expressive<V> + Clone,
E: Entity<V>,
{
pub fn derive_from_spec<E2: Entity<V> + 'static, X, C, R>(
base: &Table<T, E2>,
spec: &VistaSpec<X, C, R>,
select: S,
inherit_columns: &[String],
inherit_relations: &[String],
build_column: impl Fn(&str, &ColumnSpec<C>) -> Result<T::Column<T::AnyType>>,
) -> Result<Self>
where
T: 'static,
E: 'static,
{
let columns: Vec<&str> = inherit_columns.iter().map(String::as_str).collect();
let relations: Vec<&str> = inherit_relations.iter().map(String::as_str).collect();
let mut table = Self::derive_from(
base,
spec.name.clone(),
move |_| select,
&columns,
&relations,
);
table.add_spec_columns(&spec.columns, &build_column)?;
if let Some(id) = &spec.id_column {
table.set_id_field(id);
}
table.with_contained_specs(&spec.contained, build_column)
}
}