security_client_rs/
pagination.rs1use 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
34pub trait Page {
36 type Item;
37
38 fn total(&self) -> u64;
40 fn items(&self) -> &[Self::Item];
41 fn into_items(self) -> Vec<Self::Item>;
42
43 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
134pub(crate) trait Paged<'a>: Clone + Send + 'a {
136 type Page: Page + Send + 'a;
137
138 fn with_page(self, page: u32) -> Self;
139 fn fetch(self) -> BoxFuture<'a, Result<Self::Page>>;
142}
143
144enum Cursor<B> {
146 Next {
148 builder: B,
149 page: u32,
150 seen: u64,
151 },
152 Limited,
154 Done,
155}
156
157pub(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
225macro_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 pub fn pages(self) -> ::futures_util::stream::BoxStream<'a, $crate::Result<$page>> {
244 $crate::pagination::pages(self, None)
245 }
246
247 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 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 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;