packages feed

pfile-0.1.0.0: src/PFile/Profile/Internal/Serialization.hs

{- |
Module:      PFile.Profile.Internal.Serialization
Copyright:   (c) 2024 Illia Shkroba
License:     BSD3
Maintainer:  Illia Shkroba <is@pjwstk.edu.pl>
Stability:   unstable
Portability: non-portable (Non-Unix systems are not supported)

Types and functions for profiles serialization.
-}

{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE OverloadedStrings #-}

module PFile.Profile.Internal.Serialization
  ( load
  , showLoadError
  , LoadError (..)
  , dump
  , showDumpError
  , DumpError (..)
  ) where

import           Data.Aeson
  ( eitherDecodeFileStrict
  , encodeFile
  )
import           PFile.Aeson                    (encodePretty)
import           PFile.Env                      (Env)
import           PFile.Error                    (liftIOWithError, modifyError)
import qualified PFile.Log                      as Log
import qualified PFile.Path                     as Path
import           PFile.Profile.Internal.Profile
  ( Name (..)
  , Profile (..)
  , profileState
  )
import           Protolude

-- | Load 'Profile' named 'Name' from its
-- 'PFile.Profile.Internal.Profile.profileState'.
--
-- @since 0.1.0.0
load ::
     (MonadReader Env m, MonadError LoadError m, MonadIO m) => Name -> m Profile
load name = do
  statePath <- profileState name
  Log.info
    $  "Load profile \"" <> unName name <> "\" from state: "
    <> Path.showAbsolute statePath
  profile <- eitherDecodeFileStrict (Path.unAbsolute statePath)
    `liftIOWithError` LoadError statePath
    >>= either (throwError . DecodeError statePath) pure
  Log.info
    $  "Loaded profile \"" <> unName name <> "\":\n"
    <> encodePretty profile
  pure profile

showLoadError :: LoadError -> Text
showLoadError = \case
  LoadError path cause
    -> "Unable to load profile from " <> Path.showAbsolute path
    <> " because of: " <> show cause
  DecodeError path cause
    -> "Unable to decode profile from " <> Path.showAbsolute path
    <> " because of: \"" <> toS cause <> "\"."

-- | Error thrown by 'load'.
--
-- @since 0.1.0.0
data LoadError
  = LoadError !Path.Absolute !IOException
  -- ^ 'IOException' was encountered during 'eitherDecodeFileStrict'.
  | DecodeError !Path.Absolute ![Char]
  -- ^ Decoding error was encountered during 'eitherDecodeFileStrict'.

-- | Dump 'Profile' to its 'PFile.Profile.Internal.Profile.profileState'.
--
-- @since 0.1.0.0
dump ::
     (MonadReader Env m, MonadError DumpError m, MonadIO m) => Profile -> m ()
dump profile@Profile {name} = do
  Log.info
    $  "Dump profile \"" <> unName name <> "\":\n"
    <> encodePretty profile
  statePath <- profileState name
  Path.createParent statePath
    & modifyError CreateParentInDumpError
  encodeFile (Path.unAbsolute statePath) profile
    `liftIOWithError` DumpError statePath
  Log.info
    $  "Dumped profile \"" <> unName name <> "\" to state: "
    <> Path.showAbsolute statePath

showDumpError :: DumpError -> Text
showDumpError = \case
  CreateParentInDumpError cause -> Path.showCreateParentError cause
  DumpError path cause
    -> "Unable to dump profile to " <> Path.showAbsolute path
    <> " because of: " <> show cause

-- | Error thrown by 'dump'.
--
-- @since 0.1.0.0
data DumpError
  = CreateParentInDumpError !Path.CreateParentError
  -- ^ Unable to create a parent directory for
  -- 'PFile.Profile.Internal.Profile.profileState'.
  | DumpError !Path.Absolute !IOException
  -- ^ 'IOException' was encountered during 'encodeFile'.