Skip to main content

stac/api/
adapters.rs

1//! Generic adapters between search client traits.
2//!
3//! These adapters allow any implementation of one search trait to be used as
4//! another. For example, any [`ItemsClient`] can be wrapped in
5//! [`PagedItemsStream`] to gain a [`StreamItemsClient`] implementation that
6//! paginates via the [`ItemCollection::next`] field.
7
8#[cfg(feature = "async")]
9mod async_adapters {
10    use crate::Collection;
11    use crate::api::client::{
12        CollectionsClient, ItemsClient, PagedCollectionsClient, StreamCollectionsClient,
13        StreamItemsClient,
14    };
15    use crate::api::{Item, ItemCollection, Search};
16    use async_stream::try_stream;
17    use futures_core::Stream;
18    use std::future::Future;
19
20    /// Adapter that wraps an [`ItemsClient`] to provide [`StreamItemsClient`].
21    #[derive(Debug)]
22    pub struct PagedItemsStream<T> {
23        inner: T,
24    }
25
26    impl<T> PagedItemsStream<T> {
27        /// Wraps a search client.
28        pub fn new(inner: T) -> Self {
29            Self { inner }
30        }
31
32        /// Returns the wrapped client.
33        pub fn into_inner(self) -> T {
34            self.inner
35        }
36    }
37
38    impl<T> StreamItemsClient for PagedItemsStream<T>
39    where
40        T: ItemsClient + Clone + Send + Sync,
41        T::Error: Send,
42    {
43        type Error = T::Error;
44
45        fn search_stream(
46            &self,
47            search: Search,
48        ) -> impl Future<
49            Output = Result<impl Stream<Item = Result<Item, T::Error>> + Send, T::Error>,
50        > + Send {
51            let client = self.inner.clone();
52            async move {
53                let page = client.search(search.clone()).await?;
54                Ok(stream_pages(client, search, page))
55            }
56        }
57    }
58
59    /// Streams items from an [`ItemsClient`] using token/skip-based pagination.
60    pub fn stream_pages<T>(
61        client: T,
62        initial_search: Search,
63        initial_page: ItemCollection,
64    ) -> impl Stream<Item = Result<Item, T::Error>> + Send
65    where
66        T: ItemsClient + Clone + Send + Sync,
67        T::Error: Send,
68    {
69        try_stream! {
70            let mut page = initial_page;
71            let mut current_search = initial_search;
72            loop {
73                if page.items.is_empty() {
74                    break;
75                }
76                let next = page.next.clone();
77                for item in page.items {
78                    yield item;
79                }
80                match next {
81                    Some(next_fields) => {
82                        current_search.additional_fields.extend(next_fields);
83                        page = client.search(current_search.clone()).await?;
84                    }
85                    None => break,
86                }
87            }
88        }
89    }
90
91    /// Blanket impl for non-paginated collection clients.
92    impl<T> StreamCollectionsClient for T
93    where
94        T: CollectionsClient + Clone + Send + Sync,
95        T::Error: Send,
96    {
97        type Error = T::Error;
98
99        fn collections_stream(
100            &self,
101        ) -> impl Future<
102            Output = Result<impl Stream<Item = Result<Collection, T::Error>> + Send, T::Error>,
103        > + Send {
104            let client = self.clone();
105            async move {
106                let collections = client.collections().await?;
107                Ok(futures::stream::iter(collections.into_iter().map(Ok)))
108            }
109        }
110    }
111
112    /// Streams collections from a [`PagedCollectionsClient`] using cursor pagination.
113    pub fn stream_pages_collections<T>(
114        client: T,
115        initial_page: Vec<Collection>,
116        initial_token: Option<String>,
117    ) -> impl Stream<Item = Result<Collection, T::Error>> + Send
118    where
119        T: PagedCollectionsClient + Clone + Send + Sync,
120        T::Error: Send,
121    {
122        try_stream! {
123            let mut page = initial_page;
124            let mut cursor = initial_token;
125            loop {
126                if page.is_empty() {
127                    break;
128                }
129                let next_cursor = cursor;
130                for collection in page {
131                    yield collection;
132                }
133                match next_cursor {
134                    Some(t) => {
135                        let (next_page, next_t) = client.collections_page(Some(t)).await?;
136                        page = next_page;
137                        cursor = next_t;
138                    }
139                    None => break,
140                }
141            }
142        }
143    }
144}
145
146#[cfg(feature = "async")]
147pub use async_adapters::{PagedItemsStream, stream_pages, stream_pages_collections};
148
149#[cfg(feature = "geoarrow")]
150mod geoarrow_adapters {
151    use super::super::client::{ArrowItemsClient, ItemsClient};
152    use super::super::{Item, ItemCollection, Search};
153    use std::future::Future;
154
155    /// Generic adapter that wraps any `Iterator<Item = Result<RecordBatch, E>>`
156    /// and a schema into an [`arrow_array::RecordBatchReader`].
157    #[allow(missing_debug_implementations)]
158    pub struct RecordBatchReaderAdapter<I> {
159        inner: I,
160        schema: arrow_schema::SchemaRef,
161    }
162
163    impl<I> RecordBatchReaderAdapter<I> {
164        /// Creates a new adapter from an iterator and a schema.
165        pub fn new(inner: I, schema: arrow_schema::SchemaRef) -> Self {
166            Self { inner, schema }
167        }
168    }
169
170    impl<I, E> Iterator for RecordBatchReaderAdapter<I>
171    where
172        I: Iterator<Item = Result<arrow_array::RecordBatch, E>>,
173        E: std::error::Error + Send + Sync + 'static,
174    {
175        type Item = Result<arrow_array::RecordBatch, arrow_schema::ArrowError>;
176
177        fn next(&mut self) -> Option<Self::Item> {
178            self.inner
179                .next()
180                .map(|r| r.map_err(|e| arrow_schema::ArrowError::ExternalError(Box::new(e))))
181        }
182    }
183
184    impl<I, E> arrow_array::RecordBatchReader for RecordBatchReaderAdapter<I>
185    where
186        I: Iterator<Item = Result<arrow_array::RecordBatch, E>>,
187        E: std::error::Error + Send + Sync + 'static,
188    {
189        fn schema(&self) -> arrow_schema::SchemaRef {
190            self.schema.clone()
191        }
192    }
193
194    impl<T> ItemsClient for T
195    where
196        T: ArrowItemsClient + Send + Sync,
197        T::Error: std::error::Error + Send + Sync + 'static,
198    {
199        type Error = crate::Error;
200
201        fn search(
202            &self,
203            search: Search,
204        ) -> impl Future<Output = Result<ItemCollection, Self::Error>> + Send {
205            let result: Result<Vec<Item>, crate::Error> = self
206                .search_to_arrow(search)
207                .map_err(|err| crate::Error::ArrowAdapterClient(Box::new(err)))
208                .and_then(crate::geoarrow::json::from_record_batch_reader);
209            async move { Ok(ItemCollection::from(result?)) }
210        }
211    }
212
213    #[cfg(feature = "async")]
214    mod async_geoarrow_adapters {
215        use super::ArrowItemsClient;
216        use crate::api::client::StreamItemsClient;
217        use crate::api::{Item, Search};
218        use futures_core::Stream;
219        use std::future::Future;
220        use std::pin::Pin;
221        use std::task::{Context, Poll};
222
223        struct ArrowItemStream<R> {
224            reader: R,
225            current_items: std::vec::IntoIter<Item>,
226        }
227
228        impl<R> ArrowItemStream<R> {
229            fn new(reader: R) -> Self {
230                Self {
231                    reader,
232                    current_items: Vec::new().into_iter(),
233                }
234            }
235        }
236
237        impl<R> Unpin for ArrowItemStream<R> {}
238
239        impl<R> Stream for ArrowItemStream<R>
240        where
241            R: arrow_array::RecordBatchReader,
242        {
243            type Item = Result<Item, crate::Error>;
244
245            fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
246                let this = self.get_mut();
247                loop {
248                    if let Some(item) = this.current_items.next() {
249                        return Poll::Ready(Some(Ok(item)));
250                    }
251
252                    match this.reader.next() {
253                        Some(Ok(batch)) => {
254                            match crate::geoarrow::json::record_batch_to_json_rows(batch) {
255                                Ok(items) => {
256                                    this.current_items = items.into_iter();
257                                }
258                                Err(err) => return Poll::Ready(Some(Err(err))),
259                            }
260                        }
261                        Some(Err(err)) => return Poll::Ready(Some(Err(err.into()))),
262                        None => return Poll::Ready(None),
263                    }
264                }
265            }
266        }
267
268        impl<T> StreamItemsClient for T
269        where
270            T: ArrowItemsClient + Send + Sync,
271            T::Error: std::error::Error + Send + Sync + 'static,
272            for<'a> T::RecordBatchStream<'a>: Send,
273        {
274            type Error = crate::Error;
275
276            fn search_stream(
277                &self,
278                search: Search,
279            ) -> impl Future<
280                Output = Result<impl Stream<Item = Result<Item, Self::Error>> + Send, Self::Error>,
281            > + Send {
282                let reader = self
283                    .search_to_arrow(search)
284                    .map_err(|err| crate::Error::ArrowAdapterClient(Box::new(err)));
285                async move { Ok(ArrowItemStream::new(reader?)) }
286            }
287        }
288    }
289}
290
291#[cfg(feature = "geoarrow")]
292pub use geoarrow_adapters::RecordBatchReaderAdapter;