packages feed

porcupine-core-0.1.0.0: src/Data/Locations/SerializationMethod.hs

{-# LANGUAGE DataKinds                 #-}
{-# LANGUAGE DefaultSignatures         #-}
{-# LANGUAGE DeriveAnyClass            #-}
{-# LANGUAGE DeriveFunctor             #-}
{-# LANGUAGE DeriveGeneric             #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts          #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE GADTs                     #-}
{-# LANGUAGE MultiParamTypeClasses     #-}
{-# LANGUAGE OverloadedStrings         #-}
{-# LANGUAGE Rank2Types                #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE StandaloneDeriving        #-}
{-# LANGUAGE TemplateHaskell           #-}
{-# LANGUAGE TupleSections             #-}
{-# LANGUAGE TypeOperators             #-}
{-# OPTIONS_GHC -Wall #-}

module Data.Locations.SerializationMethod where

import           Control.Lens                hiding ((:>))
import           Control.Funflow.ContentHashable
import           Data.Aeson                  as A
import qualified Data.Attoparsec.Lazy        as AttoL
import qualified Data.Binary.Builder         as BinBuilder
import qualified Data.ByteString             as BS
import qualified Data.ByteString.Lazy        as LBS
import qualified Data.ByteString.Streaming   as BSS
import           Data.Char                   (ord)
import qualified Data.Csv                    as Csv
import qualified Data.Csv.Builder            as CsvBuilder
import qualified Data.Csv.Parser             as CsvParser
import           Codec.Compression.Zlib      as Zlib
import           Data.DocRecord
import           Data.DocRecord.OptParse     (RecordUsableWithCLI)
import qualified Data.HashMap.Strict         as HM
import           Data.Locations.LocVariable
import           Data.Locations.LogAndErrors
import           Data.Maybe
import           Data.Monoid                 (First (..))
import qualified Data.Text                   as T
import qualified Data.Text.Encoding          as TE
import qualified Data.Text.Lazy              as LT
import qualified Data.Text.Lazy.Encoding     as LTE
import           Data.Typeable
import qualified Data.Vector                 as V
import qualified Data.Yaml                   as Y
import           Katip
import           GHC.Generics
import           Streaming
import qualified Streaming.Prelude           as S
import qualified Streaming.Zip               as SZip


-- | A file extension
type FileExt = T.Text

type FromAtomicFn' i a = i -> Either String a

-- | How to read an @a@ from some identified type @i@, which is meant to be a
-- general-purpose intermediate representation, like 'A.Value'.
data FromAtomicFn a =
  forall i. (Typeable i) => FromAtomicFn (FromAtomicFn' i a)
deriving instance Functor FromAtomicFn

instance Show (FromAtomicFn a) where
  show _ = "<FromAtomicFn>"

fromAtomicFn
  :: forall i a. (Typeable i)
  => [Maybe FileExt]
  -> FromAtomicFn' i a
  -> HM.HashMap (TypeRep,Maybe FileExt) (FromAtomicFn a)
fromAtomicFn exts f = HM.fromList $ map (\ext -> ((argTypeRep,ext), FromAtomicFn f)) exts
  where
    argTypeRep = typeOf (undefined :: i)

allFromAtomicFnsWithType :: forall i ext a. (Typeable i)
                         => HM.HashMap (TypeRep,Maybe ext) (FromAtomicFn a)
                         -> [(ext, FromAtomicFn' i a)]
allFromAtomicFnsWithType = mapMaybe fltr . HM.toList
  where
    wanted = typeOf (undefined :: i)
    fltr ((_,Nothing),_) = Nothing
    fltr ((tr,Just ext), FromAtomicFn (f :: FromAtomicFn' i' a))
      | tr == wanted = case eqT :: Maybe (i:~:i') of
          Just Refl -> Just (ext, f)
          Nothing   -> error $ "allFromAtomicFnsWithType: some function doesn't deal with type "
                        ++ show wanted ++ " when it should"
      | otherwise = Nothing


type FromStreamFn' i a =
  forall m. (LogMask m) => Stream (Of i) m () -> m a

-- | How to read an @a@ from some @Stream (Of i) m r@
data FromStreamFn a =
  forall i. (Typeable i) => FromStreamFn (FromStreamFn' i a)

instance Functor FromStreamFn where
  fmap f (FromStreamFn g) = FromStreamFn $ \s -> do
    f <$> g s

instance Show (FromStreamFn a) where
  show _ = "<FromStreamFn>"

fromStreamFn
  :: forall i a. (Typeable i)
  => [Maybe FileExt]
  -> FromStreamFn' i a
  -> HM.HashMap (TypeRep,Maybe FileExt) (FromStreamFn a)
fromStreamFn exts f = HM.fromList $ map (\ext -> ((argTypeRep,ext), FromStreamFn f)) exts
  where
    argTypeRep = typeOf (undefined :: i)

newtype FromStreamFn'' i a = FromStreamFn'' (FromStreamFn' i a)

allFromStreamFnsWithType :: forall i ext a. (Typeable i)
                         => HM.HashMap (TypeRep,Maybe ext) (FromStreamFn a)
                         -> [(ext, FromStreamFn'' i a)]
allFromStreamFnsWithType = mapMaybe fltr . HM.toList
  where
    wanted = typeOf (undefined :: i)
    fltr ((_,Nothing),_) = Nothing
    fltr ((tr,Just ext), FromStreamFn (f :: FromStreamFn' i' a))
      | tr == wanted = case eqT :: Maybe (i:~:i') of
          Just Refl -> Just (ext, FromStreamFn'' f)
          Nothing   -> error $ "allFromStreamFnsWithType: some function doesn't deal with type "
                        ++ show wanted ++ " when it should"
      | otherwise = Nothing

-- | A function to read @a@ from a 'DocRec'
data ReadFromConfigFn a = forall rs. (Typeable rs) => ReadFromConfigFn (DocRec rs -> a)
deriving instance Functor ReadFromConfigFn

instance Show (ReadFromConfigFn a) where
  show _ = "<ReadFromConfigFn>"

-- | Here, "serial" is short for "serialization method". 'SerialReaders' is the
-- **covariant** part of 'SerialsFor'. It describes the different ways a serial
-- can be used to obtain data.
data SerialReaders a = SerialReaders
  { -- TODO: Establish whether we should remove readersFromAtomic? It is often
    -- equivalent to reading from a stream of just one element, and therefore
    -- mostly duplicates code.
    _serialReadersFromAtomic ::
        HM.HashMap (TypeRep,Maybe FileExt) (FromAtomicFn a)
       -- ^ How to read data from an intermediate type (like 'A.Value' or
       -- 'T.Text'). As much as possible these intermediate atomic
       -- representations should be **strict**.
  , _serialReadersFromStream ::
        HM.HashMap (TypeRep,Maybe FileExt) (FromStreamFn a)
       -- ^ How to read data from a stream of intermediate data types (like
       -- strict ByteStrings). Each one of them being strict as much as
       -- possible.
  }
  deriving (Functor, Show)

makeLenses ''SerialReaders

instance Semigroup (SerialReaders a) where
  SerialReaders a s <> SerialReaders a' s' =
    SerialReaders (HM.unionWith const a a') (HM.unionWith const s s')
instance Monoid (SerialReaders a) where
  mempty = SerialReaders mempty mempty

-- | How to turn an @a@ into some identified type @i@, which is meant to a
-- general purpose intermediate representation, like 'A.Value' or even 'T.Text'.
data ToAtomicFn a =
  forall i. (Typeable i) => ToAtomicFn (a -> i)

instance Show (ToAtomicFn a) where
  show _ = "<ToAtomicFn>"

toAtomicFn :: forall i a. (Typeable i)
           => [Maybe FileExt]
           -> (a -> i)
           -> HM.HashMap (TypeRep,Maybe FileExt) (ToAtomicFn a)
toAtomicFn exts f = HM.fromList $ map (\ext -> ((argTypeRep,ext), ToAtomicFn f)) exts
  where
    argTypeRep = typeOf (undefined :: i)

allToAtomicFnsWithType :: forall i ext a. (Typeable i)
                         => HM.HashMap (TypeRep,Maybe ext) (ToAtomicFn a)
                         -> [(ext, a -> i)]
allToAtomicFnsWithType = mapMaybe fltr . HM.toList
  where
    wanted = typeOf (undefined :: i)
    fltr ((_,Nothing),_) = Nothing
    fltr ((tr,Just ext), ToAtomicFn (f :: a -> i'))
      | tr == wanted = case eqT :: Maybe (i:~:i') of
          Just Refl -> Just (ext, f)
          Nothing   -> error $ "allToAtomicFnsWithType: some function doesn't deal with type "
                        ++ show wanted ++ " when it should"
      | otherwise = Nothing

-- -- | How to turn an @a@ into some @Stream (Of i) m ()@
-- data ToStreamFn a =
--   forall i. (Typeable i)
--   => ToStreamFn (forall m. (LogMask m)
--                  => a -> Stream (Of i) m ())

-- instance Show (ToStreamFn a) where
--   show _ = "<ToStreamFn>"

-- singletonToStreamFn
--   :: forall i a. (Typeable i)
--   => Maybe FileExt
--   -> (forall m. (LogMask m) => a -> Stream (Of i) m ())
--   -> HM.HashMap (TypeRep,Maybe FileExt) (ToStreamFn a)
-- singletonToStreamFn ext f = HM.singleton (argTypeRep,ext) (ToStreamFn f)
--   where argTypeRep = typeOf (undefined :: i)

-- | The contravariant part of 'ReadFromConfigFn'. Permits to write default values
-- of the input config
data WriteToConfigFn a = forall rs. (Typeable rs, RecordUsableWithCLI rs)
                      => WriteToConfigFn (a -> DocRec rs)

instance Show (WriteToConfigFn a) where
  show _ = "<WriteToConfigFn>"

-- | The writing part of a serial. 'SerialWriters' describes the different ways
-- a serial can be used to serialize (write) data.
data SerialWriters a = SerialWriters
  { _serialWritersToAtomic :: HM.HashMap (TypeRep,Maybe FileExt) (ToAtomicFn a)
      -- ^ How to write the data to an intermediate type (like 'A.Value'). As
      -- much as possible this intermediate type should be **lazy**.

  -- , _serialWritersToStream :: HM.HashMap (TypeRep,Maybe FileExt) (ToStreamFn a)
  --     -- ^ How to write the data to an external file or storage.
  }
  deriving (Show)

makeLenses ''SerialWriters

instance Semigroup (SerialWriters a) where
  SerialWriters a <> SerialWriters a' = SerialWriters (HM.unionWith const a a')
instance Monoid (SerialWriters a) where
  mempty = SerialWriters mempty

instance Contravariant SerialWriters where
  contramap f sw = SerialWriters
    { _serialWritersToAtomic = fmap (\(ToAtomicFn f') -> ToAtomicFn $ f' . f)
                               (_serialWritersToAtomic sw)
    -- , _serialWritersToStream = fmap (\(ToStreamFn f') -> ToStreamFn $ f' . f)
    --                            (_serialWritersToStream sw)
    }

-- | Links a serialization method to a prefered file extension, if this is
-- relevant.
class SerializationMethod serial where
  -- | If @Just x@, @x@ should correspond to one of the keys in
  -- _serialReadersFromStream or _serialWritersToAtomic.
  getSerialDefaultExt :: serial -> Maybe FileExt
  getSerialDefaultExt _ = Nothing

-- | Tells whether some type @a@ can be serialized by some _serial_ (serialization
-- method).
class (SerializationMethod serial) => SerializesWith serial a where
  getSerialWriters :: serial -> SerialWriters a

-- | Tells whether some type @a@ can be deserialized by some _serial_
-- (serialization method).
class (SerializationMethod serial) => DeserializesWith serial a where
  getSerialReaders :: serial -> SerialReaders a

-- * Serialization to/from JSON and YAML, which both use the same intermediary
-- type, Data.Aeson.Value

-- | Has 'SerializesWith' & 'DeserializesWith' instances that permits to
-- store/load JSON and YAML files and 'A.Value's.
data JSONSerial = JSONSerial  -- ^ Expects @.json@ files by default, but supports
                              -- @.yaml@/@.yml@ files too "for free"
                | YAMLSerial  -- ^ Expects @.yaml@/@.yml@ files by default, but
                              -- supports @.json@ files too "for free"

-- | For when you want a JSON **only** or YAML **only** serialization, but tied to a
-- specific extension. It's more restrictive than 'JSONSerial' in the sense that
-- JSONSerialWithExt cannot read from values from the configuration (because in
-- the config we only have an Aeson Value, without an associated extension, so
-- we cannot know for sure this Value corresponds to the expected extension)
data JSONSerialWithExt = JSONSerialWithExt FileExt
                            -- ^ Expects files of a given extension, ONLY
                            -- formatted in JSON (YAML not provided "for free")
                       | YAMLSerialWithExt FileExt
                            -- ^ Expects files of a given extension, ONLY
                            -- formatted in YAML (JSON not provided "for free")

instance SerializationMethod JSONSerial where
  getSerialDefaultExt JSONSerial = Just "json"
  getSerialDefaultExt YAMLSerial = Just "yaml"

instance SerializationMethod JSONSerialWithExt where
  getSerialDefaultExt (JSONSerialWithExt e) = Just e
  getSerialDefaultExt (YAMLSerialWithExt e) = Just e

-- | To lazy bytestring of JSON
toAtomicJSON, toAtomicYAML
  :: ToJSON a
  => [FileExt] -> HM.HashMap (TypeRep, Maybe FileExt) (ToAtomicFn a)
toAtomicJSON exts =
  toAtomicFn (map Just exts) A.encode

-- | To lazy bytestring of YAML
toAtomicYAML exts =
  toAtomicFn (map Just exts) $ LBS.fromStrict . Y.encode

instance (ToJSON a) => SerializesWith JSONSerial a where
  getSerialWriters _srl = mempty
    { _serialWritersToAtomic =
        toAtomicFn [Nothing] A.toJSON  -- To A.Value, doesn't need an extension
        <> toAtomicJSON ["json"]
        <> toAtomicYAML ["yaml","yml"] }

instance (ToJSON a) => SerializesWith JSONSerialWithExt a where
  getSerialWriters (JSONSerialWithExt ext) = mempty
    { _serialWritersToAtomic = toAtomicJSON [ext] }
  getSerialWriters (YAMLSerialWithExt ext) = mempty
    { _serialWritersToAtomic = toAtomicYAML [ext] }

parseJSONEither :: (A.FromJSON t) => A.Value -> Either String t
parseJSONEither x = case A.fromJSON x of
  A.Success s -> Right s
  A.Error r   -> Left r
{-# INLINE parseJSONEither #-}

-- | From strict bytestring of JSON
fromAtomicJSON, fromAtomicYAML
  :: FromJSON a
  => [FileExt] -> HM.HashMap (TypeRep, Maybe FileExt) (FromAtomicFn a)
fromAtomicJSON exts =
  fromAtomicFn (map Just exts) A.eitherDecodeStrict

-- | From strict bytestring of YAML
fromAtomicYAML exts =
  fromAtomicFn (map Just exts) $
    over _Left displayException . Y.decodeEither'

-- | From a stream of strict bytestrings of JSON
fromJSONStream, fromYAMLStream
  :: FromJSON a
  => [FileExt] -> HM.HashMap (TypeRep, Maybe FileExt) (FromStreamFn a)
fromJSONStream exts = fromStreamFn (map Just exts) $ \strm -> do
  BSS.toStrict_ (BSS.fromChunks strm) >>= decodeJ
    -- TODO: Enhance this so we don't have to accumulate the whole
  where
    decodeJ x = case A.eitherDecodeStrict x of
      Right y  -> return y
      Left msg -> throwWithPrefix msg

-- | From a stream of strict bytestrings of YAML
fromYAMLStream exts = fromStreamFn (map Just exts) (decodeYAMLStream . BSS.fromChunks)

decodeYAMLStream :: (LogThrow m, FromJSON a) => BSS.ByteString m () -> m a
decodeYAMLStream strm = do
  BSS.toStrict_ strm >>= decodeY -- TODO: same than above
  where
    decodeY x = case Y.decodeEither' x of
      Right y  -> return y
      Left exc -> logAndThrowM exc

instance (FromJSON a) => DeserializesWith JSONSerial a where
  getSerialReaders _srl = mempty
    { _serialReadersFromAtomic =
        fromAtomicFn [Nothing] parseJSONEither -- From A.Value, doesn't need an
                                               -- extension
        <> fromAtomicJSON ["json"]
        <> fromAtomicYAML ["yaml","yml"]
    , _serialReadersFromStream =
        fromJSONStream ["json"]
        -- TODO: Add reading from a stream of JSON objects (which would
        -- therefore be considered a JSON array of objects?)
        <>
        fromYAMLStream ["yaml","yml"] }

instance (FromJSON a) => DeserializesWith JSONSerialWithExt a where
  getSerialReaders (JSONSerialWithExt ext) = mempty
    { _serialReadersFromAtomic = fromAtomicJSON [ext]
    , _serialReadersFromStream = fromJSONStream [ext] }
  getSerialReaders (YAMLSerialWithExt ext) = mempty
    { _serialReadersFromAtomic = fromAtomicYAML [ext]
    , _serialReadersFromStream = fromYAMLStream [ext] }


-- * Helpers to write to and from binary representations

class ToBinaryBuilder serial a where
  toBinaryBuilder :: serial -> a -> BinBuilder.Builder

-- | Recommendation: instances should implement fromLazyByteString and
-- fromByteStream whenever possible.
class FromByteStream serial a where
  fromLazyByteString :: serial -> LBS.ByteString -> Either String a
  fromLazyByteString s = fromStrictByteString s . LBS.toStrict
  fromStrictByteString :: serial -> BS.ByteString -> Either String a
  fromStrictByteString s = fromLazyByteString s . LBS.fromStrict
  fromByteStream :: (LogThrow m) => serial -> BSS.ByteString m () -> m a
  fromByteStream s bss = do
    bs <- BSS.toLazy_ bss  -- This default implementation is stricter than
                                 -- it needs to be
    case fromLazyByteString s bs of
      Left msg -> throwWithPrefix msg
      Right y -> return y

getSerialWriters_ToBinaryBuilder
  :: (SerializationMethod srl, ToBinaryBuilder srl a) => srl -> SerialWriters a
getSerialWriters_ToBinaryBuilder srl = mempty
  { _serialWritersToAtomic =
    toAtomicFn [getSerialDefaultExt srl] $
      BinBuilder.toLazyByteString . toBinaryBuilder srl }

getSerialReaders_FromByteStream
  :: (SerializationMethod srl, FromByteStream srl a) => srl -> SerialReaders a
getSerialReaders_FromByteStream srl = mempty
    { _serialReadersFromStream =
        fromStreamFn [getSerialDefaultExt srl] (fromByteStream srl . BSS.fromChunks)
    , _serialReadersFromAtomic =  -- From strict bytestring
        fromAtomicFn [getSerialDefaultExt srl] (fromStrictByteString srl)
    }

-- * Serialization to/from CSV

-- | Data with header not known in advance, that can be converted to/from CSV,
-- keeping track of the header
data Tabular a = Tabular
  { tabularHeader :: Maybe [T.Text]
  , tabularData :: a }
  deriving (Show, Generic, ToJSON, FromJSON)

-- | Data that can be converted to/from CSV, with previous knowledge of the
-- headers
newtype Records a = Records { fromRecords :: a }

instance (Show a) => Show (Records a) where
  show = show . fromRecords

instance (ToJSON a) => ToJSON (Records a) where
  toJSON = toJSON . fromRecords

instance (FromJSON a) => FromJSON (Records a) where
  parseJSON = fmap Records . parseJSON

-- | Can serialize and deserialize any @Tabular a@ from a CSV file
data CSVSerial = CSVSerial
  { csvSerialExt       :: FileExt
    -- ^ The extension to use (csv, tsv, txt, etc.)
  , csvSerialHasHeader :: Bool
    -- ^ The csv file contains a header (to skip or to read/write). Must be True
    -- if we want to read 'Records' from it
  , csvSerialDelimiter :: Char
    -- ^ The character (@,@, @\t@, etc.) to use as a field delimiter.
  }

instance SerializationMethod CSVSerial where
  getSerialDefaultExt = Just . csvSerialExt

instance (Foldable f, Csv.ToRecord a) => ToBinaryBuilder CSVSerial (Tabular (f a)) where
  toBinaryBuilder (CSVSerial _ hasHeader delim) (Tabular mbHeader dat) =
    mbAddHeader $ foldMap encField dat
    where
      mbAddHeader | hasHeader = maybe id (<>) (encHeader <$> mbHeader)
                  | otherwise = id
      encodeOpts = Csv.defaultEncodeOptions {Csv.encDelimiter = fromIntegral $ ord delim}
      encHeader = CsvBuilder.encodeRecordWith encodeOpts
      encField = CsvBuilder.encodeRecordWith encodeOpts

instance (Foldable f, Csv.ToNamedRecord a, Csv.DefaultOrdered a)
  => ToBinaryBuilder CSVSerial (Records (f a)) where
  toBinaryBuilder (CSVSerial _ hasHeader delim) (Records dat) =
    mbAddHeader $ foldMap encField dat
    where
      mbAddHeader | hasHeader = (encHeader (Csv.headerOrder (undefined :: a)) <>)
                  | otherwise = id
      encodeOpts = Csv.defaultEncodeOptions {Csv.encDelimiter = fromIntegral $ ord delim}
      encHeader = CsvBuilder.encodeHeaderWith encodeOpts
      encField = CsvBuilder.encodeDefaultOrderedNamedRecordWith encodeOpts

instance (Csv.FromRecord a) => FromByteStream CSVSerial (Tabular (V.Vector a)) where
  fromLazyByteString (CSVSerial _ hasHeader delim) bs = do
    (mbHeader, rest) <- if hasHeader
      then case AttoL.parse (CsvParser.header delim') bs of
             AttoL.Fail _ _ err -> Left err
             AttoL.Done rest r  -> return (Just r, rest)
      else return (Nothing, bs)
    let mbHeader' = map TE.decodeUtf8 . V.toList <$> mbHeader
    Tabular mbHeader' <$> Csv.decodeWith decOpts Csv.NoHeader rest
    where
      delim' = fromIntegral $ ord delim
      decOpts = Csv.defaultDecodeOptions {Csv.decDelimiter=delim'}

instance (Csv.FromNamedRecord a) => FromByteStream CSVSerial (Records (V.Vector a)) where
  fromLazyByteString (CSVSerial _ hasHeader delim) bs =
    if not hasHeader then error "CANNOT USE ColNamed on CSV files without headers"
    else do
      (_, v) <- Csv.decodeByNameWith decOpts bs
      return $ Records v
    where
      decOpts = Csv.defaultDecodeOptions {Csv.decDelimiter=fromIntegral $ ord delim}

instance (Foldable f, Csv.ToRecord a) => SerializesWith CSVSerial (Tabular (f a)) where
  getSerialWriters = getSerialWriters_ToBinaryBuilder

instance (Foldable f, Csv.ToNamedRecord a, Csv.DefaultOrdered a)
  => SerializesWith CSVSerial (Records (f a)) where
  getSerialWriters = getSerialWriters_ToBinaryBuilder

instance (Csv.FromRecord a) => DeserializesWith CSVSerial (Tabular (V.Vector a)) where
  getSerialReaders = getSerialReaders_FromByteStream

instance (Csv.FromNamedRecord a) => DeserializesWith CSVSerial (Records (V.Vector a)) where
  getSerialReaders = getSerialReaders_FromByteStream

-- * "Serialization" to/from bytestrings

-- | ByteStringSerial is just a reader of strict ByteStrings and writer of lazy
-- ByteStrings. It's the simplest SerializationMethod possible
newtype ByteStringSerial = ByteStringSerial { bsSerialSpecificExt :: Maybe FileExt }

instance SerializationMethod ByteStringSerial where
  getSerialDefaultExt (ByteStringSerial ext) = ext

instance SerializesWith ByteStringSerial LBS.ByteString where
  getSerialWriters (ByteStringSerial ext) = mempty
    { _serialWritersToAtomic = toAtomicFn [ext] id }
    -- TODO: Add base64 encoding so it can be read/written from/to JSON strings
    -- too

-- We only deserialize *strict* bytestrings, in order not to hide the fact that
-- the data must be accumulated from the stream we read if you want to break
-- away from it

instance DeserializesWith ByteStringSerial BS.ByteString where
  getSerialReaders (ByteStringSerial ext) = mempty
    { _serialReadersFromAtomic =
        fromAtomicFn [ext] Right
    , _serialReadersFromStream =
        fromStreamFn [ext] S.mconcat_ }

-- * Serialization to/from plain text

-- | Can read from text files or raw input strings in the pipeline configuration
-- file. Should be used only for small files or input strings. If we should
-- accept only some extension, specify it. Else just use Nothing.
newtype PlainTextSerial = PlainTextSerial { plainTextSerialSpecificExt :: Maybe FileExt }

instance SerializationMethod PlainTextSerial where
  getSerialDefaultExt (PlainTextSerial ext) = ext

instance SerializesWith PlainTextSerial T.Text where
  getSerialWriters (PlainTextSerial ext) = mempty
    { _serialWritersToAtomic =
      toAtomicFn [Nothing] (\t -> LT.fromChunks [t]) -- To lazy text
      <> toAtomicFn [ext] (\t -> LTE.encodeUtf8 $ LT.fromChunks [t]) -- To lazy bytestring
      <> toAtomicFn [ext] toJSON  -- To A.Value
    }

instance SerializesWith PlainTextSerial LT.Text where
  getSerialWriters (PlainTextSerial ext) = mempty
    { _serialWritersToAtomic =
      toAtomicFn [Nothing] id -- To lazy text
      <> toAtomicFn [ext] LTE.encodeUtf8 -- To lazy bytestring
      <> toAtomicFn [ext] toJSON  -- To A.Value
    }

instance DeserializesWith PlainTextSerial T.Text where
  getSerialReaders (PlainTextSerial ext) = mempty
    { _serialReadersFromAtomic =
        fromAtomicFn [Nothing] Right
        <> fromAtomicFn [ext] parseJSONEither
        <> fromAtomicFn [ext] (Right . TE.decodeUtf8)
    , _serialReadersFromStream =
        fromStreamFn [ext] S.mconcat_
        <>
        fromStreamFn [ext] (fmap TE.decodeUtf8 . S.mconcat_)
    }

-- * Serialization of options

-- | Contains any set of options that should be exposed via the CLI
data RecOfOptions field where
  RecOfOptions :: (Typeable rs, RecordUsableWithCLI rs) => Rec field rs -> RecOfOptions field

type DocRecOfOptions = RecOfOptions DocField

-- | A serialization method used for options which can have a default value,
-- that can be exposed through the configuration.
data OptionsSerial a = forall rs. (Typeable rs, RecordUsableWithCLI rs)
                   => OptionsSerial (a -> DocRec rs) (DocRec rs -> a)
instance SerializationMethod (OptionsSerial a)
instance SerializesWith (OptionsSerial a) a where
  getSerialWriters (OptionsSerial f _) = mempty
    { _serialWritersToAtomic =
        toAtomicFn [Nothing] (RecOfOptions . f) }
instance DeserializesWith (OptionsSerial a) a where
  getSerialReaders (OptionsSerial _ (f :: DocRec rs -> a)) = mempty
    { _serialReadersFromAtomic =
        let conv :: DocRecOfOptions -> Either String a
            conv (RecOfOptions r) = case cast r of
              Just r' -> Right $ f r'
              Nothing -> Left "OptionsSerial: _serialReadersFromAtomic: Not the right fields"
        in fromAtomicFn [Nothing] conv }


-- * Combining serializers and deserializers into one structure

-- | Can serialize @a@ and deserialize @b@.
data SerialsFor a b = SerialsFor
  { _serialWriters        :: SerialWriters a
  , _serialReaders        :: SerialReaders b
  , _serialDefaultExt     :: First FileExt
  , _serialRepetitionKeys :: [LocVariable] }
  deriving (Show)

makeLenses ''SerialsFor

-- | An equivaluent of 'Void', to avoid orphan instances
data NoWrite

instance (Monad m) => ContentHashable m NoWrite where
  contentHashUpdate ctx _ = contentHashUpdate ctx ()

-- | Just for symmetry with 'NoWrite'
data NoRead = NoRead
  deriving (Eq, Ord, Show)

instance Semigroup NoRead where
  _ <> _ = NoRead
instance Monoid NoRead where
  mempty = NoRead

instance (Monad m) => ContentHashable m NoRead where
  contentHashUpdate ctx _ = contentHashUpdate ctx ()

-- | Can serialize and deserialize @a@. Use 'dimap' to transform it
type BidirSerials a = SerialsFor a a

-- | Can only serialize @a@. Use 'lmap' to transform it.
type PureSerials a = SerialsFor a NoRead

-- | Can only deserialize @a@. Use 'rmap' to transform it.
type PureDeserials a = SerialsFor NoWrite a

instance Profunctor SerialsFor where
  lmap f (SerialsFor sers desers ext rk) = SerialsFor (contramap f sers) desers ext rk
  rmap f (SerialsFor sers desers ext rk) = SerialsFor sers (fmap f desers) ext rk

instance Semigroup (SerialsFor a b) where
  SerialsFor s d ext rk <> SerialsFor s' d' ext' _ =
    SerialsFor (s<>s') (d<>d') (ext<>ext') rk
instance Monoid (SerialsFor a b) where
  mempty = SerialsFor mempty mempty mempty []

-- | Changes the serialization function used by default
setDefaultSerial :: FileExt -> SerialsFor a b -> SerialsFor a b
setDefaultSerial = set serialDefaultExt . First . Just

-- | Packs together ways to serialize and deserialize some data @a@
someBidirSerial :: (SerializesWith s a, DeserializesWith s a) => s -> BidirSerials a
someBidirSerial s =
  SerialsFor (getSerialWriters s) (getSerialReaders s) (First $ getSerialDefaultExt s) []

makeBidir :: PureSerials a -> PureDeserials a -> BidirSerials a
makeBidir (SerialsFor sers _ ext rk) (SerialsFor _ desers ext' _) =
  SerialsFor sers desers (ext<>ext') rk

-- | Packs together ways to serialize some data @a@
somePureSerial :: (SerializesWith s a) => s -> PureSerials a
somePureSerial s =
  SerialsFor (getSerialWriters s) mempty (First $ getSerialDefaultExt s) []

-- | Packs together ways to deserialize and deserialize some data @a@
somePureDeserial :: (DeserializesWith s a) => s -> PureDeserials a
somePureDeserial s =
  SerialsFor mempty (getSerialReaders s) (First $ getSerialDefaultExt s) []

eraseSerials :: SerialsFor a b -> PureDeserials b
eraseSerials (SerialsFor _ desers ext rk) = SerialsFor mempty desers ext rk

eraseDeserials :: SerialsFor a b -> PureSerials a
eraseDeserials (SerialsFor sers _ ext rk) = SerialsFor sers mempty ext rk


-- * Retrieve conversion functions from a 'SerialsFor' @a@ @b@

-- | Tries to get a conversion function to some type @i@
getToAtomicFn :: forall i a b. (Typeable i) => SerialsFor a b -> Maybe (a -> i)
getToAtomicFn ser = do
  ToAtomicFn (f :: a -> i') <-
    HM.lookup (typeOf (undefined :: i),Nothing) (ser ^. serialWriters . serialWritersToAtomic)
  case eqT :: Maybe (i' :~: i) of
    Just Refl -> return f
    Nothing -> error $ "getToAtomicFn: Some conversion function isn't properly indexed. Should not happen"

-- | Tries to get a conversion function from some type @i@
getFromAtomicFn :: forall i a b. (Typeable i) => SerialsFor a b -> Maybe (FromAtomicFn' i b)
getFromAtomicFn ser = do
  FromAtomicFn (f :: FromAtomicFn' i' b) <-
    HM.lookup (typeOf (undefined :: i),Nothing) (ser ^. serialReaders . serialReadersFromAtomic)
  case eqT :: Maybe (i' :~: i) of
    Just Refl -> return f
    Nothing -> error $ "getFromAtomicFn: Some conversion function isn't properly indexed. Should not happen"


-- * Serialization for compressed formats

-- | Wraps all the functions in the serial so for each serial (extension) @xxx@
-- supported, we know also support @xxxzlib@. Doesn't change the default
-- extension
addZlibSerials :: SerialsFor a b -> SerialsFor a b
addZlibSerials = over serialWriters (over serialWritersToAtomic editTA)
              . over serialReaders (over serialReadersFromAtomic editFA
                                  . over serialReadersFromStream editFS)
  where
    editTA hm = (hm <>) $ mconcat $ flip map (allToAtomicFnsWithType hm) $
      \(ext, f) ->
        toAtomicFn [Just $ ext <> "zlib"] $ Zlib.compress . f  -- Lazy bytestring
    editFA hm = (hm <>) $ mconcat $ flip map (allFromAtomicFnsWithType hm) $
      \(ext, f) ->
        fromAtomicFn [Just $ ext <> "zlib"] $
          f . LBS.toStrict . Zlib.decompress . LBS.fromStrict  -- Strict bytestring
    editFS hm = (hm <>) $ mconcat $ flip map (allFromStreamFnsWithType hm) $
      \(ext, FromStreamFn'' f) ->
        fromStreamFn [Just $ ext <> "zlib"] $
          f . BSS.toChunks . SZip.decompress SZip.defaultWindowBits . BSS.fromChunks

-- | Adds warnings when deserializing values /from a stream/
addDeserialWarnings :: (b -> [String]) -> SerialsFor a b -> SerialsFor a b
addDeserialWarnings f = serialReaders . serialReadersFromStream . traversed %~ addW
  where
    addW (FromStreamFn g) = FromStreamFn $ \s -> do
      a <- g s
      let warnings = f a
      mapM_ (logFM WarningS . logStr) warnings
      return a


-- -- | Traverses to the repetition keys stored in the access functions of a
-- -- 'SerialsFor'
-- serialsRepetitionKeys :: Traversal' (SerialsFor a b) [LocVariable]
-- serialsRepetitionKeys f (SerialsFor writers readers ext rk) =
--   rebuild <$> (serialWritersToOutputFile . traversed . writeToLocRepetitionKeys) f writers
--           <*> (serialReadersFromInputFile . traversed . readFromLocRepetitionKeys) f readers
--   where
--     rebuild w r = SerialsFor w r ext rk