xdg-basedir-compliant 1.0.2 → 1.1.0
raw patch · 8 files changed
+132/−70 lines, 8 filesdep +directorydep +pathPVP ok
version bump matches the API change (PVP)
Dependencies added: directory, path
API changes (from Hackage documentation)
- System.XDG.Internal: getUserHome :: Env -@> FilePath
+ System.XDG.Error: InvalidPath :: FilePath -> XDGError
+ System.XDG.Internal: requireAbsDir :: FilePath -> Error XDGError -@> Path Abs Dir
+ System.XDG.Internal: requireEnv :: String -> XDGEnv String
+ System.XDG.Internal: requireRelFile :: FilePath -> Error XDGError -@> Path Rel File
+ System.XDG.Internal: type XDGEnv a = '[Env, Error XDGError] >@> a
- System.XDG.Env: getEnv :: forall r_ael0. Member Env r_ael0 => String -> Sem r_ael0 (Maybe String)
+ System.XDG.Env: getEnv :: forall r_afN7. Member Env r_afN7 => String -> Sem r_afN7 (Maybe String)
- System.XDG.Error: FileNotFound :: FilePath -> XDGError
+ System.XDG.Error: FileNotFound :: Path Abs File -> XDGError
- System.XDG.FileSystem: [ReadFile] :: FilePath -> ReadFile f m f
+ System.XDG.FileSystem: [ReadFile] :: Path Abs File -> ReadFile f m f
- System.XDG.FileSystem: readFile :: forall f_affG r_afgz. Member (ReadFile f_affG) r_afgz => FilePath -> Sem r_afgz f_affG
+ System.XDG.FileSystem: readFile :: forall f_ahVj r_ahWc. Member (ReadFile f_ahVj) r_ahWc => Path Abs File -> Sem r_ahWc f_ahVj
- System.XDG.FileSystem: type FileList a = [(FilePath, a)]
+ System.XDG.FileSystem: type FileList a = [(Path Abs File, a)]
- System.XDG.Internal: appendEnvFiles :: Monoid b => (Env -@> [FilePath]) -> (a -> b) -> FilePath -> '[Env, Error XDGError, ReadFile a] >@> b
+ System.XDG.Internal: appendEnvFiles :: Monoid b => XDGEnv [Path Abs Dir] -> (a -> b) -> FilePath -> XDGReader a b
- System.XDG.Internal: getCacheHome :: Env -@> FilePath
+ System.XDG.Internal: getCacheHome :: XDGEnv (Path Abs Dir)
- System.XDG.Internal: getConfigDirs :: Env -@> [FilePath]
+ System.XDG.Internal: getConfigDirs :: XDGEnv [Path Abs Dir]
- System.XDG.Internal: getConfigHome :: Env -@> FilePath
+ System.XDG.Internal: getConfigHome :: XDGEnv (Path Abs Dir)
- System.XDG.Internal: getDataDirs :: Env -@> [FilePath]
+ System.XDG.Internal: getDataDirs :: XDGEnv [Path Abs Dir]
- System.XDG.Internal: getDataHome :: Env -@> FilePath
+ System.XDG.Internal: getDataHome :: XDGEnv (Path Abs Dir)
- System.XDG.Internal: getEnvDirs :: (Env -@> FilePath) -> String -> [String] -> Env -@> [FilePath]
+ System.XDG.Internal: getEnvDirs :: XDGEnv (Path Abs Dir) -> String -> [String] -> XDGEnv [Path Abs Dir]
- System.XDG.Internal: getEnvHome :: String -> FilePath -> Env -@> FilePath
+ System.XDG.Internal: getEnvHome :: String -> Path Rel Dir -> XDGEnv (Path Abs Dir)
- System.XDG.Internal: getRuntimeDir :: '[Env, Error XDGError] >@> FilePath
+ System.XDG.Internal: getRuntimeDir :: XDGEnv (Path Abs Dir)
- System.XDG.Internal: getStateHome :: Env -@> FilePath
+ System.XDG.Internal: getStateHome :: XDGEnv (Path Abs Dir)
- System.XDG.Internal: readFileFromDir :: ('[Env, Error XDGError] >@> FilePath) -> FilePath -> XDGReader a a
+ System.XDG.Internal: readFileFromDir :: XDGEnv (Path Abs Dir) -> FilePath -> XDGReader a a
- System.XDG.Internal: readFileFromDirs :: (Env -@> [FilePath]) -> FilePath -> '[Env, Error XDGError, ReadFile a] >@> a
+ System.XDG.Internal: readFileFromDirs :: XDGEnv [Path Abs Dir] -> FilePath -> XDGReader a a
Files
- CHANGELOG +2/−0
- README.md +1/−1
- src/System/XDG.hs +12/−14
- src/System/XDG/Error.hs +3/−1
- src/System/XDG/FileSystem.hs +4/−3
- src/System/XDG/Internal.hs +68/−47
- test/Spec.hs +35/−2
- xdg-basedir-compliant.cabal +7/−2
+ CHANGELOG view
@@ -0,0 +1,2 @@+1.1.0+ * BREAKING CHANGE: now relative paths are treated as invalid
README.md view
@@ -32,7 +32,7 @@ - [X] get path - [X] read file - [ ] write file-- [ ] treat relative paths as invalid+- [X] treat relative paths as invalid # Build
src/System/XDG.hs view
@@ -9,44 +9,42 @@ When an environment variable is missing that must be defined, they will raise a `MissingEnv` exception. This applies to @$HOME@ and @$XDG_RUNTIME_DIR@.++As per the specification, these functions will treat any relative path in the+environment variables as invalid. They will be skipped when operating on a list of+paths. Functions that return a single path will raise an `InvalidPath` exception. -} module System.XDG where import Data.ByteString.Lazy ( ByteString )-import Data.Either ( fromRight )-import Polysemy-import Polysemy.Error ( runError )-import Polysemy.Operators-import System.XDG.Env-import System.XDG.Error-import System.XDG.FileSystem+import Path ( fromAbsDir ) import qualified System.XDG.Internal as In {-| Returns the content of @$XDG_DATA_HOME@ or its default value. -} getDataHome :: IO FilePath-getDataHome = In.runXDGIO In.getDataHome+getDataHome = fromAbsDir <$> In.runXDGIO In.getDataHome {-| Returns the content of @$XDG_CONFIG_HOME@ or its default value. -} getConfigHome :: IO FilePath-getConfigHome = In.runXDGIO In.getConfigHome+getConfigHome = fromAbsDir <$> In.runXDGIO In.getConfigHome {-| Returns the content of @$XDG_STATE_HOME@ or its default value. -} getStateHome :: IO FilePath-getStateHome = In.runXDGIO In.getStateHome+getStateHome = fromAbsDir <$> In.runXDGIO In.getStateHome {-| Returns the content of @$XDG_CACHE_HOME@ or its default value. -} getCacheHome :: IO FilePath-getCacheHome = In.runXDGIO In.getCacheHome+getCacheHome = fromAbsDir <$> In.runXDGIO In.getCacheHome {-| Returns the content of @$XDG_RUNTIME_DIR@. -} getRuntimeDir :: IO FilePath-getRuntimeDir = In.runXDGIO In.getRuntimeDir+getRuntimeDir = fromAbsDir <$> In.runXDGIO In.getRuntimeDir {-| Returns the list of data dirs taken from @$XDG_DATA_HOME@ and @$XDG_DATA_DIRS@ or their default values. -} getDataDirs :: IO [FilePath]-getDataDirs = In.runXDGIO In.getDataDirs+getDataDirs = map fromAbsDir <$> In.runXDGIO In.getDataDirs {-| Returns the content of the first readable file in the data dirs if there is one. It will try the files in order of decreasing imporance.@@ -69,7 +67,7 @@ {-| Returns the list of config dirs taken from @$XDG_CONFIG_HOME@ and @$XDG_CONFIG_DIRS@ or their default values. -} getConfigDirs :: IO [FilePath]-getConfigDirs = In.runXDGIO In.getConfigDirs+getConfigDirs = map fromAbsDir <$> In.runXDGIO In.getConfigDirs {-| Returns the content of the first readable file in the config dirs if there is one. It will try the files in order of decreasing imporance.
src/System/XDG/Error.hs view
@@ -1,12 +1,14 @@ module System.XDG.Error where import Control.Exception+import Path data XDGError- = FileNotFound FilePath+ = FileNotFound (Path Abs File) | NoReadableFile | MissingEnv String+ | InvalidPath FilePath deriving (Eq, Show) instance Exception XDGError
src/System/XDG/FileSystem.hs view
@@ -4,6 +4,7 @@ import qualified Control.Exception as IO import qualified Data.ByteString.Lazy as BS+import Path import Polysemy import Polysemy.Error import qualified System.IO.Error as IO@@ -11,12 +12,12 @@ data ReadFile f m a where- ReadFile ::FilePath -> ReadFile f m f+ ReadFile ::Path Abs File -> ReadFile f m f makeSem ''ReadFile -type FileList a = [(FilePath, a)]+type FileList a = [(Path Abs File, a)] runReadFileList@@ -34,6 +35,6 @@ let notFound :: IO.IOException -> Maybe XDGError notFound e = if IO.isDoesNotExistError e then Just $ FileNotFound path else Nothing- result <- embed $ IO.tryJust notFound $ BS.readFile path+ result <- embed $ IO.tryJust notFound $ BS.readFile $ toFilePath path either throw pure result )
src/System/XDG/Internal.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-} @@ -7,35 +8,46 @@ import Data.ByteString.Lazy ( ByteString ) import Data.Foldable ( fold ) import Data.List.Split ( endBy )-import Data.Maybe ( fromMaybe )+import Data.Maybe ( catMaybes+ , fromMaybe+ )+import Path ( (</>)+ , Abs+ , Dir+ , File+ , Path+ , Rel+ , mkRelDir+ , parseAbsDir+ , parseRelFile+ ) import Polysemy import Polysemy.Error import Polysemy.Operators import Prelude hiding ( readFile )-import System.FilePath ( (</>) )-import qualified System.IO.Error as IO import System.XDG.Env import System.XDG.Error import System.XDG.FileSystem -getDataHome :: Env -@> FilePath-getDataHome = getEnvHome "XDG_DATA_HOME" ".local/share"+getDataHome :: XDGEnv (Path Abs Dir)+getDataHome = getEnvHome "XDG_DATA_HOME" $(mkRelDir ".local/share") -getConfigHome :: Env -@> FilePath-getConfigHome = getEnvHome "XDG_CONFIG_HOME" ".config"+getConfigHome :: XDGEnv (Path Abs Dir)+getConfigHome = getEnvHome "XDG_CONFIG_HOME" $(mkRelDir ".config") -getStateHome :: Env -@> FilePath-getStateHome = getEnvHome "XDG_STATE_HOME" ".local/state"+getStateHome :: XDGEnv (Path Abs Dir)+getStateHome = getEnvHome "XDG_STATE_HOME" $(mkRelDir ".local/state") -getCacheHome :: Env -@> FilePath-getCacheHome = getEnvHome "XDG_CACHE_HOME" ".local/cache"+getCacheHome :: XDGEnv (Path Abs Dir)+getCacheHome = getEnvHome "XDG_CACHE_HOME" $(mkRelDir ".local/cache") -getRuntimeDir :: '[Env, Error XDGError] >@> FilePath-getRuntimeDir = maybe (throw $ MissingEnv env) pure =<< getEnv env- where env = "XDG_RUNTIME_DIR"+getRuntimeDir :: XDGEnv (Path Abs Dir)+getRuntimeDir = do+ dir <- requireEnv "XDG_RUNTIME_DIR"+ requireAbsDir dir -getDataDirs :: Env -@> [FilePath]+getDataDirs :: XDGEnv [Path Abs Dir] getDataDirs = getEnvDirs getDataHome "XDG_DATA_DIRS" ["/usr/local/share/", "/usr/share/"] @@ -45,7 +57,7 @@ readData :: Monoid b => (a -> b) -> FilePath -> XDGReader a b readData = appendEnvFiles getDataDirs -getConfigDirs :: Env -@> [FilePath]+getConfigDirs :: XDGEnv [Path Abs Dir] getConfigDirs = getEnvDirs getConfigHome "XDG_CONFIG_DIRS" ["/etc/xdg"] readConfigFile :: FilePath -> '[Env, Error XDGError, ReadFile a] >@> a@@ -64,57 +76,66 @@ readRuntimeFile = readFileFromDir getRuntimeDir +type XDGEnv a = '[Env , Error XDGError] >@> a+ type XDGReader a b = '[Env , Error XDGError , ReadFile a] >@> b -getUserHome :: Env -@> FilePath-getUserHome = fromMaybe "" <$> getEnv "HOME" --TODO: throw error if no $HOME+requireEnv :: String -> XDGEnv String+requireEnv env = maybe (throw $ MissingEnv env) pure =<< getEnv env -getEnvHome :: String -> FilePath -> Env -@> FilePath-getEnvHome env defaultHome = do- home <- getUserHome- fromMaybe (home </> defaultHome) <$> getEnv env+requireAbsDir :: FilePath -> (Error XDGError) -@> Path Abs Dir+requireAbsDir path = maybe (throw $ InvalidPath path) pure $ parseAbsDir path -getEnvDirs :: (Env -@> FilePath) -> String -> [String] -> Env -@> [FilePath]-getEnvDirs getHome env defaultDirs = do- dirsHome <- getHome- dirs <- fromMaybe defaultDirs . noEmpty . fmap (endBy ":") <$> getEnv env- pure $ dirsHome : dirs+requireRelFile :: FilePath -> (Error XDGError) -@> Path Rel File+requireRelFile path = maybe (throw $ InvalidPath path) pure $ parseRelFile path++getEnvHome :: String -> Path Rel Dir -> XDGEnv (Path Abs Dir)+getEnvHome env defaultDir = do+ dir <- (>>= parseAbsDir) <$> getEnv env+ maybe getDefault pure dir where+ getDefault = do+ home <- requireEnv "HOME"+ home' <- requireAbsDir home+ pure $ home' </> defaultDir++getEnvDirs+ :: (XDGEnv (Path Abs Dir)) -> String -> [String] -> XDGEnv [Path Abs Dir]+getEnvDirs getUserDir env defaultDirs = do+ userDir <- catch (Just <$> getUserDir) (const $ pure Nothing)+ dirs <- fromMaybe defaultDirs . noEmpty . fmap (endBy ":") <$> getEnv env+ pure $ catMaybes $ userDir : (map parseAbsDir dirs)+ where noEmpty (Just []) = Nothing noEmpty x = x -readFileFromDir- :: '[Env, Error XDGError] >@> FilePath -> FilePath -> XDGReader a a-readFileFromDir getDir file = do- dir <- getDir- readFile $ dir </> file+readFileFromDir :: XDGEnv (Path Abs Dir) -> FilePath -> XDGReader a a+readFileFromDir getDir subPath = do+ subFile <- requireRelFile subPath+ dir <- getDir+ readFile $ dir </> subFile -readFileFromDirs- :: Env -@> [FilePath]- -> FilePath- -> '[Env , Error XDGError , ReadFile a] >@> a-readFileFromDirs getDirs file = do+readFileFromDirs :: XDGEnv [Path Abs Dir] -> FilePath -> XDGReader a a+readFileFromDirs getDirs subPath = do+ subFile <- requireRelFile subPath+ let tryOne dir next = catch (readFile $ dir </> subFile) (const next) dirs <- getDirs foldr tryOne (throw NoReadableFile) dirs- where tryOne dir next = catch (readFile $ dir </> file) (const next) appendEnvFiles- :: Monoid b- => Env -@> [FilePath]- -> (a -> b)- -> FilePath- -> '[Env , Error XDGError , ReadFile a] >@> b-appendEnvFiles getDirs parse file = do- files <- map (</> file) <$> getDirs+ :: Monoid b => XDGEnv [Path Abs Dir] -> (a -> b) -> FilePath -> XDGReader a b+appendEnvFiles getDirs parse subPath = do+ subFile <- requireRelFile subPath+ files <- map (</> subFile) <$> getDirs fold- <$> traverse (\file -> catch (parse <$> readFile file) (pure mempty)) files+ <$> traverse (\path -> catch (parse <$> readFile path) (pure mempty)) files maybeRead :: XDGReader a a -> XDGReader a (Maybe a) maybeRead action = catch (Just <$> action) (\case NoReadableFile -> pure Nothing- error -> throw error+ err -> throw err )
test/Spec.hs view
@@ -1,15 +1,25 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-orphans #-} + import Data.Aeson ( decode ) import Data.ByteString.Lazy ( ByteString )+import Data.List ( intercalate ) import Data.Maybe ( fromJust , fromMaybe ) import Data.Monoid ( Sum(..) )+import Data.String ( IsString(..) )+import Path hiding ( (</>) ) import Polysemy import Polysemy.Error import Polysemy.Operators+import System.Directory ( getCurrentDirectory ) import System.Environment ( setEnv )+import System.FilePath ( (</>) ) import qualified System.XDG as XDGIO import System.XDG.Env import System.XDG.Error@@ -18,6 +28,14 @@ import Test.Hspec +instance IsString (Path Abs Dir) where+ fromString = fromMaybe undefined . parseAbsDir++instance IsString (Path Abs File) where+ fromString = fromMaybe undefined . parseAbsFile+++ bareEnv :: EnvList bareEnv = [("HOME", "/alice")] @@ -31,6 +49,14 @@ , ("XDG_RUNTIME_DIR", "/runtime") ] +badEnv :: EnvList+badEnv =+ [ ("HOME" , "/alice")+ , ("XDG_DATA_HOME", "data/")+ , ("XDG_DATA_DIRS", "/foo:./bar:/baz:foo/../bar")+ ]++ userFiles :: FileList Integer userFiles = [ ("/data/foo/bar" , 1)@@ -68,6 +94,8 @@ it "finds the data home" $ do testXDG bareEnv [] getDataHome `shouldBe` Right "/alice/.local/share" testXDG fullEnv [] getDataHome `shouldBe` Right "/data"+ testXDG badEnv [] getDataDirs+ `shouldBe` Right ["/alice/.local/share", "/foo", "/baz"] it "finds the config home" $ do testXDG bareEnv [] getConfigHome `shouldBe` Right "/alice/.config" testXDG fullEnv [] getConfigHome `shouldBe` Right "/config"@@ -103,11 +131,16 @@ `shouldBe` Right 300 describe "IO interpreter" $ do it "opens a data file" $ do- setEnv "XDG_DATA_HOME" "./test/dir1"- setEnv "XDG_DATA_DIRS" "./test/dir2:./test/dir3"+ cwd <- getCurrentDirectory+ setEnv "XDG_DATA_HOME" $ cwd </> "test/dir1"+ setEnv "XDG_DATA_DIRS" $ intercalate ":" $ map+ (cwd </>)+ ["test/dir2", "test/dir3"] (decodeInteger . fromJust) <$> XDGIO.readDataFile "foo/bar.json" `shouldReturn` 1 it "merges data files" $ do XDGIO.readData decodeInteger "foo/bar.json" `shouldReturn` 3 XDGIO.readData decodeInteger "foo/baz.json" `shouldReturn` 30++
xdg-basedir-compliant.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: xdg-basedir-compliant-version: 1.0.2+version: 1.1.0 synopsis: XDG Basedir description: See README.md category: System@@ -19,6 +19,7 @@ build-type: Simple extra-source-files: README.md+ CHANGELOG source-repository head type: git@@ -44,11 +45,13 @@ RankNTypes ScopedTypeVariables TypeOperators- ghc-options: -fplugin=Polysemy.Plugin+ ghc-options: -Wall -fplugin=Polysemy.Plugin build-depends: base >=4.7 && <5 , bytestring+ , directory , filepath+ , path , polysemy , polysemy-plugin , polysemy-zoo@@ -77,8 +80,10 @@ , aeson , base >=4.7 && <5 , bytestring+ , directory , filepath , hspec+ , path , polysemy , polysemy-plugin , polysemy-zoo