Skip to main content

omry_integration/
bridge.rs

1//! Module with APIs that make it more convenient to test the Omry RPC server.
2use anyhow::{Context, Result};
3use omry_search::wrappers::SearchParametersWrapper;
4use omry_service::archiving::{Record, RecordMeta, RecordParams};
5use omry_service::common::RecordId;
6use omry_service::search::SearchResponse;
7use omry_service::service::{MetaHit, SearchRespMeta};
8use omry_service::{OmryApi, PONG};
9use tokio::task::JoinSet;
10use tracing::instrument;
11
12/// Trait to "bridge" the user-facing application with the flora backend
13/// via [`omry_service`].
14#[allow(async_fn_in_trait)]
15pub trait ApiBridge {
16    /// Returns a reference to the underlying tarpc client used by this bridge.
17    fn api(&self) -> &OmryApi;
18
19    /// Pings the flora server.
20    ///
21    /// Returns the expected [`PONG`] response on success.
22    #[instrument(skip(self))]
23    async fn ping(&self) -> Result<String> {
24        let rx = self
25            .api()
26            .ping()
27            .await
28            .context("error sending ping request")?;
29        let resp = rx.await?;
30        if resp == PONG {
31            Ok(resp)
32        } else {
33            anyhow::bail!("Unexpected ping response: {resp}")
34        }
35    }
36
37    /// Adds a single record to the flora archive.
38    #[instrument(skip(self, record))]
39    async fn add_record(&self, record: RecordParams) -> Result<RecordId> {
40        let (tx, mut rx) = self.api().add().await.context("error calling add")?;
41        tx.send(record).await.context("Failed to send record")?;
42        drop(tx);
43
44        let id = rx
45            .recv()
46            .await?
47            .ok_or_else(|| anyhow::anyhow!("got empty response"))?;
48        Ok(id)
49    }
50
51    /// Adds multiple records.
52    #[instrument(skip(self, records))]
53    async fn add_many_records(&self, records: Vec<RecordParams>) -> Result<Vec<RecordId>> {
54        let (tx, mut rx) = self.api().add().await.context("error calling add")?;
55        let n_recs = records.len();
56
57        let mut set = JoinSet::new();
58        for rec in records {
59            let tx = tx.clone();
60            set.spawn(async move { tx.send(rec).await.context("error sending record") });
61        }
62
63        set.join_all().await;
64        drop(tx);
65
66        let mut ids = Vec::with_capacity(n_recs);
67        while let Some(id) = rx.recv().await.context("error receiving response")? {
68            ids.push(id);
69        }
70
71        ids.sort_unstable();
72        Ok(ids)
73    }
74
75    /// Queries the backend for the number of records it currently stores.
76    #[instrument(skip(self))]
77    async fn count(&self) -> Result<i64> {
78        let rx = self.api().count().await.context("failed calling count")?;
79        Ok(rx.await?)
80    }
81
82    /// Queries the flora backend for records with the given IDs.
83    #[instrument(skip(self))]
84    async fn get_records(&self, record_ids: Vec<RecordId>) -> Result<Vec<Record>> {
85        let (tx, mut rx) = self
86            .api()
87            .record()
88            .await
89            .context("failed calling get record")?;
90        let mut set = JoinSet::new();
91
92        for id in &record_ids {
93            let tx = tx.clone();
94            let id = *id;
95            set.spawn(async move { tx.send(id).await });
96        }
97
98        drop(tx);
99
100        set.join_all().await;
101        let mut records = Vec::with_capacity(record_ids.len());
102        while let Some(record) = rx.recv().await? {
103            records.push(record);
104        }
105
106        // the returned records may not be in the same order as our input IDs
107        #[allow(clippy::expect_used)]
108        records.sort_by_key(|rec| {
109            let this_id = rec.id().expect("returned record should have an ID");
110            record_ids
111                .iter()
112                .position(|id| id == &this_id)
113                .expect("returned ID should be present among original IDs")
114        });
115
116        Ok(records)
117    }
118
119    /// Returns all the records in the database.
120    #[instrument(skip(self))]
121    async fn get_all_records(&self) -> Result<Vec<Record>> {
122        let mut rx = self
123            .api()
124            .all_records()
125            .await
126            .context("Failed to invoke 'all_records' RPC")?;
127
128        let mut recs = vec![];
129        while let Some(rec) = rx.recv().await? {
130            recs.push(rec);
131        }
132
133        Ok(recs)
134    }
135
136    /// Returns the metadata for all the records in the database.
137    #[instrument(skip(self))]
138    async fn get_all_records_meta(&self) -> Result<Vec<RecordMeta>> {
139        let mut rx = self
140            .api()
141            .all_meta()
142            .await
143            .context("Failed to invoke 'all_meta' RPC")?;
144
145        let mut rec_metas = vec![];
146        while let Some(rec) = rx.recv().await? {
147            rec_metas.push(rec);
148        }
149
150        Ok(rec_metas)
151    }
152
153    /// Returns record metadatas, most recent first.
154    #[instrument(skip(self))]
155    async fn most_recent_meta(&self) -> Result<Vec<RecordMeta>> {
156        let mut rx = self
157            .api()
158            .most_recent_meta()
159            .await
160            .context("Error invoking 'most_recent_meta'")?;
161
162        let mut rec_metas = vec![];
163        while let Some(rec) = rx.recv().await? {
164            rec_metas.push(rec);
165        }
166
167        Ok(rec_metas)
168    }
169
170    /// Sends a search request to the flora backend.
171    #[instrument(skip(self))]
172    async fn search(&self, search_parameters: SearchParametersWrapper) -> Result<SearchResponse> {
173        let mut rx = self
174            .api()
175            .search(search_parameters)
176            .await
177            .context("failed while calling search")?;
178
179        let mut metahits = vec![];
180        while let Some(item) = rx.recv().await? {
181            metahits.push(item);
182        }
183
184        metahits.sort_by_key(|hit| hit.index);
185        let last_mh = metahits.pop().context("No search response")?;
186        let MetaHit {
187            meta,
188            index: _,
189            hit: last_hit,
190        } = last_mh;
191        let SearchRespMeta {
192            found,
193            out_of,
194            page,
195            request_params,
196            search_time,
197        } = meta;
198
199        let mut hits = Vec::with_capacity(found);
200        for mh in metahits {
201            let Some(hit) = mh.hit else {
202                // any hit being None means there aren't any hits,
203                // just one item with metadata.
204                break;
205            };
206            hits.push(hit);
207        }
208
209        if let Some(hit) = last_hit {
210            hits.push(hit);
211        }
212
213        let resp = SearchResponse {
214            found,
215            out_of,
216            page,
217            request_params,
218            search_time,
219            hits,
220        };
221        Ok(resp)
222    }
223
224    /// Sends a requests for the Omry API to delete record(s) with
225    /// the given IDs.
226    #[instrument(skip(self))]
227    async fn delete(&self, ids: Vec<RecordId>) -> Result<usize> {
228        tracing::debug!(
229            "Will make {} RPCs to delete the given IDs (one per each)",
230            ids.len()
231        );
232
233        let (tx, mut rx) = self.api().delete_record().await?;
234        let mut set = JoinSet::new();
235
236        for id in ids {
237            let tx = tx.clone();
238            set.spawn(async move { tx.send(id).await });
239        }
240
241        drop(tx);
242
243        set.join_all().await;
244
245        let mut n_deleted = 0usize;
246        let mut n_errors = 0usize;
247
248        loop {
249            match rx.recv().await {
250                Ok(Some(num_affected)) => n_deleted += num_affected,
251                Ok(None) => break,
252                Err(e) => {
253                    tracing::error!("{e}");
254                    n_errors += 1;
255                }
256            }
257        }
258
259        if n_errors != 0 {
260            anyhow::bail!("{n_errors} records couldn't be scheduled for deletion due to errors.");
261        }
262
263        Ok(n_deleted)
264    }
265
266    /// Returns the version information for the server application.
267    #[instrument(skip(self))]
268    async fn version(&self) -> Result<String> {
269        let rx = self
270            .api()
271            .version()
272            .await
273            .context("Failed to invoke 'version' RPC")?;
274
275        Ok(rx.await?)
276    }
277}