packages feed

xdg-basedir-compliant (empty) → 1.0.2

raw patch · 9 files changed

+597/−0 lines, 9 filesdep +QuickCheckdep +aesondep +base

Dependencies added: QuickCheck, aeson, base, bytestring, filepath, hspec, polysemy, polysemy-plugin, polysemy-zoo, split, xdg-basedir-compliant

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Pierre Thierry (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Pierre Thierry nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,46 @@+# XDG Basedir implementation++This is a compliant implementation of the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).++## Compliance++- [ ] `$XDG_DATA_HOME`+  - [X] get path+  - [X] read file+  - [ ] write file+- [ ] `$XDG_CONFIG_HOME`+  - [X] get path+  - [X] read file+  - [ ] write file+- [ ] `$XDG_STATE_HOME`+  - [X] get path+  - [X] read file+  - [ ] write file+- [X] `$XDG_DATA_DIRS`+  - [X] get path list+  - [X] read best file+  - [X] merge files+- [X] `$XDG_CONFIG_DIRS`+  - [X] get path list+  - [X] read best file+  - [X] merge files+- [ ] `$XDG_CACHE_HOME`+  - [X] get path+  - [X] read file+  - [ ] write file+- [ ] `$XDG_RUNTIME_DIR`+  - [X] get path+  - [X] read file+  - [ ] write file+- [ ] treat relative paths as invalid++# Build++You can build the project with:++```+hpack+cabal build+```++If you have Nix installed, `shell.nix` provides an environment with all the needed dependencies. It is suitable for `nix-direnv`.
+ src/System/XDG.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE DataKinds #-}++{-|+Module      : System.XDG+Description : XDG Basedir functions++These functions implement the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).+++When an environment variable is missing that must be defined, they will+raise a `MissingEnv` exception. This applies to @$HOME@ and @$XDG_RUNTIME_DIR@.+-}+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 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++{-| Returns the content of @$XDG_CONFIG_HOME@ or its default value. -}+getConfigHome :: IO FilePath+getConfigHome = In.runXDGIO In.getConfigHome++{-| Returns the content of @$XDG_STATE_HOME@ or its default value. -}+getStateHome :: IO FilePath+getStateHome = In.runXDGIO In.getStateHome++{-| Returns the content of @$XDG_CACHE_HOME@ or its default value. -}+getCacheHome :: IO FilePath+getCacheHome = In.runXDGIO In.getCacheHome++{-| Returns the content of @$XDG_RUNTIME_DIR@. -}+getRuntimeDir :: IO FilePath+getRuntimeDir = 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++{-| 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.+++To read @$XDG_DATA_DIRS\/subdir\/filename@:++@+> readDataFile "subdir/filename"+@+-}+readDataFile :: FilePath -> IO (Maybe ByteString)+readDataFile file = In.runXDGIO $ In.maybeRead $ In.readDataFile file++{-| Parse all readable data files into a monoid and append them.+The append operation will operate left to right in the order of decreasing importance. -}+readData :: Monoid b => (ByteString -> b) -> FilePath -> IO b+readData parse file = In.runXDGIO $ In.readData parse file++{-| 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++{-| 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.+++To read @$XDG_CONFIG_DIRS\/subdir\/filename@:++@+> readConfigFile "subdir/filename"+@+-}+readConfigFile :: FilePath -> IO (Maybe ByteString)+readConfigFile file = In.runXDGIO $ In.maybeRead $ In.readConfigFile file++{-| Parse all readable config files into a monoid and append them.+The append operation will operate left to right in the order of decreasing importance. -}+readConfig :: Monoid b => (ByteString -> b) -> FilePath -> IO b+readConfig parse file = In.runXDGIO $ In.readConfig parse file++{-| Returns the content of the cache file if it exists.++@+> readCacheFile "subdir/filename"+@+-}+readCacheFile :: FilePath -> IO (Maybe ByteString)+readCacheFile file = In.runXDGIO $ In.maybeRead $ In.readCacheFile file++{-| Returns the content of the state file if it exists.++@+> readStateFile "subdir/filename"+@+-}+readStateFile :: FilePath -> IO (Maybe ByteString)+readStateFile file = In.runXDGIO $ In.maybeRead $ In.readStateFile file++{-| Returns the content of the runtime file if it exists.++@+> readRuntimeFile "subdir/filename"+@+-}+readRuntimeFile :: FilePath -> IO (Maybe ByteString)+readRuntimeFile file = In.runXDGIO $ In.maybeRead $ In.readStateFile file++
+ src/System/XDG/Env.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE TemplateHaskell, DataKinds #-}+{-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-}++module System.XDG.Env where++import           Polysemy+import           System.Environment             ( lookupEnv )+++data Env m a where+  GetEnv ::String -> Env m (Maybe String)++makeSem ''Env+++type EnvList = [(String, String)]++runEnvList :: EnvList -> InterpreterFor Env r+runEnvList envs = interpret (\(GetEnv name) -> pure $ lookup name envs)+++runEnvIO :: Member (Embed IO) r => InterpreterFor Env r+runEnvIO = interpret (\(GetEnv name) -> embed $ lookupEnv name)
+ src/System/XDG/Error.hs view
@@ -0,0 +1,16 @@+module System.XDG.Error where++import           Control.Exception+++data XDGError+  = FileNotFound FilePath+  | NoReadableFile+  | MissingEnv String+  deriving (Eq, Show)++instance Exception XDGError+++throwIOLeft :: Exception e => Either e a -> IO a+throwIOLeft = either throwIO pure
+ src/System/XDG/FileSystem.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE TemplateHaskell, DataKinds #-}++module System.XDG.FileSystem where++import qualified Control.Exception             as IO+import qualified Data.ByteString.Lazy          as BS+import           Polysemy+import           Polysemy.Error+import qualified System.IO.Error               as IO+import           System.XDG.Error+++data ReadFile f m a where+  ReadFile ::FilePath -> ReadFile f m f++makeSem ''ReadFile+++type FileList a = [(FilePath, a)]+++runReadFileList+  :: Member (Error XDGError) r => FileList a -> InterpreterFor (ReadFile a) r+runReadFileList files = interpret+  (\(ReadFile path) ->+    maybe (throw $ FileNotFound path) pure $ lookup path files+  )++runReadFileIO+  :: Members '[Embed IO , Error XDGError] r+  => InterpreterFor (ReadFile BS.ByteString) r+runReadFileIO = interpret+  (\(ReadFile path) -> do+    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+    either throw pure result+  )
+ src/System/XDG/Internal.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE LambdaCase #-}++module System.XDG.Internal where++import qualified Control.Exception             as IO+import           Data.ByteString.Lazy           ( ByteString )+import           Data.Foldable                  ( fold )+import           Data.List.Split                ( endBy )+import           Data.Maybe                     ( fromMaybe )+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"++getConfigHome :: Env -@> FilePath+getConfigHome = getEnvHome "XDG_CONFIG_HOME" ".config"++getStateHome :: Env -@> FilePath+getStateHome = getEnvHome "XDG_STATE_HOME" ".local/state"++getCacheHome :: Env -@> FilePath+getCacheHome = getEnvHome "XDG_CACHE_HOME" ".local/cache"++getRuntimeDir :: '[Env, Error XDGError] >@> FilePath+getRuntimeDir = maybe (throw $ MissingEnv env) pure =<< getEnv env+  where env = "XDG_RUNTIME_DIR"++getDataDirs :: Env -@> [FilePath]+getDataDirs =+  getEnvDirs getDataHome "XDG_DATA_DIRS" ["/usr/local/share/", "/usr/share/"]++readDataFile :: FilePath -> '[Env , Error XDGError , ReadFile a] >@> a+readDataFile = readFileFromDirs getDataDirs++readData :: Monoid b => (a -> b) -> FilePath -> XDGReader a b+readData = appendEnvFiles getDataDirs++getConfigDirs :: Env -@> [FilePath]+getConfigDirs = getEnvDirs getConfigHome "XDG_CONFIG_DIRS" ["/etc/xdg"]++readConfigFile :: FilePath -> '[Env, Error XDGError, ReadFile a] >@> a+readConfigFile = readFileFromDirs getConfigDirs++readConfig :: Monoid b => (a -> b) -> FilePath -> XDGReader a b+readConfig = appendEnvFiles getConfigDirs++readStateFile :: FilePath -> XDGReader a a+readStateFile = readFileFromDir getStateHome++readCacheFile :: FilePath -> XDGReader a a+readCacheFile = readFileFromDir getCacheHome++readRuntimeFile :: FilePath -> XDGReader a a+readRuntimeFile = readFileFromDir getRuntimeDir+++type XDGReader a b = '[Env , Error XDGError , ReadFile a] >@> b++getUserHome :: Env -@> FilePath+getUserHome = fromMaybe "" <$> getEnv "HOME" --TODO: throw error if no $HOME++getEnvHome :: String -> FilePath -> Env -@> FilePath+getEnvHome env defaultHome = do+  home <- getUserHome+  fromMaybe (home </> defaultHome) <$> getEnv env++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+ 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++readFileFromDirs+  :: Env -@> [FilePath]+  -> FilePath+  -> '[Env , Error XDGError , ReadFile a] >@> a+readFileFromDirs getDirs file = do+  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+  fold+    <$> traverse (\file -> catch (parse <$> readFile file) (pure mempty)) files++maybeRead :: XDGReader a a -> XDGReader a (Maybe a)+maybeRead action = catch+  (Just <$> action)+  (\case+    NoReadableFile -> pure Nothing+    error          -> throw error+  )+++runXDGIO :: XDGReader ByteString a -> IO a+runXDGIO action = do+  result <- runM $ runError $ runReadFileIO $ runEnvIO action+  either IO.throwIO pure result
+ test/Spec.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE DataKinds #-}++import           Data.Aeson                     ( decode )+import           Data.ByteString.Lazy           ( ByteString )+import           Data.Maybe                     ( fromJust+                                                , fromMaybe+                                                )+import           Data.Monoid                    ( Sum(..) )+import           Polysemy+import           Polysemy.Error+import           Polysemy.Operators+import           System.Environment             ( setEnv )+import qualified System.XDG                    as XDGIO+import           System.XDG.Env+import           System.XDG.Error+import           System.XDG.FileSystem+import           System.XDG.Internal+import           Test.Hspec+++bareEnv :: EnvList+bareEnv = [("HOME", "/alice")]++fullEnv :: EnvList+fullEnv =+  [ ("HOME"           , "/alice")+  , ("XDG_CONFIG_HOME", "/config")+  , ("XDG_DATA_HOME"  , "/data")+  , ("XDG_STATE_HOME" , "/state")+  , ("XDG_CACHE_HOME" , "/cache")+  , ("XDG_RUNTIME_DIR", "/runtime")+  ]++userFiles :: FileList Integer+userFiles =+  [ ("/data/foo/bar"   , 1)+  , ("/config/foo/bar" , 10)+  , ("/state/foo/bar"  , 100)+  , ("/cache/foo/bar"  , 200)+  , ("/runtime/foo/bar", 300)+  ]++sysFiles :: FileList Integer+sysFiles =+  [ ("/usr/local/share/foo/bar", 2)+  , ("/usr/share/foo/bar"      , 3)+  , ("/etc/xdg/foo/bar"        , 20)+  ]++allFiles :: FileList Integer+allFiles = userFiles ++ sysFiles++testXDG+  :: EnvList+  -> FileList Integer+  -> '[Env , ReadFile Integer , Error XDGError] @> a+  -> Either XDGError a+testXDG env files = run . runError . runReadFileList files . runEnvList env++decodeInteger :: ByteString -> Sum Integer+decodeInteger = Sum . fromMaybe 0 . decode+++main :: IO ()+main = hspec $ do+  describe "XDG" $ do+    describe "pure interpreter" $ do+      it "finds the data home" $ do+        testXDG bareEnv [] getDataHome `shouldBe` Right "/alice/.local/share"+        testXDG fullEnv [] getDataHome `shouldBe` Right "/data"+      it "finds the config home" $ do+        testXDG bareEnv [] getConfigHome `shouldBe` Right "/alice/.config"+        testXDG fullEnv [] getConfigHome `shouldBe` Right "/config"+      it "finds the state home" $ do+        testXDG bareEnv [] getStateHome `shouldBe` Right "/alice/.local/state"+        testXDG fullEnv [] getStateHome `shouldBe` Right "/state"+      it "opens a data file" $ do+        testXDG fullEnv allFiles (readDataFile "foo/bar") `shouldBe` Right 1+        testXDG fullEnv sysFiles (readDataFile "foo/bar") `shouldBe` Right 2+        testXDG fullEnv allFiles (readDataFile "foo/baz")+          `shouldBe` Left NoReadableFile+        testXDG fullEnv [] (maybeRead $ readDataFile "foo/bar")+          `shouldBe` Right Nothing+      it "merges data files" $ do+        testXDG fullEnv allFiles (readData show "foo/bar")+          `shouldBe` Right "123"+        testXDG fullEnv [] (readData show "foo/bar") `shouldBe` Right ""+      it "opens a config file" $ do+        testXDG fullEnv userFiles (readConfigFile "foo/bar") `shouldBe` Right 10+        testXDG fullEnv sysFiles (readConfigFile "foo/bar") `shouldBe` Right 20+        testXDG fullEnv allFiles (readConfigFile "foo/baz")+          `shouldBe` Left NoReadableFile+      it "merges config files" $ do+        testXDG fullEnv allFiles (readConfig show "foo/bar")+          `shouldBe` Right "1020"+        testXDG fullEnv [] (readConfig show "foo/bar") `shouldBe` Right ""+      it "opens a state file" $ do+        testXDG fullEnv userFiles (readStateFile "foo/bar") `shouldBe` Right 100+      it "opens a cache file" $ do+        testXDG fullEnv userFiles (readCacheFile "foo/bar") `shouldBe` Right 200+      it "opens a runtime file" $ do+        testXDG fullEnv userFiles (readRuntimeFile "foo/bar")+          `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"+        (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
@@ -0,0 +1,87 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.6.+--+-- see: https://github.com/sol/hpack++name:           xdg-basedir-compliant+version:        1.0.2+synopsis:       XDG Basedir+description:    See README.md+category:       System+homepage:       https://github.com/kephas/xdg-basedir-compliant#readme+bug-reports:    https://github.com/kephas/xdg-basedir-compliant/issues+author:         Pierre Thierry+maintainer:     pierre@nothos.net+copyright:      2022 Pierre Thierry+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md++source-repository head+  type: git+  location: https://github.com/kephas/xdg-basedir-compliant++library+  exposed-modules:+      System.XDG+      System.XDG.Env+      System.XDG.Error+      System.XDG.FileSystem+      System.XDG.Internal+  other-modules:+      Paths_xdg_basedir_compliant+  hs-source-dirs:+      src+  default-extensions:+      BlockArguments+      FlexibleContexts+      GADTs+      LambdaCase+      PolyKinds+      RankNTypes+      ScopedTypeVariables+      TypeOperators+  ghc-options: -fplugin=Polysemy.Plugin+  build-depends:+      base >=4.7 && <5+    , bytestring+    , filepath+    , polysemy+    , polysemy-plugin+    , polysemy-zoo+    , split+  default-language: Haskell2010++test-suite xdg-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_xdg_basedir_compliant+  hs-source-dirs:+      test+  default-extensions:+      BlockArguments+      FlexibleContexts+      GADTs+      LambdaCase+      PolyKinds+      RankNTypes+      ScopedTypeVariables+      TypeOperators+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -fplugin=Polysemy.Plugin+  build-depends:+      QuickCheck+    , aeson+    , base >=4.7 && <5+    , bytestring+    , filepath+    , hspec+    , polysemy+    , polysemy-plugin+    , polysemy-zoo+    , split+    , xdg-basedir-compliant+  default-language: Haskell2010