streamly-core-0.3.1: src/Streamly/Internal/FileSystem/PosixPath/Node.hs
{-# LANGUAGE TemplateHaskell #-}
-- For constraints on "append"
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
#if defined(IS_WINDOWS)
#define OS_NAME Windows
#define OS_PATH WindowsPath
#else
#define OS_NAME Posix
#define OS_PATH PosixPath
#endif
-- |
-- Module : Streamly.Internal.FileSystem.OS_PATH.Node
-- Copyright : (c) 2023 Composewell Technologies
-- License : BSD3
-- Maintainer : streamly@composewell.com
-- Portability : GHC
--
-- This module provides a type safe path append operation by distinguishing
-- paths between files and directories. Files are represented by the @File
-- OS_PATH@ type and directories are represented by the @Dir OS_PATH@ type.
--
-- This distinction provides safety against appending a path to a file. Append
-- operation allows appending to only 'Dir' types.
--
module Streamly.Internal.FileSystem.OS_PATH.Node
(
-- * Types
File (..)
, Dir (..)
, IsNode
-- * Statically Verified Path Literals
-- | Quasiquoters.
, dir
, file
-- * Statically Verified Path Strings
-- | Template Haskell expression splices.
, dirE
, fileE
-- * Operations
, join
)
where
import Control.Monad ((>=>))
import Language.Haskell.TH (Q, Exp)
import Language.Haskell.TH.Syntax (lift)
import Language.Haskell.TH.Quote (QuasiQuoter)
import Streamly.Internal.Data.Path (IsPath(..))
import Streamly.Internal.FileSystem.Path.Common (OS(..), mkQ)
import Streamly.Internal.FileSystem.OS_PATH (OS_PATH(..))
import qualified Streamly.Internal.FileSystem.Path.Common as Common
import qualified Streamly.Internal.FileSystem.OS_PATH as OsPath
{- $setup
>>> :m
>>> :set -XQuasiQuotes
For APIs that have not been released yet.
>>> import Streamly.Internal.FileSystem.PosixPath (PosixPath)
>>> import Streamly.Internal.FileSystem.PosixPath.Node (File, Dir, file, dir)
>>> import qualified Streamly.Internal.FileSystem.PosixPath as Path
>>> import qualified Streamly.Internal.FileSystem.PosixPath.Node as Node
-}
newtype File a = File a
newtype Dir a = Dir a
-- | Constraint to check if a type uses 'File' or 'Dir' as the outermost
-- constructor.
class IsNode a
instance IsNode (File a)
instance IsNode (Dir a)
instance IsPath OS_PATH (File OS_PATH) where
unsafeFromPath = File
fromPath p@(OS_PATH arr) = do
!_ <- Common.validateFile OS_NAME arr
pure $ File p
toPath (File p) = p
instance IsPath OS_PATH (Dir OS_PATH) where
unsafeFromPath = Dir
fromPath p = pure (Dir p)
toPath (Dir p) = p
------------------------------------------------------------------------------
-- Statically Verified Strings
------------------------------------------------------------------------------
-- XXX We can lift the array directly, ByteArray has a lift instance. Does that
-- work better?
liftDir :: Dir OS_PATH -> Q Exp
liftDir (Dir p) =
[| unsafeFromPath (OsPath.unsafeFromString $(lift $ OsPath.toString $ toPath p)) :: Dir OS_PATH |]
liftFile :: File OS_PATH -> Q Exp
liftFile (File p) =
[| unsafeFromPath (OsPath.unsafeFromString $(lift $ OsPath.toString $ toPath p)) :: File OS_PATH |]
-- | Generates a Haskell expression of type @Dir OS_PATH@.
--
dirE :: String -> Q Exp
dirE = either (error . show) liftDir . (OsPath.fromString >=> fromPath)
-- | Generates a Haskell expression of type @File OS_PATH@.
--
fileE :: String -> Q Exp
fileE = either (error . show) liftFile . (OsPath.fromString >=> fromPath)
------------------------------------------------------------------------------
-- Statically Verified Literals
------------------------------------------------------------------------------
-- XXX Define folds or parsers to parse the paths.
-- XXX Build these on top of the str quasiquoter so that we get interpolation
-- for free. Interpolated vars if any have to be of appropriate type depending
-- on the context so that we can splice them safely.
-- | Generates a @Dir OS_PATH@ type from a quoted literal.
--
-- >>> Path.toString (Path.toPath ([dir|usr|] :: Dir PosixPath))
-- "usr"
--
dir :: QuasiQuoter
dir = mkQ dirE
-- | Generates a @File OS_PATH@ type from a quoted literal.
--
-- >>> Path.toString (Path.toPath ([file|usr|] :: File PosixPath))
-- "usr"
--
file :: QuasiQuoter
file = mkQ fileE
-- The only safety we need for paths is: (1) The first path can only be a Dir
-- type path, and (2) second path can only be a Seg path.
-- | Append a 'Dir' or 'File' path to a 'Dir' path.
--
-- >>> Path.toString (Path.toPath (Node.join [dir|/usr|] [dir|bin|] :: Dir PosixPath))
-- "/usr/bin"
-- >>> Path.toString (Path.toPath (Node.join [dir|/usr|] [file|bin|] :: File PosixPath))
-- "/usr/bin"
--
-- Fails if the second path is a specific location and not a path segment.
--
{-# INLINE join #-}
join :: (IsPath OS_PATH (a OS_PATH), IsNode (a OS_PATH)) =>
Dir OS_PATH -> a OS_PATH -> a OS_PATH
join (Dir a) b =
unsafeFromPath $ OsPath.unsafeJoin (toPath a) (toPath b)