use std::fmt; use itertools::Itertools; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use uuid::Uuid; use crate::cloud_object::ObjectIdType; /// Convert ID enums into and from a hashed UUID. pub trait HashableId: Sized + Send + Sync { fn to_hash(&self) -> String; fn from_hash(hash: &str) -> Option; } #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, schemars::JsonSchema)] #[schemars(description = "A client-generated unique identifier.")] pub struct ClientId(Uuid); impl HashableId for ClientId { fn to_hash(&self) -> String { self.to_string() } fn from_hash(hash: &str) -> Option { hash.strip_prefix("Client-") .and_then(|s| Uuid::parse_str(s).ok()) .map(ClientId) } } impl ClientId { pub fn new() -> ClientId { Self(Uuid::new_v4()) } pub fn sqlite_hash(&self) -> String { self.to_string() } } impl Default for ClientId { fn default() -> Self { ClientId::new() } } impl fmt::Display for ClientId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Client-{}", self.0) } } impl From for ClientId { fn from(s: String) -> Self { ClientId::from_hash(&s).unwrap_or_default() } } /// ID of an object in the sync queue. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, schemars::JsonSchema)] #[schemars(description = "Identifier for a synced object, either local or server-assigned.")] pub enum SyncId { /// Item has not been sync-ed yet. Using a client-created UUID. #[schemars( description = "A locally-generated identifier for an object not yet synced to the server." )] ClientId(ClientId), /// Item has been sync-ed to the cloud. Using the server ID. #[schemars(description = "A server-assigned identifier for a synced object.")] ServerId(ServerId), } impl SyncId { pub fn from_object_id(id: K) -> Self where K: ToServerId, { Self::ServerId(id.to_server_id()) } pub fn uid(&self) -> ObjectUid { match self { Self::ClientId(id) => id.to_string(), Self::ServerId(id) => id.uid(), } } pub fn sqlite_uid_hash(&self, object_id_type: ObjectIdType) -> String { match self { SyncId::ClientId(id) => id.sqlite_hash(), SyncId::ServerId(id) => id.sqlite_type_and_uid_hash(object_id_type), } } /// If this item has been synced to the cloud, extract its server ID. pub fn into_server(self) -> Option { match self { Self::ServerId(id) => Some(id), Self::ClientId(_) => None, } } pub fn into_client(self) -> Option { match self { Self::ServerId(_) => None, Self::ClientId(id) => Some(id), } } } impl settings_value::SettingsValue for SyncId {} impl fmt::Display for SyncId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::ServerId(id) => id.fmt(f), Self::ClientId(id) => id.fmt(f), } } } impl From for SyncId { fn from(id: ServerId) -> SyncId { SyncId::ServerId(id) } } /// Custom serialize function for SyncIds. impl Serialize for SyncId { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { SyncId::ServerId(server_id) => server_id.serialize(serializer), SyncId::ClientId(client_id) => client_id.to_hash().serialize(serializer), } } } /// Custom deserialize function for SyncIds. impl<'de> Deserialize<'de> for SyncId { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s: String = Deserialize::deserialize(deserializer)?; // We try to deserialize as a ClientID, which only succeeds if the ID is prefixed with `Client-`. // If that fails, we assume this is a server id and create a server ID. if let Some(hashed) = ClientId::from_hash(s.as_str()) { Ok(SyncId::ClientId(hashed)) } else { Ok(SyncId::ServerId(ServerId::from_string_lossy(s))) } } } /// Length of the ServerId, should be in sync with the length picked for the server. const SERVER_ID_LENGTH: usize = 22; /// ServerId is a representation of a string-based unique ID we generate on the server, /// of length SERVER_ID_LENGTH. /// Because it's of fixed length, it can implement the Copy trait /// (in contrast to simply using a String type). #[derive(Clone, Copy, Default, Hash, PartialEq, Eq, schemars::JsonSchema)] #[schemars(description = "A server-assigned unique identifier.")] pub struct ServerId([char; SERVER_ID_LENGTH]); /// For server IDs, this is the value that is stored /// in the database. For client IDs, it is of the form "Client-{id}". /// Used to index into cloud model and in most object read, write, and metadata /// mutation server APIs. pub type ObjectUid = String; /// Corresponds to what is stored for a given object id within the local sqlite /// database. Needed for backwards compatibility of the sqlite db following a refactor /// that stripped the object type away from SyncID. /// /// Of the format {sqlite_prefix}-{uid}. /// /// Other than sqlite model events, this id is used for embedded objects within notebooks. pub type HashedSqliteId = String; /// UID for API keys. pub type ApiKeyUid = String; #[derive(Debug, thiserror::Error)] pub enum ParseServerIdError { #[error("ServerId must be exactly {SERVER_ID_LENGTH} characters, got {len}")] InvalidLength { len: usize }, } /// Removes the prefix from sqlite IDs to extract the UIDs. Should not be used unless there /// is not other way to cleanly do the conversion, i.e., when we don't know the ID type. #[allow(clippy::result_unit_err)] pub fn parse_sqlite_id_to_uid(hashed_sqlite_id: HashedSqliteId) -> Result { let Some(uid) = hashed_sqlite_id.split("-").last() else { return Err(()); }; Ok(uid.to_owned()) } impl ServerId { /// Convert a string input to a server ID. If the string is not exactly /// [`SERVER_ID_LENGTH`] characters long, it will be truncated or padded as /// necessary. pub fn from_string_lossy(id: impl AsRef) -> Self { let id = id.as_ref(); Self::try_from(id).unwrap_or_else(|err| { if cfg!(debug_assertions) { panic!("{err}"); } // ServerIds need to be exactly 22 characters, so to prevent a crash, we'll normalize // the string. Nothing that uses it will work, but it's better than crashing. let normalized = Self::normalize_id_str(id, 0); Self::try_from(normalized).expect("id should convert") }) } /// Normalizes a string to be exactly 22 characters long. fn normalize_id_str(input: &str, prefix_length: usize) -> String { let available_len = SERVER_ID_LENGTH - prefix_length; let truncated = if input.len() > available_len { &input[input.len() - available_len..] } else { input }; format!("{truncated:0>available_len$}") } pub fn uid(&self) -> ObjectUid { (*self).into() } /// We need this API for backwards compatibility with local sqlite data. /// In sqlite, objects are stored in object typy, uid pairs of the format /// {sqlite-prefix}-{uid}. For example, for a workflow this would be /// "Workflow-{uid}". pub fn sqlite_type_and_uid_hash(&self, object_id_type: ObjectIdType) -> HashedSqliteId { format!("{}-{}", object_id_type.sqlite_prefix(), self) } } impl TryFrom<&str> for ServerId { type Error = ParseServerIdError; fn try_from(s: &str) -> Result { match s.chars().collect_array() { Some(chars) => Ok(Self(chars)), None => Err(ParseServerIdError::InvalidLength { len: s.chars().count(), }), } } } impl TryFrom for ServerId { type Error = ParseServerIdError; fn try_from(id: String) -> Result { Self::try_from(id.as_str()) } } /// Creates a conversion between an i64 and a corresponding deterministic ServerId for use in tests. /// An i64 like 123 will be converted to "test_uid00000000000123". #[cfg(any(test, feature = "test-util"))] impl From for ServerId { fn from(id: i64) -> Self { let prefix = "test_uid"; let id_str = id.abs().to_string(); let normalized = format!( "{}{}", prefix, Self::normalize_id_str(&id_str, prefix.len()) ); Self::try_from(normalized).expect("normalized string should always be valid") } } impl From for String { fn from(id: ServerId) -> String { String::from_iter(id.0) } } /// We need our own implementation of serialize, due to ServerId being essentially a char array. /// The default serializer in this case would spit a string that looks like an array, instead of a /// nicely formatted string that we want. This implementation would serialize ServerId('a', 'b') to /// "ab" instead. impl Serialize for ServerId { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let s: String = (*self).into(); serializer.serialize_str(&s) } } impl<'de> Deserialize<'de> for ServerId { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s: String = Deserialize::deserialize(deserializer)?; ServerId::try_from(s.as_str()).map_err(serde::de::Error::custom) } } impl std::fmt::Display for ServerId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { use std::fmt::Write; for ch in self.0.iter() { f.write_char(*ch)?; } Ok(()) } } impl std::fmt::Debug for ServerId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { write!(f, "ServerId({self})") } } pub trait ToServerId { fn to_server_id(&self) -> ServerId; } #[derive(Clone, Debug, PartialEq)] pub struct ServerIdAndType { pub id: ServerId, pub id_type: ObjectIdType, } impl ServerIdAndType { pub fn sqlite_type_and_uid_hash(&self) -> HashedSqliteId { self.id.sqlite_type_and_uid_hash(self.id_type) } } /// string_id_traits is a macro used for generating implementations for the type aliases on /// ServerId, implements different To/From and Display, and HashableId traits. /// Takes type and desired prefix for HashableId. #[macro_export] macro_rules! server_id_traits { ($t:ty, $prefix:literal) => { #[cfg(any(test, feature = "test-util"))] impl From for $t { fn from(id: i64) -> Self { Self(id.into()) } } impl From for $t { fn from(id: String) -> Self { Self($crate::ids::ServerId::from_string_lossy(id)) } } impl From<$t> for String { fn from(id: $t) -> String { id.0.into() } } impl std::fmt::Display for $t { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { write!(f, "{}", self.0) } } impl From<$t> for $crate::ids::ServerId { fn from(id: $t) -> Self { id.0 } } impl $crate::ids::HashableId for $t { fn to_hash(&self) -> String { format!("{}-{}", $prefix, self) } fn from_hash(hash: &str) -> Option<$t> { hash.strip_prefix(&format!("{}-", $prefix)) .map(|s| s.to_string().into()) } } impl From<$crate::ids::ServerId> for $t { fn from(id: $crate::ids::ServerId) -> Self { Self(id) } } impl $crate::ids::ToServerId for $t { fn to_server_id(&self) -> $crate::ids::ServerId { self.0 } } }; } #[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Default)] pub struct FolderId(ServerId); server_id_traits! { FolderId, "Folder" } impl From for SyncId { fn from(id: FolderId) -> Self { Self::ServerId(id.into()) } } /// Object ID type that is common for all generic string objects. #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct GenericStringObjectId(ServerId); crate::server_id_traits! { GenericStringObjectId, "GenericStringObject" } impl From for SyncId { fn from(id: GenericStringObjectId) -> Self { Self::ServerId(id.into()) } } impl GenericStringObjectId { pub fn uid(&self) -> ObjectUid { self.0.uid() } }