packages feed

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

{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE Rank2Types          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell     #-}
{-# LANGUAGE TypeOperators       #-}
{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE MultiParamTypeClasses #-}

module Data.Locations.VirtualFile
  ( LocationTreePathItem
  , module Data.Locations.SerializationMethod
  , Profunctor(..)
  , VirtualFile(..), LayeredReadScheme(..)
  , BidirVirtualFile, DataSource, DataSink
  , VFileIntent(..), VFileDescription(..)
  , RecOfOptions(..)
  , VFileImportance(..)
  , Cacher(..)
  , vfileSerials
  , vfileAsBidir, vfileImportance
  , vfileEmbeddedValue
  , getConvertedEmbeddedValue, setConvertedEmbeddedValue
  , tryMergeLayersForVFile
  , vfileOriginalPath, showVFileOriginalPath
  , vfileLayeredReadScheme
  , vfileVoided
  , vfiReadSuccess, vfiWriteSuccess, vfiError
  , dataSource, dataSink, bidirVirtualFile
  , makeSink, makeSource
  , documentedFile
  , withEmbeddedValue
  , usesLayeredMapping, canBeUnmapped, unmappedByDefault
  , usesCacherWithIdent
  , getVFileDescription
  , describeVFileAsSourceSink, describeVFileExtensions, describeVFileTypes
  , describeVFileAsRecOfOptions
  , clockVFileAccesses
  , defaultCacherWithIdent
  ) where

import           Control.Funflow
import           Control.Funflow.ContentHashable
import           Control.Lens
import           Data.Aeson                         (Value)
import           Data.Default
import           Data.DocRecord
import qualified Data.HashMap.Strict                as HM
import qualified Data.HashSet                       as HS
import           Data.List                          (intersperse)
import           Data.List.NonEmpty                 (NonEmpty (..))
import           Data.Locations.Accessors
import           Data.Locations.Loc
import           Data.Locations.LocationTree
import           Data.Locations.LocVariable
import           Data.Locations.Mappings            (HasDefaultMappingRule (..),
                                                     LocShortcut (..))
import           Data.Locations.SerializationMethod
import           Data.Maybe
import           Data.Monoid                        (First (..))
import           Data.Profunctor                    (Profunctor (..))
import           Data.Representable
import           Data.Semigroup                     (sconcat)
import           Data.Store                         (Store)
import qualified Data.Text                          as T
import           Data.Type.Equality
import           Data.Typeable
import           Katip


-- * The general 'VirtualFile' type

-- | Tells how the file is meant to be read
data LayeredReadScheme b where
  SingleLayerRead     :: LayeredReadScheme b
    -- No layered reading accepted
  LayeredRead         :: Semigroup b => LayeredReadScheme b
    -- A layered reading combining all the layers with (<>)
  LayeredReadWithNull :: Monoid b => LayeredReadScheme b
    -- Like 'LayeredRead', and handles mapping to no layer (mempty)

-- | Tells how the accesses to this 'VirtualFile' should be logged
data VFileImportance = VFileImportance
  { _vfiReadSuccess  :: Severity
  , _vfiWriteSuccess :: Severity
  , _vfiError        :: Severity
  , _vfiClockAccess  :: Bool }
  deriving (Show)

makeLenses ''VFileImportance

instance Default VFileImportance where
  def = VFileImportance InfoS NoticeS ErrorS False

-- | A virtual file in the location tree to which we can write @a@ and from
-- which we can read @b@.
data VirtualFile a b = VirtualFile
  { _vfileOriginalPath      :: [LocationTreePathItem]
  , _vfileLayeredReadScheme :: LayeredReadScheme b
  , _vfileEmbeddedValue     :: Maybe b
  , _vfileMappedByDefault   :: Bool
  , _vfileImportance        :: VFileImportance
  , _vfileDocumentation     :: Maybe T.Text
  , _vfileWriteCacher       :: Cacher (a, Either String SomeHashableLocs) ()
  , _vfileReadCacher        :: Cacher (Either String SomeHashableLocs) b
  , _vfileSerials           :: SerialsFor a b }

makeLenses ''VirtualFile

-- How we derive the default configuration for mapping some VirtualFile
instance HasDefaultMappingRule (VirtualFile a b) where
  getDefaultLocShortcut vf = if vf ^. vfileMappedByDefault
    then Just $
      case vf ^? vfileSerials . serialRepetitionKeys . filtered (not . null) of
        Nothing -> DeriveWholeLocFromTree defExt
        -- LIMITATION: For now we suppose that every reading/writing function in
        -- the serials has the same repetition keys
        Just rkeys -> DeriveLocPrefixFromTree $
          let toVar rkey = SoV_Variable rkey
              locStr = StringWithVars $ (SoV_String "-")
                       : intersperse (SoV_String "-") (map toVar rkeys)
          in PathWithExtension locStr $ T.unpack defExt
    else Nothing
    where
      defExt =
        case vf ^. vfileSerials . serialDefaultExt of
          First (Just ext) -> ext
          _                -> T.pack ""

-- For now, given the requirement of PTask, VirtualFile has to be a Monoid
-- because a VirtualTree also has to.
instance Semigroup (VirtualFile a b) where
  VirtualFile p l v m i d wc rc s <> VirtualFile _ _ _ _ _ _ _ _ s' =
    VirtualFile p l v m i d wc rc (s<>s')
instance Monoid (VirtualFile a b) where
  mempty = VirtualFile [] SingleLayerRead Nothing True def Nothing NoCache NoCache mempty

-- | The Profunctor instance is forgetful, it forgets about the mapping scheme
-- and the caching properties.
instance Profunctor VirtualFile where
  dimap f g (VirtualFile p _ v m i d _ _ s) =
    VirtualFile p SingleLayerRead (g <$> v) m i d NoCache NoCache $ dimap f g s


-- * Obtaining a description of how the 'VirtualFile' should be used

-- | Describes how a virtual file is meant to be used
data VFileIntent =
  VFForWriting | VFForReading | VFForRW | VFForCLIOptions
  deriving (Show, Eq)

-- | Gives the purpose of the 'VirtualFile'. Used to document the pipeline and check
-- mappings to physical files.
data VFileDescription = VFileDescription
  { vfileDescIntent             :: Maybe VFileIntent
                        -- ^ How is the 'VirtualFile' meant to be used
  , vfileDescEmbeddableInConfig :: Bool
                        -- ^ True if the data can be read directly from the
                        -- pipeline's config file
  , vfileDescEmbeddableInOutput :: Bool
                        -- ^ True if the data can be written directly in the
                        -- pipeline's output location tree
  , vfileDescPossibleExtensions :: [FileExt]
                        -- ^ Possible extensions for the files this virtual file
                        -- can be mapped to (prefered extension is the first)
  } deriving (Show)

-- | Gives a 'VirtualFileDescription'. To be used on files stored in the
-- VirtualTree.
getVFileDescription :: VirtualFile a b -> VFileDescription
getVFileDescription vf =
  VFileDescription intent readableFromConfig writableInOutput exts
  where
    (SerialsFor
      (SerialWriters toA)
      (SerialReaders fromA fromS)
      prefExt
      _) = _vfileSerials vf
    intent
      | HM.null fromA && HM.null fromS && HM.null toA = Nothing
      | HM.null fromA && HM.null fromS = Just VFForWriting
      | HM.null toA = Just VFForReading
      | Just _ <- vf ^. vfileEmbeddedValue = Just VFForCLIOptions
      | otherwise = Just VFForRW
    extSet = HS.fromList . mapMaybe snd . HM.keys
    otherExts = extSet toA <> extSet fromA <> extSet fromS
    exts = case prefExt of
             First (Just e) -> e:(HS.toList $ HS.delete e otherExts)
             _              -> HS.toList otherExts
    typeOfAesonVal = typeOf (undefined :: Value)
    readableFromConfig = (typeOfAesonVal,Nothing) `HM.member` fromA
    writableInOutput = (typeOfAesonVal,Nothing) `HM.member` toA

describeVFileAsSourceSink :: VirtualFile a b -> String
describeVFileAsSourceSink vf =
  sourceSink
  ++ (if vfileDescEmbeddableInConfig vfd then " (embeddable)" else "")
  ++ (case vf ^. vfileSerials.serialRepetitionKeys of
        [] -> ""
        lvs -> " repeated over " ++ concat
          (intersperse ", " (map (("\""++) . (++"\"") . unLocVariable) lvs)))
  where
    sourceSink = case vfileDescIntent vfd of
      Nothing -> ""
      Just i -> case i of
        VFForWriting -> "DATA SINK"
        VFForReading -> "DATA SOURCE"
        VFForRW -> "BIDIR VFILE"
        VFForCLIOptions -> "OPTION SOURCE"
    vfd = getVFileDescription vf

describeVFileAsRecOfOptions :: (Typeable a, Typeable b) => VirtualFile a b -> Int -> String
describeVFileAsRecOfOptions vf charLimit =
  case (vf ^? vfileAsBidir) >>= getConvertedEmbeddedValue of
    Just (RecOfOptions record :: DocRecOfOptions) ->
      "\n--- Fields ---\n" ++ T.unpack (showDocumentation charLimit record)
    _ -> ""

describeVFileExtensions :: VirtualFile a b -> String
describeVFileExtensions vf =
  "Accepts " ++ T.unpack (T.intercalate (T.pack ", ") (vfileDescPossibleExtensions vfd))
  where vfd = getVFileDescription vf

describeVFileTypes :: forall a b. (Typeable a, Typeable b) => VirtualFile a b -> Int -> String
describeVFileTypes _ charLimit
  | a == b = "Receives & emits: " ++ cap (show a)
  | b == typeOf (undefined :: NoRead) = "Receives " ++ cap (show a)
  | a == typeOf (undefined :: NoWrite) = "Emits " ++ cap (show b)
  | otherwise = "Receives " ++ cap (show a) ++ " & emits " ++ cap (show b)
  where
    cap x | length x >= charLimit = take charLimit x ++ "..."
          | otherwise = x
    a = typeOf (undefined :: a)
    b = typeOf (undefined :: b)

-- | Just for logs and error messages
showVFileOriginalPath :: VirtualFile a b -> String
showVFileOriginalPath = T.unpack . toTextRepr .  LTP . _vfileOriginalPath

-- | Embeds a value inside the 'VirtualFile'. This value will be considered the
-- base layer if we read extra @b@'s from external physical files.
withEmbeddedValue :: b -> VirtualFile a b -> VirtualFile a b
withEmbeddedValue = set vfileEmbeddedValue . Just

-- | Indicates that the file uses layered mapping
usesLayeredMapping :: (Semigroup b) => VirtualFile a b -> VirtualFile a b
usesLayeredMapping =
  vfileLayeredReadScheme .~ LayeredRead

-- | Indicates that the file uses layered mapping, and additionally can be left
-- unmapped (ie. mapped to null)
canBeUnmapped :: (Monoid b) => VirtualFile a b -> VirtualFile a b
canBeUnmapped =
  vfileLayeredReadScheme .~ LayeredReadWithNull

-- | Indicates that the file should be mapped to null by default
unmappedByDefault :: (Monoid b) => VirtualFile a b -> VirtualFile a b
unmappedByDefault =
    (vfileLayeredReadScheme .~ LayeredReadWithNull)
  . (vfileMappedByDefault .~ False)

-- | Gives a documentation to the 'VirtualFile'
documentedFile :: T.Text -> VirtualFile a b -> VirtualFile a b
documentedFile doc = vfileDocumentation .~ Just doc

-- | Sets the file's reads and writes to be cached. Useful if the file is bound
-- to a source/sink that takes time to respond, such as an HTTP endpoint, or
-- that uses an expensive text serialization method (like JSON or XML).
usesCacherWithIdent :: (ContentHashable Identity a, Store b)
                    => Int -> VirtualFile a b -> VirtualFile a b
usesCacherWithIdent ident =
    (vfileWriteCacher .~ defaultCacherWithIdent ident)
  . (vfileReadCacher .~ defaultCacherWithIdent ident)

-- * Creating VirtualFiles and convertings between its different subtypes (bidir
-- files, sources and sinks)

-- | A virtual file which depending on the situation can be written or read
type BidirVirtualFile a = VirtualFile a a

-- | A virtual file that's only readable
type DataSource a = VirtualFile NoWrite a

-- | A virtual file that's only writable
type DataSink a = VirtualFile a NoRead

-- | Creates a virtuel file from its virtual path and ways serialize/deserialize
-- the data. You should prefer 'dataSink' and 'dataSource' for clarity when the
-- file is meant to be readonly or writeonly.
virtualFile :: [LocationTreePathItem] -> SerialsFor a b -> VirtualFile a b
virtualFile path sers = VirtualFile path SingleLayerRead Nothing True def Nothing NoCache NoCache sers

-- | Creates a virtual file from its virtual path and ways to deserialize the
-- data.
dataSource :: [LocationTreePathItem] -> SerialsFor a b -> DataSource b
dataSource path = makeSource . virtualFile path

-- | Creates a virtual file from its virtual path and ways to serialize the
-- data.
dataSink :: [LocationTreePathItem] -> SerialsFor a b -> DataSink a
dataSink path = makeSink . virtualFile path

-- | Like 'virtualFile', but constrained to bidirectional serials, for clarity
bidirVirtualFile :: [LocationTreePathItem] -> BidirSerials a -> BidirVirtualFile a
bidirVirtualFile = virtualFile

-- | Turns the 'VirtualFile' into a pure source
makeSource :: VirtualFile a b -> DataSource b
makeSource vf = vf{_vfileSerials=eraseSerials $ _vfileSerials vf
                  ,_vfileWriteCacher=NoCache}

-- | Turns the 'VirtualFile' into a pure sink
makeSink :: VirtualFile a b -> DataSink a
makeSink vf = vf{_vfileSerials=eraseDeserials $ _vfileSerials vf
                ,_vfileLayeredReadScheme=LayeredReadWithNull
                ,_vfileReadCacher=NoCache
                ,_vfileEmbeddedValue=Nothing}


-- * Traversals to the content of the VirtualFile, when it already embeds some
-- value

-- | If we have the internal proof that a VirtualFile is actually bidirectional,
  -- we convert it.
vfileAsBidir :: forall a b. (Typeable a, Typeable b)
             => Traversal' (VirtualFile a b) (BidirVirtualFile a)
vfileAsBidir f vf = case eqT :: Maybe (a :~: b) of
  Just Refl -> f vf
  Nothing   -> pure vf

-- | Gives access to a version of the VirtualFile without type params. The
-- original path isn't settable.
vfileVoided :: Lens' (VirtualFile a b) (VirtualFile NoWrite NoRead)
vfileVoided f (VirtualFile p l v m i d wc rc s) =
  rebuild <$> f (VirtualFile p SingleLayerRead Nothing m i d NoCache NoCache mempty)
  where
    rebuild (VirtualFile _ _ _ m' i' d' _ _ _) =
      VirtualFile p l v m' i' d' wc rc s

-- | If the 'VirtualFile' has an embedded value convertible to type @i@, we get
-- it.
getConvertedEmbeddedValue
  :: (Typeable i)
  => BidirVirtualFile a
  -> Maybe i
getConvertedEmbeddedValue vf = do
  toA <- getToAtomicFn (vf ^. vfileSerials)
  toA <$> vf ^. vfileEmbeddedValue

-- | If the 'VirtualFile' can hold a embedded value of type @a@ that's
-- convertible from type @i@, we set it. Note that the conversion may fail, we
-- return Left if the VirtualFile couldn't be set.
setConvertedEmbeddedValue
  :: forall a b i. (Typeable i)
  => VirtualFile a b
  -> i
  -> Either String (VirtualFile a b)
setConvertedEmbeddedValue vf i =
  case getFromAtomicFn (vf ^. vfileSerials) of
    Nothing -> Left $ showVFileOriginalPath vf ++
               ": no conversion function is available to transform type " ++ show (typeOf (undefined :: i))
    Just fromA -> do
      i' <- fromA i
      return $ vf & vfileEmbeddedValue .~ Just i'

-- | Tries to convert each @i@ layer to and from type @b@ and find a
-- Monoid/Semigroup instance for @b@ in the vfileLayeredReadScheme, so we can
-- merge these layers. So if we have more that one layer, this will fail if the
-- file doesn't use LayeredRead.
tryMergeLayersForVFile
  :: forall a b i. (Typeable i)
  => VirtualFile a b
  -> [i]
  -> Either String b
tryMergeLayersForVFile vf layers = let ser = vf ^. vfileSerials in
  case getFromAtomicFn ser of
    Nothing -> Left $ showVFileOriginalPath vf ++
               ": no conversion functions are available to transform back and forth type "
               ++ show (typeOf (undefined :: i))
    Just fromA -> do
      case (layers, vf^.vfileLayeredReadScheme) of
        ([], LayeredReadWithNull) -> return mempty
        ([], _) -> Left $ "tryMergeLayersForVFile: " ++ showVFileOriginalPath vf
                   ++ " doesn't support mapping to no layers"
        ([x], _) -> fromA x
        (x:xs, LayeredRead) -> sconcat <$> traverse fromA (x:|xs)
        (xs, LayeredReadWithNull) -> mconcat <$> traverse fromA xs
        (_, _) -> Left $ "tryMergeLayersForVFile: " ++ showVFileOriginalPath vf
                  ++ " cannot use several layers of data"

-- | Sets vfileImportance . vfiClockAccess to True. This way each access to the
-- file will be clocked and logged.
clockVFileAccesses :: VirtualFile a b -> VirtualFile a b
clockVFileAccesses = vfileImportance . vfiClockAccess .~ True