miso-1.15.0.0: src/Miso/UUID.hs
-----------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- |
-- Module : Miso.UUID
-- Copyright : (C) 2016-2026 David M. Johnson
-- License : BSD3-style (see the file LICENSE)
-- Maintainer : David M. Johnson <code@dmj.io>
-- Stability : experimental
-- Portability : non-portable
--
-- = Overview
--
-- "Miso.UUID" provides a t'UUID' type, as defined in
-- <http://tools.ietf.org/html/rfc4122 RFC 4122>, backed by a
-- 'Miso.String.MisoString'. Its API mirrors @Data.UUID@ from the
-- <https://hackage.haskell.org/package/uuid uuid> package.
--
-- New v4 UUIDs are generated with the browser's
-- <https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID crypto.randomUUID()>.
-- Parsing accepts the canonical @8-4-4-4-12@ hexadecimal form, in either
-- case, and normalizes it to lowercase.
--
-- = Quick start
--
-- @
-- import "Miso.UUID" (t'UUID')
-- import qualified "Miso.UUID" as UUID
--
-- -- Generate a fresh identifier
-- freshId :: IO t'UUID'
-- freshId = UUID.'nextRandom'
--
-- -- Parse one
-- parsed :: Maybe t'UUID'
-- parsed = UUID.'fromString' \"550e8400-e29b-41d4-a716-446655440000\"
--
-- -- Check for the nil UUID
-- isUnset :: t'UUID' -> Bool
-- isUnset = UUID.'null'
-- @
--
-- = Instances
--
-- t'UUID' can be converted to and from 'Miso.String.MisoString', JSON,
-- 'Miso.DSL.JSVal', and used as a route capture with "Miso.Router".
-- Its 'Show' and 'Read' instances use the unquoted @8-4-4-4-12@ form,
-- like @Data.UUID@.
----------------------------------------------------------------------------
module Miso.UUID
( -- ** Types
UUID
-- ** String conversion
, toString
, fromString
, toText
, fromText
, toASCIIBytes
, fromASCIIBytes
, toLazyASCIIBytes
, fromLazyASCIIBytes
-- ** Binary conversion
, toByteString
, fromByteString
, toWords
, fromWords
, toWords64
, fromWords64
-- ** Nil
, null
, nil
-- ** Generation
, nextRandom
) where
-----------------------------------------------------------------------------
import Control.Monad ((<=<))
import Data.Bifunctor (first)
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import Data.Bool (bool)
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BL8
import Data.Char (digitToInt, intToDigit, isHexDigit, isSpace, toLower)
import Data.List (intercalate)
import qualified Data.List as List
import qualified Data.Text as T
import Data.Word (Word32, Word64)
import Prelude hiding (null)
-----------------------------------------------------------------------------
import Miso.DSL (FromJSVal (..), ToJSVal (..), jsg, (#))
import Miso.JSON (FromJSON (..), ToJSON (..))
import qualified Miso.JSON as JSON
import Miso.Router (Router (..), capture, toPath)
import Miso.String (FromMisoString (..), MisoString, ToMisoString (..), fromMisoString)
import Miso.Util.Parser (ParseError)
import qualified Miso.Util.Parser as Parser
-----------------------------------------------------------------------------
-- | A universally unique identifier, as defined in <http://tools.ietf.org/html/rfc4122 RFC 4122>.
newtype UUID = UUID MisoString
deriving (Eq, Ord)
-----------------------------------------------------------------------------
instance Show UUID where
showsPrec _ = showString . toString
-----------------------------------------------------------------------------
instance Read UUID where
readsPrec _ str =
case fromString (take 36 s) of
Nothing -> []
Just u -> [(u, drop 36 s)]
where
s = dropWhile isSpace str
-----------------------------------------------------------------------------
parse :: MisoString -> Either (ParseError UUID Char) UUID
parse =
Parser.parse
( UUID
. toMisoString
. fmap toLower
<$> traverse (Parser.satisfy . bool isHexDigit (== '-') . (== '-')) nilPattern
<* Parser.endOfInput
)
. fromMisoString
-----------------------------------------------------------------------------
nilPattern :: String
nilPattern = "00000000-0000-0000-0000-000000000000"
-----------------------------------------------------------------------------
-- | Convert a t'UUID' to its @8-4-4-4-12@ string form, in lowercase.
toString :: UUID -> String
toString = fromMisoString . toMisoString
-----------------------------------------------------------------------------
-- | Parse a t'UUID' from its @8-4-4-4-12@ string form.
fromString :: String -> Maybe UUID
fromString = either (const Nothing) Just . parse . toMisoString
-----------------------------------------------------------------------------
-- | Like 'toString', but to t'T.Text'.
toText :: UUID -> T.Text
toText = fromMisoString . toMisoString
-----------------------------------------------------------------------------
-- | Like 'fromString', but from t'T.Text'.
fromText :: T.Text -> Maybe UUID
fromText = fromString . T.unpack
-----------------------------------------------------------------------------
-- | Like 'toString', but to an ASCII-encoded strict 'B8.ByteString'.
toASCIIBytes :: UUID -> B8.ByteString
toASCIIBytes = B8.pack . toString
-----------------------------------------------------------------------------
-- | Like 'fromString', but from an ASCII-encoded strict 'B8.ByteString'.
fromASCIIBytes :: B8.ByteString -> Maybe UUID
fromASCIIBytes = fromString . B8.unpack
-----------------------------------------------------------------------------
-- | Like 'toString', but to an ASCII-encoded lazy 'BL.ByteString'.
toLazyASCIIBytes :: UUID -> BL.ByteString
toLazyASCIIBytes = BL8.pack . toString
-----------------------------------------------------------------------------
-- | Like 'fromString', but from an ASCII-encoded lazy 'BL.ByteString'.
fromLazyASCIIBytes :: BL.ByteString -> Maybe UUID
fromLazyASCIIBytes = fromString . BL8.unpack
-----------------------------------------------------------------------------
-- | Encode a t'UUID' as 16 bytes, in network byte order.
toByteString :: UUID -> BL.ByteString
toByteString u =
BL.pack [ fromIntegral (w `shiftR` n) | w <- [hi, lo], n <- [56, 48 .. 0] ]
where
(hi, lo) = toWords64 u
-----------------------------------------------------------------------------
-- | Decode a t'UUID' from 16 bytes, in network byte order. Returns
-- 'Nothing' if the input is not exactly 16 bytes long.
fromByteString :: BL.ByteString -> Maybe UUID
fromByteString bs
| length bytes == 16 = Just (fromWords64 (word hi) (word lo))
| otherwise = Nothing
where
bytes = BL.unpack bs
(hi, lo) = splitAt 8 bytes
word = List.foldl' (\acc b -> acc `shiftL` 8 .|. fromIntegral b) 0
-----------------------------------------------------------------------------
-- | Convert a t'UUID' to four 32-bit words, most significant first.
toWords :: UUID -> (Word32, Word32, Word32, Word32)
toWords u =
( fromIntegral (hi `shiftR` 32)
, fromIntegral hi
, fromIntegral (lo `shiftR` 32)
, fromIntegral lo
) where
(hi, lo) = toWords64 u
-----------------------------------------------------------------------------
-- | Build a t'UUID' from four 32-bit words, most significant first.
fromWords :: Word32 -> Word32 -> Word32 -> Word32 -> UUID
fromWords a b c d = fromWords64 (join a b) (join c d)
where
join x y = fromIntegral x `shiftL` 32 .|. fromIntegral y
-----------------------------------------------------------------------------
-- | Convert a t'UUID' to two 64-bit words, most significant first.
toWords64 :: UUID -> (Word64, Word64)
toWords64 u = (word hi, word lo)
where
(hi, lo) = splitAt 16 (filter (/= '-') (toString u))
word = List.foldl' (\acc c -> acc `shiftL` 4 .|. fromIntegral (digitToInt c)) 0
-----------------------------------------------------------------------------
-- | Build a t'UUID' from two 64-bit words, most significant first.
fromWords64 :: Word64 -> Word64 -> UUID
fromWords64 hi lo = UUID $ toMisoString $ intercalate "-" [a, b, c, d, e]
where
hex w = [ intToDigit (fromIntegral (w `shiftR` n .&. 0xf)) | n <- [60, 56 .. 0] ]
(a, r1) = splitAt 8 (hex hi <> hex lo)
(b, r2) = splitAt 4 r1
(c, r3) = splitAt 4 r2
(d, e) = splitAt 4 r3
-----------------------------------------------------------------------------
-- | The nil UUID, as defined in <http://tools.ietf.org/html/rfc4122 RFC 4122>.
-- It is a UUID of all zeros.
nil :: UUID
nil = UUID $ toMisoString nilPattern
-----------------------------------------------------------------------------
-- | Returns True if the passed-in UUID is the 'nil' UUID.
null :: UUID -> Bool
null = (== nil)
-----------------------------------------------------------------------------
instance ToMisoString UUID where
toMisoString (UUID s) = s
-----------------------------------------------------------------------------
instance FromMisoString UUID where
fromMisoStringEither = first show . parse
-----------------------------------------------------------------------------
instance ToJSON UUID where
toJSON = toJSON . toMisoString
-----------------------------------------------------------------------------
instance FromJSON UUID where
parseJSON = either fail pure . fromMisoStringEither <=< parseJSON
-----------------------------------------------------------------------------
instance ToJSVal UUID where
toJSVal = toJSVal . toMisoString
-----------------------------------------------------------------------------
instance FromJSVal UUID where
fromJSVal = fmap (JSON.parseMaybe parseJSON =<<) . fromJSVal
-----------------------------------------------------------------------------
instance Router UUID where
fromRoute = pure . toPath . toMisoString
routeParser = capture
-----------------------------------------------------------------------------
-- | Generate a v4 'UUID' using a cryptographically secure random number generator.
nextRandom :: IO UUID
nextRandom = UUID <$> (fromJSValUnchecked =<< (jsg "crypto" # "randomUUID") ())
-----------------------------------------------------------------------------