use futures_util::{
StreamExt, TryStreamExt,
future::BoxFuture,
stream::{self, BoxStream},
};
use serde_json::Value;
use crate::{
Error, Result,
cases::{Case, CasePage, CommentPage},
exceptions::ExceptionPage,
fleet::FleetPage,
security::{DetectionRule, RulePage},
};
pub trait Page {
type Item;
fn total(&self) -> u64;
fn items(&self) -> &[Self::Item];
fn into_items(self) -> Vec<Self::Item>;
fn number(&self) -> Option<u32> {
None
}
}
impl Page for RulePage {
type Item = DetectionRule;
fn total(&self) -> u64 {
self.total
}
fn number(&self) -> Option<u32> {
Some(self.page)
}
fn items(&self) -> &[DetectionRule] {
&self.data
}
fn into_items(self) -> Vec<DetectionRule> {
self.data
}
}
impl Page for CasePage {
type Item = Case;
fn total(&self) -> u64 {
self.total
}
fn number(&self) -> Option<u32> {
Some(self.page)
}
fn items(&self) -> &[Case] {
&self.cases
}
fn into_items(self) -> Vec<Case> {
self.cases
}
}
impl Page for CommentPage {
type Item = Value;
fn total(&self) -> u64 {
self.total
}
fn number(&self) -> Option<u32> {
Some(self.page)
}
fn items(&self) -> &[Value] {
&self.comments
}
fn into_items(self) -> Vec<Value> {
self.comments
}
}
impl<T> Page for ExceptionPage<T> {
type Item = T;
fn total(&self) -> u64 {
self.total
}
fn number(&self) -> Option<u32> {
Some(self.page)
}
fn items(&self) -> &[T] {
&self.data
}
fn into_items(self) -> Vec<T> {
self.data
}
}
impl<T> Page for FleetPage<T> {
type Item = T;
fn total(&self) -> u64 {
self.total
}
fn number(&self) -> Option<u32> {
Some(self.page)
}
fn items(&self) -> &[T] {
&self.items
}
fn into_items(self) -> Vec<T> {
self.items
}
}
pub(crate) trait Paged<'a>: Clone + Send + 'a {
type Page: Page + Send + 'a;
fn with_page(self, page: u32) -> Self;
fn fetch(self) -> BoxFuture<'a, Result<Self::Page>>;
}
enum Cursor<B> {
Next {
builder: B,
page: u32,
seen: u64,
},
Limited,
Done,
}
pub(crate) fn pages<'a, B: Paged<'a>>(
builder: B,
max_pages: Option<u32>,
) -> BoxStream<'a, Result<B::Page>> {
let start = match max_pages {
Some(0) => Cursor::Limited,
_ => Cursor::Next {
builder,
page: 1,
seen: 0,
},
};
stream::try_unfold(start, move |cursor| next_page(cursor, max_pages)).boxed()
}
async fn next_page<'a, B: Paged<'a>>(
cursor: Cursor<B>,
max_pages: Option<u32>,
) -> Result<Option<(B::Page, Cursor<B>)>> {
let (builder, page, seen) = match cursor {
Cursor::Next {
builder,
page,
seen,
} => (builder, page, seen),
Cursor::Limited => {
return Err(Error::PageLimit {
max_pages: max_pages.unwrap_or_default(),
});
}
Cursor::Done => return Ok(None),
};
let result = builder.clone().with_page(page).fetch().await?;
if let Some(returned) = result.number().filter(|&returned| returned != page) {
return Err(Error::UnexpectedPage {
requested: page,
returned,
});
}
let count = result.items().len() as u64;
let seen = seen.saturating_add(count);
let next = match page.checked_add(1) {
_ if count == 0 || seen >= result.total() => Cursor::Done,
_ if max_pages.is_some_and(|max| page >= max) => Cursor::Limited,
Some(page) => Cursor::Next {
builder,
page,
seen,
},
None => Cursor::Limited,
};
Ok(Some((result, next)))
}
pub(crate) fn items<'a, B: Paged<'a>>(
builder: B,
max_pages: Option<u32>,
) -> BoxStream<'a, Result<<B::Page as Page>::Item>>
where
<B::Page as Page>::Item: Send + 'a,
{
pages(builder, max_pages)
.map_ok(|page| stream::iter(page.into_items().into_iter().map(Ok)))
.try_flatten()
.boxed()
}
macro_rules! paginated {
($($name:ident => $page:ty),* $(,)?) => {$(
impl<'a> $crate::pagination::Paged<'a> for $name<'a> {
type Page = $page;
fn with_page(self, page: u32) -> Self {
self.page(page)
}
fn fetch(self) -> ::futures_util::future::BoxFuture<'a, $crate::Result<$page>> {
::std::boxed::Box::pin(async move { self.send().await?.json().await })
}
}
impl<'a> $name<'a> {
pub fn pages(self) -> ::futures_util::stream::BoxStream<'a, $crate::Result<$page>> {
$crate::pagination::pages(self, None)
}
pub fn items(
self,
) -> ::futures_util::stream::BoxStream<
'a,
$crate::Result<<$page as $crate::pagination::Page>::Item>,
> {
$crate::pagination::items(self, None)
}
pub fn bounded_pages(
self,
max_pages: u32,
) -> ::futures_util::stream::BoxStream<'a, $crate::Result<$page>> {
$crate::pagination::pages(self, Some(max_pages))
}
pub fn bounded_items(
self,
max_pages: u32,
) -> ::futures_util::stream::BoxStream<
'a,
$crate::Result<<$page as $crate::pagination::Page>::Item>,
> {
$crate::pagination::items(self, Some(max_pages))
}
}
)*};
}
pub(crate) use paginated;