first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
pub use user_uid::{TEST_USER_EMAIL, TEST_USER_UID, UserUid};
|
||||
pub use warp_server_auth::user_uid;
|
||||
@@ -0,0 +1,74 @@
|
||||
use warp_graphql::scalars::time::ServerTimestamp;
|
||||
|
||||
use super::{
|
||||
CloudObjectEventEntrypoint, GenericStringObjectFormat, GenericStringObjectUniqueKey, Owner,
|
||||
RevisionAndLastEditor, SerializedModel, ServerPermissions,
|
||||
};
|
||||
use crate::ids::{ClientId, FolderId, ServerIdAndType};
|
||||
|
||||
/// Helper struct that contains all the info needed to create an object on the server.
|
||||
pub struct CreateObjectRequest {
|
||||
pub serialized_model: Option<SerializedModel>,
|
||||
pub title: Option<String>,
|
||||
pub owner: Owner,
|
||||
pub client_id: ClientId,
|
||||
pub initial_folder_id: Option<FolderId>,
|
||||
pub entrypoint: CloudObjectEventEntrypoint,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct BulkCreateGenericStringObjectsRequest {
|
||||
pub id: ClientId,
|
||||
pub format: GenericStringObjectFormat,
|
||||
pub uniqueness_key: Option<GenericStringObjectUniqueKey>,
|
||||
pub serialized_model: SerializedModel,
|
||||
pub initial_folder_id: Option<FolderId>,
|
||||
pub entrypoint: CloudObjectEventEntrypoint,
|
||||
}
|
||||
|
||||
/// The data returned by the server when an object is created, generic to any object type.
|
||||
#[derive(Debug)]
|
||||
pub struct CreatedCloudObject {
|
||||
pub client_id: ClientId,
|
||||
pub revision_and_editor: RevisionAndLastEditor,
|
||||
pub metadata_ts: ServerTimestamp,
|
||||
pub server_id_and_type: ServerIdAndType,
|
||||
pub creator_uid: Option<String>,
|
||||
pub permissions: ServerPermissions,
|
||||
}
|
||||
|
||||
/// Result of attempting to create a cloud object.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum CreateCloudObjectResult {
|
||||
/// The object creation was successful.
|
||||
Success {
|
||||
created_cloud_object: CreatedCloudObject,
|
||||
},
|
||||
/// The object creation was denied due to an expected user error.
|
||||
UserFacingError(String),
|
||||
/// The object creation was rejected because the generic string object had
|
||||
/// already been created by another client.
|
||||
GenericStringObjectUniqueKeyConflict,
|
||||
}
|
||||
|
||||
/// Result of attempting to bulk create a cloud object.
|
||||
#[derive(Debug)]
|
||||
pub enum BulkCreateCloudObjectResult {
|
||||
/// The bulk object creation was successful.
|
||||
Success {
|
||||
created_cloud_objects: Vec<CreatedCloudObject>,
|
||||
},
|
||||
/// The bulk object creation was rejected because at least one generic string object had
|
||||
/// already been created by another client.
|
||||
GenericStringObjectUniqueKeyConflict,
|
||||
}
|
||||
|
||||
/// The creation-specific data returned by the server, which is inserted into CloudModel and persisted
|
||||
/// just once.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct ServerCreationInfo {
|
||||
pub server_id_and_type: ServerIdAndType,
|
||||
pub creator_uid: Option<String>,
|
||||
pub permissions: ServerPermissions,
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{
|
||||
CloudObjectMetadata, CloudObjectPermissions, CloudObjectStatuses, CloudObjectSyncStatus,
|
||||
ConflictStatus, GenericServerObject, NumInFlightRequests, ObjectType, Owner,
|
||||
};
|
||||
use crate::ids::{ClientId, SyncId};
|
||||
|
||||
/// A portable payload for persisting or otherwise upserting a cloud object without app-local event types.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CloudObjectUpsertParams<M> {
|
||||
pub id: SyncId,
|
||||
pub object_type: ObjectType,
|
||||
pub metadata: CloudObjectMetadata,
|
||||
pub permissions: CloudObjectPermissions,
|
||||
pub model: M,
|
||||
}
|
||||
|
||||
/// A generic implementation of cloud objects that can be used for any model and id types.
|
||||
///
|
||||
/// For instance, rather than directly implementing the CloudObject trait, CloudObjects can
|
||||
/// implement GenericCloudObject<K, M> where K is their id type and M is their model type.
|
||||
///
|
||||
/// For example, CloudNotebook becomes:
|
||||
///
|
||||
/// pub type CloudNotebook = GenericCloudObject<NotebookId, CloudNotebookModel>
|
||||
///
|
||||
/// The advantage of using the generic model is you get common implementations
|
||||
/// of CloudObject methods like ```versions``` for free.
|
||||
///
|
||||
/// See the comments for CloudObject to understand the relationship between
|
||||
/// this trait, CloudObject and CloudModelType. They are tightly coupled.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenericCloudObject<K, M> {
|
||||
pub id: SyncId,
|
||||
pub metadata: CloudObjectMetadata,
|
||||
pub permissions: CloudObjectPermissions,
|
||||
/// Tracks whether this object has a conflict with the server version.
|
||||
/// This is runtime state (not persisted) - conflicts are always NoConflicts when loaded from SQLite.
|
||||
pub conflict_status: ConflictStatus<GenericServerObject<K, M>>,
|
||||
|
||||
// Intentionally not public to prevent users of this class from holding
|
||||
// onto references to the model outside of this struct.
|
||||
//
|
||||
// This is an Arc in order to support clone-on-write semantics for the model.
|
||||
// By wrapping the model in an Arc, clones become cheap, and we can avoid
|
||||
// doing deep clones of the model whenever the containing object is cloned.
|
||||
//
|
||||
// Callers who want to update the model need to call set_model to update the
|
||||
// entire model atomically.
|
||||
model: Arc<M>,
|
||||
}
|
||||
|
||||
impl<K, M> PartialEq for GenericCloudObject<K, M>
|
||||
where
|
||||
M: PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id && self.model() == other.model()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, M> GenericCloudObject<K, M> {
|
||||
/// Gets a reference to the model held by the object.
|
||||
pub fn model(&self) -> &M {
|
||||
&self.model
|
||||
}
|
||||
|
||||
/// Returns a shared handle to the model.
|
||||
pub fn shared_model(&self) -> Arc<M> {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
/// Sets a new version of the model on the object, replacing the old version.
|
||||
pub fn set_model(&mut self, model: M) {
|
||||
self.model = model.into();
|
||||
}
|
||||
|
||||
/// Constructs a new instance of this model with the given id, model, metadata and permissions.
|
||||
pub fn new(
|
||||
id: SyncId,
|
||||
model: M,
|
||||
metadata: CloudObjectMetadata,
|
||||
permissions: CloudObjectPermissions,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
model: model.into(),
|
||||
metadata,
|
||||
permissions,
|
||||
conflict_status: ConflictStatus::NoConflicts,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new GenericCloudObject with the given model, owner, and initial folder id.
|
||||
/// This is for the local creation flow, as opposed to creating from a server update.
|
||||
pub fn new_local(
|
||||
model: M,
|
||||
owner: Owner,
|
||||
initial_folder_id: Option<SyncId>,
|
||||
client_id: ClientId,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: SyncId::ClientId(client_id),
|
||||
model: model.into(),
|
||||
metadata: CloudObjectMetadata {
|
||||
pending_changes_statuses: CloudObjectStatuses {
|
||||
content_sync_status: CloudObjectSyncStatus::InFlight(NumInFlightRequests(1)),
|
||||
has_pending_metadata_change: false,
|
||||
has_pending_permissions_change: false,
|
||||
pending_untrash: false,
|
||||
pending_delete: false,
|
||||
},
|
||||
folder_id: initial_folder_id,
|
||||
revision: Default::default(),
|
||||
metadata_last_updated_ts: Default::default(),
|
||||
current_editor_uid: Default::default(),
|
||||
trashed_ts: Default::default(),
|
||||
// Objects created from the client are never welcome objects.
|
||||
is_welcome_object: false,
|
||||
creator_uid: None,
|
||||
last_editor_uid: None,
|
||||
last_task_run_ts: None,
|
||||
},
|
||||
permissions: CloudObjectPermissions {
|
||||
owner,
|
||||
anyone_with_link: None,
|
||||
guests: Default::default(),
|
||||
permissions_last_updated_ts: None,
|
||||
},
|
||||
conflict_status: ConflictStatus::NoConflicts,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`GenericCloudObject`] from a [`GenericServerObject`].
|
||||
pub fn new_from_server(server_object: GenericServerObject<K, M>) -> Self {
|
||||
Self {
|
||||
id: server_object.id,
|
||||
model: server_object.model.into(),
|
||||
metadata: CloudObjectMetadata::new_from_server(server_object.metadata),
|
||||
permissions: CloudObjectPermissions::new_from_server(server_object.permissions),
|
||||
conflict_status: ConflictStatus::NoConflicts,
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks this object as being in conflict with the provided object.
|
||||
pub fn set_conflicting_object(&mut self, object: Arc<GenericServerObject<K, M>>) {
|
||||
self.conflict_status = ConflictStatus::ConflictingChanges { object };
|
||||
}
|
||||
|
||||
pub fn update_from_server_object(&mut self, server_object: GenericServerObject<K, M>) {
|
||||
// Check if we should create a conflict or apply the update.
|
||||
if self.metadata.has_pending_content_changes() || self.conflict_status.has_conflicts() {
|
||||
// There are pending changes, so this creates a conflict.
|
||||
self.conflict_status = ConflictStatus::ConflictingChanges {
|
||||
object: Arc::new(server_object),
|
||||
};
|
||||
} else {
|
||||
// No pending changes, apply the server update.
|
||||
self.metadata
|
||||
.update_revision_from_server(&server_object.metadata);
|
||||
self.model = server_object.model.into();
|
||||
self.conflict_status = ConflictStatus::NoConflicts;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns portable upsert parameters for this object.
|
||||
pub fn upsert_params(&self, object_type: ObjectType) -> CloudObjectUpsertParams<M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
CloudObjectUpsertParams {
|
||||
id: self.id,
|
||||
object_type,
|
||||
metadata: self.metadata.clone(),
|
||||
permissions: self.permissions.clone(),
|
||||
model: self.model().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this object into portable upsert parameters.
|
||||
pub fn into_upsert_params(self, object_type: ObjectType) -> CloudObjectUpsertParams<M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
let Self {
|
||||
id,
|
||||
metadata,
|
||||
permissions,
|
||||
model,
|
||||
conflict_status: _,
|
||||
} = self;
|
||||
let model = Arc::try_unwrap(model).unwrap_or_else(|model| (*model).clone());
|
||||
CloudObjectUpsertParams {
|
||||
id,
|
||||
object_type,
|
||||
metadata,
|
||||
permissions,
|
||||
model,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, M> From<CloudObjectUpsertParams<M>> for GenericCloudObject<K, M> {
|
||||
fn from(params: CloudObjectUpsertParams<M>) -> Self {
|
||||
Self::new(params.id, params.model, params.metadata, params.permissions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use super::{GenericStringObjectFormat, ObjectType, SerializedModel, ServerObjectModel};
|
||||
|
||||
/// A serializer goes from a model to a string and back.
|
||||
pub trait Serializer<M>: Debug + Clone + 'static {
|
||||
fn model_format() -> GenericStringObjectFormat;
|
||||
fn serialize(model: &M) -> SerializedModel;
|
||||
fn deserialize_owned(serialized: &str) -> Result<M>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// A `GenericStringModel` is a generic implementation of model types that can serialize to/from string.
|
||||
/// given a particular serializer.
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct GenericStringModel<M, S>
|
||||
where
|
||||
S: Serializer<M>,
|
||||
{
|
||||
pub string_model: M,
|
||||
_serializer: PhantomData<fn() -> S>,
|
||||
}
|
||||
|
||||
impl<M, S> GenericStringModel<M, S>
|
||||
where
|
||||
S: Serializer<M>,
|
||||
{
|
||||
pub fn deserialize_owned(serialized: &str) -> Result<Self> {
|
||||
S::deserialize_owned(serialized).map(Self::new)
|
||||
}
|
||||
|
||||
pub fn new(model: M) -> Self {
|
||||
Self {
|
||||
string_model: model,
|
||||
_serializer: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_model(&self) -> &M {
|
||||
&self.string_model
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, S> ServerObjectModel for GenericStringModel<M, S>
|
||||
where
|
||||
M: Debug + Clone + Send + Sync + 'static,
|
||||
S: Serializer<M>,
|
||||
{
|
||||
fn object_type(&self) -> ObjectType {
|
||||
ObjectType::GenericStringObject(S::model_format())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
use std::any::Any;
|
||||
use std::fmt::{self, Debug};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{ObjectType, ServerMetadata, ServerPermissions};
|
||||
use crate::ids::SyncId;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum ConflictStatus<T> {
|
||||
#[default]
|
||||
NoConflicts,
|
||||
ConflictingChanges {
|
||||
object: Arc<T>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T> ConflictStatus<T> {
|
||||
/// Utility function that allows for a more ergonomic way of figuring out whether there is a
|
||||
/// conflict (for cases where we don't care about the conflict details).
|
||||
pub fn has_conflicts(&self) -> bool {
|
||||
matches!(self, ConflictStatus::ConflictingChanges { .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// Common behavior that server-backed models expose to generic server objects.
|
||||
pub trait ServerObjectModel: Debug + Clone + Send + Sync + 'static {
|
||||
/// Returns the object type for this model.
|
||||
fn object_type(&self) -> ObjectType;
|
||||
}
|
||||
|
||||
/// Common trait for server objects that allows us to use them as trait objects
|
||||
/// and downcast to concrete types when needed.
|
||||
pub trait ServerObject: Debug + Send + Sync {
|
||||
/// Returns the object type of this server object
|
||||
fn object_type(&self) -> ObjectType;
|
||||
|
||||
/// Returns this object as a ref to the Any type. Needed for typecasts.
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
/// Returns the trait object as a concrete type reference by downcasting it.
|
||||
/// Returns None if the downcast fails.
|
||||
fn as_concrete_type<K, M>(
|
||||
server_object: &dyn ServerObject,
|
||||
) -> Option<&GenericServerObject<K, M>>
|
||||
where
|
||||
Self: Sized,
|
||||
K: 'static,
|
||||
M: 'static,
|
||||
{
|
||||
server_object
|
||||
.as_any()
|
||||
.downcast_ref::<GenericServerObject<K, M>>()
|
||||
}
|
||||
|
||||
/// Returns a cloned boxed version of this server object.
|
||||
/// Note that we can't force the ServerObject trait to derive from Cloned
|
||||
/// directly because that would make the trait not object safe. This
|
||||
/// is a workaround.
|
||||
fn clone_box(&self) -> Box<dyn ServerObject>;
|
||||
}
|
||||
|
||||
/// An object that maps directly to the data returned from the server
|
||||
/// for a given model and id type.
|
||||
pub struct GenericServerObject<K, M> {
|
||||
pub id: SyncId,
|
||||
pub model: M,
|
||||
pub metadata: ServerMetadata,
|
||||
pub permissions: ServerPermissions,
|
||||
_marker: PhantomData<fn() -> K>,
|
||||
}
|
||||
|
||||
impl<K, M> Clone for GenericServerObject<K, M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self::new(
|
||||
self.id,
|
||||
self.model.clone(),
|
||||
self.metadata.clone(),
|
||||
self.permissions.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, M> Debug for GenericServerObject<K, M>
|
||||
where
|
||||
M: Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("GenericServerObject")
|
||||
.field("id", &self.id)
|
||||
.field("model", &self.model)
|
||||
.field("metadata", &self.metadata)
|
||||
.field("permissions", &self.permissions)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, M> GenericServerObject<K, M> {
|
||||
/// Constructs a server object from its server-provided parts.
|
||||
pub fn new(
|
||||
id: SyncId,
|
||||
model: M,
|
||||
metadata: ServerMetadata,
|
||||
permissions: ServerPermissions,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
model,
|
||||
metadata,
|
||||
permissions,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K, M> From<&'a dyn ServerObject> for Option<&'a GenericServerObject<K, M>>
|
||||
where
|
||||
K: 'static,
|
||||
M: 'static,
|
||||
{
|
||||
fn from(value: &'a dyn ServerObject) -> Self {
|
||||
value.as_any().downcast_ref::<GenericServerObject<K, M>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K, M> From<&'a Box<dyn ServerObject>> for Option<&'a GenericServerObject<K, M>>
|
||||
where
|
||||
K: 'static,
|
||||
M: 'static,
|
||||
{
|
||||
fn from(value: &'a Box<dyn ServerObject>) -> Self {
|
||||
value.as_ref().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, M> ServerObject for GenericServerObject<K, M>
|
||||
where
|
||||
K: 'static,
|
||||
M: ServerObjectModel,
|
||||
{
|
||||
fn object_type(&self) -> ObjectType {
|
||||
self.model.object_type()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn ServerObject> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use warp_graphql::queries::get_updated_cloud_objects::UpdatedObjectInput;
|
||||
|
||||
use super::RevisionAndLastEditor;
|
||||
|
||||
/// Result of attempting to update a cloud object.
|
||||
#[derive(Debug)]
|
||||
pub enum UpdateCloudObjectResult<T> {
|
||||
/// The update was successful and the object now has the specified revision.
|
||||
Success {
|
||||
revision_and_editor: RevisionAndLastEditor,
|
||||
},
|
||||
/// The update was rejected because the update was not sent from the current revision in
|
||||
/// storage. The object and revision in storage are returned.
|
||||
Rejected { object: T },
|
||||
}
|
||||
|
||||
/// Helper struct that contains all the info needed to fetch changed objects from the server.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ObjectsToUpdate {
|
||||
pub notebooks: Vec<UpdatedObjectInput>,
|
||||
pub workflows: Vec<UpdatedObjectInput>,
|
||||
pub folders: Vec<UpdatedObjectInput>,
|
||||
pub generic_string_objects: Vec<UpdatedObjectInput>,
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
pub mod sharing;
|
||||
|
||||
use crate::cloud_object::{GenericStringObjectFormat, ObjectIdType, ObjectType};
|
||||
use crate::ids::{HashedSqliteId, ObjectUid, ServerId, SyncId};
|
||||
|
||||
/// Enum to use to pass down type and id between actions to avoid multiplying actions whenever we
|
||||
/// need to pass the object id, etc.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum CloudObjectTypeAndId {
|
||||
Notebook(SyncId),
|
||||
Workflow(SyncId),
|
||||
Folder(SyncId),
|
||||
GenericStringObject {
|
||||
object_type: GenericStringObjectFormat,
|
||||
id: SyncId,
|
||||
},
|
||||
}
|
||||
|
||||
impl CloudObjectTypeAndId {
|
||||
pub fn from_id_and_type(id: SyncId, object_type: ObjectType) -> Self {
|
||||
match object_type {
|
||||
ObjectType::Notebook => Self::Notebook(id),
|
||||
ObjectType::Workflow => Self::Workflow(id),
|
||||
ObjectType::Folder => Self::Folder(id),
|
||||
ObjectType::GenericStringObject(format) => Self::GenericStringObject {
|
||||
object_type: format,
|
||||
id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uid(self) -> ObjectUid {
|
||||
match self {
|
||||
Self::Notebook(id) => id.uid(),
|
||||
Self::Workflow(id) => id.uid(),
|
||||
Self::Folder(id) => id.uid(),
|
||||
Self::GenericStringObject { id, .. } => id.uid(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sync_id(self) -> SyncId {
|
||||
match self {
|
||||
Self::Notebook(id)
|
||||
| Self::Workflow(id)
|
||||
| Self::Folder(id)
|
||||
| Self::GenericStringObject { id, .. } => id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sqlite_uid_hash(self) -> HashedSqliteId {
|
||||
match self {
|
||||
CloudObjectTypeAndId::Notebook(id) => id.sqlite_uid_hash(ObjectIdType::Notebook),
|
||||
CloudObjectTypeAndId::Workflow(id) => id.sqlite_uid_hash(ObjectIdType::Workflow),
|
||||
CloudObjectTypeAndId::Folder(id) => id.sqlite_uid_hash(ObjectIdType::Folder),
|
||||
CloudObjectTypeAndId::GenericStringObject { object_type: _, id } => {
|
||||
id.sqlite_uid_hash(ObjectIdType::GenericStringObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_id_type(&self) -> ObjectIdType {
|
||||
match self {
|
||||
CloudObjectTypeAndId::Notebook(_) => ObjectIdType::Notebook,
|
||||
CloudObjectTypeAndId::Workflow(_) => ObjectIdType::Workflow,
|
||||
CloudObjectTypeAndId::GenericStringObject { .. } => ObjectIdType::GenericStringObject,
|
||||
CloudObjectTypeAndId::Folder(_) => ObjectIdType::Folder,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_type(&self) -> ObjectType {
|
||||
match self {
|
||||
CloudObjectTypeAndId::Notebook(_) => ObjectType::Notebook,
|
||||
CloudObjectTypeAndId::Workflow(_) => ObjectType::Workflow,
|
||||
CloudObjectTypeAndId::Folder(_) => ObjectType::Folder,
|
||||
CloudObjectTypeAndId::GenericStringObject { object_type, .. } => {
|
||||
ObjectType::GenericStringObject(*object_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_folder_id(self) -> Option<SyncId> {
|
||||
match self {
|
||||
CloudObjectTypeAndId::Notebook(_) => None,
|
||||
CloudObjectTypeAndId::Workflow(_) => None,
|
||||
CloudObjectTypeAndId::GenericStringObject { .. } => None,
|
||||
CloudObjectTypeAndId::Folder(f) => Some(f),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_notebook_id(self) -> Option<SyncId> {
|
||||
match self {
|
||||
CloudObjectTypeAndId::Notebook(id) => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_generic_string_object_id(self) -> Option<SyncId> {
|
||||
match self {
|
||||
CloudObjectTypeAndId::GenericStringObject { object_type: _, id } => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_server_id(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
CloudObjectTypeAndId::Notebook(SyncId::ServerId(_))
|
||||
| CloudObjectTypeAndId::Workflow(SyncId::ServerId(_))
|
||||
| CloudObjectTypeAndId::Folder(SyncId::ServerId(_))
|
||||
| CloudObjectTypeAndId::GenericStringObject {
|
||||
id: SyncId::ServerId(_),
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn server_id(self) -> Option<ServerId> {
|
||||
match self {
|
||||
CloudObjectTypeAndId::Notebook(SyncId::ServerId(notebook_id)) => Some(notebook_id),
|
||||
CloudObjectTypeAndId::Workflow(SyncId::ServerId(workflow_id)) => Some(workflow_id),
|
||||
CloudObjectTypeAndId::Folder(SyncId::ServerId(folder_id)) => Some(folder_id),
|
||||
CloudObjectTypeAndId::GenericStringObject {
|
||||
id: SyncId::ServerId(json_object_id),
|
||||
..
|
||||
} => Some(json_object_id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drive_row_position_id(self) -> String {
|
||||
format!("WarpDriveRow_{}", self.uid())
|
||||
}
|
||||
|
||||
pub fn from_generic_string_object(object_type: GenericStringObjectFormat, id: SyncId) -> Self {
|
||||
Self::GenericStringObject { object_type, id }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use galaxy_graphql::object_permissions::AccessLevel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session_sharing_protocol::common::{ProfileData as SessionSharingProfileData, Role};
|
||||
|
||||
use crate::auth::UserUid;
|
||||
use crate::cloud_object::Owner;
|
||||
use crate::ids::ServerId;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum SharingAccessLevel {
|
||||
View,
|
||||
Edit,
|
||||
Full,
|
||||
}
|
||||
|
||||
impl SharingAccessLevel {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
SharingAccessLevel::View => "Can view",
|
||||
SharingAccessLevel::Edit => "Can edit",
|
||||
SharingAccessLevel::Full => "Full access",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
SharingAccessLevel::View => "view",
|
||||
SharingAccessLevel::Edit => "edit",
|
||||
SharingAccessLevel::Full => "access",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not this access level implies the `Trash` action.
|
||||
pub fn can_trash(self) -> bool {
|
||||
self >= SharingAccessLevel::Edit
|
||||
}
|
||||
|
||||
/// Whether or not this access level implies the `DeletePermanently` action.
|
||||
pub fn can_delete(self) -> bool {
|
||||
self >= SharingAccessLevel::Full
|
||||
}
|
||||
|
||||
/// Whether or not this access level implies the `ChangeOwner` action.
|
||||
pub fn can_move_drive(self) -> bool {
|
||||
self >= SharingAccessLevel::Full
|
||||
}
|
||||
|
||||
/// Whether or not this access level implies the `EditAccess` action.
|
||||
pub fn can_edit_access(self) -> bool {
|
||||
self >= SharingAccessLevel::Full
|
||||
}
|
||||
|
||||
/// Convert this access level to a serializable value, which can be parsed by [`FromStr`].
|
||||
pub fn to_serializable_value(self) -> &'static str {
|
||||
match self {
|
||||
SharingAccessLevel::View => "VIEW",
|
||||
SharingAccessLevel::Edit => "EDIT",
|
||||
SharingAccessLevel::Full => "FULL",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SharingAccessLevel {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"VIEW" => Ok(Self::View),
|
||||
"EDIT" => Ok(Self::Edit),
|
||||
"FULL" => Ok(Self::Full),
|
||||
_ => Err(anyhow::anyhow!("unknown access level {value}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AccessLevel> for SharingAccessLevel {
|
||||
fn from(server_access: AccessLevel) -> Self {
|
||||
match server_access {
|
||||
AccessLevel::Viewer => Self::View,
|
||||
AccessLevel::Editor => Self::Edit,
|
||||
AccessLevel::Full => Self::Full,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharingAccessLevel> for AccessLevel {
|
||||
fn from(val: SharingAccessLevel) -> Self {
|
||||
match val {
|
||||
SharingAccessLevel::View => AccessLevel::Viewer,
|
||||
SharingAccessLevel::Edit => AccessLevel::Editor,
|
||||
SharingAccessLevel::Full => AccessLevel::Full,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for SharingAccessLevel {
|
||||
fn from(role: Role) -> Self {
|
||||
match role {
|
||||
Role::Reader => Self::View,
|
||||
Role::Executor => Self::Edit,
|
||||
Role::Full => Self::Full,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharingAccessLevel> for Role {
|
||||
fn from(access_level: SharingAccessLevel) -> Self {
|
||||
match access_level {
|
||||
SharingAccessLevel::View => Self::Reader,
|
||||
SharingAccessLevel::Edit | SharingAccessLevel::Full => Self::Executor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum LinkSharingSubjectType {
|
||||
None,
|
||||
Anyone,
|
||||
}
|
||||
/// A `Subject` is someone with access to a shared object, like its owner or a directly-added
|
||||
/// guest.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Subject {
|
||||
User(UserKind),
|
||||
#[allow(dead_code)]
|
||||
PendingUser {
|
||||
email: Option<String>,
|
||||
},
|
||||
Team(TeamKind),
|
||||
AnyoneWithLink(LinkSharingSubjectType),
|
||||
}
|
||||
|
||||
/// A kind of user. In all cases, there is an underlying Warp account, but it's represented
|
||||
/// differently in certain cases.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UserKind {
|
||||
/// A Warp user account, tracked in the [`UserProfiles`] model.
|
||||
Account(UserUid),
|
||||
/// A session-sharing participant.
|
||||
// TODO(CLD-2283): Remove this once we have Firebase UIDs for shared session participants.
|
||||
SharedSessionParticipant(SessionSharingProfileData),
|
||||
}
|
||||
|
||||
/// A kind of team. Team permission updates are propagated differently for
|
||||
/// shared sessions, so we need to store different info in certain cases.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TeamKind {
|
||||
Team {
|
||||
team_uid: ServerId,
|
||||
},
|
||||
/// The team of the shared session sharer.
|
||||
SharedSessionTeam {
|
||||
team_uid: ServerId,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl TeamKind {
|
||||
/// Gets the team UID.
|
||||
pub fn team_uid(&self) -> ServerId {
|
||||
match self {
|
||||
TeamKind::Team { team_uid } => *team_uid,
|
||||
TeamKind::SharedSessionTeam { team_uid, .. } => *team_uid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Subject {
|
||||
/// Convert an [`Owner`] into the closest [`Subject`] type.
|
||||
pub fn from_owner(owner: Owner) -> Self {
|
||||
match owner {
|
||||
Owner::User { user_uid } => Subject::User(UserKind::Account(user_uid)),
|
||||
Owner::Team { team_uid } => Subject::Team(TeamKind::Team { team_uid }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the user UID for this subject, if it has one.
|
||||
pub fn user_uid(&self) -> Option<UserUid> {
|
||||
match self {
|
||||
Subject::User(user_kind) => match user_kind {
|
||||
UserKind::Account(user_uid) => Some(*user_uid),
|
||||
UserKind::SharedSessionParticipant(profile_data) => {
|
||||
Some(UserUid::new(profile_data.firebase_uid.as_str()))
|
||||
}
|
||||
},
|
||||
Subject::PendingUser { .. } => None,
|
||||
Subject::Team(_) => None,
|
||||
Subject::AnyoneWithLink(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if this subject refers to a given Firebase user directly.
|
||||
pub fn is_user(&self, other_uid: UserUid) -> bool {
|
||||
match self {
|
||||
Subject::User(UserKind::Account(user_uid)) => *user_uid == other_uid,
|
||||
Subject::User(UserKind::SharedSessionParticipant(profile_data)) => {
|
||||
profile_data.firebase_uid.as_str() == other_uid.as_str()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the team UID for this subject, if it has one.
|
||||
pub fn team_uid(&self) -> Option<ServerId> {
|
||||
match self {
|
||||
Subject::Team(team_kind) => Some(team_kind.team_uid()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for UserKind {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Account(self_uid), Self::Account(other_uid)) => self_uid == other_uid,
|
||||
// Shared session participant data does not implement `PartialEq`. We only compare
|
||||
// `UserKind`s in tests, so support isn't yet needed.
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<GenericStringObjectId> for SyncId {
|
||||
fn from(id: GenericStringObjectId) -> Self {
|
||||
Self::ServerId(id.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl GenericStringObjectId {
|
||||
pub fn uid(&self) -> ObjectUid {
|
||||
self.0.uid()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! This crate defines the low-level, model-agnostic cloud object substrate shared by Warp crates.
|
||||
//!
|
||||
//! It owns server-facing identifiers, user identifiers, object metadata, object type and format
|
||||
//! definitions, and sharing or drive primitives that do not depend on concrete Warp object models.
|
||||
//!
|
||||
//! It should remain independent of model-specific payloads, SQLite persistence, app runtime state,
|
||||
//! and UI rendering concerns.
|
||||
|
||||
pub mod auth;
|
||||
pub mod cloud_object;
|
||||
pub mod drive;
|
||||
pub mod ids;
|
||||
|
||||
pub use auth::UserUid;
|
||||
Reference in New Issue
Block a user