ron-0.13: lib/RON/Text/Serialize/UUID.hs
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
module RON.Text.Serialize.UUID (
serializeUuid,
serializeUuidAtom,
serializeUuidKey,
uuidToString,
uuidToText,
) where
import RON.Prelude
import Data.Bits (countLeadingZeros, shiftL, xor)
import Data.ByteString qualified as BS
import Data.ByteString.Char8 qualified as BSC
import Data.ByteString.Lazy qualified as BSL
import Data.ByteString.Lazy.Char8 qualified as BSLC
import Data.Foldable (minimumBy)
import Data.Text qualified as Text
import RON.Base64 qualified as Base64
import RON.UUID (UUID (..), UuidFields (..), split, zero)
import RON.Util.Word (
Word2,
Word60,
ls60,
safeCast,
pattern B00,
pattern B0000,
pattern B01,
pattern B10,
pattern B11,
)
-- | Serialize UUID without context (used for test)
serializeUuid :: UUID -> ByteStringL
serializeUuid this =
BSL.fromStrict $ case uuidVariant of
B00 -> unzipped thisFields
_ -> serializeUuidGeneric this
where
thisFields@UuidFields{..} = split this
uuidToString :: UUID -> String
uuidToString = BSLC.unpack . serializeUuid
uuidToText :: UUID -> Text
uuidToText = Text.pack . uuidToString
-- | Serialize UUID in op key context
serializeUuidKey ::
-- | same key in the previous op (default is 'zero')
UUID ->
-- | previous key of the same op (default is 'zero')
UUID ->
-- | this
UUID ->
ByteStringL
serializeUuidKey prevKey prev this =
BSL.fromStrict $ case thisFields.uuidVariant of
B00 ->
minimumByLength $
unzipped thisFields
: zipIfDefaultVariant prevKey this
++ [ "`" <> z
| prev /= zero
, z <- zipIfDefaultVariant prev this
]
_ -> serializeUuidGeneric this
where
thisFields = split this
-- | Serialize UUID in op value (atom) context
serializeUuidAtom ::
-- | previous
UUID ->
-- | this
UUID ->
ByteStringL
serializeUuidAtom prev this =
BSL.fromStrict $ case thisFields.uuidVariant of
B00 ->
minimumByLength $
unzipped thisFields
: (guard (prev /= zero) *> zipIfDefaultVariant prev this)
_ -> serializeUuidGeneric this
where
thisFields = split this
zipIfDefaultVariant :: UUID -> UUID -> [ByteString]
zipIfDefaultVariant prev this =
[ z
| (split prev).uuidVariant == B00
, Just z <- [zipUuid (split prev) (split this)]
]
unzipped :: UuidFields -> ByteString
unzipped UuidFields{..} = x' <> y'
where
variety = case uuidVariety of
B0000 -> ""
_ -> BS.singleton (Base64.encodeLetter4 uuidVariety) <> "/"
x' = variety <> Base64.encode60short uuidValue
y' = case (uuidVersion, uuidOrigin) of
(B00, safeCast -> 0 :: Word64) -> ""
_ ->
serializeVersion uuidVersion
`BSC.cons` Base64.encode60short uuidOrigin
zipUuid :: UuidFields -> UuidFields -> Maybe ByteString
zipUuid prev this
| prev == this = Just ""
| canReuseValue = valueZip
| otherwise = Nothing
where
canReuseValue = prev{uuidValue = this.uuidValue} == this
valueZip = zipPrefix prev.uuidValue this.uuidValue
zipPrefix :: Word60 -> Word60 -> Maybe ByteString
zipPrefix prev this
| commonBits >= 6 * 10 = pure ""
| commonBits >= 6 * 9 = ok ')' 9
| commonBits >= 6 * 8 = ok ']' 8
| commonBits >= 6 * 7 = ok '}' 7
| commonBits >= 6 * 6 = ok '{' 6
| commonBits >= 6 * 5 = ok '[' 5
| commonBits >= 6 * 4 = ok '(' 4
| otherwise = Nothing
where
ok c n = pure $ BSC.cons c $ encode60short' $ safeCast this `shiftL` (6 * n)
commonBits =
countLeadingZeros (safeCast prev `xor` safeCast this :: Word64) - 4
encode60short' = \case
0 -> ""
w -> Base64.encode60short $ ls60 w
serializeVersion :: Word2 -> Char
serializeVersion = \case
B00 -> '$'
B01 -> '%'
B10 -> '+'
B11 -> '-'
serializeUuidGeneric :: UUID -> ByteString
serializeUuidGeneric (UUID x y) = Base64.encode64 x <> Base64.encode64 y
-- | XXX Partial for lists!
minimumByLength :: (Foldable f) => f ByteString -> ByteString
minimumByLength = minimumBy $ comparing BS.length