Skip to main content

finance_query/models/
pagination.rs

1//! Resumable provider pages. Cursors contain no credentials.
2
3use crate::{FinanceError, Operation, Provider, Result};
4
5/// Bump whenever a request's cursor identity changes meaning, so older cursors are rejected.
6const CURSOR_VERSION: u8 = 1;
7use serde::{Deserialize, Serialize};
8
9/// One provider response, without collecting subsequent pages.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[non_exhaustive]
12pub struct ProviderPage<T> {
13    /// Records returned on this page.
14    pub items: Vec<T>,
15    /// Persist with the records before requesting the next page.
16    pub next: Option<PageCursor>,
17    /// Provider that owns this page and its continuation.
18    pub provider_id: Provider,
19    /// Provider request identifier.
20    pub request_id: Option<String>,
21    /// Provider-reported result count, when supplied.
22    pub results_count: Option<usize>,
23    /// Provider query count; this need not equal the number of bars.
24    pub query_count: Option<usize>,
25    /// Symbol reported by the response.
26    pub reported_symbol: Option<String>,
27    /// Adjustment reported by the response.
28    pub adjusted: Option<bool>,
29}
30
31impl<T> ProviderPage<T> {
32    /// A page from `provider_id`, with no response metadata. `next` is `None` on
33    /// the last page; otherwise build it with [`PageCursor::continuation`].
34    pub fn new(items: Vec<T>, provider_id: Provider, next: Option<PageCursor>) -> Self {
35        Self {
36            items,
37            next,
38            provider_id,
39            request_id: None,
40            results_count: None,
41            query_count: None,
42            reported_symbol: None,
43            adjusted: None,
44        }
45    }
46}
47
48/// A serializable continuation bound to the original operation and request.
49///
50/// Treat persisted cursors as opaque values. Validation runs again when used.
51#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
52pub struct PageCursor {
53    pub(crate) version: u8,
54    pub(crate) provider: Provider,
55    pub(crate) operation: String,
56    pub(crate) request: String,
57    pub(crate) target: String,
58}
59
60impl std::fmt::Debug for PageCursor {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("PageCursor")
63            .field("version", &self.version)
64            .field("provider", &self.provider)
65            .finish_non_exhaustive()
66    }
67}
68
69impl PageCursor {
70    /// Provider to which this continuation belongs.
71    pub fn provider(&self) -> Provider {
72        self.provider
73    }
74
75    /// A continuation for an adapter to return in [`ProviderPage::next`].
76    ///
77    /// `provider` must be the adapter's own id and `target` is whatever state the
78    /// adapter needs to fetch the next page. The routing layer binds the cursor to
79    /// the operation and request, and checks both before handing it back.
80    pub fn continuation(provider: Provider, target: String) -> Self {
81        Self {
82            version: CURSOR_VERSION,
83            provider,
84            operation: String::new(),
85            request: String::new(),
86            target,
87        }
88    }
89
90    /// The adapter state passed to [`continuation`](Self::continuation).
91    pub fn target(&self) -> &str {
92        &self.target
93    }
94
95    pub(crate) fn bind(&mut self, operation: Operation, request: &str) {
96        self.operation = operation.as_str().into();
97        self.request = request.into();
98    }
99
100    pub(crate) fn validate(&self, operation: Operation, request: &str) -> Result<()> {
101        if self.version != CURSOR_VERSION
102            || self.operation != operation.as_str()
103            || self.request != request
104        {
105            return Err(FinanceError::InvalidParameter {
106                param: "cursor".into(),
107                reason: "cursor version, operation or request does not match".into(),
108            });
109        }
110        Ok(())
111    }
112}