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