etwin_core 0.11.0

Core crate for Eternal-Twin
Documentation
use crate::core::{Instant, IntPercentage};
use crate::link::VersionedEtwinLink;
use crate::temporal::LatestTemporal;
use crate::types::AnyError;
use async_trait::async_trait;
use auto_impl::auto_impl;
#[cfg(feature = "serde")]
use etwin_serde_tools::{serialize_ordered_map, serialize_ordered_set, Deserialize, Serialize, Serializer};
#[cfg(feature = "sqlx")]
use sqlx::{postgres, Postgres};
use std::collections::{HashMap, HashSet};
use thiserror::Error;

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GetDinoparcUserOptions {
  pub server: DinoparcServer,
  pub id: DinoparcUserId,
  pub time: Option<Instant>,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GetDinoparcDinozOptions {
  pub server: DinoparcServer,
  pub id: DinoparcDinozId,
  pub time: Option<Instant>,
}

declare_new_enum!(
  pub enum DinoparcServer {
    #[str("dinoparc.com")]
    DinoparcCom,
    #[str("en.dinoparc.com")]
    EnDinoparcCom,
    #[str("sp.dinoparc.com")]
    SpDinoparcCom,
  }
  pub type ParseError = DinoparcServerParseError;
  const SQL_NAME = "dinoparc_server";
);

declare_decimal_id! {
  pub struct DinoparcUserId(u32);
  pub type ParseError = DinoparcUserIdParseError;
  const BOUNDS = 0..1_000_000_000;
  const SQL_NAME = "dinoparc_user_id";
}

impl DinoparcUserId {
  pub const fn and_server(&self, server: DinoparcServer) -> DinoparcUserIdRef {
    DinoparcUserIdRef { server, id: *self }
  }
}

declare_new_string! {
  pub struct DinoparcUsername(String);
  pub type ParseError = DinoparcUsernameParseError;
  const PATTERN = r"^[0-9A-Za-z_-]{1,14}$";
  const SQL_NAME = "dinoparc_username";
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DinoparcPassword(String);

impl DinoparcPassword {
  pub fn new(raw: String) -> Self {
    Self(raw)
  }

  pub fn as_str(&self) -> &str {
    &self.0
  }
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DinoparcCredentials {
  pub server: DinoparcServer,
  pub username: DinoparcUsername,
  pub password: DinoparcPassword,
}

declare_new_string! {
  /// A Dinoparc session key.
  ///
  /// It correspond to the value of the `sid` cookie.
  ///
  /// - `oetxjSBD3FEqDlLLNffGUY0NLKMmDDjv`
  /// - `pJ5zOeaKuw0mjGB9xdGVJuRdpCASjmBl`
  /// - `LlkSCMQW5fESPSOUVt3FMrqBwXwAhwzj`
  pub struct DinoparcSessionKey(String);
  pub type ParseError = DinoparcSessionKeyParseError;
  const PATTERN = r"^[0-9a-zA-Z]{32}$";
  const SQL_NAME = "dinoparc_session_key";
}

declare_new_string! {
  pub struct DinoparcMachineId(String);
  pub type ParseError = DinoparcMachineIdParseError;
  const PATTERN = r"^[0-9a-zA-Z]{32}$";
  const SQL_NAME = "dinoparc_machine_id";
}

impl DinoparcMachineId {
  pub const LENGTH: usize = 32;
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DinoparcSession {
  pub ctime: Instant,
  pub atime: Instant,
  pub key: DinoparcSessionKey,
  pub user: ShortDinoparcUser,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StoredDinoparcSession {
  pub key: DinoparcSessionKey,
  pub user: DinoparcUserIdRef,
  pub ctime: Instant,
  pub atime: Instant,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename = "DinoparcUser"))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ShortDinoparcUser {
  pub server: DinoparcServer,
  pub id: DinoparcUserId,
  pub username: DinoparcUsername,
}

impl ShortDinoparcUser {
  pub const fn as_ref(&self) -> DinoparcUserIdRef {
    DinoparcUserIdRef {
      server: self.server,
      id: self.id,
    }
  }
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename = "DinoparcUser"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArchivedDinoparcUser {
  pub server: DinoparcServer,
  pub id: DinoparcUserId,
  pub archived_at: Instant,
  pub username: DinoparcUsername,
  pub coins: Option<LatestTemporal<u32>>,
  // pub bills: Option<LatestTemporal<u32>>,
  pub dinoz: Option<LatestTemporal<Vec<DinoparcDinozIdRef>>>,
  #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_ordered_opt_temporal_map"))]
  pub inventory: Option<LatestTemporal<HashMap<DinoparcItemId, u32>>>,
  pub collection: Option<LatestTemporal<DinoparcCollection>>,
}

/// `ArchivedDinoparcUser` extend with `etwin` to provide Eternaltwin-specific data.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename = "DinoparcUser"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EtwinDinoparcUser {
  pub server: DinoparcServer,
  pub id: DinoparcUserId,
  pub archived_at: Instant,
  pub username: DinoparcUsername,
  pub coins: Option<LatestTemporal<u32>>,
  pub dinoz: Option<LatestTemporal<Vec<DinoparcDinozIdRef>>>,
  #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_ordered_opt_temporal_map"))]
  pub inventory: Option<LatestTemporal<HashMap<DinoparcItemId, u32>>>,
  pub collection: Option<LatestTemporal<DinoparcCollection>>,
  pub etwin: VersionedEtwinLink,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename = "DinoparcUser"))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DinoparcUserIdRef {
  pub server: DinoparcServer,
  pub id: DinoparcUserId,
}

declare_decimal_id! {
  pub struct DinoparcDinozId(u32);
  pub type ParseError = DinoparcDinozIdParseError;
  const BOUNDS = 0..1_000_000_000;
  const SQL_NAME = "dinoparc_dinoz_id";
}

impl DinoparcDinozId {
  pub const fn and_server(&self, server: DinoparcServer) -> DinoparcDinozIdRef {
    DinoparcDinozIdRef { server, id: *self }
  }
}

declare_decimal_id! {
  pub struct DinoparcItemId(u32);
  pub type ParseError = DinoparcItemIdParseError;
  const BOUNDS = 1..1_000_000_000;
  const SQL_NAME = "dinoparc_item_id";
}

declare_new_string! {
  pub struct DinoparcDinozName(String);
  pub type ParseError = DinoparcDinozNameParseError;
  const PATTERN = r"^.{1,50}$";
  const SQL_NAME = "dinoparc_dinoz_name";
}

declare_new_string! {
  pub struct DinoparcDinozSkin(String);
  pub type ParseError = DinoparcDinozSkinParseError;
  const PATTERN = r"^.{1,30}$";
  const SQL_NAME = "dinoparc_dinoz_skin";
}

declare_decimal_id! {
  pub struct DinoparcLocationId(u8);
  pub type ParseError = DinoparcLocationIdParseError;
  const BOUNDS = 0..23;
  const SQL_NAME = "dinoparc_location_id";
}

declare_decimal_id! {
  pub struct DinoparcRewardId(u8);
  pub type ParseError = DinoparcRewardIdParseError;
  const BOUNDS = 1..=49;
  const SQL_NAME = "dinoparc_reward_id";
}

declare_new_string! {
  pub struct DinoparcEpicRewardKey(String);
  pub type ParseError = DinoparcEpicRewardKeyParseError;
  const PATTERN = r"^[a-z0-9_]{1,30}$";
  const SQL_NAME = "dinoparc_epic_reward_key";
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename = "DinoparcDinoz"))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DinoparcDinozIdRef {
  pub server: DinoparcServer,
  pub id: DinoparcDinozId,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename = "DinoparcDinoz"))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ShortDinoparcDinozWithLocation {
  pub server: DinoparcServer,
  pub id: DinoparcDinozId,
  pub name: Option<DinoparcDinozName>,
  pub location: Option<DinoparcLocationId>,
}

impl ShortDinoparcDinozWithLocation {
  pub const fn as_ref(&self) -> DinoparcDinozIdRef {
    DinoparcDinozIdRef {
      server: self.server,
      id: self.id,
    }
  }
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename = "DinoparcDinoz"))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ShortDinoparcDinozWithLevel {
  pub server: DinoparcServer,
  pub id: DinoparcDinozId,
  pub name: Option<DinoparcDinozName>,
  pub level: u16,
}

impl ShortDinoparcDinozWithLevel {
  pub const fn as_ref(&self) -> DinoparcDinozIdRef {
    DinoparcDinozIdRef {
      server: self.server,
      id: self.id,
    }
  }
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename = "DinoparcDinoz"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DinoparcDinoz {
  pub server: DinoparcServer,
  pub id: DinoparcDinozId,
  pub race: DinoparcDinozRace,
  /// Raw skin code
  pub skin: DinoparcDinozSkin,
  pub level: u16,
  #[cfg_attr(feature = "serde", serde(flatten))]
  pub named: Option<NamedDinoparcDinozFields>,
}

/// Fields only available on named Dinoz
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NamedDinoparcDinozFields {
  pub name: DinoparcDinozName,
  pub location: DinoparcLocationId,
  pub life: IntPercentage,
  pub experience: IntPercentage,
  pub danger: i16,
  pub in_tournament: bool,
  pub elements: DinoparcDinozElements,
  #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_ordered_map"))]
  pub skills: HashMap<DinoparcSkill, DinoparcSkillLevel>,
}

impl DinoparcDinoz {
  pub const fn as_ref(&self) -> DinoparcDinozIdRef {
    DinoparcDinozIdRef {
      server: self.server,
      id: self.id,
    }
  }

  pub const fn name(&self) -> Option<&DinoparcDinozName> {
    // TODO(demurgos): Remove this clippy exception once `Option::map` is const-compatible (or clippy stop complaining)
    #[allow(clippy::manual_map)]
    if let Some(named) = self.named.as_ref() {
      Some(&named.name)
    } else {
      None
    }
  }
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename = "DinoparcDinoz"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArchivedDinoparcDinoz {
  pub server: DinoparcServer,
  pub id: DinoparcDinozId,
  pub archived_at: Instant,
  pub name: Option<LatestTemporal<Option<DinoparcDinozName>>>,
  pub owner: Option<LatestTemporal<ShortDinoparcUser>>,
  pub location: Option<LatestTemporal<DinoparcLocationId>>,
  pub race: Option<LatestTemporal<DinoparcDinozRace>>,
  pub skin: Option<LatestTemporal<DinoparcDinozSkin>>,
  pub life: Option<LatestTemporal<IntPercentage>>,
  pub level: Option<LatestTemporal<u16>>,
  pub experience: Option<LatestTemporal<IntPercentage>>,
  pub danger: Option<LatestTemporal<i16>>,
  pub in_tournament: Option<LatestTemporal<bool>>,
  pub elements: Option<LatestTemporal<DinoparcDinozElements>>,
  #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_ordered_opt_temporal_map"))]
  pub skills: Option<LatestTemporal<HashMap<DinoparcSkill, DinoparcSkillLevel>>>,
}

pub type EtwinDinoparcDinoz = ArchivedDinoparcDinoz;

declare_new_int! {
  pub struct DinoparcSkillLevel(u8);
  pub type RangeError = DinoparcSkillLevelRangeError;
  const BOUNDS = 0..=5;
  type SqlType = i16;
  const SQL_NAME = "dinoparc_skill_level";
}

declare_new_enum!(
  pub enum DinoparcDinozRace {
    #[str("Cargou")]
    Cargou,
    #[str("Castivore")]
    Castivore,
    #[str("Gluon")]
    Gluon,
    #[str("Gorilloz")]
    Gorilloz,
    #[str("Hippoclamp")]
    Hippoclamp,
    #[str("Kabuki")]
    Kabuki,
    #[str("Korgon")]
    Korgon,
    #[str("Kump")]
    Kump,
    #[str("Moueffe")]
    Moueffe,
    #[str("Ouistiti")]
    Ouistiti,
    #[str("Picori")]
    Picori,
    #[str("Pigmou")]
    Pigmou,
    #[str("Pteroz")]
    Pteroz,
    #[str("Rokky")]
    Rokky,
    #[str("Santaz")]
    Santaz,
    #[str("Serpantin")]
    Serpantin,
    #[str("Sirain")]
    Sirain,
    #[str("Wanwan")]
    Wanwan,
    #[str("Winks")]
    Winks,
  }
  pub type ParseError = DinoparcDinozRaceParseError;
  const SQL_NAME = "dinoparc_dinoz_race";
);

impl DinoparcDinozRace {
  pub fn from_skin_code(skin: &str) -> Self {
    if let Some(c) = skin.chars().next() {
      match c {
        '0' => Self::Moueffe,
        '1' => Self::Picori,
        '2' => Self::Castivore,
        '3' => Self::Sirain,
        '4' => Self::Winks,
        '5' => Self::Gorilloz,
        '6' => Self::Cargou,
        '7' => Self::Hippoclamp,
        '8' => Self::Rokky,
        '9' => Self::Pigmou,
        'A' => Self::Wanwan,
        'B' => Self::Gluon,
        'C' => Self::Kump,
        'D' => Self::Pteroz,
        'E' => Self::Santaz,
        'F' => Self::Ouistiti,
        'G' => Self::Korgon,
        'H' => Self::Kabuki,
        'I' => Self::Serpantin,
        _ => Self::Moueffe,
      }
    } else {
      Self::Moueffe
    }
  }
}

/// Data in the left bar for logged-in users
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DinoparcDinozElements {
  pub fire: u16,
  pub earth: u16,
  pub water: u16,
  pub thunder: u16,
  pub air: u16,
}

#[cfg(feature = "sqlx")]
impl sqlx::Type<Postgres> for DinoparcDinozElements {
  fn type_info() -> postgres::PgTypeInfo {
    postgres::PgTypeInfo::with_name("dinoparc_dinoz_elements")
  }

  fn compatible(ty: &postgres::PgTypeInfo) -> bool {
    *ty == Self::type_info() || *ty == postgres::PgTypeInfo::with_name("raw_dinoparc_dinoz_elements")
  }
}

#[cfg(feature = "sqlx")]
impl<'r> sqlx::Decode<'r, Postgres> for DinoparcDinozElements {
  fn decode(value: postgres::PgValueRef<'r>) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
    let mut decoder = postgres::types::PgRecordDecoder::new(value)?;

    let fire = decoder.try_decode::<crate::pg_num::PgU16>()?;
    let earth = decoder.try_decode::<crate::pg_num::PgU16>()?;
    let water = decoder.try_decode::<crate::pg_num::PgU16>()?;
    let thunder = decoder.try_decode::<crate::pg_num::PgU16>()?;
    let air = decoder.try_decode::<crate::pg_num::PgU16>()?;

    Ok(Self {
      fire: fire.into(),
      earth: earth.into(),
      water: water.into(),
      thunder: thunder.into(),
      air: air.into(),
    })
  }
}

#[cfg(feature = "sqlx")]
impl<'q> sqlx::Encode<'q, Postgres> for DinoparcDinozElements {
  fn encode_by_ref(&self, buf: &mut postgres::PgArgumentBuffer) -> sqlx::encode::IsNull {
    let mut encoder = postgres::types::PgRecordEncoder::new(buf);
    encoder.encode(crate::pg_num::PgU16::from(self.fire));
    encoder.encode(crate::pg_num::PgU16::from(self.earth));
    encoder.encode(crate::pg_num::PgU16::from(self.water));
    encoder.encode(crate::pg_num::PgU16::from(self.thunder));
    encoder.encode(crate::pg_num::PgU16::from(self.air));
    encoder.finish();
    sqlx::encode::IsNull::No
  }
}

declare_new_enum!(
  pub enum DinoparcSkill {
    #[str("Bargain")]
    Bargain,
    #[str("Camouflage")]
    Camouflage,
    #[str("Climb")]
    Climb,
    #[str("Cook")]
    Cook,
    #[str("Counterattack")]
    Counterattack,
    #[str("Dexterity")]
    Dexterity,
    #[str("Dig")]
    Dig,
    #[str("EarthApprentice")]
    EarthApprentice,
    #[str("FireApprentice")]
    FireApprentice,
    #[str("FireProtection")]
    FireProtection,
    #[str("Intelligence")]
    Intelligence,
    #[str("Juggle")]
    Juggle,
    #[str("Jump")]
    Jump,
    #[str("Luck")]
    Luck,
    #[str("MartialArts")]
    MartialArts,
    #[str("Medicine")]
    Medicine,
    #[str("Mercenary")]
    Mercenary,
    #[str("Music")]
    Music,
    #[str("Navigation")]
    Navigation,
    #[str("Perception")]
    Perception,
    #[str("Provoke")]
    Provoke,
    #[str("Run")]
    Run,
    #[str("Saboteur")]
    Saboteur,
    #[str("ShadowPower")]
    ShadowPower,
    #[str("Spy")]
    Spy,
    #[str("Stamina")]
    Stamina,
    #[str("Steal")]
    Steal,
    #[str("Strategy")]
    Strategy,
    #[str("Strength")]
    Strength,
    #[str("Survival")]
    Survival,
    #[str("Swim")]
    Swim,
    #[str("ThunderApprentice")]
    ThunderApprentice,
    #[str("TotemThief")]
    TotemThief,
    #[str("WaterApprentice")]
    WaterApprentice,
  }
  pub type ParseError = DinoparcSkillParseError;
  const SQL_NAME = "dinoparc_skill";
);

/// Data in the left bar for logged-in users
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DinoparcSessionUser<U = ShortDinoparcUser> {
  pub user: U,
  pub coins: u32,
  pub dinoz: Vec<ShortDinoparcDinozWithLocation>,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DinoparcInventoryResponse<U = ShortDinoparcUser> {
  pub session_user: DinoparcSessionUser<U>,
  #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_ordered_map"))]
  pub inventory: HashMap<DinoparcItemId, u32>,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DinoparcExchangeWithResponse<U = ShortDinoparcUser> {
  pub session_user: DinoparcSessionUser<U>,
  pub own_bills: u32,
  pub own_dinoz: Vec<ShortDinoparcDinozWithLevel>,
  pub other_user: ShortDinoparcUser,
  pub other_dinoz: Vec<ShortDinoparcDinozWithLevel>,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DinoparcCollectionResponse<U = ShortDinoparcUser> {
  pub session_user: DinoparcSessionUser<U>,
  pub collection: DinoparcCollection,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DinoparcCollection {
  #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_ordered_set"))]
  pub rewards: HashSet<DinoparcRewardId>,
  #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_ordered_set"))]
  pub epic_rewards: HashSet<DinoparcEpicRewardKey>,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DinoparcDinozResponse<U = ShortDinoparcUser> {
  pub session_user: DinoparcSessionUser<U>,
  pub dinoz: DinoparcDinoz,
}

#[async_trait]
#[auto_impl(&, Arc)]
pub trait DinoparcClient: Send + Sync {
  /// Returns the id of two distinct existing users on the provided Dinoparc server.
  /// These users should be used when an `exchangeWith` query is required to fetch the
  /// full Dinoz list but the specific target does not matter.
  async fn get_preferred_exchange_with(&self, server: DinoparcServer) -> [DinoparcUserId; 2];

  async fn create_session(&self, options: &DinoparcCredentials) -> Result<DinoparcSession, AnyError>;

  async fn test_session(
    &self,
    server: DinoparcServer,
    key: &DinoparcSessionKey,
  ) -> Result<Option<DinoparcSession>, AnyError>;

  async fn get_dinoz(&self, session: &DinoparcSession, id: DinoparcDinozId) -> Result<DinoparcDinozResponse, AnyError>;

  async fn get_exchange_with(
    &self,
    session: &DinoparcSession,
    recipient: DinoparcUserId,
  ) -> Result<DinoparcExchangeWithResponse, AnyError>;

  async fn get_inventory(&self, session: &DinoparcSession) -> Result<DinoparcInventoryResponse, AnyError>;

  async fn get_collection(&self, session: &DinoparcSession) -> Result<DinoparcCollectionResponse, AnyError>;
}

#[async_trait]
#[auto_impl(&, Arc)]
pub trait DinoparcStore: Send + Sync {
  async fn touch_short_user(&self, options: &ShortDinoparcUser) -> Result<ArchivedDinoparcUser, AnyError>;

  async fn touch_inventory(&self, response: &DinoparcInventoryResponse) -> Result<(), AnyError>;

  async fn touch_collection(&self, response: &DinoparcCollectionResponse) -> Result<(), AnyError>;

  async fn touch_dinoz(&self, response: &DinoparcDinozResponse) -> Result<(), AnyError>;

  async fn touch_exchange_with(&self, response: &DinoparcExchangeWithResponse) -> Result<(), AnyError>;

  async fn get_dinoz(&self, options: &GetDinoparcDinozOptions) -> Result<Option<ArchivedDinoparcDinoz>, AnyError>;

  async fn get_user(&self, options: &GetDinoparcUserOptions) -> Result<Option<ArchivedDinoparcUser>, AnyError>;

  async fn get_short_user(&self, options: &GetDinoparcUserOptions) -> Result<Option<ShortDinoparcUser>, AnyError>;
}

#[derive(Debug, Error)]
pub enum GetExchangeWithError {
  #[error("A user cannot exchange with itself")]
  SelfExchange,
}

// TODO: Move to serde_tools
#[cfg(feature = "serde")]
pub fn serialize_ordered_opt_temporal_map<K: Ord + Serialize, V: Serialize, S: Serializer>(
  value: &Option<LatestTemporal<HashMap<K, V>>>,
  serializer: S,
) -> Result<S::Ok, S::Error> {
  value
    .as_ref()
    .map(|t| {
      t.as_ref()
        .map(|m| m.iter().collect::<std::collections::BTreeMap<_, _>>())
    })
    .serialize(serializer)
}

#[cfg(test)]
mod test {
  #[cfg(feature = "serde")]
  use crate::core::{Instant, IntPercentage, PeriodLower};
  #[cfg(feature = "serde")]
  use crate::dinoparc::{
    ArchivedDinoparcDinoz, ArchivedDinoparcUser, DinoparcDinozElements, DinoparcDinozIdRef, DinoparcDinozRace,
    DinoparcServer, DinoparcSkill, DinoparcSkillLevel, ShortDinoparcUser,
  };
  #[cfg(feature = "serde")]
  use crate::temporal::{ForeignRetrieved, ForeignSnapshot, LatestTemporal};
  #[cfg(feature = "serde")]
  use std::collections::HashMap;
  #[cfg(feature = "serde")]
  use std::fs;

  #[cfg(feature = "serde")]
  #[allow(clippy::unnecessary_wraps)]
  fn get_archived_dinoparc_user_alice_rokky() -> ArchivedDinoparcUser {
    ArchivedDinoparcUser {
      server: DinoparcServer::DinoparcCom,
      id: "1".parse().unwrap(),
      archived_at: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
      username: "alice".parse().unwrap(),
      coins: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: 10000,
        },
      }),
      inventory: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: {
            let mut inventory = HashMap::new();
            inventory.insert("4".parse().unwrap(), 10);
            inventory
          },
        },
      }),
      collection: None,
      dinoz: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: vec![DinoparcDinozIdRef {
            server: DinoparcServer::DinoparcCom,
            id: "2".parse().unwrap(),
          }],
        },
      }),
    }
  }

  #[cfg(feature = "serde")]
  #[test]
  fn read_archived_dinoparc_user_alice_rokky() {
    let s =
      fs::read_to_string("../../test-resources/core/dinoparc/archived-dinoparc-user/alice-rokky/value.json").unwrap();
    let actual: ArchivedDinoparcUser = serde_json::from_str(&s).unwrap();
    let expected = get_archived_dinoparc_user_alice_rokky();
    assert_eq!(actual, expected);
  }

  #[cfg(feature = "serde")]
  #[test]
  fn write_archived_dinoparc_user_alice_rokky() {
    let value = get_archived_dinoparc_user_alice_rokky();
    let actual: String = serde_json::to_string_pretty(&value).unwrap();
    let expected =
      fs::read_to_string("../../test-resources/core/dinoparc/archived-dinoparc-user/alice-rokky/value.json").unwrap();
    assert_eq!(&actual, expected.trim());
  }

  #[cfg(feature = "serde")]
  #[allow(clippy::unnecessary_wraps)]
  fn get_archived_dinoparc_dinoz_yasumi() -> ArchivedDinoparcDinoz {
    ArchivedDinoparcDinoz {
      server: DinoparcServer::EnDinoparcCom,
      id: "765483".parse().unwrap(),
      archived_at: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
      name: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: Some("Yasumi".parse().unwrap()),
        },
      }),
      owner: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: ShortDinoparcUser {
            server: DinoparcServer::EnDinoparcCom,
            id: "681579".parse().unwrap(),
            username: "Kapox".parse().unwrap(),
          },
        },
      }),
      location: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: "0".parse().unwrap(),
        },
      }),
      race: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: DinoparcDinozRace::Wanwan,
        },
      }),
      skin: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: "Ac9OrgxOWu1pd7Fp".parse().unwrap(),
        },
      }),
      life: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: IntPercentage::new(30).unwrap(),
        },
      }),
      level: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: 12,
        },
      }),
      experience: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: IntPercentage::new(13).unwrap(),
        },
      }),
      danger: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: 116,
        },
      }),
      in_tournament: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: false,
        },
      }),
      elements: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: DinoparcDinozElements {
            fire: 10,
            earth: 0,
            water: 0,
            thunder: 7,
            air: 2,
          },
        },
      }),
      skills: Some(LatestTemporal {
        latest: ForeignSnapshot {
          period: PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0)),
          retrieved: ForeignRetrieved {
            latest: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
          },
          value: {
            let mut skills = HashMap::new();
            skills.insert(DinoparcSkill::Dexterity, DinoparcSkillLevel::new(2).unwrap());
            skills.insert(DinoparcSkill::Intelligence, DinoparcSkillLevel::new(5).unwrap());
            skills.insert(DinoparcSkill::Strength, DinoparcSkillLevel::new(5).unwrap());
            skills.insert(DinoparcSkill::MartialArts, DinoparcSkillLevel::new(5).unwrap());
            skills
          },
        },
      }),
    }
  }

  #[cfg(feature = "serde")]
  #[test]
  fn read_archived_dinoparc_dinoz_yasumi() {
    let s = fs::read_to_string("../../test-resources/core/dinoparc/etwin-dinoparc-dinoz/yasumi/value.json").unwrap();
    let actual: ArchivedDinoparcDinoz = serde_json::from_str(&s).unwrap();
    let expected = get_archived_dinoparc_dinoz_yasumi();
    assert_eq!(actual, expected);
  }

  #[cfg(feature = "serde")]
  #[test]
  fn write_archived_dinoparc_dinoz_yasumi() {
    let value = get_archived_dinoparc_dinoz_yasumi();
    let actual: String = serde_json::to_string_pretty(&value).unwrap();
    let expected =
      fs::read_to_string("../../test-resources/core/dinoparc/etwin-dinoparc-dinoz/yasumi/value.json").unwrap();
    assert_eq!(&actual, expected.trim());
  }
}