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::{RawRecord, 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
103        // The last item from the stream is `None`, indicating we've reached the end.
104        // The outer `None` would be for when the channel got closed.
105        while let Some(Some(record)) = rx.recv().await? {
106            records.push(record);
107        }
108
109        // the returned records may not be in the same order as our input IDs
110        #[allow(clippy::expect_used)]
111        records.sort_by_key(|rec| {
112            let this_id = rec.id().expect("returned record should have an ID");
113            record_ids
114                .iter()
115                .position(|id| id == &this_id)
116                .expect("returned ID should be present among original IDs")
117        });
118
119        Ok(records)
120    }
121
122    /// Returns all the records in the database.
123    #[instrument(skip(self))]
124    async fn get_all_records(&self) -> Result<Vec<Record>> {
125        let mut start_row = None;
126
127        let mut recs = vec![];
128        'paginate: loop {
129            let mut rx = self
130                .api()
131                .all_records(start_row)
132                .await
133                .context("Failed to invoke 'all_records' RPC")?;
134
135            while let Some(maybe_rec) = rx.recv().await? {
136                let Some(rec) = maybe_rec else {
137                    break 'paginate;
138                };
139
140                recs.push(rec);
141            }
142
143            start_row = Some(recs.len());
144        }
145
146        Ok(recs)
147    }
148
149    /// Returns the metadata for all the records in the database.
150    #[instrument(skip(self))]
151    async fn get_all_records_meta(&self) -> Result<Vec<RecordMeta>> {
152        let mut rec_metas = vec![];
153        let mut start_row = None;
154
155        'paginate: loop {
156            let mut rx = self
157                .api()
158                .all_meta(start_row)
159                .await
160                .context("Failed to invoke 'all_meta' RPC")?;
161
162            while let Some(maybe_rec) = rx.recv().await? {
163                let Some(rec) = maybe_rec else {
164                    break 'paginate;
165                };
166
167                rec_metas.push(rec);
168            }
169
170            start_row = Some(rec_metas.len());
171        }
172
173        Ok(rec_metas)
174    }
175
176    /// Returns raw data for all the records in the DB.
177    #[instrument(skip(self))]
178    async fn get_all_raw(&self) -> Result<Vec<RawRecord>> {
179        let mut raw_recs = vec![];
180        let mut start_row = None;
181
182        'paginate: loop {
183            let mut rx = self
184                .api()
185                .all_raw(start_row)
186                .await
187                .context("error invoking 'all_raw'")?;
188
189            while let Some(maybe_rec) = rx.recv().await? {
190                let Some(rec) = maybe_rec else {
191                    break 'paginate;
192                };
193
194                raw_recs.push(rec);
195            }
196
197            start_row = Some(raw_recs.len());
198        }
199
200        Ok(raw_recs)
201    }
202
203    /// Returns record metadatas, most recent first.
204    #[instrument(skip(self))]
205    async fn most_recent_meta(&self) -> Result<Vec<RecordMeta>> {
206        let mut rec_metas = vec![];
207        let mut start_idx = None;
208
209        'paginate: loop {
210            let mut rx = self
211                .api()
212                .most_recent_meta(start_idx)
213                .await
214                .context("Error invoking 'most_recent_meta'")?;
215
216            while let Some(maybe_rec) = rx.recv().await? {
217                let Some(rec) = maybe_rec else {
218                    break 'paginate;
219                };
220
221                rec_metas.push(rec);
222            }
223
224            start_idx = Some(rec_metas.len());
225        }
226
227        Ok(rec_metas)
228    }
229
230    /// Sends a search request to the flora backend.
231    #[instrument(skip(self))]
232    async fn search(&self, search_parameters: SearchParametersWrapper) -> Result<SearchResponse> {
233        let mut rx = self
234            .api()
235            .search(search_parameters)
236            .await
237            .context("failed while calling search")?;
238
239        let mut metahits = vec![];
240        while let Some(item) = rx.recv().await? {
241            metahits.push(item);
242        }
243
244        metahits.sort_by_key(|hit| hit.index);
245        let last_mh = metahits.pop().context("No search response")?;
246        let MetaHit {
247            meta,
248            index: _,
249            hit: last_hit,
250        } = last_mh;
251        let SearchRespMeta {
252            found,
253            out_of,
254            page,
255            request_params,
256            search_time,
257        } = meta;
258
259        let mut hits = Vec::with_capacity(found);
260        for mh in metahits {
261            let Some(hit) = mh.hit else {
262                // any hit being None means there aren't any hits,
263                // just one item with metadata.
264                break;
265            };
266            hits.push(hit);
267        }
268
269        if let Some(hit) = last_hit {
270            hits.push(hit);
271        }
272
273        let resp = SearchResponse {
274            found,
275            out_of,
276            page,
277            request_params,
278            search_time,
279            hits,
280        };
281        Ok(resp)
282    }
283
284    /// Sends a requests for the Omry API to delete record(s) with
285    /// the given IDs.
286    #[instrument(skip(self))]
287    async fn delete(&self, ids: Vec<RecordId>) -> Result<usize> {
288        tracing::debug!(
289            "Will make {} RPCs to delete the given IDs (one per each)",
290            ids.len()
291        );
292
293        let (tx, mut rx) = self.api().delete_record().await?;
294        let mut set = JoinSet::new();
295
296        for id in ids {
297            let tx = tx.clone();
298            set.spawn(async move { tx.send(id).await });
299        }
300
301        drop(tx);
302
303        set.join_all().await;
304
305        let mut n_deleted = 0usize;
306        let mut n_errors = 0usize;
307
308        loop {
309            match rx.recv().await {
310                Ok(Some(num_affected)) => n_deleted += num_affected,
311                Ok(None) => break,
312                Err(e) => {
313                    tracing::error!("{e}");
314                    n_errors += 1;
315                }
316            }
317        }
318
319        if n_errors != 0 {
320            anyhow::bail!("{n_errors} records couldn't be scheduled for deletion due to errors.");
321        }
322
323        Ok(n_deleted)
324    }
325
326    /// Sends a request to update records in Omry,
327    /// and returns a list of IDs for records that were successfully updated.
328    #[instrument(skip_all)]
329    async fn update(&self, records: Vec<RecordParams>) -> Result<Vec<RecordId>> {
330        let (tx, mut rx) = self
331            .api()
332            .update_record()
333            .await
334            .context("error invoking 'update_record' RPC")?;
335
336        let mut set = JoinSet::new();
337        for rec in records {
338            let tx = tx.clone();
339            set.spawn(async move { tx.send(rec).await });
340        }
341
342        drop(tx);
343        set.join_all().await;
344
345        let mut ids = vec![];
346        while let Some(id) = rx.recv().await? {
347            ids.push(id);
348        }
349
350        Ok(ids)
351    }
352
353    /// Returns the version information for the server application.
354    #[instrument(skip(self))]
355    async fn version(&self) -> Result<String> {
356        let rx = self
357            .api()
358            .version()
359            .await
360            .context("Failed to invoke 'version' RPC")?;
361
362        Ok(rx.await?)
363    }
364}