use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use ciborium::Value as CborValue;
use vantage_core::{Result, error};
use vantage_dataset::traits::ReadableValueSet;
use vantage_types::Record;
use vantage_vista::{ReferenceKind, Vista};
use vantage_vista_factory::{Relation, VistaCatalog};
pub type BuildFn = Arc<dyn Fn(&Record<CborValue>, Vista) -> Result<Vista> + Send + Sync>;
pub type FetchFn = Arc<
dyn Fn(Vista) -> Pin<Box<dyn Future<Output = Result<Vec<Record<CborValue>>>> + Send>>
+ Send
+ Sync,
>;
pub enum Source {
Id,
Column { from: String, to: Option<String> },
Build(BuildFn),
}
pub enum Fetch {
PerRow,
Custom(FetchFn),
}
pub enum Detail {
Catalog(String),
Fixed(Arc<Vista>),
}
impl Detail {
fn name(&self) -> &str {
match self {
Detail::Catalog(name) => name,
Detail::Fixed(vista) => vista.name(),
}
}
}
pub struct MergeRule {
pub columns: Vec<String>,
}
impl MergeRule {
fn wants(&self, key: &str) -> bool {
self.columns.is_empty() || self.columns.iter().any(|c| c == key)
}
pub fn apply(&self, dest: &mut Record<CborValue>, detail: &Record<CborValue>) {
for (k, v) in detail {
if self.wants(k) {
dest.insert(k.clone(), v.clone());
}
}
}
}
pub struct Augmentation {
pub detail: Detail,
pub source: Source,
pub fetch: Fetch,
pub merge: MergeRule,
}
impl Augmentation {
pub async fn augment_row(
&self,
dio_name: &str,
master_id_column: &str,
row: &mut Record<CborValue>,
catalog: &VistaCatalog,
) -> Result<()> {
match self.fetch_one(master_id_column, row, catalog).await? {
Some(detail) => {
self.merge.apply(row, &detail);
let merged: Vec<String> = detail
.iter()
.filter(|(k, _)| self.merge.wants(k))
.map(|(k, v)| format!("{k}={}", scalar_text(v)))
.collect();
tracing::info!(
target: "vantage_diorama::augment",
dio = %dio_name,
detail = %self.detail.name(),
key = %self.key_display(row, master_id_column),
merged = %merged.join(" "),
"augment completed",
);
}
None => {
tracing::debug!(
target: "vantage_diorama::augment",
dio = %dio_name,
detail = %self.detail.name(),
key = %self.key_display(row, master_id_column),
"augment found no detail record — row stays as listed",
);
}
}
Ok(())
}
fn key_display(&self, row: &Record<CborValue>, master_id_column: &str) -> String {
let field = match &self.source {
Source::Column { from, .. } => from.as_str(),
Source::Id | Source::Build(_) => master_id_column,
};
row.get(field).map(scalar_text).unwrap_or_default()
}
fn base_vista(&self, catalog: &VistaCatalog) -> Result<Vista> {
match &self.detail {
Detail::Catalog(name) => catalog.build_vista(name),
Detail::Fixed(vista) => vista
.source
.clone_shell()
.map(|shell| Vista::new(vista.name().to_string(), shell))
.ok_or_else(|| {
error!(
"augment: fixed detail vista's shell is not cloneable",
table = vista.name()
)
}),
}
}
async fn fetch_one(
&self,
master_id_column: &str,
row: &Record<CborValue>,
catalog: &VistaCatalog,
) -> Result<Option<Record<CborValue>>> {
let base = self.base_vista(catalog)?;
match &self.fetch {
Fetch::PerRow => match &self.source {
Source::Id => {
base.get_value_with_row(&self.key(row, master_id_column)?, row)
.await
}
Source::Column { from, to: None } => {
base.get_value_with_row(&self.key(row, from)?, row).await
}
Source::Column {
from,
to: Some(col),
} => {
let mut base = base;
self.narrow_eq(&mut base, col, from, row)?;
Ok(base.get_some_value().await?.map(|(_, r)| r))
}
Source::Build(f) => Ok(f(row, base)?.get_some_value().await?.map(|(_, r)| r)),
},
Fetch::Custom(f) => {
let detail = self.resolve_detail(master_id_column, row, catalog)?;
Ok(f(detail).await?.into_iter().next())
}
}
}
fn resolve_detail(
&self,
master_id_column: &str,
row: &Record<CborValue>,
catalog: &VistaCatalog,
) -> Result<Vista> {
let mut base = self.base_vista(catalog)?;
match &self.source {
Source::Id => {
let detail_id = self.detail_id_column(&base)?;
self.narrow_eq(&mut base, &detail_id, master_id_column, row)?;
Ok(base)
}
Source::Column { from, to } => {
let fk = match to {
Some(c) => c.clone(),
None => self.detail_id_column(&base)?,
};
self.narrow_eq(&mut base, &fk, from, row)?;
Ok(base)
}
Source::Build(f) => f(row, base),
}
}
fn narrow_eq(
&self,
base: &mut Vista,
detail_column: &str,
master_field: &str,
row: &Record<CborValue>,
) -> Result<()> {
Relation::single_key(
"augment",
self.detail.name(),
ReferenceKind::HasOne,
detail_column.to_string(),
master_field.to_string(),
)
.narrow(base, row)
}
fn detail_id_column(&self, base: &Vista) -> Result<String> {
base.get_id_column().map(str::to_string).ok_or_else(|| {
error!(
"augment: detail vista has no id column",
table = self.detail.name()
)
})
}
fn key(&self, row: &Record<CborValue>, field: &str) -> Result<String> {
match row.get(field) {
Some(CborValue::Text(s)) => Ok(s.clone()),
Some(CborValue::Integer(i)) => Ok(i128::from(*i).to_string()),
Some(_) => Err(error!(
"augment: key field is not a string/int",
field = field
)),
None => Err(error!(
"augment: master row missing key field",
field = field
)),
}
}
}
fn scalar_text(v: &CborValue) -> String {
match v {
CborValue::Text(s) => s.clone(),
CborValue::Integer(i) => i128::from(*i).to_string(),
CborValue::Float(f) => f.to_string(),
CborValue::Bool(b) => b.to_string(),
CborValue::Null => "null".to_string(),
_ => "<nested>".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_overwrites_master_columns_on_clash() {
let rule = MergeRule { columns: vec![] };
let mut dest: Record<CborValue> = [("id".to_string(), CborValue::Text("master".into()))]
.into_iter()
.collect();
let detail: Record<CborValue> = [
("id".to_string(), CborValue::Text("detail".into())),
("extra".to_string(), CborValue::Text("v".into())),
]
.into_iter()
.collect();
rule.apply(&mut dest, &detail);
assert_eq!(dest.get("id"), Some(&CborValue::Text("detail".into())));
assert_eq!(dest.get("extra"), Some(&CborValue::Text("v".into())));
}
#[test]
fn merge_respects_explicit_column_list() {
let rule = MergeRule {
columns: vec!["extra".into()],
};
let mut dest: Record<CborValue> = Record::default();
let detail: Record<CborValue> = [
("extra".to_string(), CborValue::Text("v".into())),
("skipme".to_string(), CborValue::Text("no".into())),
]
.into_iter()
.collect();
rule.apply(&mut dest, &detail);
assert_eq!(dest.get("extra"), Some(&CborValue::Text("v".into())));
assert!(dest.get("skipme").is_none());
}
}