use std::{iter, mem};
use log::warn;
use serde::{Deserialize, Serialize};
use simd_json::{json_typed, owned};
use self::{
identity_operation::{IdentityOperationData, identity_operation_type::IdentityOperationType},
snapshot::{history_step::IdentityHistoryStep, timeline::IdentityTimeline},
};
use crate::replica::{
Replica,
cache::impl_cache,
entity::{
Entity, EntityRead,
id::entity_id::EntityId,
identity::IdentityStub,
lamport,
operation::{Operation, operations::Operations},
},
};
pub mod identity_operation;
pub mod legacy;
pub mod snapshot;
#[derive(Debug, Deserialize, Serialize)]
pub struct Identity {
id: EntityId<Identity>,
operations: Operations<Self>,
create_time: lamport::Time,
edit_time: lamport::Time,
current_head: gix::ObjectId,
}
impl Entity for Identity {
type HistoryStep = IdentityHistoryStep;
type OperationData = IdentityOperationData;
type Timeline = IdentityTimeline;
const FORMAT_VERSION: usize = 4;
const NAMESPACE: &str = "identities";
const TYPENAME: &str = "Identity";
fn operations(&self) -> &Operations<Self>
where
Self: Sized,
{
&self.operations
}
unsafe fn from_parts(
operations: Operations<Self>,
create_time: lamport::Time,
edit_time: lamport::Time,
current_head: gix::ObjectId,
) -> Self
where
Self: Sized,
{
warn!("Constructing an Identity with a invalid id.");
Self {
id: operations.root().id(),
operations,
create_time,
edit_time,
current_head,
}
}
fn create_time(&self) -> &lamport::Time
where
Self: Sized,
{
&self.create_time
}
fn edit_time(&self) -> &lamport::Time
where
Self: Sized,
{
&self.edit_time
}
fn current_head(&self) -> &gix::oid
where
Self: Sized,
{
&self.current_head
}
fn id(&self) -> EntityId<Self>
where
Self: Sized,
{
self.id
}
}
#[allow(missing_docs)]
pub mod read {
use super::legacy;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Reading the legacy data failed: {0}")]
LegacyRead(#[from] legacy::read::Error),
#[error("This identity does not contain legacy version entries")]
EmptyIdentity,
#[error("This identity contained a last version entry, with a missing clock name: {0}")]
MissingClock(String),
}
}
impl From<read::Error> for crate::replica::entity::read::Error<Identity> {
fn from(value: read::Error) -> Self {
Self::CustomRead(value)
}
}
impl EntityRead for Identity {
type CustomReadError = read::Error;
#[allow(clippy::too_many_lines)]
fn read(
replica: &Replica,
id: EntityId<Self>,
) -> Result<Self, crate::replica::entity::read::Error<Self>>
where
Self: Sized,
{
impl_cache!(@mk_table "identities");
let last_id = Identity::last_git_commit(replica, id)?;
let mut key = id.as_id().as_slice().to_owned();
key.extend(last_id.as_slice());
impl_cache! {@lookup replica.db(), key.as_slice()}
let mut version_entries =
Self::read_legacy(replica, id).map_err(read::Error::LegacyRead)?;
if version_entries.is_empty() {
return Err(read::Error::EmptyIdentity)?;
}
let last_version_clocks = mem::take(&mut version_entries.last_mut().expect("Exists").times);
let operations: Operations<Identity> = {
let mut ops: Vec<Operation<Identity>> = vec![];
let mut first_version = version_entries.remove(0);
ops.push(
Operation::<Identity>::from_value(
json_typed! { owned,
{
"type": u64::from(IdentityOperationType::Create),
"timestamp": first_version.unix_time,
"nonce": Into::<String>::into(first_version.nonce),
"name": first_version.name.unwrap_or("<Unnamed identity>".to_owned()),
}
},
IdentityStub { id },
)
.expect("The json is hard-coded"),
);
first_version.name = None;
for version in iter::once(first_version).chain(version_entries.into_iter()) {
if let Some(name) = version.name {
ops.push(
Operation::<Identity>::from_value(
json_typed! {owned,
{
"type": u64::from(IdentityOperationType::SetName),
"timestamp": version.unix_time,
"nonce": Into::<String>::into(version.nonce),
"name": name,
}
},
IdentityStub { id },
)
.expect("The json is hard-coded"),
);
}
if let Some(email) = version.email {
ops.push(
Operation::<Identity>::from_value(
json_typed! {owned,
{
"type": u64::from(IdentityOperationType::SetEmail),
"timestamp": version.unix_time,
"nonce": Into::<String>::into(version.nonce),
"email": email,
}
},
IdentityStub { id },
)
.expect("The json is hard-coded"),
);
}
if let Some(login_name) = version.login {
ops.push(
Operation::<Identity>::from_value(
json_typed! {owned,
{
"type": u64::from(IdentityOperationType::SetLoginName),
"timestamp": version.unix_time,
"nonce": Into::<String>::into(version.nonce),
"login_name": login_name,
}
},
IdentityStub { id },
)
.expect("The json is hard-coded"),
);
}
if let Some(avatar_url) = version.avatar_url {
ops.push(
Operation::<Identity>::from_value(
json_typed! {owned,
{
"type": u64::from(IdentityOperationType::SetAvatarUrl),
"timestamp": version.unix_time,
"nonce": Into::<String>::into(version.nonce),
"url": avatar_url.to_string(),
}
},
IdentityStub { id },
)
.expect("The json is hard-coded"),
);
}
if let Some(metadata) = version.metadata {
let mut newer_metadata = owned::Object::new();
unsafe {
for (key, value) in metadata {
newer_metadata.insert_nocheck(key, value.into());
}
}
ops.push(
Operation::<Identity>::from_value(
json_typed! {owned,
{
"type": u64::from(IdentityOperationType::SetMetadata),
"timestamp": version.unix_time,
"nonce": Into::<String>::into(version.nonce),
"metadata": owned::Value::Object(Box::new(newer_metadata))
}
},
IdentityStub { id },
)
.expect("The json is hard-coded"),
);
}
}
Operations::<Identity>::from_operations(ops)?
};
let (create_time, edit_time) = {
let create = last_version_clocks
.iter()
.find_map(|(key, value)| {
if key == "bugs-create" {
Some(value)
} else {
None
}
})
.unwrap_or(&1);
let edit = last_version_clocks
.iter()
.find_map(|(key, value)| {
if key == "bugs-edit" {
Some(value)
} else {
None
}
})
.unwrap_or(&1);
(lamport::Time::from(*create), lamport::Time::from(*edit))
};
let me = Self {
id,
operations,
create_time,
edit_time,
current_head: last_id,
};
impl_cache! {@populate replica.db(), key.as_slice(), &me}
Ok(me)
}
}