Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
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<Self>;
|
||||
}
|
||||
|
||||
#[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<ClientId> {
|
||||
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<String> 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<K>(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<ServerId> {
|
||||
match self {
|
||||
Self::ServerId(id) => Some(id),
|
||||
Self::ClientId(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_client(self) -> Option<ClientId> {
|
||||
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<ServerId> for SyncId {
|
||||
fn from(id: ServerId) -> SyncId {
|
||||
SyncId::ServerId(id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom serialize function for SyncIds.
|
||||
impl Serialize for SyncId {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<ObjectUid, ()> {
|
||||
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<str>) -> 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<Self, Self::Error> {
|
||||
match s.chars().collect_array() {
|
||||
Some(chars) => Ok(Self(chars)),
|
||||
None => Err(ParseServerIdError::InvalidLength {
|
||||
len: s.chars().count(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ServerId {
|
||||
type Error = ParseServerIdError;
|
||||
|
||||
fn try_from(id: String) -> Result<Self, Self::Error> {
|
||||
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<i64> 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<ServerId> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let s: String = (*self).into();
|
||||
serializer.serialize_str(&s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ServerId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<i64> for $t {
|
||||
fn from(id: i64) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> 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<FolderId> for SyncId {
|
||||
fn from(id: FolderId) -> Self {
|
||||
Self::ServerId(id.into())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user