app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

availability_query.rs (7288B)


      1 //! Pure structural scopes and bounded owned local availability queries.
      2 //!
      3 //! These values need no signing capability and perform no query or external
      4 //! effect. Runtime admission and the actual validated storage snapshot remain
      5 //! the responsibility of the later local query adapter.
      6 
      7 use std::fmt;
      8 
      9 use harvestcircle_domain::{
     10     AvailabilityOrderKey, AvailabilityPage, AvailabilityPageContinuation, AvailabilityPageCursor,
     11     AvailabilityPageLimit, AvailabilityQueryContext, AvailabilityQueryError,
     12     AvailabilityQueryFilters, AvailabilityQueryFingerprint, PublicKey,
     13 };
     14 
     15 use crate::SessionGeneration;
     16 
     17 /// Caller-supplied local owner/context/session bindings without admission proof.
     18 #[derive(Clone, Copy, Eq, PartialEq)]
     19 pub struct AvailabilityLocalQueryScope {
     20     owner: PublicKey,
     21     context: AvailabilityQueryContext,
     22     session_generation: SessionGeneration,
     23 }
     24 
     25 impl AvailabilityLocalQueryScope {
     26     #[must_use]
     27     pub const fn new(
     28         owner: PublicKey,
     29         context: AvailabilityQueryContext,
     30         session_generation: SessionGeneration,
     31     ) -> Self {
     32         Self {
     33             owner,
     34             context,
     35             session_generation,
     36         }
     37     }
     38 
     39     #[must_use]
     40     pub const fn owner(&self) -> PublicKey {
     41         self.owner
     42     }
     43 
     44     #[must_use]
     45     pub const fn context(&self) -> &AvailabilityQueryContext {
     46         &self.context
     47     }
     48 
     49     #[must_use]
     50     pub const fn session_generation(&self) -> SessionGeneration {
     51         self.session_generation
     52     }
     53 
     54     /// Compares structural bindings without admitting an account or storage.
     55     ///
     56     /// # Errors
     57     ///
     58     /// Returns `ScopeMismatch` for a different owner/context identity and
     59     /// `StaleQuery` for a different store/source/projection/session binding.
     60     pub fn validate_current(&self, current: &Self) -> Result<(), AvailabilityQueryError> {
     61         let original_context = self.context();
     62         let current_context = current.context();
     63         if self.owner() != current.owner()
     64             || original_context.context_id() != current_context.context_id()
     65         {
     66             return Err(AvailabilityQueryError::ScopeMismatch);
     67         }
     68         if original_context.store_generation() != current_context.store_generation()
     69             || original_context.source_revision() != current_context.source_revision()
     70             || original_context.projection_generation() != current_context.projection_generation()
     71             || self.session_generation() != current.session_generation()
     72         {
     73             return Err(AvailabilityQueryError::StaleQuery);
     74         }
     75         Ok(())
     76     }
     77 }
     78 
     79 impl fmt::Debug for AvailabilityLocalQueryScope {
     80     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     81         formatter
     82             .debug_struct("AvailabilityLocalQueryScope")
     83             .field("source_revision", &self.context.source_revision())
     84             .field(
     85                 "projection_generation",
     86                 &self.context.projection_generation(),
     87             )
     88             .field("session_generation", &self.session_generation.value())
     89             .finish_non_exhaustive()
     90     }
     91 }
     92 
     93 /// Immutable complete request with an independently validated public cursor.
     94 #[derive(Clone, Eq, PartialEq)]
     95 pub struct ScopedAvailabilityQuery {
     96     scope: AvailabilityLocalQueryScope,
     97     filters: AvailabilityQueryFilters,
     98     limit: AvailabilityPageLimit,
     99     cursor: Option<AvailabilityPageCursor>,
    100     fingerprint: AvailabilityQueryFingerprint,
    101 }
    102 
    103 impl ScopedAvailabilityQuery {
    104     /// Binds the complete request and validates borrowed cursor bytes before
    105     /// retaining their owned representation.
    106     ///
    107     /// # Errors
    108     ///
    109     /// Returns the static cursor-contract error for an oversized, malformed or
    110     /// different-request cursor. This does not admit the structural local scope.
    111     pub fn new(
    112         scope: AvailabilityLocalQueryScope,
    113         filters: AvailabilityQueryFilters,
    114         limit: AvailabilityPageLimit,
    115         cursor: Option<&str>,
    116     ) -> Result<Self, AvailabilityQueryError> {
    117         let fingerprint = AvailabilityQueryFingerprint::new(
    118             scope.owner(),
    119             scope.context(),
    120             scope.session_generation().value(),
    121             &filters,
    122             limit,
    123         );
    124         let cursor = cursor
    125             .map(|value| AvailabilityPageCursor::parse(value, fingerprint))
    126             .transpose()?;
    127         Ok(Self {
    128             scope,
    129             filters,
    130             limit,
    131             cursor,
    132             fingerprint,
    133         })
    134     }
    135 
    136     #[must_use]
    137     pub const fn scope(&self) -> &AvailabilityLocalQueryScope {
    138         &self.scope
    139     }
    140 
    141     #[must_use]
    142     pub const fn filters(&self) -> &AvailabilityQueryFilters {
    143         &self.filters
    144     }
    145 
    146     #[must_use]
    147     pub const fn limit(&self) -> AvailabilityPageLimit {
    148         self.limit
    149     }
    150 
    151     #[must_use]
    152     pub const fn cursor(&self) -> Option<&AvailabilityPageCursor> {
    153         self.cursor.as_ref()
    154     }
    155 
    156     #[must_use]
    157     pub const fn fingerprint(&self) -> AvailabilityQueryFingerprint {
    158         self.fingerprint
    159     }
    160 
    161     /// Compares the caller's structural bindings without checking installed
    162     /// accounts, signing state, storage admission or performing a query.
    163     ///
    164     /// # Errors
    165     ///
    166     /// Returns `ScopeMismatch` for a different owner/context identity and
    167     /// `StaleQuery` for a different store/source/projection/session binding.
    168     pub fn validate_scope(
    169         &self,
    170         current: &AvailabilityLocalQueryScope,
    171     ) -> Result<(), AvailabilityQueryError> {
    172         self.scope.validate_current(current)
    173     }
    174 
    175     /// Moves bounded rows into a page at this query's exact projection.
    176     ///
    177     /// The caller must supply actual ordered selected rows and the correct last
    178     /// position for its validated snapshot. This method neither scans nor
    179     /// derives a continuation from item payloads. `End` describes only this
    180     /// local query snapshot, without network freshness/completeness guarantees.
    181     ///
    182     /// # Errors
    183     ///
    184     /// Returns `Capacity` if the supplied row count exceeds this request's limit.
    185     pub fn page<T>(
    186         &self,
    187         items: Vec<T>,
    188         next: Option<AvailabilityOrderKey>,
    189     ) -> Result<AvailabilityPage<T>, AvailabilityQueryError> {
    190         if items.len() > usize::from(self.limit.rows()) {
    191             return Err(AvailabilityQueryError::Capacity);
    192         }
    193         let continuation = match next {
    194             None => AvailabilityPageContinuation::End,
    195             Some(after) => AvailabilityPageContinuation::More(AvailabilityPageCursor::encode(
    196                 self.fingerprint,
    197                 after,
    198             )),
    199         };
    200         AvailabilityPage::new(
    201             self.limit,
    202             items,
    203             continuation,
    204             self.scope.context().projection_generation(),
    205         )
    206     }
    207 }
    208 
    209 impl fmt::Debug for ScopedAvailabilityQuery {
    210     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    211         formatter
    212             .debug_struct("ScopedAvailabilityQuery")
    213             .field("scope", &self.scope)
    214             .field("filters", &self.filters)
    215             .field("limit", &self.limit)
    216             .field("cursor_present", &self.cursor.is_some())
    217             .finish_non_exhaustive()
    218     }
    219 }