porcupine-core-0.1.0.0: src/Data/Locations/Accessors.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC "-fno-warn-incomplete-uni-patterns" #-}
{-# OPTIONS_GHC "-fno-warn-missing-signatures" #-}
{-# OPTIONS_GHC "-fno-warn-redundant-constraints" #-}
module Data.Locations.Accessors
( module Control.Monad.ReaderSoup.Resource
, FromJSON(..), ToJSON(..)
, LocationAccessor(..)
, LocOf, LocWithVarsOf
, SomeGLoc(..), SomeLoc, SomeLocWithVars
, SomeHashableLocs
, toHashableLocs
, FieldWithAccessors
, Rec(..), ElField(..)
, MayProvideLocationAccessors(..)
, SomeLocationAccessor(..)
, AvailableAccessors
, LocResolutionM
, BasePorcupineContexts
, (<--)
, baseContexts, baseContextsWithScribeParams
, pattern L
, splitAccessorsFromArgRec
, withParsedLocs, withParsedLocsWithVars, resolvePathToSomeLoc, resolveYamlDocToSomeLoc
, writeLazyByte, readLazyByte, readText, writeText
) where
import Control.Funflow.ContentHashable
import Control.Lens (over, (^.), _1)
import Control.Monad.IO.Unlift
import Control.Monad.ReaderSoup
import Control.Monad.ReaderSoup.Resource
import Control.Monad.Trans.Resource
import Data.Aeson
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Streaming as BSS
import qualified Data.HashMap.Strict as HM
import Data.Locations.Loc
import Data.Locations.LogAndErrors
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.Vinyl
import Data.Vinyl.Functor
import qualified Data.Yaml as Y
import GHC.TypeLits
import Katip
import System.Directory (createDirectoryIfMissing,
createFileLink,
doesPathExist)
import qualified System.FilePath as Path
import qualified System.IO.Temp as Tmp
import System.TaskPipeline.Logger
-- | A location where no variables left to be instanciated.
type LocOf l = GLocOf l String
-- | A location that contains variables needing to be instanciated.
type LocWithVarsOf l = GLocOf l StringWithVars
-- | Creates some Loc type, indexed over a symbol (see ReaderSoup for how that
-- symbol should be used), and equipped with functions to access it in some
-- Monad
class ( MonadMask m, MonadIO m
, TypedLocation (GLocOf l) )
=> LocationAccessor m (l::Symbol) where
-- | Generalized location. The implementation is completely to the discretion
-- of the LocationAccessor, but it must be serializable in json, and it must
-- be able to contain "variable bits" (that will correspond for instance to
-- indices). These "variable bits" must be exposed through the parameter @a@
-- in @GLocOf l a@, and @GLocOf l@ must be a Traversable. @a@ will always be
-- an instance of 'IsLocString'. The rest of the implementation of
-- 'LocationAccessor' doesn't have to work in the most general case @GLocOf l
-- a@, as when all variables have been replaced by their final values, @a@ is
-- just @String@.
data GLocOf l :: * -> *
locExists :: LocOf l -> m Bool
writeBSS :: LocOf l -> BSS.ByteString m r -> m r
readBSS :: LocOf l -> (BSS.ByteString m () -> m b) -> m b
copy :: LocOf l -> LocOf l -> m ()
copy locFrom locTo = readBSS locFrom (writeBSS locTo)
withLocalBuffer :: (FilePath -> m a) -> LocOf l -> m a
-- If we have a local resource accessor, we use it:
default withLocalBuffer :: (MonadResource m)
=> (FilePath -> m a) -> LocOf l -> m a
withLocalBuffer f loc =
Tmp.withSystemTempDirectory "pipeline-tools-tmp" writeAndUpload
where
writeAndUpload tmpDir = do
let tmpFile = tmpDir Path.</> "out"
res <- f tmpFile
_ <- readBSS (L (localFile tmpFile)) (writeBSS loc)
return res
-- | Reifies an instance of LocationAccessor
data SomeLocationAccessor m where
SomeLocationAccessor :: (KnownSymbol l, LocationAccessor m l)
=> Label l -> SomeLocationAccessor m
-- | This class is meant to be implemented by every label used in the reader
-- soup. It tells whether this label provides LocationAccessors (usually zero or
-- 1).
class MayProvideLocationAccessors m l where
getLocationAccessors :: Label l -> [SomeLocationAccessor m]
default getLocationAccessors :: (KnownSymbol l, LocationAccessor m l)
=> Label l -> [SomeLocationAccessor m]
getLocationAccessors x = [SomeLocationAccessor x]
-- | By default, no accessor is provided
instance {-# OVERLAPPABLE #-} MayProvideLocationAccessors m l where
getLocationAccessors _ = []
-- | Packs together the args to run a context of the ReaderSoup, and if
-- available, an instance of LocationAccessor
type FieldWithAccessors m =
Compose ((,) [SomeLocationAccessor m]) ElField
-- | Much like (=:) builds an ElField, (<--) builds a Field composed with
-- LocationAccessors (if available)
(<--) :: (KnownSymbol l, MayProvideLocationAccessors m l)
=> Label l -> args -> FieldWithAccessors m (l:::args)
lbl <-- args = Compose (getLocationAccessors lbl, lbl =: args)
-- | All the LocationAccessors available to the system during a run, so that
-- when we encounter an Aeson Value corresponding to some LocOf, we may try them
-- all and use the first one that matches.
newtype AvailableAccessors m = AvailableAccessors [SomeLocationAccessor m]
-- | Retrieves the list of all available LocationAccessors
--
-- The ArgsForSoupConsumption constraint is redundant, but it is placed here to
-- help type inference when using this function.
splitAccessorsFromArgRec
:: (ArgsForSoupConsumption args)
=> Rec (FieldWithAccessors (ReaderSoup (ContextsFromArgs args))) args
-> ( AvailableAccessors (ReaderSoup (ContextsFromArgs args))
, Rec ElField args )
splitAccessorsFromArgRec = over _1 AvailableAccessors . rtraverse getCompose
-- `(,) a` is an Applicative if a is a Monoid, so this will merge all the lists
-- of SomeLocationAccessors
-- * Making "resource" a LocationAccessor
checkLocal :: String -> Loc -> (LocalFilePath -> p) -> p
checkLocal _ (LocalFile fname) f = f fname
checkLocal funcName loc _ = error $ funcName ++ ": location " ++ show loc ++ " isn't a LocalFile"
-- | Accessing local resources
instance (MonadResource m, MonadMask m) => LocationAccessor m "resource" where
newtype GLocOf "resource" a = L (URL a)
deriving (Functor, Foldable, Traversable, ToJSON, TypedLocation)
locExists (L l) = checkLocal "locExists" l $
liftIO . doesPathExist . (^. pathWithExtensionAsRawFilePath)
writeBSS (L l) body = checkLocal "writeBSS" l $ \path -> do
let raw = path ^. pathWithExtensionAsRawFilePath
liftIO $ createDirectoryIfMissing True (Path.takeDirectory raw)
BSS.writeFile raw body
readBSS (L l) f = checkLocal "readBSS" l $ \path ->
f $ BSS.readFile $ path ^. pathWithExtensionAsRawFilePath
withLocalBuffer f (L l) = checkLocal "withLocalBuffer" l $ \path ->
f $ path ^. pathWithExtensionAsRawFilePath
copy (L l1) (L l2) =
checkLocal "copy" l1 $ \path1 ->
checkLocal "copy (2nd argument)" l2 $ \path2 ->
liftIO $ createFileLink
(path1 ^. pathWithExtensionAsRawFilePath)
(path2 ^. pathWithExtensionAsRawFilePath)
instance (MonadResource m, MonadMask m) => MayProvideLocationAccessors m "resource"
instance (IsLocString a) => Show (GLocOf "resource" a) where
show (L l) = show l -- Not automatically derived to avoid the 'L' constructor
-- being added
instance (IsLocString a) => FromJSON (GLocOf "resource" a) where
parseJSON v = do
loc <- parseJSON v
case loc of
LocalFile{} -> return $ L loc
_ -> fail "Isn't a local file"
-- * Treating locations in a general manner
-- | Some generalized location. Wraps a @GLocOf l a@ where @l@ is a
-- 'LocationAccessor' in monad @m@.
data SomeGLoc m a = forall l. (LocationAccessor m l) => SomeGLoc (GLocOf l a)
instance Functor (SomeGLoc m) where
fmap f (SomeGLoc l) = SomeGLoc $ fmap f l
instance Foldable (SomeGLoc m) where
foldMap f (SomeGLoc l) = foldMap f l
instance Traversable (SomeGLoc m) where
traverse f (SomeGLoc l) = SomeGLoc <$> traverse f l
type SomeLoc m = SomeGLoc m String
type SomeLocWithVars m = SomeGLoc m StringWithVars
instance Show (SomeLoc m) where
show (SomeGLoc l) = show l
instance Show (SomeLocWithVars m) where
show (SomeGLoc l) = show l
instance ToJSON (SomeLoc m) where
toJSON (SomeGLoc l) = toJSON l
instance ToJSON (SomeLocWithVars m) where
toJSON (SomeGLoc l) = toJSON l
-- | 'SomeLoc' turned into something that can be hashed
newtype SomeHashableLocs = SomeHashableLocs [Value]
-- TODO: We go through Aeson.Value representation of the locations to update
-- the hash. That's not terribly efficient, we should measure if that's a
-- problem.
instance (Monad m) => ContentHashable m SomeHashableLocs where
contentHashUpdate ctx (SomeHashableLocs vals) = contentHashUpdate ctx vals
toHashableLocs :: [SomeLoc m] -> SomeHashableLocs
toHashableLocs = SomeHashableLocs . map toJSON
-- * Some helper functions to directly read write/read bytestring into/from
-- locations
writeLazyByte
:: (LocationAccessor m l)
=> LocOf l
-> LBS.ByteString
-> m ()
writeLazyByte loc = writeBSS loc . BSS.fromLazy
-- The following functions are DEPRECATED, because converting to a lazy
-- ByteString with BSS.toLazy_ isn't actually lazy
readLazyByte
:: (LocationAccessor m l)
=> LocOf l
-> m LBS.ByteString
readLazyByte loc = readBSS loc BSS.toLazy_
readText
:: (LocationAccessor m l)
=> LocOf l
-> m T.Text
readText loc =
LT.toStrict . LTE.decodeUtf8 <$> readLazyByte loc
writeText
:: (LocationAccessor m l)
=> LocOf l
-> T.Text
-> m ()
writeText loc = writeBSS loc . BSS.fromStrict . TE.encodeUtf8
-- * Base contexts, providing LocationAccessor to local filesystem resources
type BasePorcupineContexts =
'[ "katip" ::: ContextFromName "katip"
, "resource" ::: ContextFromName "resource" ]
-- | Use it as the base of the record you give to 'runPipelineTask'. Use '(:&)'
-- to stack other contexts and LocationAccessors on top of it
baseContexts topNamespace =
#katip <-- ContextRunner (runLogger topNamespace maxVerbosityLoggerScribeParams)
:& #resource <-- useResource
:& RNil
-- | Like 'baseContext' but allows you to set the 'LoggerScribeParams'. Useful
-- when no CLI is used (see 'NoConfig' and 'ConfigFileOnly')
baseContextsWithScribeParams topNamespace scribeParams =
#katip <-- ContextRunner (runLogger topNamespace scribeParams)
:& #resource <-- useResource
:& RNil
-- * Parsing and resolving locations, tying them to one LocationAccessor
-- | The context in which aeson Values can be resolved to actual Locations
type LocResolutionM m = ReaderT (AvailableAccessors m) m
newtype ErrorsFromAccessors = ErrorsFromAccessors Object
deriving (ToObject, ToJSON)
instance LogItem ErrorsFromAccessors where
payloadKeys _ _ = AllKeys
errsFromAccs :: Object -> ErrorsFromAccessors
errsFromAccs = ErrorsFromAccessors . HM.singleton "errorsFromAccessors" . Object
-- | Finds in the accessors list a way to parse a list of JSON values that
-- should correspond to some `LocOf l` type
withParsedLocsWithVars
:: (LogThrow m)
=> [Value]
-> (forall l. (LocationAccessor m l)
=> [LocWithVarsOf l] -> LocResolutionM m r)
-> LocResolutionM m r
withParsedLocsWithVars aesonVals f = do
AvailableAccessors allAccessors <- ask
case allAccessors of
[] -> throwWithPrefix $ "List of accessors is empty"
_ -> return ()
loop allAccessors mempty
where
showJ = LT.unpack . LT.intercalate ", " . map (LTE.decodeUtf8 . encode)
loop [] errCtxs =
katipAddContext (errsFromAccs errCtxs) $
throwWithPrefix $ "Location(s) " ++ showJ aesonVals
++ " cannot be used by the location accessors in place."
loop (SomeLocationAccessor (lbl :: Label l) : accs) errCtxs =
case mapM fromJSON aesonVals of
Success a -> f (a :: [LocWithVarsOf l])
Error e -> loop accs (errCtxs <>
HM.singleton (T.pack $ symbolVal lbl) (String $ T.pack e))
-- | Finds in the accessors list a way to parse a list of JSON values that
-- should correspond to some `LocOf l` type
withParsedLocs :: (LogThrow m)
=> [Value]
-> (forall l. (LocationAccessor m l)
=> [LocOf l] -> LocResolutionM m r)
-> LocResolutionM m r
withParsedLocs aesonVals f = do
AvailableAccessors allAccessors <- ask
case allAccessors of
[] -> throwWithPrefix $ "List of accessors is empty"
_ -> return ()
loop allAccessors mempty
where
showJ = LT.unpack . LT.intercalate ", " . map (LTE.decodeUtf8 . encode)
loop [] errCtxs =
katipAddContext (errsFromAccs errCtxs) $
throwWithPrefix $ "Location(s) " ++ showJ aesonVals
++ " cannot be used by the location accessors in place."
loop (SomeLocationAccessor (lbl :: Label l) : accs) errCtxs =
case mapM fromJSON aesonVals of
Success a -> f (a :: [LocOf l])
Error e -> loop accs (errCtxs <>
HM.singleton (T.pack $ symbolVal lbl) (String $ T.pack e))
-- | The string will be parsed as a YAML value. It can be a simple string or the
-- representation used by some location acccessor. Every accessor will be
-- tried. Will fail if no accessor can handle the YAML value.
resolveYamlDocToSomeLoc
:: (LogThrow m)
=> String
-> LocResolutionM m (SomeLoc m)
resolveYamlDocToSomeLoc doc = do
val <- Y.decodeThrow $ TE.encodeUtf8 $ T.pack doc
withParsedLocs [val] $ \[l] -> return $ SomeGLoc l
-- | For locations which can be expressed as a simple String. The path will be
-- used as a JSON string. Will fail if no accessor can handle the path.
resolvePathToSomeLoc
:: (LogThrow m)
=> FilePath
-> LocResolutionM m (SomeLoc m)
resolvePathToSomeLoc p =
withParsedLocs [String $ T.pack p] $ \[l] -> return $ SomeGLoc l