use super::views;
use crate::clients::{ManagementClients, SINGLE_REQUEST_BUDGET, TOKEN_BUDGET, WALK_BUDGET};
use crate::error::{CliError, SecretSet};
use crate::json::OValue;
use crate::pagination::{self, ListParams};
use scm::RequestOptions;
use scm::airs::management::PaginationOptions;
use scm::airs::management::models::FiniteNumber;
use scm::pagination::{CollectOptions, CollectionResult};
use std::collections::HashSet;
use std::time::Duration;
pub struct Listed<T> {
pub items: Vec<T>,
pub more: bool,
}
pub struct OffsetList<P, A> {
pub id_key: &'static str,
pub page: P,
pub all: A,
pub project: fn(&OValue) -> OValue,
}
pub struct Listing {
pub docs: Vec<OValue>,
pub status: String,
}
impl<P, A> OffsetList<P, A> {
pub async fn read<T>(
self,
clients: &ManagementClients,
params: &ListParams,
secrets: &SecretSet,
fail: impl Fn(&scm::Error, Duration) -> CliError,
) -> Result<Listing, CliError>
where
T: serde::Serialize,
P: AsyncFnOnce(PaginationOptions, &RequestOptions) -> Result<Listed<T>, scm::Error>,
A: AsyncFnOnce(u32, CollectOptions, &RequestOptions) -> CollectionResult<T, u64>,
{
let (mut rows, status) = if params.all {
let options = super::options(WALK_BUDGET)?;
clients.authenticate(Some(&options), WALK_BUDGET).await?;
let failed = |error: &scm::Error| fail(error, WALK_BUDGET);
let collect =
pagination::collect_options(params.max).map_err(|error| failed(&error))?;
let collection = (self.all)(params.limit, collect, &options)
.await
.map_err(|error| failed(error.error()))?;
let rows =
unique_rows(&collection.items, self.id_key).map_err(|error| failed(&error))?;
let status = walk_status(collection.completion, rows.len(), params.max)
.map_err(|error| failed(&error))?;
(rows, status)
} else {
clients.authenticate(None, TOKEN_BUDGET).await?;
let failed = |error: &scm::Error| fail(error, SINGLE_REQUEST_BUDGET);
let mut page = PaginationOptions::default();
page.offset = params.offset;
page.limit = params.limit;
let listed = (self.page)(page, &super::options(SINGLE_REQUEST_BUDGET)?)
.await
.map_err(|error| failed(&error))?;
let rows = rows_of(&listed.items).map_err(|error| failed(&error))?;
let status = pagination::showing_page(rows.len(), listed.more);
(rows, status)
};
sanitize_all(&mut rows, secrets);
Ok(Listing {
docs: rows.iter().map(self.project).collect(),
status,
})
}
}
pub fn more_rows(next: Option<&FiniteNumber>) -> bool {
next.and_then(FiniteNumber::as_f64)
.is_some_and(|next| next > 0.0)
}
pub fn rows_of<T: serde::Serialize>(items: &[T]) -> Result<Vec<OValue>, scm::Error> {
items.iter().map(views::ordered).collect()
}
pub fn check_unique(rows: &[OValue], id_key: &str) -> Result<(), scm::Error> {
let mut seen = HashSet::new();
for row in rows {
if let Some(id) = views::text(row, id_key)
&& !seen.insert(id.to_owned())
{
return Err(scm::Error::new(scm::ErrorKind::Pagination));
}
}
Ok(())
}
pub fn unique_rows<T: serde::Serialize>(
items: &[T],
id_key: &str,
) -> Result<Vec<OValue>, scm::Error> {
let rows = rows_of(items)?;
check_unique(&rows, id_key)?;
Ok(rows)
}
pub fn sanitize_all(rows: &mut [OValue], secrets: &SecretSet) {
for row in rows.iter_mut() {
views::strip_api_keys(row, secrets);
}
for row in rows.iter_mut() {
views::withhold(row, secrets);
}
}
pub fn walk_status(
completion: scm::pagination::Completion,
returned: usize,
max: u64,
) -> Result<String, scm::Error> {
let end = pagination::walk_end(completion, max).ok_or_else(pagination::incomplete_walk)?;
Ok(pagination::all_status(returned, end, max))
}
#[cfg(test)]
mod tests {
use super::*;
fn row(text: &str) -> OValue {
crate::json::parse(text).unwrap()
}
#[test]
fn a_repeated_identity_is_the_pagination_error() {
let rows = [row(r#"{"id":"a"}"#), row(r#"{"id":"b"}"#), row("{}")];
assert!(check_unique(&rows, "id").is_ok());
let repeated = [row(r#"{"id":"a"}"#), row(r#"{"id":"a"}"#)];
assert_eq!(
check_unique(&repeated, "id").unwrap_err().kind(),
scm::ErrorKind::Pagination
);
}
#[test]
fn a_secret_seen_in_one_row_is_withheld_in_every_row() {
let secrets = SecretSet::default();
let mut rows = [
row(r#"{"name":"a FAKE-K9 b"}"#),
row(r#"{"api_key":"FAKE-K9"}"#),
];
sanitize_all(&mut rows, &secrets);
assert_eq!(
crate::json::to_compact(&rows).unwrap(),
r#"[{"name":"[withheld]"},{}]"#
);
}
}