etwin_link_store 0.10.2

Link store implementation
Documentation
use async_trait::async_trait;
use etwin_core::clock::Clock;
use etwin_core::core::RawUserDot;
use etwin_core::dinoparc::{DinoparcServer, DinoparcUserId, DinoparcUserIdRef};
use etwin_core::hammerfest::{HammerfestServer, HammerfestUserId, HammerfestUserIdRef};
use etwin_core::link::{
  DeleteLinkError, DeleteLinkOptions, GetLinkOptions, GetLinksFromEtwinOptions, LinkStore, OldRawLink, RawLink,
  RemoteUserIdRef, TouchLinkError, TouchLinkOptions, VersionedRawLink, VersionedRawLinks,
};
use etwin_core::twinoid::{TwinoidUserId, TwinoidUserIdRef};
use etwin_core::types::AnyError;
use etwin_core::user::UserId;
use std::collections::HashMap;
use std::sync::RwLock;

struct RawLinkHistory<T: RemoteUserIdRef> {
  current: Option<RawLink<T>>,
  old: Vec<OldRawLink<T>>,
}

impl<T: RemoteUserIdRef> Default for RawLinkHistory<T> {
  fn default() -> Self {
    Self {
      current: None,
      old: vec![],
    }
  }
}

struct StoreState {
  from_dinoparc: HashMap<(DinoparcServer, DinoparcUserId), RawLinkHistory<DinoparcUserIdRef>>,
  to_dinoparc: HashMap<(UserId, DinoparcServer), RawLinkHistory<DinoparcUserIdRef>>,
  from_hammerfest: HashMap<(HammerfestServer, HammerfestUserId), RawLinkHistory<HammerfestUserIdRef>>,
  to_hammerfest: HashMap<(UserId, HammerfestServer), RawLinkHistory<HammerfestUserIdRef>>,
  from_twinoid: HashMap<TwinoidUserId, RawLinkHistory<TwinoidUserIdRef>>,
  to_twinoid: HashMap<UserId, RawLinkHistory<TwinoidUserIdRef>>,
}

impl StoreState {
  fn new() -> Self {
    Self {
      from_dinoparc: HashMap::new(),
      to_dinoparc: HashMap::new(),
      from_hammerfest: HashMap::new(),
      to_hammerfest: HashMap::new(),
      from_twinoid: HashMap::new(),
      to_twinoid: HashMap::new(),
    }
  }
}

pub struct MemLinkStore<TyClock: Clock> {
  clock: TyClock,
  state: RwLock<StoreState>,
}

impl<TyClock> MemLinkStore<TyClock>
where
  TyClock: Clock,
{
  pub fn new(clock: TyClock) -> Self {
    Self {
      clock,
      state: RwLock::new(StoreState::new()),
    }
  }
}

#[async_trait]
impl<TyClock> LinkStore for MemLinkStore<TyClock>
where
  TyClock: Clock,
{
  async fn touch_dinoparc_link(
    &self,
    options: &TouchLinkOptions<DinoparcUserIdRef>,
  ) -> Result<VersionedRawLink<DinoparcUserIdRef>, TouchLinkError<DinoparcUserIdRef>> {
    let mut state = self.state.write().unwrap();
    let state: &mut StoreState = &mut state;
    touch_link(
      &mut state.from_dinoparc,
      &mut state.to_dinoparc,
      (options.remote.server, options.remote.id),
      (options.etwin.id, options.remote.server),
      || {
        let now = self.clock.now();
        let link: RawLink<DinoparcUserIdRef> = RawLink {
          link: RawUserDot {
            time: now,
            user: options.linked_by,
          },
          unlink: (),
          etwin: options.etwin,
          remote: options.remote,
        };
        link
      },
    )
  }

  async fn touch_hammerfest_link(
    &self,
    options: &TouchLinkOptions<HammerfestUserIdRef>,
  ) -> Result<VersionedRawLink<HammerfestUserIdRef>, TouchLinkError<HammerfestUserIdRef>> {
    let mut state = self.state.write().unwrap();
    let state: &mut StoreState = &mut state;
    touch_link(
      &mut state.from_hammerfest,
      &mut state.to_hammerfest,
      (options.remote.server, options.remote.id),
      (options.etwin.id, options.remote.server),
      || {
        let now = self.clock.now();
        let link: RawLink<HammerfestUserIdRef> = RawLink {
          link: RawUserDot {
            time: now,
            user: options.linked_by,
          },
          unlink: (),
          etwin: options.etwin,
          remote: options.remote,
        };
        link
      },
    )
  }

  async fn touch_twinoid_link(
    &self,
    options: &TouchLinkOptions<TwinoidUserIdRef>,
  ) -> Result<VersionedRawLink<TwinoidUserIdRef>, TouchLinkError<TwinoidUserIdRef>> {
    let mut state = self.state.write().unwrap();
    let state: &mut StoreState = &mut state;
    touch_link(
      &mut state.from_twinoid,
      &mut state.to_twinoid,
      options.remote.id,
      options.etwin.id,
      || {
        let now = self.clock.now();
        let link: RawLink<TwinoidUserIdRef> = RawLink {
          link: RawUserDot {
            time: now,
            user: options.linked_by,
          },
          unlink: (),
          etwin: options.etwin,
          remote: options.remote,
        };
        link
      },
    )
  }

  async fn delete_dinoparc_link(
    &self,
    options: &DeleteLinkOptions<DinoparcUserIdRef>,
  ) -> Result<VersionedRawLink<DinoparcUserIdRef>, DeleteLinkError<DinoparcUserIdRef>> {
    let mut state = self.state.write().unwrap();
    let state: &mut StoreState = &mut state;
    delete_link(
      &mut state.from_dinoparc,
      &mut state.to_dinoparc,
      (options.remote.server, options.remote.id),
      (options.etwin.id, options.remote.server),
      |start| {
        let now = self.clock.now();
        let link: OldRawLink<DinoparcUserIdRef> = OldRawLink {
          link: start.link,
          unlink: RawUserDot {
            time: now,
            user: options.unlinked_by,
          },
          etwin: options.etwin,
          remote: options.remote,
        };
        link
      },
      || DeleteLinkError::NotFound(options.etwin, options.remote),
    )
    .map(|_| Default::default())
  }

  async fn delete_hammerfest_link(
    &self,
    options: &DeleteLinkOptions<HammerfestUserIdRef>,
  ) -> Result<VersionedRawLink<HammerfestUserIdRef>, DeleteLinkError<HammerfestUserIdRef>> {
    let mut state = self.state.write().unwrap();
    let state: &mut StoreState = &mut state;
    delete_link(
      &mut state.from_hammerfest,
      &mut state.to_hammerfest,
      (options.remote.server, options.remote.id),
      (options.etwin.id, options.remote.server),
      |start| {
        let now = self.clock.now();
        let link: OldRawLink<HammerfestUserIdRef> = OldRawLink {
          link: start.link,
          unlink: RawUserDot {
            time: now,
            user: options.unlinked_by,
          },
          etwin: options.etwin,
          remote: options.remote,
        };
        link
      },
      || DeleteLinkError::NotFound(options.etwin, options.remote),
    )
    .map(|_| Default::default())
  }

  async fn delete_twinoid_link(
    &self,
    options: &DeleteLinkOptions<TwinoidUserIdRef>,
  ) -> Result<VersionedRawLink<TwinoidUserIdRef>, DeleteLinkError<TwinoidUserIdRef>> {
    let mut state = self.state.write().unwrap();
    let state: &mut StoreState = &mut state;
    delete_link(
      &mut state.from_twinoid,
      &mut state.to_twinoid,
      options.remote.id,
      options.etwin.id,
      |start| {
        let now = self.clock.now();
        let link: OldRawLink<TwinoidUserIdRef> = OldRawLink {
          link: start.link,
          unlink: RawUserDot {
            time: now,
            user: options.unlinked_by,
          },
          etwin: options.etwin,
          remote: options.remote,
        };
        link
      },
      || DeleteLinkError::NotFound(options.etwin, options.remote),
    )
    .map(|_| Default::default())
  }

  async fn get_link_from_dinoparc(
    &self,
    options: &GetLinkOptions<DinoparcUserIdRef>,
  ) -> Result<VersionedRawLink<DinoparcUserIdRef>, AnyError> {
    // assert!(options.time.is_none());
    let state = self.state.read().unwrap();
    let link = state.from_dinoparc.get(&(options.remote.server, options.remote.id));

    match link {
      None => Ok(VersionedRawLink {
        current: None,
        old: vec![],
      }),
      Some(link) => {
        let link: VersionedRawLink<DinoparcUserIdRef> = VersionedRawLink {
          current: link.current.clone(),
          old: vec![],
        };
        Ok(link)
      }
    }
  }

  async fn get_link_from_hammerfest(
    &self,
    options: &GetLinkOptions<HammerfestUserIdRef>,
  ) -> Result<VersionedRawLink<HammerfestUserIdRef>, AnyError> {
    // assert!(options.time.is_none());
    let state = self.state.read().unwrap();
    let link = state.from_hammerfest.get(&(options.remote.server, options.remote.id));

    match link {
      None => Ok(VersionedRawLink {
        current: None,
        old: vec![],
      }),
      Some(link) => {
        let link: VersionedRawLink<HammerfestUserIdRef> = VersionedRawLink {
          current: link.current.clone(),
          old: vec![],
        };
        Ok(link)
      }
    }
  }

  async fn get_link_from_twinoid(
    &self,
    options: &GetLinkOptions<TwinoidUserIdRef>,
  ) -> Result<VersionedRawLink<TwinoidUserIdRef>, AnyError> {
    // assert!(options.time.is_none());
    let state = self.state.read().unwrap();
    let link = state.from_twinoid.get(&options.remote.id);

    match link {
      None => Ok(VersionedRawLink {
        current: None,
        old: vec![],
      }),
      Some(link) => {
        let link: VersionedRawLink<TwinoidUserIdRef> = VersionedRawLink {
          current: link.current.clone(),
          old: vec![],
        };
        Ok(link)
      }
    }
  }

  async fn get_links_from_etwin(&self, options: &GetLinksFromEtwinOptions) -> Result<VersionedRawLinks, AnyError> {
    let state = self.state.read().unwrap();
    let mut links = VersionedRawLinks::default();

    for srv in DinoparcServer::iter() {
      let empty = RawLinkHistory::<DinoparcUserIdRef>::default();
      let link = state.to_dinoparc.get(&(options.etwin.id, srv)).unwrap_or(&empty);
      match srv {
        DinoparcServer::DinoparcCom => links.dinoparc_com.current = link.current.clone(),
        DinoparcServer::EnDinoparcCom => links.en_dinoparc_com.current = link.current.clone(),
        DinoparcServer::SpDinoparcCom => links.sp_dinoparc_com.current = link.current.clone(),
      }
    }

    for srv in HammerfestServer::iter() {
      let empty = RawLinkHistory::<HammerfestUserIdRef>::default();
      let link = state.to_hammerfest.get(&(options.etwin.id, srv)).unwrap_or(&empty);
      match srv {
        HammerfestServer::HammerfestEs => links.hammerfest_es.current = link.current.clone(),
        HammerfestServer::HammerfestFr => links.hammerfest_fr.current = link.current.clone(),
        HammerfestServer::HfestNet => links.hfest_net.current = link.current.clone(),
      }
    }

    {
      let empty = RawLinkHistory::<TwinoidUserIdRef>::default();
      let link = state.to_twinoid.get(&options.etwin.id).unwrap_or(&empty);
      links.twinoid.current = link.current.clone();
    }

    Ok(links)
  }
}

#[cfg(feature = "neon")]
impl<TyClock> neon::prelude::Finalize for MemLinkStore<TyClock> where TyClock: Clock {}

fn touch_link<FK: Eq + core::hash::Hash, TK: Eq + core::hash::Hash, R: RemoteUserIdRef>(
  from: &mut HashMap<FK, RawLinkHistory<R>>,
  to: &mut HashMap<TK, RawLinkHistory<R>>,
  from_key: FK,
  to_key: TK,
  link: impl FnOnce() -> RawLink<R>,
) -> Result<VersionedRawLink<R>, TouchLinkError<R>> {
  let linked_etwin = from.entry(from_key).or_default();
  let linked_remote = to.entry(to_key).or_default();

  match (&mut linked_etwin.current, &mut linked_remote.current) {
    (from @ None, to @ None) => {
      let link: RawLink<R> = link();
      *from = Some(link.clone());
      *to = Some(link.clone());
      let link: VersionedRawLink<R> = VersionedRawLink {
        current: Some(link),
        old: vec![],
      };
      Ok(link)
    }
    (Some(linked_etwin), None) => Err(TouchLinkError::ConflictEtwin(linked_etwin.etwin)),
    (None, Some(linked_remote)) => Err(TouchLinkError::ConflictRemote(linked_remote.remote.clone())),
    (Some(linked_etwin), Some(linked_remote)) => Err(TouchLinkError::ConflictBoth(
      linked_etwin.etwin,
      linked_remote.remote.clone(),
    )),
  }
}

fn delete_link<FK: Eq + core::hash::Hash, TK: Eq + core::hash::Hash, R: RemoteUserIdRef>(
  from: &mut HashMap<FK, RawLinkHistory<R>>,
  to: &mut HashMap<TK, RawLinkHistory<R>>,
  from_key: FK,
  to_key: TK,
  old_link: impl FnOnce(RawLink<R>) -> OldRawLink<R>,
  not_found_err: impl FnOnce() -> DeleteLinkError<R>,
) -> Result<(), DeleteLinkError<R>> {
  let linked_etwin = from.get_mut(&from_key);
  let linked_remote = to.get_mut(&to_key);
  let (linked_etwin, linked_remote) = match (linked_etwin, linked_remote) {
    (Some(linked_etwin), Some(linked_remote)) => (linked_etwin, linked_remote),
    _ => return Err(not_found_err()),
  };

  match (&mut linked_etwin.current, &mut linked_remote.current) {
    (from @ Some(_), to @ Some(_)) if from == to => {
      let link = old_link(from.take().unwrap());
      *to = None;
      linked_etwin.old.push(link.clone());
      linked_remote.old.push(link);
      Ok(())
    }
    _ => Err(not_found_err()),
  }
}

#[cfg(test)]
mod test {
  use crate::mem::MemLinkStore;
  use crate::test::TestApi;
  use etwin_core::clock::VirtualClock;
  use etwin_core::core::Instant;
  use etwin_core::dinoparc::DinoparcStore;
  use etwin_core::hammerfest::HammerfestStore;
  use etwin_core::link::LinkStore;
  use etwin_core::user::UserStore;
  use etwin_core::uuid::Uuid4Generator;
  use etwin_dinoparc_store::mem::MemDinoparcStore;
  use etwin_hammerfest_store::mem::MemHammerfestStore;
  use etwin_user_store::mem::MemUserStore;
  use std::sync::Arc;

  #[allow(clippy::type_complexity)]
  fn make_test_api() -> TestApi<
    Arc<VirtualClock>,
    Arc<dyn DinoparcStore>,
    Arc<dyn HammerfestStore>,
    Arc<dyn LinkStore>,
    Arc<dyn UserStore>,
  > {
    let clock = Arc::new(VirtualClock::new(Instant::ymd_hms(2020, 1, 1, 0, 0, 0)));
    let dinoparc_store: Arc<dyn DinoparcStore> = Arc::new(MemDinoparcStore::new(Arc::clone(&clock)));
    let hammerfest_store: Arc<dyn HammerfestStore> = Arc::new(MemHammerfestStore::new(Arc::clone(&clock)));
    let link_store: Arc<dyn LinkStore> = Arc::new(MemLinkStore::new(Arc::clone(&clock)));
    let user_store: Arc<dyn UserStore> = Arc::new(MemUserStore::new(Arc::clone(&clock), Uuid4Generator));

    TestApi {
      clock,
      dinoparc_store,
      hammerfest_store,
      link_store,
      user_store,
    }
  }

  test_link_store!(|| make_test_api());
}