packages feed

calamity-0.7.0.0: Calamity/Types/Snowflake.hs

-- | The snowflake type
module Calamity.Types.Snowflake (
  Snowflake (..),
  HasID (..),
  type HasID',
  HasIDField (..),
  HasIDFieldCoerce (..),
  type HasIDFieldCoerce',
  coerceSnowflake,
) where

import Data.Aeson
import Data.Bits
import Data.Data
import Data.Hashable
import Data.Kind
import Data.Text.Read
import qualified Data.Vector.Unboxing as U
import Data.Word
import GHC.Records (HasField (getField))
import TextShow
import Web.HttpApiData (ToHttpApiData)

-- Thanks sbrg
-- https://github.com/saevarb/haskord/blob/d1bb07bcc4f3dbc29f2dfd3351ff9f16fc100c07/haskord-lib/src/Haskord/Types/Common.hs#L78
newtype Snowflake (t :: Type) = Snowflake
  { fromSnowflake :: Word64
  }
  deriving (Eq, Ord, Data)
  deriving newtype (Show, TextShow, FromJSONKey)
  deriving newtype (ToJSONKey, U.Unboxable)
  deriving newtype (ToHttpApiData)

-- I'm pretty sure that Word64's hash just being 'fromIntegral' is a bad idea when
-- attempting to use it in a hashmap, so swizzle the bits a bit to give a good
-- distribution of bits
instance Hashable (Snowflake t) where
  hashWithSalt salt (Snowflake a) =
    let initial = fromIntegral @_ @Word64 $ hashWithSalt salt a
        round1 = (initial `shiftR` 30 `xor` initial) * 0xbf58476d1ce4e5b9
        round2 = (round1 `shiftR` 27 `xor` round1) * 0xbf58476d1ce4e5b9
        round3 = (round2 `shiftR` 31 `xor` round2)
     in fromIntegral @_ @Int round3

instance ToJSON (Snowflake t) where
  toJSON (Snowflake s) = String . showt $ s

instance FromJSON (Snowflake t) where
  parseJSON = withText "Snowflake" $ \t -> do
    n <- case decimal t of
      Right (n, _) -> pure n
      Left e -> fail e
    pure $ Snowflake n

coerceSnowflake :: Snowflake a -> Snowflake b
coerceSnowflake (Snowflake t) = Snowflake t

-- | A typeclass for types that contain snowflakes of type `b`
class HasID b a where
  -- | Retrieve the ID from the type
  getID :: a -> Snowflake b

type HasID' a = HasID a a

-- | A newtype wrapper for deriving HasID generically
newtype HasIDField field a = HasIDField a

instance (HasID b c, HasField field a c) => HasID b (HasIDField field a) where
  getID (HasIDField a) = getID @b @c $ getField @field a

{- | A data `a` which contains an ID of type `Snowflake c`
   which should be swapped with `Snowflake b` upon fetching
-}
newtype HasIDFieldCoerce field a c = HasIDFieldCoerce a

type HasIDFieldCoerce' field a = HasIDFieldCoerce field a a

instance (HasID c d, HasField field a d) => HasID b (HasIDFieldCoerce field a c) where
  getID (HasIDFieldCoerce a) = coerceSnowflake . getID @c $ getField @field a

instance HasID a (Snowflake a) where
  getID = id