packages feed

hscuid-1.0.0: lib/Web/Cuid.hs

{-# LANGUAGE CPP, ForeignFunctionInterface #-}

{-|
Stability: stable
Portability: portable

You can generate a new CUID inside any IO-enabled monad using this module's
one exported function:

>>> cuid <- newCuid
>>> print cuid
"ciaafthr00000qhpm0jp81gry"
-}
module Web.Cuid (
    Cuid, newCuid
) where

import Control.Monad (liftM)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Char (ord)
import Data.Monoid (mconcat, (<>))
import Data.IORef (IORef, newIORef, atomicModifyIORef')
import Data.String (fromString)
import Data.Text (Text)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Formatting (Format, base, fitRight, sformat, left, (%.))
import Network.HostName (getHostName)
import System.IO.Unsafe (unsafePerformIO)
import System.Random (randomRIO)

#if defined(mingw32_HOST_OS)
import System.Win32 (ProcessId, failIfZero)
#else
import System.Posix.Process (getProcessID)
#endif

-- | Convenience type so that you don't have to import Text downstream. Note that
-- this is strict Text.
type Cuid = Text

-- | Generate a new random CUID.
newCuid :: MonadIO m => m Cuid
newCuid = concatM [c, time, count, fingerprint, random, random] where
    -- The CUID starts with a letter so it's usable in HTML element IDs.
    c = return (fromString "c")

    -- The second chunk is the timestamp. Note that this means it is possible
    -- to determine the time a particular CUID was created.
    time = liftM (sformat number . millis) getPOSIXTime

    -- To avoid collisions on the same machine, add a global counter to each ID.
    count = liftM (sformat numberPadded) (postIncrement counter)

    -- To avoid collisions between separate machines, generate a 'fingerprint'
    -- from details which are hopefully unique to this machine - PID and hostname.
    fingerprint = do
        pid <- getPid
        hostname <- getHostName
        let hostSum = 36 + length hostname + sum (map ord hostname)
            twoOfNum = sformat (fitRight 2 %. number)
        return (twoOfNum pid <> twoOfNum hostSum)

    -- And some randomness for good measure. Note that System.Random is not a
    -- source of crypto-strength randomness.
    random = liftM (sformat numberPadded) (randomRIO (0, maxValue))

    -- Evaluate IO actions and concatenate their results.
    concatM actions = liftM mconcat (liftIO $ sequence actions)

    -- POSIX time library gives the result in fractional seconds.
    millis posix = round (posix * 1000)

-- CUID calls for a globally incrementing counter per machine. This is ugly,
-- but it satisfies the requirement.
counter :: IORef Int
counter = unsafePerformIO (newIORef 0)
-- Don't want two different counters being created because of inlining.
-- For more info: https://wiki.haskell.org/Top_level_mutable_state
{-# NOINLINE counter #-}

-- Increment the counter, and return the value before it was incremented.
postIncrement :: MonadIO m => IORef Int -> m Int
postIncrement c = liftIO (atomicModifyIORef' c incrementAndWrap) where
    incrementAndWrap count = (succ count `mod` maxValue, count)

-- These constants are to do with number formatting.
formatBase, blockSize, maxValue :: Int
formatBase = 36
blockSize = 4
maxValue = formatBase ^ blockSize

-- Number formatters for converting to the correct base and padding.
number, numberPadded :: Format Text (Int -> Text)
(number, numberPadded) = (toBase, toBlockSize %. toBase) where
    toBlockSize = left blockSize '0'
    toBase = base formatBase

-- Get the ID of the current process. This function has a platform-specific
-- implementation. Fun times.
getPid :: MonadIO m => m Int

#if defined(mingw32_HOST_OS)

foreign import stdcall unsafe "windows.h GetCurrentProcessId"
    c_GetCurrentProcessId :: IO ProcessId

getCurrentProcessId :: IO ProcessId
getCurrentProcessId = failIfZero "GetCurrentProcessId" c_GetCurrentProcessId

getPid = liftM fromIntegral (liftIO getCurrentProcessId)

#else

getPid = liftM fromIntegral (liftIO getProcessID)

#endif