packages feed

gamgee-1.0.0: src/Gamgee/Effects/JSONStore.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE DeriveAnyClass      #-}
{-# LANGUAGE DeriveGeneric       #-}
{-# LANGUAGE DerivingStrategies  #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE NoImplicitPrelude   #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE PolyKinds           #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell     #-}
{-# LANGUAGE TypeOperators       #-}

module Gamgee.Effects.JSONStore
  ( -- * Effect
    JSONStore (..)

    -- * Actions
  , jsonEncode
  , jsonDecode

    -- * Interpretation
  , runGamgeeJSONStore
  ) where

import qualified Data.Aeson               as Aeson
import qualified Gamgee.Effects.ByteStore as BS
import qualified Gamgee.Effects.Error     as Err
import qualified Gamgee.Token             as Token
import           Polysemy                 (Member, Sem)
import qualified Polysemy                 as P
import qualified Polysemy.Error           as P
import           Relude



----------------------------------------------------------------------------------------------------
-- An abstract JSON store effect that encodes and decodes JSON objects
----------------------------------------------------------------------------------------------------

data JSONStore o m a where
  JsonEncode :: o -> JSONStore o m ()
  JsonDecode :: JSONStore o m o

P.makeSem ''JSONStore


----------------------------------------------------------------------------------------------------
-- Gamgee Configuration
----------------------------------------------------------------------------------------------------

data Config = Config {
  configVersion  :: Word32
  , configTokens :: Token.Tokens
  }
  deriving stock    (Generic)
  deriving anyclass (Aeson.FromJSON, Aeson.ToJSON)

currentConfigVersion :: Word32
currentConfigVersion = 1

initialConfig :: Config
initialConfig = Config {
  configVersion = currentConfigVersion
  , configTokens = fromList []
  }


----------------------------------------------------------------------------------------------------
-- Interpret JSONStore backed by a ByteStore
----------------------------------------------------------------------------------------------------

runJSONStore :: (o -> Sem (BS.ByteStore : r) Config) -- ^ Function to map object to a JSON serializable value
             -> (Config -> Sem (BS.ByteStore : r) o) -- ^ Function to map a JSON value to the object
             -> (String -> Sem (BS.ByteStore : r) o) -- ^ Function to handle errors in decoding
             -> Sem (JSONStore o : r) a
             -> Sem (BS.ByteStore : r) a
runJSONStore convertE convertD handleError =
  P.reinterpret $ \case
    JsonEncode o -> do
      j <- convertE o
      BS.writeByteStore $ Aeson.encode j
    JsonDecode   -> do
      bytes <- BS.readByteStore
      let
        config = maybe (Right initialConfig) Aeson.eitherDecode' bytes
      either handleError convertD config

runGamgeeJSONStore :: Member (P.Error Err.EffError) r => Sem (JSONStore Token.Tokens : r) a -> Sem (BS.ByteStore : r) a
runGamgeeJSONStore = runJSONStore tokensToConfig jsonToTokens handleDecodeError
  where
    tokensToConfig :: Token.Tokens -> Sem r Config
    tokensToConfig ts = return $ Config { configVersion = currentConfigVersion, configTokens = ts }

    jsonToTokens :: Member (P.Error Err.EffError) r => Config -> Sem r Token.Tokens
    jsonToTokens cfg = if configVersion cfg == currentConfigVersion
                       then return (configTokens cfg)
                       else P.throw $ Err.UnsupportedConfigVersion $ configVersion cfg

    handleDecodeError :: Member (P.Error Err.EffError) r => String -> Sem r a
    handleDecodeError msg = P.throw $ Err.JSONDecodeError $ toText msg