packages feed

keyed-vals-0.2.0.0: src/KeyedVals/Handle/Typed.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_HADDOCK prune not-home #-}

{- |
Module      : KeyedVals.Handle.Typed
Copyright   : (c) 2022 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD3

Provides typeclasses, data types and combinators that constrain the @types@ of
keys and values accessed in the key-value store, whilst also linking them to specific
storage paths.
-}
module KeyedVals.Handle.Typed (
  -- * How use this module
  -- $use

  -- * type-and-path-constrained Handle combinators
  TypedKVs,
  countKVs,
  loadFrom,
  loadKVs,
  loadSlice,
  mayLoadFrom,
  modKVs,
  saveTo,
  saveKVs,
  updateKVs,

  -- * link key-value collections to a path
  PathOf (..),
  VaryingPathOf (..),
  rawPath,
  expand,
  prepend,
  append,

  -- * unify @PathOf@/@VaryingPathOf@
  TypedPath (..),
  TypedKey,
  pathKey,
  pathOf,
  key,
  (//),

  -- * module re-exports
  module KeyedVals.Handle,
  module KeyedVals.Handle.Codec,
) where

import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as C8
import Data.Functor ((<&>))
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
import Data.Proxy (Proxy (Proxy))
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
import KeyedVals.Handle (
  Handle,
  HandleErr (..),
  Key,
  Selection (..),
  close,
 )
import qualified KeyedVals.Handle as H
import KeyedVals.Handle.Codec
import Numeric.Natural


{- $use

 This section contains information on how to store data using this library.
 It starts with a preamble that shows the directives and imports used in the
 examples below

 > {\-# LANGUAGE DeriveGeneric #-\}
 > {\-# LANGUAGE DerivingVia #-\}
 > {\-# LANGUAGE OverloadedStrings #-\}
 > {\-# LANGUAGE StandaloneDeriving #-\}
 >
 > import Data.Aeson (FromJSON, ToJSON)
 > import Data.Text (Text)
 > import GHC.Generics (Generic)
 > import KeyedVals.Handle.Codec.Aeson (AesonOf(..))
 > import KeyedVals.Handle.Codec.HttpApiData (HttpApiDataOf(..))
 > import qualified KeyedVals.Handle.Mem as Mem
 > import KeyedVals.Handle.Typed
 > import Web.HttpApiData (FromHttpApiData, ToHttpApiData)

 Usage is fairly simple: 'PathOf' and possibly a 'VaryingPathOf' instance for
 storable data types are declared. They describe how the data type is encoded
 and decoded and where in the key-value store the data should be saved.

 For example, given this data type:

 > data Person = Person
 >   { name :: Text
 >   , age  :: Int
 >   } deriving (Eq, Show, Generic)

 Suppose each @Person@ is to be stored as JSON, via the @Generic@
 implementation, e.g,

 > instance FromJSON Person
 > instance ToJSON Person

 Also suppose each Person is stored with an Int key. To enable that, define a
 @newtype@ of @Int@, e.g,

 > newtype PersonID = PersonID Int
 >   deriving stock (Eq, Show)
 >   deriving (ToHttpApiData, FromHttpApiData, Num, Ord) via Int

 Also, suppose the collection of @Person@s keyed by @PersonID@ is stored at a
 specific fixed path in the key-value store. E.g, it is to be used as a runtime
 cache to speed up access to person data, so the path @/runtime/cache/persons@
 is used.

 To specify all this, first define @DecodeKV@ and @EncodeKV@ instances for
 @Person@:

 > deriving via (AesonOf Person) instance DecodeKV Person
 > deriving via (AesonOf Person) instance EncodeKV Person

 .. and do the same for @PersonID@:

 > deriving via (HttpApiDataOf Int) instance DecodeKV PersonID
 > deriving via (HttpApiDataOf Int) instance EncodeKV PersonID

 Then declare a @PathOf@ instance that binds the types together with the path:

 > instance PathOf Person where
 >   type KVPath Person = "/runtime/cache/persons"
 >   type KeyType Person = PersonID

 Note: the @DecodeKV@ and @EncodeKV@ deriving statements above were
 standalone for illustrative purposes. In most cases, they ought to be part
 of the deriving clause of the data type. E.g,

 > newtype AnotherID = AnotherID Int
 >   deriving stock (Eq, Show)
 >   deriving (ToHttpApiData, FromHttpApiData, Num, Ord) via Int
 >   deriving (DecodeKV, EncodeKV) via (HttpApiDataOf Int)

 Now one can load and fetch @Person@s from a storage backend using the functions
 in this module, e.g:

 > >>> handle <- Mem.new
 > >>> tim = Person { name = "Tim", age = 48 }
 > >>> saveTo handle (key 1) tim
 > Right ()
 > >>> loadFrom handle (key 1)
 > Right (Person { name = "Tim", age = 48 })

 Suppose that in addition to the main collection of @Person@s, it's necessary to
 store a distinct list of the friends of each @Person@. I.e, store a small keyed
 collection of @Person@s per person.

 One way to achieve is to store each such collection at a similar path, e.g
 suppose the friends for the person with @anID@ are stored at
 @/app/person/<anId>/friends@.

 This can be implemented using the existing types along with another newtype
 that has @PathOf@ and @VaryingPathOf@ instances as follows

 > newtype Friend = Friend Person
 >   deriving stock (Eq, Show)
 >   deriving (FromJSON, ToJSON, EncodeKV, DecodeKV) via Person
 >
 > instance PathOf Friend where
 >   type KVPath Friend = "/app/person/{}/friends"
 >   type KeyType Friend = FriendID -- as defined earlier
 >
 > instance VaryingPathOf Friend where
 >   type PathVar Friend = PersonID
 >   modifyPath _ = expand -- implements modifyPath by expanding the braces to PathVar

 This allows @Friends@ to be saved or fetched as follows:

 > >>> dave = Person { name = "Dave", age = 61 }
 > >>> saveTo handle (key 2) dave -- save in main person list
 > Right ()
 > >>> saveTo handle ( 1 // 2) (Friend dave) -- save as friend of tim (person 1)
 > Right ()
-}


-- | Obtains the path indicted by a 'TypedPath' as a 'Key'.
pathKey :: forall v. TypedPath v -> Key
pathKey Fixed = rawPath @v Proxy
pathKey (Variable part) = modifyPath @v Proxy part $ rawPath @v Proxy


-- | Derives the 'TypedPath' corresponding to a 'TypedKey'.
pathOf :: TypedKey v -> TypedPath v
pathOf (ToKey _) = Fixed
pathOf (Extended part _) = Variable part


{- | Represents a related group of @values@ each stored using a key of type
 @'KeyType' <value type>@
-}
type TypedKVs value = Map (KeyType value) value


{- | Links the storage path of a group of key-values to the types of the key and
   value.
-}
class
  ( KnownSymbol (KVPath value)
  , EncodeKV (KeyType value)
  , DecodeKV (KeyType value)
  ) =>
  PathOf value
  where
  -- * the storage path where the key-values are saved
  type KVPath value :: Symbol


  -- * the type of keys in this group of key-values
  type KeyType value


{- | Allow the storage path specifed by @'PathOf'@ to vary so that related
  groups of key-values may be stored in similar, related paths.
-}
class PathOf value => VaryingPathOf value where
  -- * @PathVar@ is specified to modify the path
  type PathVar value


  -- * Combines the raw 'KVPath' and @PathVar to obtain a new path.
  modifyPath :: Proxy value -> PathVar value -> Key -> Key


-- | Supports implementation of 'modifyPath' via substitution of @{}@ within the 'KVPath'.
expand :: EncodeKV a => a -> Key -> Key
expand x template =
  let (prefix, afterPre) = B.breakSubstring braces template
      suffix = B.drop (B.length braces) afterPre
      result = prefix <> encodeKV x <> suffix
   in if B.isPrefixOf braces afterPre then result else template


{- | Supports implementation of 'modifyPath'

Intended for used within the 'KVPath' of instances of 'VaryingPathOf', indicates where
 a variable should be substituted
-}
braces :: B.ByteString
braces = "{}"


-- | Supports implementaton of 'modifyPath'.
append :: EncodeKV a => Key -> a -> Key -> Key
append sep x template = template <> sep <> encodeKV x


-- | Supports implementaton of 'modifyPath'
prepend :: EncodeKV a => Key -> a -> Key -> Key
prepend sep x template = encodeKV x <> sep <> template


{- | A phantom type indicating either an instance of @'PathOf'@ or of
   @'VaryingPathOf'@.

 Allows combinators with similar behaviour for either to be defined just once,
 rather than separately for each typeclass.
-}
data TypedPath v where
  Fixed :: (PathOf v) => TypedPath v
  Variable :: (VaryingPathOf v) => PathVar v -> TypedPath v


-- | Similar to 'TypedPath', but includes an actual key along with the phantom type.
data TypedKey v where
  ToKey :: (PathOf v) => KeyType v -> TypedKey v
  Extended :: (VaryingPathOf v) => PathVar v -> KeyType v -> TypedKey v


-- | Constructs a simple 'TypedKey'.
key :: PathOf v => KeyType v -> TypedKey v
key = ToKey


-- | Constructs an extended 'TypedKey'.
infixr 5 //


(//) :: VaryingPathOf v => PathVar v -> KeyType v -> TypedKey v
a // b = Extended a b


instance EncodeKV (TypedKey v) where
  encodeKV (ToKey x) = encodeKV x
  encodeKV (Extended _ x) = encodeKV x


-- | Obtain the raw path to key-values that implement 'PathOf'.
rawPath :: forall value. PathOf value => Proxy value -> Key
rawPath _ = C8.pack $ symbolVal @(KVPath value) Proxy


-- | Like 'mayLoadFrom', but fails with 'Gone' if the value is missing.
loadFrom ::
  forall a m.
  (Monad m, DecodeKV a) =>
  Handle m ->
  TypedKey a ->
  m (Either HandleErr a)
loadFrom h aKey =
  let outer = pathKey $ pathOf aKey
      inner = encodeKV aKey
      full = outer <> "//" <> inner
   in H.loadFrom h outer inner <&> decodeOrGone' full


{- | Like @'KeyedValues.Handle.loadVal'@ with the key, path and value
 constrained by @'TypedKey'@
-}
mayLoadFrom ::
  forall a m.
  (Monad m, DecodeKV a, PathOf a) =>
  Handle m ->
  TypedKey a ->
  m (Either HandleErr (Maybe a))
mayLoadFrom h aKey =
  let outer = pathKey $ pathOf aKey
      inner = encodeKV aKey
   in H.loadFrom h outer inner <&> decodeOr'


{- | Like @'KeyedValues.Handle.saveTo'@ with the key, path and value constrained
 by @'TypedKey'@
-}
saveTo ::
  (Monad m, EncodeKV a, PathOf a) =>
  Handle m ->
  TypedKey a ->
  a ->
  m (Either HandleErr ())
saveTo h aKey someKVs =
  let outer = pathKey $ pathOf aKey
      inner = encodeKV aKey
   in H.saveTo h outer inner $ encodeKV someKVs


{- | Like @'KeyedValues.Handle.loadKVs'@ with the path and key values constrained
 by @'TypedPath'@
-}
loadKVs ::
  ( Monad m
  , DecodeKV a
  , DecodeKV (KeyType a)
  , Ord (KeyType a)
  ) =>
  Handle m ->
  TypedPath a ->
  m (Either HandleErr (TypedKVs a))
loadKVs h k = H.loadKVs h (pathKey k) >>= pure . orDecodeKVs


{- | Like @'KeyedValues.Handle.updateKVs'@ with the path and key-values
 constrained by @'TypedPath'@
-}
updateKVs ::
  (Monad m, EncodeKV a, EncodeKV (KeyType a), Ord (KeyType a)) =>
  Handle m ->
  TypedPath a ->
  TypedKVs a ->
  m (Either HandleErr ())
updateKVs h aKey = updateEncodedKVs h $ pathKey aKey


{- | Like @'KeyedValues.Handle.savedKVs'@ with the path and key-values
 constrained by @'TypedPath'@
-}
saveKVs ::
  (Monad m, EncodeKV a, EncodeKV (KeyType a), Ord (KeyType a)) =>
  Handle m ->
  TypedPath a ->
  TypedKVs a ->
  m (Either HandleErr ())
saveKVs h k = saveEncodedKVs h $ pathKey k


-- | Combines 'saveKVs' and 'loadKVs'
modKVs ::
  ( Monad m
  , EncodeKV a
  , EncodeKV (KeyType a)
  , DecodeKV a
  , DecodeKV (KeyType a)
  , Ord (KeyType a)
  ) =>
  (TypedKVs a -> TypedKVs a) ->
  Handle m ->
  TypedPath a ->
  m (Either HandleErr ())
modKVs modDict h aKey = do
  H.loadKVs h (pathKey aKey) >>= (pure . orDecodeKVs) >>= \case
    Left err -> pure $ Left err
    Right d -> saveKVs h aKey $ modDict d


{- | Like @'KeyedValues.Handle.loadSlice'@ with the path and key-values
 constrained by @'TypedPath'@
-}
loadSlice ::
  forall m a.
  ( Monad m
  , DecodeKV a
  , PathOf a
  , DecodeKV (KeyType a)
  , Ord (KeyType a)
  ) =>
  Handle m ->
  TypedPath a ->
  NonEmpty (KeyType a) ->
  m (Either HandleErr (TypedKVs a))
loadSlice h aKey keys = do
  let selection = AllOf $ fmap encodeKV keys
  H.loadSlice h (pathKey aKey) selection >>= pure . orDecodeKVs


orDecodeKVs ::
  (Ord a, DecodeKV a, DecodeKV b) =>
  Either HandleErr H.ValsByKey ->
  Either HandleErr (Map a b)
orDecodeKVs = either Left decodeKVs


{- | Like @'KeyedValues.Handle.countKVs'@ with the path and key-values
 constrained by @'TypedPath'@
-}
countKVs ::
  forall a m.
  ( Monad m
  , Ord (KeyType a)
  ) =>
  Handle m ->
  TypedPath a ->
  m (Either HandleErr Natural)
countKVs h k = H.countKVs h $ pathKey k