Skip to main content

vantage_table/references/
many.rs

1//! HasMany — one-to-many relationship where the foreign key is on the target table.
2//!
3//! Example: Bakery has many Clients (via client.bakery_id)
4
5use std::fmt::Display;
6use std::{any::Any, marker::PhantomData, sync::Arc};
7
8use vantage_core::{Result, error};
9use vantage_types::{EmptyEntity, Entity, Record};
10use vantage_vista::{JoinKey, Reference as VistaReference};
11
12use crate::{references::Reference, table::Table, traits::table_source::TableSource};
13
14pub struct HasMany<T: TableSource, SourceE: Entity<T::Value>, TargetE: Entity<T::Value>> {
15    /// Foreign key column on the target table (e.g. "bakery_id" on client)
16    target_foreign_key: String,
17    /// Factory: given a data source, produce the target table
18    build_target: Arc<dyn Fn(T) -> Result<Table<T, TargetE>> + Send + Sync>,
19    /// The target's metadata name (see `target_table_name`).
20    target_hint: Option<String>,
21    _phantom: PhantomData<SourceE>,
22}
23
24impl<T: TableSource, SourceE: Entity<T::Value>, TargetE: Entity<T::Value>>
25    HasMany<T, SourceE, TargetE>
26{
27    pub fn new(
28        target_foreign_key: impl Into<String>,
29        build_target: impl Fn(T) -> Table<T, TargetE> + Send + Sync + 'static,
30    ) -> Self {
31        Self {
32            target_foreign_key: target_foreign_key.into(),
33            build_target: Arc::new(move |source| Ok(build_target(source))),
34            target_hint: None,
35            _phantom: PhantomData,
36        }
37    }
38
39    /// A relation whose target build can fail; traversal returns the error.
40    /// `target` names the target in metadata; the build isn't run for it.
41    pub fn try_new(
42        target_foreign_key: impl Into<String>,
43        target: impl Into<String>,
44        build_target: impl Fn(T) -> Result<Table<T, TargetE>> + Send + Sync + 'static,
45    ) -> Self {
46        Self {
47            target_foreign_key: target_foreign_key.into(),
48            build_target: Arc::new(build_target),
49            target_hint: Some(target.into()),
50            _phantom: PhantomData,
51        }
52    }
53}
54
55impl<T: TableSource, SourceE: Entity<T::Value>, TargetE: Entity<T::Value>> Clone
56    for HasMany<T, SourceE, TargetE>
57{
58    fn clone(&self) -> Self {
59        Self {
60            target_foreign_key: self.target_foreign_key.clone(),
61            build_target: self.build_target.clone(),
62            target_hint: self.target_hint.clone(),
63            _phantom: PhantomData,
64        }
65    }
66}
67
68impl<T: TableSource, SourceE: Entity<T::Value>, TargetE: Entity<T::Value>> std::fmt::Debug
69    for HasMany<T, SourceE, TargetE>
70{
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("HasMany")
73            .field("target_foreign_key", &self.target_foreign_key)
74            .finish()
75    }
76}
77
78impl<
79    T: TableSource + 'static,
80    SourceE: Entity<T::Value> + 'static,
81    TargetE: Entity<T::Value> + 'static,
82> Reference for HasMany<T, SourceE, TargetE>
83where
84    T::Value: Into<ciborium::Value> + From<ciborium::Value>,
85    T::Id: Display + From<String>,
86{
87    fn columns(&self, source_id: &str, _target_id: &str) -> (String, String) {
88        (source_id.to_string(), self.target_foreign_key.clone())
89    }
90
91    fn vista_reference(&self, name: &str, target: String, source_id: &str) -> VistaReference {
92        VistaReference::has_many(
93            name,
94            target,
95            JoinKey::new(source_id, &self.target_foreign_key),
96        )
97    }
98
99    fn build_target(&self, data_source: &dyn Any) -> Result<Box<dyn Any>> {
100        let ds = data_source
101            .downcast_ref::<T>()
102            .ok_or_else(|| error!("data source type mismatch in HasMany::build_target"))?;
103        Ok(Box::new((self.build_target)(ds.clone())?))
104    }
105
106    fn build_target_erased(&self, data_source: &dyn Any) -> Result<Box<dyn Any>> {
107        let ds = data_source
108            .downcast_ref::<T>()
109            .ok_or_else(|| error!("data source type mismatch in HasMany::build_target_erased"))?;
110        Ok(Box::new(
111            (self.build_target)(ds.clone())?.into_entity::<EmptyEntity>(),
112        ))
113    }
114
115    fn resolve_from_row(
116        &self,
117        data_source: &dyn Any,
118        source_id_field: &str,
119        source_row: &dyn Any,
120    ) -> Result<Box<dyn Any>> {
121        let ds = data_source
122            .downcast_ref::<T>()
123            .ok_or_else(|| error!("data source type mismatch in HasMany::resolve_from_row"))?;
124        let row = source_row
125            .downcast_ref::<Record<T::Value>>()
126            .ok_or_else(|| error!("source row type mismatch in HasMany::resolve_from_row"))?;
127
128        let mut target = (self.build_target)(ds.clone())?;
129        let (src_col, tgt_col) = self.columns(source_id_field, "");
130        let join_value = row
131            .get(&src_col)
132            .cloned()
133            .ok_or_else(|| error!("source row missing id field", field = src_col.as_str()))?;
134        // Rows arriving from a script/JSON surface carry ids in string form.
135        let join_value = ds.coerce_reference_value(join_value);
136
137        let condition = ds.eq_value_condition(&tgt_col, join_value.clone())?;
138        target.add_condition(condition);
139        target.add_invariant(tgt_col, join_value);
140
141        Ok(Box::new(target.into_entity::<EmptyEntity>()))
142    }
143
144    /// The `try_new` target name when set, without building: a metadata
145    /// snapshot mustn't run the target's build (and an injector with it).
146    fn target_table_name(&self, data_source: &dyn Any) -> String {
147        if let Some(hint) = &self.target_hint {
148            return hint.clone();
149        }
150        let ds = data_source
151            .downcast_ref::<T>()
152            .expect("data source type mismatch in HasMany::target_table_name");
153        (self.build_target)(ds.clone())
154            .map(|target| target.table_name().to_string())
155            .unwrap_or_default()
156    }
157}