use haematite_query::page::{Origin, Resume};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use super::{Binding, Seal, codec};
use crate::serve::contracts::identifier;
use crate::serve::protocol::Version;
use crate::serve::storage::Failure;
const FORMAT: u32 = 1;
#[derive(Debug)]
pub struct Continuation {
pub branch: String,
pub version: Version,
pub resume: Resume,
pub now: Box<RawValue>,
binding: Binding,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Cursor {
records_read: u64,
pending: Origin,
discard_outputs: u64,
passed: bool,
#[serde(default)]
passed_counts: Vec<u64>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
format: u32,
branch: String,
version: Version,
resume: Cursor,
now: Box<RawValue>,
binding: Binding,
}
#[derive(Serialize)]
struct CursorView<'a> {
records_read: u64,
pending: &'a Origin,
discard_outputs: u64,
passed: bool,
#[serde(skip_serializing_if = "<[u64]>::is_empty")]
passed_counts: &'a [u64],
}
#[derive(Serialize)]
struct WireView<'a> {
format: u32,
branch: &'a str,
version: Version,
resume: CursorView<'a>,
now: &'a RawValue,
binding: &'a Binding,
}
impl Continuation {
pub fn new(
branch: String,
version: Version,
resume: Resume,
now: Box<RawValue>,
binding: Binding,
) -> Result<Self, Failure> {
let token = Self {
branch,
version,
resume,
now,
binding,
};
token.validate()?;
Ok(token)
}
fn validate(&self) -> Result<(), Failure> {
identifier(&self.branch).map_err(|_| codec::invalid())?;
if (self.resume.records_read == 0 && !self.resume.passed)
|| self
.resume
.pending
.score
.is_some_and(|score| !score.is_finite())
{
return Err(codec::invalid());
}
self.binding.validate()
}
pub fn encode(&self, seal: &Seal) -> Result<String, Failure> {
seal.seal(&self.plain()?)
}
pub fn decode(encoded: &str, seal: &Seal) -> Result<Self, Failure> {
Self::from_plain(&seal.open(encoded)?)
}
pub fn plain(&self) -> Result<Vec<u8>, Failure> {
self.validate()?;
let wire = WireView {
format: FORMAT,
branch: &self.branch,
version: self.version,
resume: CursorView {
records_read: self.resume.records_read,
pending: &self.resume.pending,
discard_outputs: self.resume.discard_outputs,
passed: self.resume.passed,
passed_counts: &self.resume.passed_counts,
},
now: &self.now,
binding: &self.binding,
};
serde_json::to_vec(&wire).map_err(|_| codec::invalid())
}
pub fn from_plain(bytes: &[u8]) -> Result<Self, Failure> {
let wire: Wire = serde_json::from_slice(bytes).map_err(|_| codec::invalid())?;
if wire.format != FORMAT {
return Err(Failure::new(
"query_continuation_format",
"continuation format is unsupported",
));
}
let token = Self::new(
wire.branch,
wire.version,
Resume {
records_read: wire.resume.records_read,
pending: wire.resume.pending,
discard_outputs: wire.resume.discard_outputs,
passed: wire.resume.passed,
passed_counts: wire.resume.passed_counts,
},
wire.now,
wire.binding,
)?;
if token.plain()? != bytes {
return Err(codec::invalid());
}
Ok(token)
}
pub fn check_binding(&self, expected: &Binding) -> Result<(), Failure> {
self.binding.check(expected)
}
pub fn check_moment(&self, branch: &str, version: Option<Version>) -> Result<(), Failure> {
if self.branch != branch || version.is_some_and(|version| version != self.version) {
return Err(Failure::new(
"query_continuation_moment",
"continuation is bound to a different branch or captured version",
));
}
Ok(())
}
}