packages feed

ron-0.13: lib/RON/UUID.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}

module RON.UUID (
    UUID (..),
    UuidFields (..),
    build,
    buildX,
    buildY,
    split,
    succValue,
    addValue,
    zero,
    pattern Zero,

    -- * Fields as lenses
    variety,
    value,
    variant,
    version,
    origin,

    -- * Name
    getName,
    liftName,
    mkName,
    mkScopedName,

    -- * Base32 encoding, suitable for file names
    decodeBase32,
    encodeBase32,
) where

import RON.Prelude

import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import Data.ByteString.Char8 qualified as BSC
import Language.Haskell.TH.Syntax (Exp, Q, liftData)
import Text.Show qualified

import RON.Base64 qualified as Base64
import RON.Util.Word (
    Word2,
    Word4,
    Word60,
    leastSignificant2,
    leastSignificant4,
    leastSignificant60,
    safeCast,
    pattern B00,
    pattern B0000,
    pattern B01,
    pattern B10,
    pattern B11,
 )

{- | Universally unique identifier of anything,
as documented in https://github.com/gritzko/ron/blob/master/uuid.md
-}
data UUID = UUID {-# UNPACK #-} !Word64 {-# UNPACK #-} !Word64
    deriving (Data, Eq, Generic, Hashable, Ord)

-- | RON-Text-encoding
instance Show UUID where
    -- showsPrec a (UUID x y) =
    --     showParen (a >= 11) $
    --     showString "UUID 0x" . showHex x . showString " 0x" . showHex y
    show this = show serialized
      where
        UUID x y = this
        UuidFields{..} = split this
        serialized = case uuidVariant of
            B00 -> unzipped
            _ -> generic
        unzipped = x' <> y'
        variety' = case uuidVariety of
            B0000 -> ""
            _ -> chr (fromIntegral $ Base64.encodeLetter4 uuidVariety) : "/"
        x' = variety' <> BSC.unpack (Base64.encode60short uuidValue)
        y' = case (uuidVersion, uuidOrigin) of
            (B00, safeCast -> 0 :: Word64) -> ""
            _ -> version' : BSC.unpack (Base64.encode60short uuidOrigin)
        generic = BSC.unpack $ Base64.encode64 x <> Base64.encode64 y
        version' = case uuidVersion of
            B00 -> '$'
            B01 -> '%'
            B10 -> '+'
            B11 -> '-'

-- | UUID split in parts
data UuidFields = UuidFields
    { uuidVariety :: !Word4
    , uuidValue :: !Word60
    , uuidVariant :: !Word2
    , uuidVersion :: !Word2
    , uuidOrigin :: !Word60
    }
    deriving (Eq, Show)

-- | Split UUID into parts
split :: UUID -> UuidFields
split (UUID x y) =
    UuidFields
        { uuidVariety = leastSignificant4 $ x `shiftR` 60
        , uuidValue = leastSignificant60 x
        , uuidVariant = leastSignificant2 $ y `shiftR` 62
        , uuidVersion = leastSignificant2 $ y `shiftR` 60
        , uuidOrigin = leastSignificant60 y
        }

-- | Build UUID from parts
build :: UuidFields -> UUID
build UuidFields{..} =
    UUID
        (buildX uuidVariety uuidValue)
        (buildY uuidVariant uuidVersion uuidOrigin)

-- | Build former 64 bits of UUID from parts
buildX :: Word4 -> Word60 -> Word64
buildX uuidVariety uuidValue =
    (safeCast uuidVariety `shiftL` 60) .|. safeCast uuidValue

-- | Build latter 64 bits of UUID from parts
buildY :: Word2 -> Word2 -> Word60 -> Word64
buildY uuidVariant uuidVersion uuidOrigin =
    (safeCast uuidVariant `shiftL` 62)
        .|. (safeCast uuidVersion `shiftL` 60)
        .|. safeCast uuidOrigin

-- | Make an unscoped (unqualified) name
mkName ::
    (MonadFail m) =>
    -- | name, max 10 Base64 letters
    ByteString ->
    m UUID
mkName nam = mkScopedName nam ""

-- | Contruct a UUID name in compile-time
liftName :: ByteString -> Q Exp
liftName = mkName >=> liftData

-- TODO(2019-01-11, cblp) typed splice

-- | Make a scoped (qualified) name
mkScopedName ::
    (MonadFail m) =>
    -- | scope, max 10 Base64 letters
    ByteString ->
    -- | local name, max 10 Base64 letters
    ByteString ->
    m UUID
mkScopedName scope nam = do
    scope' <- expectBase64x60 "UUID scope" scope $ Base64.decode60 scope
    nam' <- expectBase64x60 "UUID name" nam $ Base64.decode60 nam
    pure $
        build
            UuidFields
                { uuidVariety = B0000
                , uuidValue = scope'
                , uuidVariant = B00
                , uuidVersion = B00
                , uuidOrigin = nam'
                }
  where
    expectBase64x60 field input =
        maybe
            ( fail $
                field
                    <> ": expected a Base64-encoded 10-character string, got "
                    <> show input
            )
            pure

-- | Convert UUID to a name
getName ::
    UUID ->
    -- | @(scope, name)@ for a scoped name; @(name, "")@ for a global name
    Maybe (ByteString, ByteString)
getName uuid =
    case split uuid of
        UuidFields
            { uuidVariety = B0000
            , uuidVariant = B00
            , uuidVersion = B00
            , uuidValue
            , uuidOrigin
            } ->
                Just (x, y)
              where
                x = Base64.encode60short uuidValue
                y = case safeCast uuidOrigin :: Word64 of
                    0 -> ""
                    _ -> Base64.encode60short uuidOrigin
        _ -> Nothing

-- | UUID with all zero fields
zero :: UUID
zero = UUID 0 0

-- | UUID with all zero fields
pattern Zero :: UUID
pattern Zero = UUID 0 0

-- | Increment field 'uuidValue' of a UUID
succValue :: UUID -> UUID
succValue = build . go . split
  where
    go u@UuidFields{uuidValue} =
        u
            { uuidValue =
                if uuidValue < maxBound then succ uuidValue else uuidValue
            }

-- | Increase field 'uuidValue' of a UUID
addValue :: UUID -> Word60 -> UUID
addValue uu v = build u{uuidValue = uuidValue + v}
  where
    u@UuidFields{uuidValue} = split uu

-- | Encode a UUID to a Base32 string
encodeBase32 :: UUID -> FilePath
encodeBase32 (UUID x y) =
    BSC.unpack $
        Base64.encode64base32short x <> "-" <> Base64.encode64base32short y

-- | Decode a UUID from a Base32 string
decodeBase32 :: FilePath -> Maybe UUID
decodeBase32 fp = do
    let (x, dashy) = span (/= '-') $ map toUpper fp
    ("-", y) <- pure $ splitAt 1 dashy
    UUID
        <$> Base64.decode64base32 (BSC.pack x)
        <*> Base64.decode64base32 (BSC.pack y)

variety :: Lens' UUID Word4
variety =
    lens
        (\(UUID x _) -> leastSignificant4 $ x `shiftR` 60)
        ( \(UUID x y) v ->
            UUID (x .&. 0x0FFFFFFFFFFFFFFF .|. (safeCast v `shiftL` 60)) y
        )

value :: Lens' UUID Word60
value =
    lens
        (\(UUID x _) -> leastSignificant60 x)
        (\(UUID x y) v -> UUID (x .&. 0xF000000000000000 .|. safeCast v) y)

variant :: Lens' UUID Word2
variant =
    lens
        (\(UUID _ y) -> leastSignificant2 $ y `shiftR` 62)
        ( \(UUID x y) v ->
            UUID x (y .&. 0x3FFFFFFFFFFFFFFF .|. (safeCast v `shiftL` 62))
        )

version :: Lens' UUID Word2
version =
    lens
        (\(UUID _ y) -> leastSignificant2 $ y `shiftR` 60)
        ( \(UUID x y) v ->
            UUID x (y .&. 0xCFFFFFFFFFFFFFFF .|. (safeCast v `shiftL` 60))
        )

origin :: Lens' UUID Word60
origin =
    lens
        (\(UUID _ y) -> leastSignificant60 y)
        (\(UUID x y) v -> UUID x (y .&. 0xF000000000000000 .|. safeCast v))

type Lens s t a b = forall f. (Functor f) => (a -> f b) -> s -> f t

type Lens' s a = Lens s s a a

lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens sa sbt afb s = sbt s <$> afb (sa s)
{-# INLINE lens #-}