vantage_aws/vista/
source.rs1use async_trait::async_trait;
19use ciborium::Value as CborValue;
20use indexmap::IndexMap;
21use vantage_core::Result;
22use vantage_dataset::traits::ReadableValueSet;
23use vantage_table::table::Table;
24use vantage_types::{EmptyEntity, Record};
25use vantage_vista::{
26 Column as VistaColumn, Reference as VistaReference, ReferenceKind, TableShell, Vista,
27 VistaCapabilities, VistaMetadata,
28};
29
30use crate::AwsAccount;
31use crate::condition::AwsCondition;
32
33pub struct AwsTableShell {
34 pub(crate) table: Table<AwsAccount, EmptyEntity>,
35 pub(crate) capabilities: VistaCapabilities,
36 pub(crate) metadata: VistaMetadata,
37}
38
39impl AwsTableShell {
40 pub(crate) fn new(
41 table: Table<AwsAccount, EmptyEntity>,
42 capabilities: VistaCapabilities,
43 metadata: VistaMetadata,
44 ) -> Self {
45 Self {
46 table,
47 capabilities,
48 metadata,
49 }
50 }
51}
52
53#[async_trait]
54impl TableShell for AwsTableShell {
55 fn columns(&self) -> &IndexMap<String, VistaColumn> {
56 &self.metadata.columns
57 }
58
59 fn references(&self) -> &IndexMap<String, VistaReference> {
60 &self.metadata.references
61 }
62
63 fn id_column(&self) -> Option<&str> {
64 self.metadata.id_column.as_deref()
65 }
66
67 async fn list_vista_values(
68 &self,
69 _vista: &Vista,
70 ) -> Result<IndexMap<String, Record<CborValue>>> {
71 self.table.list_values().await
72 }
73
74 async fn get_vista_value(
75 &self,
76 _vista: &Vista,
77 id: &String,
78 ) -> Result<Option<Record<CborValue>>> {
79 let mut data = self.table.list_values().await?;
82 Ok(data.shift_remove(id))
83 }
84
85 async fn get_vista_some_value(
86 &self,
87 _vista: &Vista,
88 ) -> Result<Option<(String, Record<CborValue>)>> {
89 let data = self.table.list_values().await?;
90 Ok(data.into_iter().next())
91 }
92
93 async fn get_vista_count(&self, _vista: &Vista) -> Result<i64> {
94 Ok(self.table.list_values().await?.len() as i64)
95 }
96
97 async fn fetch_next(
104 &self,
105 _vista: &Vista,
106 token: Option<CborValue>,
107 ) -> Result<(Vec<(String, Record<CborValue>)>, Option<CborValue>)> {
108 if !self.capabilities.can_fetch_next {
109 return Err(self.default_error("fetch_next", "can_fetch_next"));
110 }
111 let account = self.table.data_source();
112 let name = self.table.table_name();
113 let mut conditions: Vec<AwsCondition> = self.table.conditions().cloned().collect();
114 if let Some(after) = token {
115 conditions.push(AwsCondition::eq("start-after".to_string(), after));
116 }
117 let resp = account.execute_rpc_page(name, &conditions).await?;
118 let truncated = match resp.get("IsTruncated") {
119 Some(serde_json::Value::String(s)) => s == "true",
120 Some(serde_json::Value::Bool(b)) => *b,
121 _ => false,
122 };
123 let rows = account.parse_records(name, resp, self.metadata.id_column.as_deref())?;
124 let next = truncated
125 .then(|| rows.keys().last().map(|k| CborValue::Text(k.clone())))
126 .flatten();
127 Ok((rows.into_iter().collect(), next))
128 }
129
130 fn clone_shell(&self) -> Option<Box<dyn TableShell>> {
134 Some(Box::new(AwsTableShell {
135 table: self.table.clone(),
136 capabilities: self.capabilities.clone(),
137 metadata: self.metadata.clone(),
138 }))
139 }
140
141 fn add_eq_condition(&mut self, field: &str, value: &CborValue) -> Result<()> {
142 self.table
146 .add_condition(AwsCondition::eq(field.to_string(), value.clone()));
147 Ok(())
148 }
149
150 fn get_ref(&self, relation: &str, row: &Record<CborValue>) -> Result<Vista> {
151 let target = self.table.get_ref_from_row::<EmptyEntity>(relation, row)?;
152 let factory = crate::vista::factory::AwsVistaFactory::new(self.table.data_source().clone());
153 factory.from_table(target)
154 }
155
156 fn get_ref_kinds(&self) -> Vec<(String, ReferenceKind)> {
157 self.table.ref_kinds()
158 }
159
160 fn capabilities(&self) -> &VistaCapabilities {
161 &self.capabilities
162 }
163
164 fn driver_name(&self) -> &'static str {
165 "aws"
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::AwsAccount;
173 use crate::models::iam;
174 use crate::vista::factory::metadata_from_table;
175 use vantage_types::EmptyEntity;
176
177 fn shell_for_iam_users() -> AwsTableShell {
178 let aws = AwsAccount::new("AKIATEST", "secret", "eu-west-2");
179 let table = iam::users_table(aws).into_entity::<EmptyEntity>();
180 let metadata = metadata_from_table(&table);
181 let capabilities = VistaCapabilities {
182 can_count: true,
183 ..VistaCapabilities::default()
184 };
185 AwsTableShell::new(table, capabilities, metadata)
186 }
187
188 #[test]
189 fn add_eq_condition_pushes_aws_condition_eq_onto_wrapped_table() {
190 let mut shell = shell_for_iam_users();
194 shell
195 .add_eq_condition("PathPrefix", &CborValue::Text("/admin/".into()))
196 .expect("add_eq_condition");
197
198 let conditions: Vec<&AwsCondition> = shell.table.conditions().collect();
199 assert_eq!(conditions.len(), 1);
200 match conditions[0] {
201 AwsCondition::Eq { field, value } => {
202 assert_eq!(field, "PathPrefix");
203 assert_eq!(value, &CborValue::Text("/admin/".into()));
204 }
205 other => panic!("expected AwsCondition::Eq, got {other:?}"),
206 }
207 }
208}