packages feed

neuron-0.4.0.0: src/app/Neuron/Zettelkasten/ID/Scheme.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoImplicitPrelude #-}

module Neuron.Zettelkasten.ID.Scheme 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.Time
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

data IDScheme a where
  IDSchemeDate :: Day -> IDScheme ()
  IDSchemeHash :: IDScheme UUID
  IDSchemeCustom :: Text -> IDScheme ()

data IDConflict
  = IDConflict_AlreadyExists
  | IDConflict_DateIDExhausted
  | 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_DateIDExhausted ->
      "Ran out of date ID indices for this day"
    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 to run an ID scheme.
genVal :: forall a. IDScheme a -> IO a
genVal = \case
  IDSchemeHash ->
    nextRandom
  IDSchemeDate _ ->
    pure ()
  IDSchemeCustom _ ->
    pure ()

-- | Create a new zettel ID based on the given scheme without conflicting with
-- the IDs of existing zettels.
nextAvailableZettelID ::
  forall a.
  -- Existing zettels
  Set ZettelID ->
  a ->
  IDScheme a ->
  Either IDConflict ZettelID
nextAvailableZettelID zs val = \case
  IDSchemeDate day -> do
    let dayIndices = nonEmpty $ sort $ flip mapMaybe (Set.toList zs) $ \case
          ZettelDateID d x
            | d == day -> Just x
          _ -> Nothing
    case last <$> dayIndices of
      Nothing -> pure $ ZettelDateID day 1
      Just 99 -> throwError IDConflict_DateIDExhausted
      Just idx -> pure $ ZettelDateID day (idx + 1)
  IDSchemeHash -> do
    let s = T.take 8 $ UUID.toText val
    if s `Set.member` (zettelIDText `Set.map` zs)
      then throwError $ IDConflict_HashConflict s
      else
        either (error . toText) (pure . ZettelCustomID) $
          parse customIDParser "<random-hash>" s
  IDSchemeCustom s -> runExcept $ do
    zid <-
      either (throwError . IDConflict_BadCustomID s) (pure . ZettelCustomID) $
        parse customIDParser "<next-id>" s
    if zid `Set.member` zs
      then throwError IDConflict_AlreadyExists
      else pure zid

deriveGEq ''IDScheme

deriveGShow ''IDScheme