haematite_cli/serve/query/
continuation.rs1use haematite_query::page::{Origin, Resume};
2use serde::{Deserialize, Serialize};
3use serde_json::value::RawValue;
4
5use super::{Binding, Seal, codec};
6use crate::serve::contracts::identifier;
7use crate::serve::protocol::Version;
8use crate::serve::storage::Failure;
9
10const FORMAT: u32 = 1;
11
12#[derive(Debug)]
13pub struct Continuation {
14 pub branch: String,
15 pub version: Version,
16 pub resume: Resume,
17 pub now: Box<RawValue>,
19 binding: Binding,
20}
21
22#[derive(Deserialize)]
23#[serde(deny_unknown_fields)]
24struct Cursor {
25 records_read: u64,
26 pending: Origin,
27 discard_outputs: u64,
28 passed: bool,
29 #[serde(default)]
30 passed_counts: Vec<u64>,
31}
32
33#[derive(Deserialize)]
34#[serde(deny_unknown_fields)]
35struct Wire {
36 format: u32,
37 branch: String,
38 version: Version,
39 resume: Cursor,
40 now: Box<RawValue>,
41 binding: Binding,
42}
43
44#[derive(Serialize)]
45struct CursorView<'a> {
46 records_read: u64,
47 pending: &'a Origin,
48 discard_outputs: u64,
49 passed: bool,
50 #[serde(skip_serializing_if = "<[u64]>::is_empty")]
51 passed_counts: &'a [u64],
52}
53
54#[derive(Serialize)]
55struct WireView<'a> {
56 format: u32,
57 branch: &'a str,
58 version: Version,
59 resume: CursorView<'a>,
60 now: &'a RawValue,
61 binding: &'a Binding,
62}
63
64impl Continuation {
65 pub fn new(
70 branch: String,
71 version: Version,
72 resume: Resume,
73 now: Box<RawValue>,
74 binding: Binding,
75 ) -> Result<Self, Failure> {
76 let token = Self {
77 branch,
78 version,
79 resume,
80 now,
81 binding,
82 };
83 token.validate()?;
84 Ok(token)
85 }
86
87 fn validate(&self) -> Result<(), Failure> {
88 identifier(&self.branch).map_err(|_| codec::invalid())?;
89 if (self.resume.records_read == 0 && !self.resume.passed)
92 || self
93 .resume
94 .pending
95 .score
96 .is_some_and(|score| !score.is_finite())
97 {
98 return Err(codec::invalid());
99 }
100 self.binding.validate()
101 }
102
103 pub fn encode(&self, seal: &Seal) -> Result<String, Failure> {
110 seal.seal(&self.plain()?)
111 }
112
113 pub fn decode(encoded: &str, seal: &Seal) -> Result<Self, Failure> {
120 Self::from_plain(&seal.open(encoded)?)
121 }
122
123 pub fn plain(&self) -> Result<Vec<u8>, Failure> {
129 self.validate()?;
130 let wire = WireView {
131 format: FORMAT,
132 branch: &self.branch,
133 version: self.version,
134 resume: CursorView {
135 records_read: self.resume.records_read,
136 pending: &self.resume.pending,
137 discard_outputs: self.resume.discard_outputs,
138 passed: self.resume.passed,
139 passed_counts: &self.resume.passed_counts,
140 },
141 now: &self.now,
142 binding: &self.binding,
143 };
144 serde_json::to_vec(&wire).map_err(|_| codec::invalid())
145 }
146
147 pub fn from_plain(bytes: &[u8]) -> Result<Self, Failure> {
156 let wire: Wire = serde_json::from_slice(bytes).map_err(|_| codec::invalid())?;
157 if wire.format != FORMAT {
158 return Err(Failure::new(
159 "query_continuation_format",
160 "continuation format is unsupported",
161 ));
162 }
163 let token = Self::new(
164 wire.branch,
165 wire.version,
166 Resume {
167 records_read: wire.resume.records_read,
168 pending: wire.resume.pending,
169 discard_outputs: wire.resume.discard_outputs,
170 passed: wire.resume.passed,
171 passed_counts: wire.resume.passed_counts,
172 },
173 wire.now,
174 wire.binding,
175 )?;
176 if token.plain()? != bytes {
177 return Err(codec::invalid());
178 }
179 Ok(token)
180 }
181
182 pub fn check_binding(&self, expected: &Binding) -> Result<(), Failure> {
187 self.binding.check(expected)
188 }
189
190 pub fn check_moment(&self, branch: &str, version: Option<Version>) -> Result<(), Failure> {
195 if self.branch != branch || version.is_some_and(|version| version != self.version) {
196 return Err(Failure::new(
197 "query_continuation_moment",
198 "continuation is bound to a different branch or captured version",
199 ));
200 }
201 Ok(())
202 }
203}