Skip to main content

vantage_table/references/
one.rs

1//! HasOne — one-to-one relationship where the foreign key is on the source table.
2//!
3//! Example: Client has one Bakery (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 HasOne<T: TableSource, SourceE: Entity<T::Value>, TargetE: Entity<T::Value>> {
15    /// Foreign key column on the source table (e.g. "bakery_id")
16    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    HasOne<T, SourceE, TargetE>
26{
27    pub fn new(
28        foreign_key: impl Into<String>,
29        build_target: impl Fn(T) -> Table<T, TargetE> + Send + Sync + 'static,
30    ) -> Self {
31        Self {
32            foreign_key: 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        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            foreign_key: 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 HasOne<T, SourceE, TargetE>
57{
58    fn clone(&self) -> Self {
59        Self {
60            foreign_key: self.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 HasOne<T, SourceE, TargetE>
70{
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("HasOne")
73            .field("foreign_key", &self.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 HasOne<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        (self.foreign_key.clone(), target_id.to_string())
89    }
90
91    fn vista_reference(&self, name: &str, target: String, _source_id: &str) -> VistaReference {
92        VistaReference::has_one(name, target, JoinKey::to_target_id(&self.foreign_key))
93    }
94
95    fn build_target(&self, data_source: &dyn Any) -> Result<Box<dyn Any>> {
96        let ds = data_source
97            .downcast_ref::<T>()
98            .ok_or_else(|| error!("data source type mismatch in HasOne::build_target"))?;
99        Ok(Box::new((self.build_target)(ds.clone())?))
100    }
101
102    fn build_target_erased(&self, data_source: &dyn Any) -> Result<Box<dyn Any>> {
103        let ds = data_source
104            .downcast_ref::<T>()
105            .ok_or_else(|| error!("data source type mismatch in HasOne::build_target_erased"))?;
106        Ok(Box::new(
107            (self.build_target)(ds.clone())?.into_entity::<EmptyEntity>(),
108        ))
109    }
110
111    fn resolve_from_row(
112        &self,
113        data_source: &dyn Any,
114        _source_id_field: &str,
115        source_row: &dyn Any,
116    ) -> Result<Box<dyn Any>> {
117        let ds = data_source
118            .downcast_ref::<T>()
119            .ok_or_else(|| error!("data source type mismatch in HasOne::resolve_from_row"))?;
120        let row = source_row
121            .downcast_ref::<Record<T::Value>>()
122            .ok_or_else(|| error!("source row type mismatch in HasOne::resolve_from_row"))?;
123
124        let mut target = (self.build_target)(ds.clone())?;
125        let target_id = target.id_field_name();
126
127        let (src_col, tgt_col) = self.columns("", &target_id);
128        let join_value = row.get(&src_col).cloned().ok_or_else(|| {
129            error!(
130                "source row missing foreign-key field",
131                field = src_col.as_str()
132            )
133        })?;
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 HasOne::target_table_name");
153        (self.build_target)(ds.clone())
154            .map(|target| target.table_name().to_string())
155            .unwrap_or_default()
156    }
157}