packages feed

keyed-vals-0.1.0.0: src/KeyedVals/Handle/Aeson.hs

{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK prune not-home #-}

{- |
Copyright   : (c) 2018-2022 Tim Emiola
SPDX-License-Identifier: BSD3
Maintainer  : Tim Emiola <tim@emio.la>

Functions that help use Aeson to load and save JSON data using the
dictionary service
-}
module KeyedVals.Handle.Aeson (
  -- * decode/encode support
  decodeOr,
  decodeOr',
  decodeOrGone,
  decodeOrGone',
  jsonVal,
  jsonKey,
  webKey,
  appendWebKey,
  substWebKey,
  prependWebKey,

  -- * decode @ValsByKey@
  decodeJsonKeyKVs,
  decodeWebKeyKVs,

  -- * save @ValsByKey@ using a @Handle@
  saveKVs,
  saveKVs',
  saveJsonKeyKVs,
  saveHttpApiKVs,
  updateHttpApiKVs,
) where

import Data.Aeson (
  FromJSON (..),
  ToJSON (..),
  eitherDecodeStrict',
  encode,
 )
import Data.Bifunctor (bimap)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LBS
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as Text
import KeyedVals.Handle.Internal
import Web.HttpApiData (FromHttpApiData (..), ToHttpApiData (..))


-- | Encode JSON as a remote value.
jsonVal :: ToJSON a => a -> Val
jsonVal = encodeJSON


webKey :: ToHttpApiData a => a -> Key
webKey = toHeader


substWebKey :: ToHttpApiData a => a -> Key -> Key
substWebKey x template =
  let (prefix, afterPre) = B.breakSubstring mustache template
      suffix = B.drop (B.length mustache) afterPre
      result = prefix <> webKey x <> suffix
   in if B.isPrefixOf mustache afterPre then result else template


appendWebKey :: ToHttpApiData a => Key -> a -> Key -> Key
appendWebKey sep x template = template <> sep <> webKey x


prependWebKey :: ToHttpApiData a => Key -> a -> Key -> Key
prependWebKey sep x template = webKey x <> sep <> template


mustache :: ByteString
mustache = "{}"


jsonKey :: ToJSON a => a -> Key
jsonKey = encodeJSON


decodeOrGone ::
  FromJSON b =>
  Key ->
  Maybe Val ->
  Either HandleErr b
decodeOrGone key x =
  case decodeOr NotDecoded x of
    Left err -> Left err
    Right mb -> maybe (Left $ Gone key) Right mb


decodeOrGone' ::
  FromJSON b =>
  Key ->
  Either HandleErr (Maybe Val) ->
  Either HandleErr b
decodeOrGone' key = either Left $ decodeOrGone key


decodeOr' ::
  FromJSON b =>
  Either HandleErr (Maybe Val) ->
  Either HandleErr (Maybe b)
decodeOr' = either Left (decodeOr NotDecoded)


-- | Decode a JSON value, transforming decode errors to type @err@ if they occur.
decodeOr ::
  (FromJSON a) =>
  (Text -> err) ->
  Maybe Val ->
  Either err (Maybe a)
decodeOr f = maybe (pure Nothing) (firstEither (f . Text.pack) . eitherDecodeStrict')


{- | Decode a @ValsByKey@ serialized as JSON.

Both the key and value types are valid to deserialize as JSON.
-}
decodeJsonKeyKVs ::
  (Ord a, FromJSON a, FromJSON b) =>
  (Text -> c) ->
  ValsByKey ->
  Either c (Map a b)
decodeJsonKeyKVs f = firstEither f . decodeKVs' decoder
  where
    decoder = firstEither Text.pack . eitherDecodeStrict'


{- | Decode a @ValsByKey@ serialized as JSON.

- The key type is deserialized as HttpApiData.
- The value type is valid to deserialize as JSON.
-}
decodeWebKeyKVs ::
  (Ord a, FromHttpApiData a, FromJSON b) =>
  (Text -> c) ->
  ValsByKey ->
  Either c (Map a b)
decodeWebKeyKVs f = firstEither f . decodeKVs' parseHeader


{- | Decode a @ValsByKey@ with values serialized as JSON.

The value type is deserialized as JSON
-}
decodeKVs' ::
  (Ord a, FromJSON b) =>
  (Val -> Either Text a) ->
  ValsByKey ->
  Either Text (Map a b)
decodeKVs' decoder =
  let step _ _ (Left x) = Left x
      step k v (Right m) = case (decoder k, eitherDecodeStrict' v) of
        (Left x, _) -> Left x
        (_, Left y) -> Left $ Text.pack y
        (Right k', Right v') -> Right $ Map.insert k' v' m
   in Map.foldrWithKey step (Right Map.empty)


{- | Encode a @ValsByKey@ serialized as JSON.

- Both @'Key's@ and @'Val's@ are encoded using 'ToJSON'
-}
saveJsonKeyKVs ::
  (Ord a, ToJSON a, ToJSON b, Monad m) =>
  (HandleErr -> err) ->
  Handle m ->
  Key ->
  Map a b ->
  m (Either err ())
saveJsonKeyKVs f = saveKVs f encodeJSON


{- | Encode a @ValsByKey@ serialized as JSON, completely replacing the current value if present.

- @'Key's@ encode using 'HttpApiData'
- @'Val's@ encode using 'ToJSON'
-}
saveHttpApiKVs ::
  (Ord a, ToHttpApiData a, ToJSON b, Monad m) =>
  (HandleErr -> err) ->
  Handle m ->
  Key ->
  Map a b ->
  m (Either err ())
saveHttpApiKVs fromHandleErr = saveKVs fromHandleErr toHeader


-- | Like 'saveHttpApiKVs', but updates the keys rather than completely replacing it.
updateHttpApiKVs ::
  (Ord a, ToHttpApiData a, ToJSON b, Monad m) =>
  (HandleErr -> err) ->
  Handle m ->
  Key ->
  Map a b ->
  m (Either err ())
updateHttpApiKVs fromHandleErr = saveOrUpdateKVs True fromHandleErr toHeader


-- | Like 'saveKVs', with 'HandleErr' as the error type.
saveKVs' ::
  (Ord a, ToJSON b, Monad m) =>
  (a -> Val) ->
  Handle m ->
  Key ->
  Map a b ->
  m (Either HandleErr ())
saveKVs' = saveKVs id


{- |  Encode a 'Map' as a 'ValsByKey' with the @'Val's@ encoded as JSON.

- The @Map@ keys is encoded as @'Key's@ using the provided function,
- The @Map@ values are encoded as @'Val's@ by conversion to JSON.
- 'HandleErr' may be transformed to different error type
-}
saveKVs ::
  (Ord a, ToJSON b, Monad m) =>
  (HandleErr -> err) ->
  (a -> Val) ->
  Handle m ->
  Key ->
  Map a b ->
  m (Either err ())
saveKVs = saveOrUpdateKVs False


{- | Encode a 'Map' as a 'ValsByKey' with the @'Val's@ encoded as JSON.

- The @Map@ keys is encoded as @'Key's@ using the provided function,
- The @Map@ values are encoded as @'Val's@ by conversion to JSON.
- Allows 'HandleErr' to be converted to a different error type.
-}
saveOrUpdateKVs ::
  (Ord a, ToJSON b, Monad m) =>
  -- | when @True@, the dict is updated
  Bool ->
  (HandleErr -> err) ->
  (a -> Val) ->
  Handle m ->
  Key ->
  Map a b ->
  m (Either err ())
saveOrUpdateKVs _ _ _ _ _ dict | Map.size dict == 0 = pure $ Right ()
saveOrUpdateKVs update toErr fromKey h key dict =
  let asRemote =
        Map.fromList
          . fmap (bimap fromKey encodeJSON)
          . Map.toList
      saver = if update then hUpdateKVs else hSaveKVs
   in fmap (firstEither toErr) $ saver h key $ asRemote dict


firstEither :: (err1 -> err2) -> Either err1 b -> Either err2 b
firstEither f = either (Left . f) Right


-- | Encode JSON as a remote value.
encodeJSON :: ToJSON a => a -> Val
encodeJSON = LBS.toStrict . encode