//! Generic, game-scoped **opaque** extension state. //! //! Core stores and versions these bytes and associates them with a canonical //! entity + a server-computed fingerprint, but never interprets them. A game //! adapter owns the payload schema/meaning. This keeps game-only wire round-trip //! state (e.g. a FIFA 17 squad's `custom[]`/`kicktakers`/`kitNumber`) durable and //! atomic with its canonical entity without leaking game concepts into Core. use serde::{Deserialize, Serialize}; /// Generic safety bounds Core enforces without interpreting the payload. pub const MAX_EXT_PAYLOAD_BYTES: usize = 64 * 1024; pub const MAX_EXT_NAMESPACE_LEN: usize = 64; /// An opaque extension payload a game adapter asks Core to persist atomically /// alongside a canonical entity. `payload` is uninterpreted bytes-as-text. #[derive(Debug, Clone, Deserialize)] pub struct OpaqueExtensionWrite { /// Opaque adapter key, e.g. `"fifa17.squad.v1"`. Core treats it as a string. pub namespace: String, /// Adapter's payload schema version (distinct from the DB storage schema). pub schema_version: i64, /// Uninterpreted payload (the adapter's serialized game-only state). pub payload: String, } impl OpaqueExtensionWrite { /// Generic bounds check — namespace non-empty/length, payload size. Semantic /// validation of the payload is the adapter's job; Core only guards size. pub fn validate(&self) -> Result<(), String> { if self.namespace.is_empty() || self.namespace.len() > MAX_EXT_NAMESPACE_LEN { return Err(format!( "namespace length {} out of bounds (1..={MAX_EXT_NAMESPACE_LEN})", self.namespace.len() )); } if self.payload.len() > MAX_EXT_PAYLOAD_BYTES { return Err(format!( "extension payload {} bytes exceeds max {MAX_EXT_PAYLOAD_BYTES}", self.payload.len() )); } Ok(()) } } /// A stored opaque extension row (read side). `canonical_fingerprint` is the /// server-computed fingerprint of the canonical entity at write time; a reader /// compares it against the entity's *current* fingerprint to detect staleness. #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct GameEntityExt { pub game_id: String, pub entity_kind: String, pub entity_id: String, pub namespace: String, pub schema_version: i64, pub canonical_fingerprint: String, pub payload: String, pub updated_at: String, }