Skip to main content

vantage_api_client/vista/
source.rs

1//! `RestApiTableShell` — owns the typed `Table<RestApi, E>` behind a
2//! `dyn TableLike` so the original entity type stays attached for
3//! reference traversal.
4//!
5//! `RestApi` already speaks `ciborium::Value` natively (the HTTP body
6//! is converted at the fetch boundary), so the shell is a pass-through
7//! with no per-record translation.
8//!
9//! YAML-declared references are carried separately (`yaml_refs`)
10//! because `Table::with_many` / `with_one` require compile-time-known
11//! build-target closures that YAML can't synthesise. At traversal
12//! time the shell consults the resolver (a factory-wide callback that
13//! maps model names to child Vistas) and threads a `DeferredFn`
14//! through the child so the parent's id resolves only when the child
15//! actually fetches.
16
17use std::sync::Arc;
18
19use async_trait::async_trait;
20use ciborium::Value as CborValue;
21use indexmap::IndexMap;
22use vantage_core::{Result, error};
23use vantage_expressions::Expression;
24use vantage_expressions::traits::expressive::{DeferredFn, ExpressiveEnum};
25use vantage_table::traits::table_like::TableLike;
26use vantage_types::Record;
27use vantage_vista::{TableShell, Vista, VistaCapabilities};
28
29use crate::vista::factory::ModelResolver;
30
31use super::any_shell::AnyTableShell;
32
33/// A single YAML-declared reference attached to a parent shell at
34/// build time. Carries the foreign-key wiring; the URL form is
35/// the child's own concern (its `api.endpoint`) — the parent only
36/// declares the relationship.
37#[derive(Clone)]
38pub(crate) struct YamlReference {
39    pub target: String,
40    pub kind: YamlReferenceKind,
41    pub foreign_key: String,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub(crate) enum YamlReferenceKind {
46    HasMany,
47    HasOne,
48}
49
50pub struct RestApiTableShell {
51    pub(crate) table: Box<dyn TableLike<Value = CborValue, Id = String>>,
52    pub(crate) capabilities: VistaCapabilities,
53    pub(crate) yaml_refs: IndexMap<String, YamlReference>,
54    pub(crate) resolver: Option<ModelResolver>,
55}
56
57impl RestApiTableShell {
58    pub(crate) fn new(
59        table: Box<dyn TableLike<Value = CborValue, Id = String>>,
60        capabilities: VistaCapabilities,
61    ) -> Self {
62        Self {
63            table,
64            capabilities,
65            yaml_refs: IndexMap::new(),
66            resolver: None,
67        }
68    }
69
70    pub(crate) fn with_yaml_refs(mut self, refs: IndexMap<String, YamlReference>) -> Self {
71        self.yaml_refs = refs;
72        self
73    }
74
75    pub(crate) fn with_resolver(mut self, resolver: ModelResolver) -> Self {
76        self.resolver = Some(resolver);
77        self
78    }
79
80    /// Construct the deferred FK condition for a YAML reference. The
81    /// returned `Expression` carries a `DeferredFn` that, when
82    /// resolved at child-fetch time, reads `source_column` out of the
83    /// parent's first record and emits it as a scalar — same shape
84    /// `condition_to_query_param` already peels for synchronous eq
85    /// conditions.
86    fn build_deferred_fk_condition(
87        parent_table: Box<dyn TableLike<Value = CborValue, Id = String>>,
88        source_column: String,
89        target_field: String,
90    ) -> Expression<CborValue> {
91        let parent_arc = Arc::new(parent_table);
92        let column = source_column;
93        let target_field_for_error = target_field.clone();
94        let deferred = DeferredFn::new(move || {
95            let parent = parent_arc.clone_box();
96            let column = column.clone();
97            let target = target_field_for_error.clone();
98            Box::pin(async move {
99                let records = parent.list_values().await?;
100                let value = records
101                    .values()
102                    .next()
103                    .and_then(|r| r.get(&column))
104                    .cloned()
105                    .ok_or_else(|| {
106                        error!(
107                            "YAML reference: parent yielded no row or column missing",
108                            source_column = column,
109                            target_field = target
110                        )
111                    })?;
112                Ok(ExpressiveEnum::Scalar(value))
113            })
114        });
115
116        Expression::new(
117            "{} = {}",
118            vec![
119                ExpressiveEnum::Nested(Expression::new(target_field, vec![])),
120                ExpressiveEnum::Nested(Expression::new(
121                    "{}",
122                    vec![ExpressiveEnum::Deferred(deferred)],
123                )),
124            ],
125        )
126    }
127}
128
129#[async_trait]
130impl TableShell for RestApiTableShell {
131    async fn list_vista_values(
132        &self,
133        _vista: &Vista,
134    ) -> Result<IndexMap<String, Record<CborValue>>> {
135        self.table.list_values().await
136    }
137
138    async fn get_vista_value(
139        &self,
140        _vista: &Vista,
141        id: &String,
142    ) -> Result<Option<Record<CborValue>>> {
143        let mut data = self.table.list_values().await?;
144        Ok(data.shift_remove(id))
145    }
146
147    async fn get_vista_some_value(
148        &self,
149        _vista: &Vista,
150    ) -> Result<Option<(String, Record<CborValue>)>> {
151        let data = self.table.list_values().await?;
152        Ok(data.into_iter().next())
153    }
154
155    async fn get_vista_count(&self, _vista: &Vista) -> Result<i64> {
156        self.table.get_count().await
157    }
158
159    fn add_eq_condition(&mut self, field: &str, value: &CborValue) -> Result<()> {
160        // Build a typed `Expression<CborValue>` and hand it through
161        // the type-erased `add_condition` API — `Table::add_condition`
162        // downcasts it back to `RestApi::Condition` on the other side.
163        let condition = crate::eq_condition(field, value.clone());
164        self.table.add_condition(Box::new(condition))
165    }
166
167    fn add_raw_condition(&mut self, condition: Box<dyn std::any::Any + Send + Sync>) -> Result<()> {
168        self.table.add_condition(condition)
169    }
170
171    fn get_ref(&self, relation: &str) -> Result<Vista> {
172        // YAML-declared references first: the factory wired them via
173        // `yaml_refs` at build time. Resolve them through the
174        // model-resolver callback so cross-driver lookups (vantage-ui's
175        // inventory) work alongside same-driver ones.
176        if let Some(yref) = self.yaml_refs.get(relation) {
177            let resolver = self.resolver.as_ref().ok_or_else(|| {
178                error!(
179                    "YAML reference requires a model resolver — call \
180                     `with_model_resolver` on the factory or register \
181                     the target spec via `register_yaml`",
182                    relation = relation
183                )
184            })?;
185
186            let mut child = resolver(&yref.target)?;
187
188            // For `has_many` the parent's id flows onto the child's FK
189            // column; for `has_one` the parent's FK column value
190            // becomes the child's id. The source column we read from
191            // the parent at fetch time differs accordingly.
192            let (source_column, target_field) = match yref.kind {
193                YamlReferenceKind::HasMany => {
194                    let parent_id = self.table.id_field_name().ok_or_else(|| {
195                        error!(
196                            "YAML has_many reference needs the parent to have an id field",
197                            relation = relation
198                        )
199                    })?;
200                    (parent_id, yref.foreign_key.clone())
201                }
202                YamlReferenceKind::HasOne => {
203                    let child_id = child
204                        .get_id_column()
205                        .map(str::to_string)
206                        .unwrap_or_else(|| "id".to_string());
207                    (yref.foreign_key.clone(), child_id)
208                }
209            };
210
211            let parent_clone = self.table.clone_box();
212            let condition =
213                Self::build_deferred_fk_condition(parent_clone, source_column, target_field);
214            child.add_raw_condition(condition)?;
215
216            return Ok(child);
217        }
218
219        // Fall through to the typed `Table` reference machinery —
220        // hand-coded `with_many` / `with_one` / `with_foreign`
221        // registrations in Rust callers go through this path.
222        let any_table = self.table.get_ref(relation)?;
223        AnyTableShell::into_vista(any_table)
224    }
225
226    fn capabilities(&self) -> &VistaCapabilities {
227        &self.capabilities
228    }
229
230    fn driver_name(&self) -> &'static str {
231        "rest-api"
232    }
233}