app-settings 0.1.0.3 → 0.2.0.0
raw patch · 4 files changed
+292/−120 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Data.AppSettings: defaultValue :: Setting a -> a
- Data.AppSettings: name :: Setting a -> String
+ Data.AppSettings: ListSetting :: String -> [a] -> Setting [a]
Files
- Data/AppSettings.hs +110/−100
- Data/AppSettingsInternal.hs +90/−0
- app-settings.cabal +31/−20
- tests/Tests.hs +61/−0
Data/AppSettings.hs view
@@ -1,72 +1,7 @@-{-# LANGUAGE RankNTypes #-}---- |--- A library to deal with application settings.--- This library deals with read-write application settings.--- You will have to specify the settings that your application--- uses, their name, types and default values.--- Setting types must implement the 'Read' and 'Show' typeclasses. ------ The settings are saved in a file in an INI-like key-value format--- (without sections).------ Reading and updating settings is done in pure code, the IO--- monad is only used to load settings and save them to disk.--- It is advised for the user to create a module in your project--- holding settings handling.------ You can then declare settings:------ > fontSize :: Setting Double--- > fontSize = Setting "fontSize" 14--- > --- > dateFormat :: Setting String--- > dateFormat = Setting "dateFormat" "%x"--- > --- > backgroundColor :: Setting (Int, Int, Int)--- > backgroundColor = Setting "backcolor" (255, 0, 0)------ Optionally you can declare the list of all your settings:------ > defaultConfig :: DefaultConfig--- > defaultConfig = getDefaultConfig $ do--- > setting fontSize--- > setting dateFormat--- > setting backgroundColor------ If you do it, 'saveSettings' will also save settings--- which have not been modified, which are still at their--- default value in the configuration file, in a commented--- form, as a documentation to the user who may open the--- configuration file.--- So for instance if you declare this default configuration--- and have set the font size to 16 but left the other--- settings untouched, the configuration file which will be--- saved will be:------ > fontSize=16--- > # dateFormat="%x"--- > # backcolor=(255,0,0)------ If you did not specify the list of settings, only the--- first line would be present in the configuration file.------ Once we declared the settings, we can read the configuration--- from disk (and your settings module should export your wrapper--- around the function offered by this library):------ > readResult <- try $ readSettings (AutoFromAppName "test")--- > case readResult of--- > Right (conf, GetSetting getSetting) -> do--- > let textSize = getSetting textSizeFromWidth--- > saveSettings getDefaultConfig (AutoFromAppName "test") conf--- > Left (x :: SomeException) -> error "Error reading the config file!"------ 'AutoFromAppName' specifies where to save the configuration file.--- And we've already covered the getSetting in this snippet, see --- the 'readSettings' documentation for further information.+{-# LANGUAGE RankNTypes, GADTs #-} module Data.AppSettings (+ -- $intro Conf, DefaultConfig, Setting(..),@@ -83,10 +18,9 @@ import System.Directory import qualified Data.Map as M import Control.Monad.State-import Text.Read (readMaybe)-import Data.Maybe (fromMaybe) import Data.Serialization+import Data.AppSettingsInternal -- http://stackoverflow.com/questions/23117205/ newtype GetSetting = GetSetting (forall a. Read a => Setting a -> a)@@ -95,23 +29,6 @@ instance Show GetSetting where show _ = "GetSetting" --- | The type of a setting.--- It contains the setting name--- (key in the configuration file) and its default value.------ It is advised to have a module in your project handling settings.--- In this module, you'd have all the settings declared at the--- toplevel, and exported.--- The rest of the application can then do------ @--- getSetting \<setting\>--- setSetting \<conf\> \<setting\> \<value\>--- @------ and so on.-data Setting a = Setting { name :: String, defaultValue :: a }- -- | Information about the default configuration. Contains -- all the settings (that you declare using 'getDefaultConfig') -- and their default values. It is useful when you save a@@ -132,9 +49,8 @@ -- | see the 'getDefaultConfig' documentation. setting :: (Show a) => Setting a -> State Conf ()-setting (Setting nameV defaultV) = do- soFar <- get- put $ M.insert nameV SettingInfo { value = show defaultV, userSet = False } soFar+setting (Setting nameV defaultV) = get >>= put . M.insert nameV SettingInfo { value = show defaultV, userSet = False }+setting (ListSetting nameV defaultV) = get >>= put . addListSettings False nameV defaultV 1 -- | Used in combination with 'setting' to register settings. -- Registering settings is optional, see 'DefaultConfig'.@@ -208,22 +124,25 @@ filePath <- getPathForLocation location writeConfigFile filePath (conf `M.union` defaults) --- TODO maybe another getSetting that'll tell you--- if the setting is invalid in the config file instead of silently--- give you the default?-getSetting' :: (Read a) => Conf -> Setting a -> a-getSetting' conf (Setting key defaultV) = fromMaybe defaultV $ getSettingValueFromConf conf key--getSettingValueFromConf :: Read a => Conf -> String -> Maybe a-getSettingValueFromConf conf key = do- asString <- M.lookup key conf- readMaybe $ value asString- -- | Change the value of a setting. You'll have to call -- 'saveSettings' so that the change is written to disk. setSetting :: (Show a) => Conf -> Setting a -> a -> Conf setSetting conf (Setting key _) v = M.insert key SettingInfo { value = show v, userSet=True } conf+-- an empty list for the XX list key is written as XX= in the config file.+setSetting conf (ListSetting key _) [] = M.insert key SettingInfo { value = "", userSet= True}+ $ cleanListSetting key conf+setSetting conf (ListSetting key _) elts = addListSettings True key elts 1+ $ cleanListSetting key conf +addListSettings :: Show b => Bool -> String -> [b] -> Int -> Conf -> Conf+addListSettings _ _ [] _ = id+addListSettings uset key (x:xs) index = M.insert keyForIndex SettingInfo { value = show x, userSet=uset }+ . addListSettings uset key xs (index+1)+ where keyForIndex = key ++ "_" ++ show index++cleanListSetting :: String -> Conf -> Conf+cleanListSetting key = M.filterWithKey (\k _ -> not $ isKeyForListSetting key k)+ getSettingsFolder :: String -> IO FilePath getSettingsFolder appName = do home <- getHomeDirectory@@ -233,3 +152,94 @@ getConfigFileName :: String -> IO String getConfigFileName appName = fmap (++"config.ini") $ getSettingsFolder appName++-- $intro+--+-- A library to deal with application settings.+-- This library deals with read-write application settings.+-- You will have to specify the settings that your application+-- uses, their name, types and default values.+-- Setting types must implement the 'Read' and 'Show' typeclasses. +--+-- The settings are saved in a file in an INI-like key-value format+-- (without sections).+--+-- Reading and updating settings is done in pure code, the IO+-- monad is only used to load settings and save them to disk.+-- It is advised for the user to create a module in your project+-- holding settings handling.+--+-- You can then declare settings:+--+-- > fontSize :: Setting Double+-- > fontSize = Setting "fontSize" 14+-- > +-- > dateFormat :: Setting String+-- > dateFormat = Setting "dateFormat" "%x"+-- > +-- > backgroundColor :: Setting (Int, Int, Int)+-- > backgroundColor = Setting "backcolor" (255, 0, 0)+--+-- Optionally you can declare the list of all your settings:+--+-- > defaultConfig :: DefaultConfig+-- > defaultConfig = getDefaultConfig $ do+-- > setting fontSize+-- > setting dateFormat+-- > setting backgroundColor+--+-- If you do it, 'saveSettings' will also save settings+-- which have not been modified, which are still at their+-- default value in the configuration file, in a commented+-- form, as a documentation to the user who may open the+-- configuration file.+-- So for instance if you declare this default configuration+-- and have set the font size to 16 but left the other+-- settings untouched, the configuration file which will be+-- saved will be:+--+-- > fontSize=16+-- > # dateFormat="%x"+-- > # backcolor=(255,0,0)+--+-- If you did not specify the list of settings, only the+-- first line would be present in the configuration file.+--+-- With an ordinary setting, one row in the configuration file+-- means one setting. That setting may of course be a list+-- for instance. This setup works very well for shorter lists+-- like [1,2,3], however if you have a list of more complex+-- items, you will get very long lines and a configuration+-- file very difficult to edit by hand.+-- For these special cases there is also the 'ListSetting'+--+-- constructor:+--+-- > testList :: Setting [String]+-- > testList = ListSetting "testList" ["list1", "list2", "list3"]+--+-- Now the configuration file looks like that:+--+-- > testList_1="list1"+-- > testList_2="list2"+-- > testList_3="list3"+--+-- Which is much more handy for big lists. An empty list is represented+-- like so:+--+-- > testList=+--+-- Once we declared the settings, we can read the configuration+-- from disk (and your settings module should export your wrapper+-- around the function offered by this library):+--+-- > readResult <- try $ readSettings (AutoFromAppName "test")+-- > case readResult of+-- > Right (conf, GetSetting getSetting) -> do+-- > let textSize = getSetting textSizeFromWidth+-- > saveSettings getDefaultConfig (AutoFromAppName "test") conf+-- > Left (x :: SomeException) -> error "Error reading the config file!"+--+-- 'AutoFromAppName' specifies where to save the configuration file.+-- And we've already covered the getSetting in this snippet, see +-- the 'readSettings' documentation for further information.
+ Data/AppSettingsInternal.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE GADTs #-}++module Data.AppSettingsInternal where++import Data.Maybe (fromMaybe)+import Data.List (isPrefixOf)+import Data.Char (isDigit)+import Text.Read (readMaybe)+import qualified Data.Map as M++import Data.Serialization++-- I can't put the comments on the constructors:+-- http://trac.haskell.org/haddock/ticket/43++-- | The type of a setting.+-- It contains the setting name+-- (key in the configuration file) and its default value.+--+-- It is advised to have a module in your project handling settings.+-- In this module, you'd have all the settings declared at the+-- toplevel, and exported.+-- The rest of the application can then do+--+-- @+-- getSetting \<setting\>+-- setSetting \<conf\> \<setting\> \<value\>+-- @+--+-- and so on.+--+-- 'Setting' declares a simple setting. A value for that setting will be stored+-- in the configuration file in a single line.+--+-- 'ListSetting' however declares a list setting.+-- While it is perfectly fine to store lists+-- using the usual Setting constructor, if you have a list+-- of more complex items, you will get very long lines and a+-- configuration file very difficult to edit or review by hand.+--+-- The ListSetting will store settings using one line per item+-- in the list:+--+-- > testList :: Setting [String]+-- > testList = ListSetting "testList" ["list1", "list2", "list3"]+--+-- Now the configuration file looks like that:+--+-- > testList_1="list1"+-- > testList_2="list2"+-- > testList_3="list3"+--+-- Also note that an empty ListSetting is stored like so:+--+-- > testList=+data Setting a where+ Setting :: String -> a -> Setting a++ ListSetting :: (Read a, Show a) => String -> [a] -> Setting [a]++isKeyForListSetting :: String -> String -> Bool+isKeyForListSetting settingKey key = (settingKey ++ "_") `isPrefixOf` key+ && (all isDigit $ drop (length settingKey+1) key)+++-- TODO maybe another getSetting that'll tell you+-- if the setting is invalid in the config file instead of silently+-- give you the default?++-- | More low-level, please use the second function you get from 'readSettings',+-- wrapped in a 'GetSetting' newtype.+getSetting' :: (Read a) => Conf -> Setting a -> a+getSetting' conf (Setting key defaultV) = fromMaybe defaultV $ getSettingValueFromConf conf key+getSetting' conf (ListSetting key defaultV) = case getSettingValueFromConf conf $ key ++ "_1" of+ -- an empty list for the XX list key is written as XX= in the config file.+ Nothing -> case M.lookup key conf of+ Nothing -> defaultV+ _ -> []+ Just x -> x : decodeListSetting conf key 2++getSettingValueFromConf :: Read a => Conf -> String -> Maybe a+getSettingValueFromConf conf key = do+ asString <- M.lookup key conf+ readMaybe $ value asString++decodeListSetting :: Read a => Conf -> String -> Int -> [a]+decodeListSetting conf key index = case getSettingValueFromConf conf fullKey of+ Nothing -> []+ Just v -> v:decodeListSetting conf key (index+1)+ where fullKey = key ++ "_" ++ show index
app-settings.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: app-settings-version: 0.1.0.3+version: 0.2.0.0 synopsis: A library to manage application settings (INI file-like) description: A library to deal with application settings.@@ -32,31 +32,41 @@ > backgroundColor :: Setting (Int, Int, Int) > backgroundColor = Setting "backcolor" (255, 0, 0) .- Optionally you can declare the list of all your settings:- .- > defaultConfig :: DefaultConfig- > defaultConfig = getDefaultConfig $ do- > setting fontSize- > setting dateFormat- > setting backgroundColor- .- If you do it, 'saveSettings' will also save settings- which have not been modified, which are still at their- default value in the configuration file, in a commented- form, as a documentation to the user who may open the- configuration file.- So for instance if you declare this default configuration- and have set the font size to 16 but left the other- settings untouched, the configuration file which will be- saved will be:+ Optionally you can declare the list of all your settings,+ in that case the application will also save the default+ values in the configuration file, but commented out: . > fontSize=16 > # dateFormat="%x" > # backcolor=(255,0,0) .- If you did not specify the list of settings, only the+ If you do not specify the list of settings, only the first line would be present in the configuration file. .+ With an ordinary setting, one row in the configuration file+ means one setting. That setting may of course be a list+ for instance. This setup works very well for shorter lists+ like [1,2,3], however if you have a list of more complex+ items, you will get very long lines and a configuration+ file very difficult to edit by hand.+ .+ For these special cases there is also the 'ListSetting'+ constructor:+ .+ > testList :: Setting [String]+ > testList = ListSetting "testList" ["list1", "list2", "list3"]+ .+ Now the configuration file looks like that:+ .+ > testList_1="list1"+ > testList_2="list2"+ > testList_3="list3"+ .+ Which is much more handy for big lists. An empty list is represented+ like so:+ .+ > testList=+ . Once we declared the settings, we can read the configuration from disk (and your settings module should export your wrapper around the function offered by this library):@@ -85,7 +95,8 @@ library exposed-modules: Data.AppSettings- other-modules: Data.Serialization+ other-modules: Data.Serialization,+ Data.AppSettingsInternal -- other-extensions: build-depends: base >=4.6 && <5, mtl == 2.1.*,
tests/Tests.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE ScopedTypeVariables #-} import Data.AppSettings+import Data.AppSettingsInternal import Test.Hspec import Test.HUnit (assertBool)@@ -17,17 +18,28 @@ textSizeFromHeight :: Setting Double textSizeFromHeight = Setting "textSizeFromHeight" 12.4 +testInlineList :: Setting [String]+testInlineList = Setting "testInlineList" ["inline1", "inline2", "inline3"]++testList :: Setting [String]+testList = ListSetting "testList" ["list1", "list2", "list3"]+ defaultConfig :: DefaultConfig defaultConfig = getDefaultConfig $ do setting textSizeFromWidth setting textSizeFromHeight setting textFill+ setting testInlineList+ setting testList main :: IO () main = hspec $ do+ describe "unit tests" $ do+ testIsKeyForListSetting describe "load config" $ do testEmptyFileDefaults testPartialFileDefaults+ testPartialFileDefaults2 testFileWithComments testInvalidFile testInvalidValue@@ -35,6 +47,8 @@ describe "save config" $ do testSaveUserSetAndDefaults createBakBeforeSaving+ describe "set setting" $ do+ testSetListSetting testEmptyFileDefaults :: Spec testEmptyFileDefaults = it "parses correctly an empty file with defaults" $ do@@ -44,6 +58,8 @@ getSetting textSizeFromWidth `shouldBe` 0.04 getSetting textSizeFromHeight `shouldBe` 12.4 getSetting textFill `shouldBe` (1,1,0,1)+ getSetting testInlineList `shouldBe` ["inline1", "inline2", "inline3"]+ getSetting testList `shouldBe` ["list1", "list2", "list3"] Left (x :: SomeException) -> assertBool (show x) False testPartialFileDefaults :: Spec@@ -54,8 +70,22 @@ getSetting textSizeFromWidth `shouldBe` 1.02 getSetting textSizeFromHeight `shouldBe` 12.4 getSetting textFill `shouldBe` (1,2,3,4)+ getSetting testInlineList `shouldBe` ["un", "deux"]+ getSetting testList `shouldBe` ["one", "two"] Left (x :: SomeException) -> assertBool (show x) False +testPartialFileDefaults2 :: Spec+testPartialFileDefaults2 = it "parses correctly a partial2 file with defaults" $ do+ readResult <- try $ readSettings (Path "tests/partial2.config")+ case readResult of+ Right (_, GetSetting getSetting) -> do+ getSetting textSizeFromWidth `shouldBe` 1.02+ getSetting textSizeFromHeight `shouldBe` 12.4+ getSetting textFill `shouldBe` (1,2,3,4)+ getSetting testInlineList `shouldBe` ["un", "deux"]+ getSetting testList `shouldBe` []+ Left (x :: SomeException) -> assertBool (show x) False+ testFileWithComments :: Spec testFileWithComments = it "parses correctly a partial file with comments" $ do readResult <- try $ readSettings (Path "tests/test-save.txt")@@ -64,6 +94,8 @@ getSetting textSizeFromWidth `shouldBe` 1.02 getSetting textSizeFromHeight `shouldBe` 12.4 getSetting textFill `shouldBe` (1,2,3,4)+ getSetting testInlineList `shouldBe` ["un", "deux"]+ getSetting testList `shouldBe` ["one", "two"] Left (x :: SomeException) -> assertBool (show x) False testInvalidValue :: Spec@@ -75,6 +107,8 @@ getSetting textSizeFromWidth `shouldBe` 0.04 getSetting textSizeFromHeight `shouldBe` 12.4 getSetting textFill `shouldBe` (1,2,3,4)+ getSetting testInlineList `shouldBe` ["inline1", "inline2", "inline3"]+ getSetting testList `shouldBe` ["list1", "list2", "list3"] Left (x :: SomeException) -> assertBool (show x) False testNonExistingFile :: Spec@@ -85,6 +119,8 @@ getSetting textSizeFromWidth `shouldBe` 0.04 getSetting textSizeFromHeight `shouldBe` 12.4 getSetting textFill `shouldBe` (1,1,0,1)+ getSetting testInlineList `shouldBe` ["inline1", "inline2", "inline3"]+ getSetting testList `shouldBe` ["list1", "list2", "list3"] Left (x :: SomeException) -> assertBool (show x) False testInvalidFile :: Spec@@ -118,3 +154,28 @@ removeFile "p.config" removeFile "p.config.bak" Left (x :: SomeException) -> assertBool (show x) False++testSetListSetting :: Spec+testSetListSetting = it "overwrites correctly a list setting" $ do+ readResult <- try $ readSettings (Path "tests/empty.config")+ case readResult of+ Right (conf, GetSetting getSetting) -> do+ getSetting testList `shouldBe` ["list1", "list2", "list3"]+ getSetting testInlineList `shouldBe` ["inline1", "inline2", "inline3"]+ let conf0 = setSetting conf testList ["un"]+ let conf1 = setSetting conf0 testInlineList ["one"]+ getSetting' conf1 testList `shouldBe` ["un"]+ getSetting' conf1 testInlineList `shouldBe` ["one"]+ let conf2 = setSetting conf1 testList []+ let conf3 = setSetting conf2 testInlineList []+ getSetting' conf3 testList `shouldBe` []+ getSetting' conf3 testInlineList `shouldBe` []+ Left (x :: SomeException) -> assertBool (show x) False++testIsKeyForListSetting :: Spec+testIsKeyForListSetting = it "checks correctly whether a key is attached to a list setting" $ do+ isKeyForListSetting "listTest" "listTest_1" `shouldBe` True+ isKeyForListSetting "listTest" "listTest_13" `shouldBe` True+ isKeyForListSetting "listTest" "listTest_unrelated" `shouldBe` False+ isKeyForListSetting "listTest" "listTest_1notanumber" `shouldBe` False+ isKeyForListSetting "listTest" "listTestX_1" `shouldBe` False