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 }