use super::models::RedTeamPagination;
use crate::{Error, ErrorKind, Result, data::Patch, pagination::Page};
pub(super) const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
pub(super) fn current_query(
operation: &str,
query: Vec<(String, String)>,
) -> Result<Vec<(String, String)>> {
use serde::Deserialize;
use serde_json::{Map, Value};
use std::{collections::BTreeMap, sync::OnceLock};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Contract {
query_kinds: BTreeMap<String, String>,
query_schema: String,
}
static CONTRACTS: OnceLock<std::result::Result<BTreeMap<String, Contract>, ()>> =
OnceLock::new();
let contracts = CONTRACTS
.get_or_init(|| serde_json::from_str(include_str!("contract.json")).map_err(|_| ()))
.as_ref()
.map_err(|_| Error::new(ErrorKind::Configuration))?;
let contract = contracts
.get(operation)
.ok_or_else(|| Error::new(ErrorKind::Configuration))?;
let mut values = Map::new();
for (key, value) in &query {
let value = match contract.query_kinds.get(key).map(String::as_str) {
Some("integer") => Value::from(
value
.parse::<u64>()
.map_err(|_| Error::new(ErrorKind::Validation))?,
),
Some("boolean") => match value.as_str() {
"true" => Value::Bool(true),
"false" => Value::Bool(false),
_ => return Err(Error::new(ErrorKind::Validation)),
},
_ => Value::String(value.clone()),
};
if values.insert(key.clone(), value).is_some() {
return Err(Error::new(ErrorKind::Validation));
}
}
crate::validation::parse(
&contract.query_schema,
Value::Object(values),
crate::validation::Mode::Request,
)?;
Ok(query)
}
#[derive(Clone, Default)]
#[non_exhaustive]
pub struct RedTeamListOptions {
pub skip: Option<u64>,
pub limit: Option<u32>,
pub search: Option<String>,
}
impl RedTeamListOptions {
pub(super) fn query(&self) -> Result<Vec<(String, String)>> {
listing(self.skip, self.limit, self.search.as_deref())
}
}
pub(super) fn listing(
skip: Option<u64>,
limit: Option<u32>,
search: Option<&str>,
) -> Result<Vec<(String, String)>> {
if skip.is_some_and(|value| value > MAX_SAFE_INTEGER) || limit == Some(0) {
return Err(Error::new(ErrorKind::Validation));
}
let mut values = Vec::new();
if let Some(value) = skip {
values.push(("skip".into(), value.to_string()));
}
if let Some(value) = limit {
values.push(("limit".into(), value.to_string()));
}
push_filter(&mut values, "search", search)?;
Ok(values)
}
pub(super) fn filter(value: &str) -> Result<()> {
if value.len() > 4096 || value.chars().any(char::is_control) {
Err(Error::new(ErrorKind::Validation))
} else {
Ok(())
}
}
pub(super) fn push_filter(
values: &mut Vec<(String, String)>,
key: &str,
value: Option<&str>,
) -> Result<()> {
if let Some(value) = value {
filter(value)?;
values.push((key.into(), value.into()));
}
Ok(())
}
pub(super) fn uuid(value: &str) -> Result<()> {
if crate::data::is_uuid(value) {
Ok(())
} else {
Err(Error::new(ErrorKind::Validation))
}
}
pub(super) fn skip_page<T>(
items: Vec<T>,
pagination: RedTeamPagination,
skip: u64,
limit: u32,
) -> Result<Page<T, u64>> {
if items.is_empty() {
return Ok(Page { items, next: None });
}
let next = skip
.checked_add(u64::try_from(items.len()).map_err(|_| Error::new(ErrorKind::Pagination))?)
.filter(|value| *value <= MAX_SAFE_INTEGER)
.ok_or_else(|| Error::new(ErrorKind::Pagination))?;
let more = match pagination.total_items {
Patch::Unset | Patch::Null => items.len() == limit as usize,
Patch::Value(total) => {
let total = total
.as_f64()
.ok_or_else(|| Error::new(ErrorKind::Pagination))?;
if total < 0.0 || total.fract() != 0.0 || total > MAX_SAFE_INTEGER as f64 {
return Err(Error::new(ErrorKind::Pagination));
}
(next as f64) < total
}
};
Ok(Page {
items,
next: more.then_some(next),
})
}
crate::redaction::redacted_debug!(RedTeamListOptions);