Skip to main content

security_client_rs/
pagination.rs

1//! Streams over paginated collections.
2//!
3//! Every `find_*` builder with pages has `pages()` and `items()`. They send one
4//! request per page, starting from the first, and stop after an empty page or
5//! once `total` items have been seen. A page whose number differs from the one
6//! requested fails with [`Error::UnexpectedPage`].
7//!
8//! `bounded_pages(n)` and `bounded_items(n)` read at most `n` pages. If more
9//! remain, the stream ends with [`Error::PageLimit`],
10//! so a stopped traversal is never mistaken for a complete one.
11//!
12//! Kibana pages by offset, so concurrent changes can skip or repeat items, and
13//! most collections refuse to page past 10,000 results. These checks do not
14//! give a consistent snapshot.
15//!
16//! Streams fetch pages as they are polled. Dropping a stream stops further
17//! requests. Starting a stream overrides a builder's selected page, but keeps
18//! its filters and page size.
19use futures_util::{
20    StreamExt, TryStreamExt,
21    future::BoxFuture,
22    stream::{self, BoxStream},
23};
24use serde_json::Value;
25
26use crate::{
27    Error, Result,
28    cases::{Case, CasePage, CommentPage},
29    exceptions::ExceptionPage,
30    fleet::FleetPage,
31    security::{DetectionRule, RulePage},
32};
33
34/// One page of a paginated collection.
35pub trait Page {
36    type Item;
37
38    /// The number of items in the whole collection when the page was read.
39    fn total(&self) -> u64;
40    fn items(&self) -> &[Self::Item];
41    fn into_items(self) -> Vec<Self::Item>;
42
43    /// The page number Kibana reports, if the page carries one.
44    fn number(&self) -> Option<u32> {
45        None
46    }
47}
48
49impl Page for RulePage {
50    type Item = DetectionRule;
51
52    fn total(&self) -> u64 {
53        self.total
54    }
55    fn number(&self) -> Option<u32> {
56        Some(self.page)
57    }
58    fn items(&self) -> &[DetectionRule] {
59        &self.data
60    }
61    fn into_items(self) -> Vec<DetectionRule> {
62        self.data
63    }
64}
65
66impl Page for CasePage {
67    type Item = Case;
68
69    fn total(&self) -> u64 {
70        self.total
71    }
72    fn number(&self) -> Option<u32> {
73        Some(self.page)
74    }
75    fn items(&self) -> &[Case] {
76        &self.cases
77    }
78    fn into_items(self) -> Vec<Case> {
79        self.cases
80    }
81}
82
83impl Page for CommentPage {
84    type Item = Value;
85
86    fn total(&self) -> u64 {
87        self.total
88    }
89    fn number(&self) -> Option<u32> {
90        Some(self.page)
91    }
92    fn items(&self) -> &[Value] {
93        &self.comments
94    }
95    fn into_items(self) -> Vec<Value> {
96        self.comments
97    }
98}
99
100impl<T> Page for ExceptionPage<T> {
101    type Item = T;
102
103    fn total(&self) -> u64 {
104        self.total
105    }
106    fn number(&self) -> Option<u32> {
107        Some(self.page)
108    }
109    fn items(&self) -> &[T] {
110        &self.data
111    }
112    fn into_items(self) -> Vec<T> {
113        self.data
114    }
115}
116
117impl<T> Page for FleetPage<T> {
118    type Item = T;
119
120    fn total(&self) -> u64 {
121        self.total
122    }
123    fn number(&self) -> Option<u32> {
124        Some(self.page)
125    }
126    fn items(&self) -> &[T] {
127        &self.items
128    }
129    fn into_items(self) -> Vec<T> {
130        self.items
131    }
132}
133
134/// A builder that can request any page of its collection.
135pub(crate) trait Paged<'a>: Clone + Send + 'a {
136    type Page: Page + Send + 'a;
137
138    fn with_page(self, page: u32) -> Self;
139    // Boxed rather than `impl Future`: Rust 1.88 cannot prove the lifetime bounds
140    // of the unboxed future inside the page stream (rust-lang/rust#100013).
141    fn fetch(self) -> BoxFuture<'a, Result<Self::Page>>;
142}
143
144/// Where a traversal stands.
145enum Cursor<B> {
146    /// Fetch `page` next, having seen `seen` items.
147    Next {
148        builder: B,
149        page: u32,
150        seen: u64,
151    },
152    /// More pages remain, but the page limit was reached.
153    Limited,
154    Done,
155}
156
157/// Streams pages from the first; `max_pages` bounds how many are read.
158pub(crate) fn pages<'a, B: Paged<'a>>(
159    builder: B,
160    max_pages: Option<u32>,
161) -> BoxStream<'a, Result<B::Page>> {
162    let start = match max_pages {
163        Some(0) => Cursor::Limited,
164        _ => Cursor::Next {
165            builder,
166            page: 1,
167            seen: 0,
168        },
169    };
170    stream::try_unfold(start, move |cursor| next_page(cursor, max_pages)).boxed()
171}
172
173async fn next_page<'a, B: Paged<'a>>(
174    cursor: Cursor<B>,
175    max_pages: Option<u32>,
176) -> Result<Option<(B::Page, Cursor<B>)>> {
177    let (builder, page, seen) = match cursor {
178        Cursor::Next {
179            builder,
180            page,
181            seen,
182        } => (builder, page, seen),
183        Cursor::Limited => {
184            return Err(Error::PageLimit {
185                max_pages: max_pages.unwrap_or_default(),
186            });
187        }
188        Cursor::Done => return Ok(None),
189    };
190    let result = builder.clone().with_page(page).fetch().await?;
191    if let Some(returned) = result.number().filter(|&returned| returned != page) {
192        return Err(Error::UnexpectedPage {
193            requested: page,
194            returned,
195        });
196    }
197    let count = result.items().len() as u64;
198    let seen = seen.saturating_add(count);
199    let next = match page.checked_add(1) {
200        _ if count == 0 || seen >= result.total() => Cursor::Done,
201        _ if max_pages.is_some_and(|max| page >= max) => Cursor::Limited,
202        Some(page) => Cursor::Next {
203            builder,
204            page,
205            seen,
206        },
207        None => Cursor::Limited,
208    };
209    Ok(Some((result, next)))
210}
211
212pub(crate) fn items<'a, B: Paged<'a>>(
213    builder: B,
214    max_pages: Option<u32>,
215) -> BoxStream<'a, Result<<B::Page as Page>::Item>>
216where
217    <B::Page as Page>::Item: Send + 'a,
218{
219    pages(builder, max_pages)
220        .map_ok(|page| stream::iter(page.into_items().into_iter().map(Ok)))
221        .try_flatten()
222        .boxed()
223}
224
225/// Adds `pages()` and `items()` to a builder whose `page` setter starts at one.
226macro_rules! paginated {
227    ($($name:ident => $page:ty),* $(,)?) => {$(
228        impl<'a> $crate::pagination::Paged<'a> for $name<'a> {
229            type Page = $page;
230
231            fn with_page(self, page: u32) -> Self {
232                self.page(page)
233            }
234
235            fn fetch(self) -> ::futures_util::future::BoxFuture<'a, $crate::Result<$page>> {
236                ::std::boxed::Box::pin(async move { self.send().await?.json().await })
237            }
238        }
239
240        impl<'a> $name<'a> {
241            /// Streams every page from the first, one request per page.
242            /// Any page set on this builder is ignored.
243            pub fn pages(self) -> ::futures_util::stream::BoxStream<'a, $crate::Result<$page>> {
244                $crate::pagination::pages(self, None)
245            }
246
247            /// Streams every item across all pages. See [`pages`](Self::pages).
248            pub fn items(
249                self,
250            ) -> ::futures_util::stream::BoxStream<
251                'a,
252                $crate::Result<<$page as $crate::pagination::Page>::Item>,
253            > {
254                $crate::pagination::items(self, None)
255            }
256
257            /// Like [`pages`](Self::pages), reading at most `max_pages` pages.
258            /// If more remain, the stream ends with [`Error::PageLimit`](crate::Error::PageLimit).
259            pub fn bounded_pages(
260                self,
261                max_pages: u32,
262            ) -> ::futures_util::stream::BoxStream<'a, $crate::Result<$page>> {
263                $crate::pagination::pages(self, Some(max_pages))
264            }
265
266            /// Like [`items`](Self::items), reading at most `max_pages` pages.
267            /// If more remain, the stream ends with [`Error::PageLimit`](crate::Error::PageLimit).
268            pub fn bounded_items(
269                self,
270                max_pages: u32,
271            ) -> ::futures_util::stream::BoxStream<
272                'a,
273                $crate::Result<<$page as $crate::pagination::Page>::Item>,
274            > {
275                $crate::pagination::items(self, Some(max_pages))
276            }
277        }
278    )*};
279}
280pub(crate) use paginated;