neuron-1.0.0.0: src/app/Neuron/Zettelkasten/ID/Scheme.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoImplicitPrelude #-}
-- TODO: Simplify or eliminate this module, now that date IDs are gone.
module Neuron.Zettelkasten.ID.Scheme
( nextAvailableZettelID,
genVal,
IDScheme (..),
IDConflict (..),
)
where
import Control.Monad.Except
import Data.GADT.Compare.TH
import Data.GADT.Show.TH
import qualified Data.Set as Set
import qualified Data.Text as T
import Data.UUID (UUID)
import qualified Data.UUID as UUID
import Data.UUID.V4 (nextRandom)
import Neuron.Zettelkasten.ID
import Relude
import Text.Megaparsec.Simple
import Text.Show
-- | The scheme to use when generating new IDs
data IDScheme a where
-- | Random IDs (default)
IDSchemeHash :: IDScheme UUID
-- | Custom ID (specified by the user)
IDSchemeCustom :: Text -> IDScheme ()
data IDConflict
= IDConflict_AlreadyExists
| IDConflict_HashConflict Text
| IDConflict_BadCustomID Text Text
deriving (Eq)
instance Show IDConflict where
show = \case
IDConflict_AlreadyExists ->
"A zettel with that ID already exists"
IDConflict_HashConflict s ->
"Hash conflict on " <> toString s <> "; try again"
IDConflict_BadCustomID s e ->
"The custom ID " <> toString s <> " is malformed: " <> toString e
-- | Produce a value that is required ahead to run an ID scheme.
genVal :: forall a. IDScheme a -> IO a
genVal = \case
IDSchemeHash ->
nextRandom
IDSchemeCustom _ ->
pure ()
-- | Create a new zettel ID based on the given scheme
--
-- This is a pure function, with all impure actions done in @genVal@
--
-- Ensures that new ID doesn't conflict with existing zettels.
nextAvailableZettelID ::
forall a.
-- Existing zettels
Set ZettelID ->
-- Seed value for the scheme
a ->
-- Scheme to use when generating an ID
IDScheme a ->
Either IDConflict ZettelID
nextAvailableZettelID zs val = \case
IDSchemeHash -> do
let s = T.take 8 $ UUID.toText val
if s `Set.member` (zettelIDSlug `Set.map` zs)
then throwError $ IDConflict_HashConflict s
else
either (error . toText) pure $
parse idParser "<random-hash>" s
IDSchemeCustom s -> runExcept $ do
zid <-
either (throwError . IDConflict_BadCustomID s) pure $
parse idParser "<next-id>" s
if zid `Set.member` zs
then throwError IDConflict_AlreadyExists
else pure zid
deriveGEq ''IDScheme
deriveGShow ''IDScheme