packages feed

mmzk-typeid-0.4.0.0: src/Data/TypeID/Class.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

-- |
-- Module      : Data.TypeID.Class
-- License     : MIT
-- Maintainer  : mmzk1526@outlook.com
-- Portability : GHC
--
-- A module with the APIs for any 'Data.TypeID.V7.TypeID'-ish identifier type.
--
-- These type classes are useful to define custom TypeID-ish identifier types.
-- For example, if one wishes to remove the constraints on prefix, or use a
-- different UUID version for the suffix.
--
module Data.TypeID.Class
  (
  -- * Type classes
    TypeIDLike
  , IDType(..)
  , IDConv(..)
  , IDGen(..)
  , decorate
  , genID
  , genID'
  , genIDs
  , checkID
  , checkIDWithEnv
  -- * Helper types
  , GenFunc(..)
  , ResWithErr(..)
  ) where

import           Control.Exception
import           Control.Monad.IO.Class
import           Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as BSL
import           Data.Kind (Type)
import           Data.Proxy
import           Data.Text (Text)
import qualified Data.Text as T
import           Data.Text.Encoding
import           Data.TypeID.Error
import           Data.UUID.V7 (UUID)
import           Data.Word

-- | A constraint synonym for a 'Data.TypeID.V7.TypeID'-ish identifier type that
-- supports ID generation and string conversion.
type TypeIDLike a = (IDType a, IDConv a, IDGen a)

-- | A type class for a 'Data.TypeID.V7.TypeID'-ish identifier type, which has a
-- 'Text' prefix and a 'UUID' suffix.
class IDType a where
  -- | Get the prefix of the identifier.
  getPrefix :: a -> Text

  -- | Get the UUID suffix of the identifier.
  getUUID :: a -> UUID

  -- | Get the timestamp of the identifier. Returns 0 if the identifier is not
  -- timestamp-based.
  getTime :: a -> Word64

-- | A type class for converting between a 'Data.TypeID.V7.TypeID'-ish
-- identifier type and some string representations.
class IDConv a where
  -- | Parse the identifier from its 'String' representation.
  string2ID :: String -> Either TypeIDError a
  string2ID = text2ID . T.pack
  {-# INLINE string2ID #-}

  -- | Parse the identifier from its string representation as a strict 'Text'.
  text2ID :: Text -> Either TypeIDError a
  text2ID = byteString2ID . BSL.fromStrict . encodeUtf8
  {-# INLINE text2ID #-}

  -- | Parse the identifier from its string representation as a lazy
  -- 'ByteString'.
  byteString2ID :: ByteString -> Either TypeIDError a
  byteString2ID = string2ID . T.unpack . decodeUtf8 . BSL.toStrict
  {-# INLINE byteString2ID #-}

  -- | Pretty-print the identifier to a 'String'.
  id2String :: a -> String
  id2String = T.unpack . id2Text
  {-# INLINE id2String #-}

  -- | Pretty-print the identifier to a strict 'Text'.
  id2Text :: a -> Text
  id2Text = decodeUtf8 . BSL.toStrict . id2ByteString
  {-# INLINE id2Text #-}

  -- | Pretty-print the identifier to a lazy 'ByteString'.
  id2ByteString :: a -> ByteString
  id2ByteString = BSL.fromStrict . encodeUtf8 . T.pack . id2String
  {-# INLINE id2ByteString #-}

  -- | Parse the identifier from its 'String' representation, throwing an error
  -- when the parsing fails.
  string2IDM :: MonadIO m => String -> m a
  string2IDM = either (liftIO . throwIO) pure . string2ID
  {-# INLINE string2IDM #-}

  -- | Parse the identifier from its string representation as a strict 'Text',
  -- throwing an error when the parsing fails.
  text2IDM :: MonadIO m => Text -> m a
  text2IDM = either (liftIO . throwIO) pure . text2ID
  {-# INLINE text2IDM #-}

  -- | Parse the identifier from its string representation as a lazy
  -- 'ByteString', throwing an error when the parsing fails.
  byteString2IDM :: MonadIO m => ByteString -> m a
  byteString2IDM = either (liftIO . throwIO) pure . byteString2ID
  {-# INLINE byteString2IDM #-}

  -- | Parse the identifier from its 'String' representation, but crashes when
  -- the parsing fails.
  unsafeString2ID :: String -> a
  unsafeString2ID = either (error . show) id . string2ID
  {-# INLINE unsafeString2ID #-}

  -- | Parse the identifier from its string representation as a strict 'Text',
  -- but crashes when the parsing fails.
  unsafeText2ID :: Text -> a
  unsafeText2ID = either (error . show) id . text2ID
  {-# INLINE unsafeText2ID #-}

  -- | Parse the identifier from its string representation as a lazy
  -- 'ByteString', but crashes when the parsing fails.
  unsafeByteString2ID :: ByteString -> a
  unsafeByteString2ID = either (error . show) id . byteString2ID
  {-# INLINE unsafeByteString2ID #-}
  {-# MINIMAL string2ID, id2String
            | string2ID, id2Text
            | string2ID, id2ByteString
            | text2ID, id2String
            | text2ID, id2Text
            | text2ID, id2ByteString
            | byteString2ID, id2String
            | byteString2ID, id2Text
            | byteString2ID, id2ByteString #-}

-- | Generate a new identifier with the given prefix.
genID :: forall a m. (IDGen a, MonadIO m) => GenFunc (IDGenPrefix a) (m a)
genID = genID_ @a @m Proxy
{-# INLINE genID #-}

-- | Similar to 'genID', but stateless. It can be a faster implementation than
-- 'genID', but it does not guarantee any stateful property, such as
-- monotonically increasing for 'UUID'v7-based identifiers.
--
-- The default implementation is the same as 'genID'.
genID' :: forall a m. (IDGen a, MonadIO m) => GenFunc (IDGenPrefix a) (m a)
genID' = genID'_ @a @m Proxy
{-# INLINE genID' #-}

-- | Generate a list of identifiers with the given prefix.
genIDs :: forall a m. (IDGen a, MonadIO m)
       => GenFunc (IDGenPrefix a) (Word16 -> m [a])
genIDs = genIDs_ @a @m Proxy
{-# INLINE genIDs #-}

-- | Generate a new identifier with the given prefix and 'UUID' suffix.
decorate :: forall a. IDGen a
         => GenFunc (IDGenPrefix a) (UUID -> ResWithErr (IDGenPrefix a) a)
decorate = decorate_ @a Proxy
{-# INLINE decorate #-}

-- | Check the validity of the identifier.
checkID :: forall a. IDGen a => a -> Maybe TypeIDError
checkID = checkID_ @a Proxy
{-# INLINE checkID #-}

-- | Check the validity of the identifier, potentially with impure criteria.
checkIDWithEnv :: forall a m. (IDGen a, MonadIO m) => a -> m (Maybe TypeIDError)
checkIDWithEnv = checkIDWithEnv_ @a @m Proxy
{-# INLINE checkIDWithEnv #-}

-- | A type class for generating 'Data.TypeID.V7.TypeID'-ish identifiers.
--
-- The methods in this type class are not directly used since each of them has
-- a dummy 'Proxy' in order to compile. We implement the methods here and use
-- the methods without the underscore suffix instead.
class IDGen a where
  -- | If the identifier has compile-time determined prefix, this type should be
  -- @'Nothing@. Otherwise it should be @'Just prefix@ where @prefix@ is the
  -- type of the prefix (e.g. 'Text').
  type IDGenPrefix a :: Maybe Type

  -- | Generate an identifier with the given prefix.
  genID_ :: MonadIO m => Proxy a -> GenFunc (IDGenPrefix a) (m a)

  -- | Similar to 'genID'_, but stateless. It can be a faster implementation
  -- than 'genID'_, but it does not guarantee any stateful property, such as
  -- monotonically increasing for 'UUID'v7-based identifiers.
  --
  -- The default implementation is the same as 'genID'_.
  genID'_ :: forall m. MonadIO m => Proxy a -> GenFunc (IDGenPrefix a) (m a)
  genID'_ = genID_ @_ @m
  {-# INLINE genID'_ #-}

  -- | Generate a list of identifiers with the given prefix.
  genIDs_ :: MonadIO m => Proxy a -> GenFunc (IDGenPrefix a) (Word16 -> m [a])

  -- | Generate a new identifier with the given prefix and 'UUID' suffix.
  decorate_ :: Proxy a
            -> GenFunc (IDGenPrefix a) (UUID -> ResWithErr (IDGenPrefix a) a)

  -- | Check the validity of the identifier.
  checkID_ :: Proxy a -> a -> Maybe TypeIDError

  -- | Check the validity of the identifier, potentially with impure criteria.
  checkIDWithEnv_ :: MonadIO m => Proxy a -> a -> m (Maybe TypeIDError)
  checkIDWithEnv_ _ = pure . checkID_ (Proxy @a)
  {-# INLINE checkIDWithEnv_ #-}

-- | A function generator based on the 'IDGenPrefix' type family.
type family GenFunc prefix res where
  GenFunc ('Just prefix) res = prefix -> res
  GenFunc 'Nothing res       = res

-- | A result that may contain an error, based on the 'IDGenPrefix' type family.
--
-- In other words, if the prefix type is already encoded in the type level,
-- we are certain that the prefix is valid, so the result type does not need the
-- @Either TypeIDError@ part.
type family ResWithErr prefix res where
  ResWithErr ('Just prefix) res = Either TypeIDError res
  ResWithErr 'Nothing res       = res