use onetaskgraph_plugin_api::{SourceError, SourceName};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::engine::{Owed, Resumption, StreamState};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct QueryResponse<T> {
pub items: Vec<T>,
pub next: Option<PageToken>,
pub plan: QueryPlan,
pub errors: Vec<SourceFailure>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
pub struct QueryPlan {
pub per_source: Vec<SourcePlan>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct SourcePlan {
pub source: SourceName,
pub kind: String,
pub pushed_down: Vec<Predicate>,
pub applied_locally: Vec<Predicate>,
pub emulated: Vec<Predicate>,
pub unavailable: Vec<Predicate>,
pub pages_fetched: u32,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum Predicate {
Label,
Status,
SearchTitle,
SearchContent,
Project,
ReverseDependencies,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct SourceFailure {
pub source: SourceName,
pub error: SourceError,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(try_from = "String", into = "String")]
pub struct PageToken(String);
impl PageToken {
pub(crate) fn encode(query: &str, owed: Option<Owed>, streams: &[StreamState]) -> Self {
let document = serde_json::to_string(&Resumption {
query: query.to_owned(),
owed,
streams: streams.to_vec(),
})
.expect("a resumption is plain data and always serialises");
Self(to_hex(&document))
}
pub fn parse(raw: impl Into<String>) -> Result<Self, SourceError> {
let token = Self(raw.into());
token.resumption()?;
Ok(token)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub(crate) fn decode(&self) -> Resumption {
self.resumption()
.expect("every way to build a PageToken validates it")
}
fn resumption(&self) -> Result<Resumption, SourceError> {
let document = from_hex(&self.0).ok_or_else(|| SourceError::Malformed {
message: "that is not a page token this engine writes: it is not even hex".to_owned(),
})?;
let resumption: Resumption =
serde_json::from_str(&document).map_err(|error| SourceError::Malformed {
message: format!("that is not a page token this engine writes: {error}"),
})?;
let streams = &resumption.streams;
if streams.is_empty() {
return Err(SourceError::Malformed {
message: "that is not a page token this engine writes: it resumes nothing"
.to_owned(),
});
}
Ok(resumption)
}
}
impl TryFrom<String> for PageToken {
type Error = SourceError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl From<PageToken> for String {
fn from(value: PageToken) -> Self {
value.0
}
}
impl std::fmt::Display for PageToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
fn to_hex(document: &str) -> String {
let mut rendered = String::with_capacity(document.len() * 2);
for byte in document.as_bytes() {
rendered.push(nibble(byte >> 4));
rendered.push(nibble(byte & 0x0f));
}
rendered
}
fn nibble(value: u8) -> char {
char::from_digit(u32::from(value), 16).expect("a nibble is a hex digit")
}
fn from_hex(raw: &str) -> Option<String> {
if !raw.len().is_multiple_of(2) {
return None;
}
let digits: Vec<u8> = raw
.chars()
.map(|digit| digit.to_digit(16))
.collect::<Option<Vec<u32>>>()?
.into_iter()
.map(|digit| u8::try_from(digit).expect("a hex digit fits in a byte"))
.collect();
let bytes: Vec<u8> = digits
.chunks(2)
.map(|pair| (pair[0] << 4) | pair[1])
.collect();
String::from_utf8(bytes).ok()
}