Skip to main content

haematite_cli/serve/query/
continuation.rs

1use 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    /// Later pages use this value; only the first page reads the clock.
18    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    /// Construct and validate a captured query continuation.
66    ///
67    /// # Errors
68    /// Returns `query_continuation_invalid` for an invalid branch, cursor or score, or propagates an invalid binding.
69    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        // A page that read only records the caller may not open passes a
90        // place having given none.
91        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    /// The token sealed by `seal`: nothing in it can be read or changed by
104    /// whoever holds it.
105    ///
106    /// # Errors
107    ///
108    /// As [`Self::plain`], and when the token cannot be sealed.
109    pub fn encode(&self, seal: &Seal) -> Result<String, Failure> {
110        seal.seal(&self.plain()?)
111    }
112
113    /// The token opened by `seal`, then checked as [`Self::from_plain`].
114    ///
115    /// # Errors
116    ///
117    /// `query_continuation_invalid` for a token `seal` did not seal or that
118    /// was altered; then every refusal of [`Self::from_plain`].
119    pub fn decode(encoded: &str, seal: &Seal) -> Result<Self, Failure> {
120        Self::from_plain(&seal.open(encoded)?)
121    }
122
123    /// The token's own canonical bytes, before sealing.
124    ///
125    /// # Errors
126    ///
127    /// When the token is not valid.
128    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    /// The token whose canonical bytes are `bytes`, opened and not yet checked
148    /// against a request.
149    ///
150    /// # Errors
151    ///
152    /// `query_continuation_format` for another format, and
153    /// `query_continuation_invalid` for bytes that are not a valid token in
154    /// their canonical form.
155    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    /// Check the continuation against the next page's request binding.
183    ///
184    /// # Errors
185    /// Propagates invalid or changed binding component refusals.
186    pub fn check_binding(&self, expected: &Binding) -> Result<(), Failure> {
187        self.binding.check(expected)
188    }
189
190    /// Check the requested branch and optional version against the captured moment.
191    ///
192    /// # Errors
193    /// Returns `query_continuation_moment` if either differs from the token.
194    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}