settings 0.1.0.0 → 0.1.0.1
raw patch · 4 files changed
+325/−5 lines, 4 files
Files
- NEWS +23/−0
- settings.cabal +2/−4
- src/Data/Settings.hs +293/−0
- src/Data/Settings/Types.hs +7/−1
NEWS view
@@ -3,6 +3,29 @@ +settings 0.1.0.1 -- 2015-09-14+==============================++General, build and documentation changes:++* Write a tutorial, see Data.Settings module++New APIs, features and enhancements:++* (None)++Bug fixes:++* (None)++Dependency changes:++* (None)+++++ settings 0.1.0.0 -- 2015-09-10 ==============================
settings.cabal view
@@ -1,5 +1,5 @@ name: settings-version: 0.1.0.0+version: 0.1.0.1 synopsis: Runtime-editable program settings. description: This library aims to be a tool for constructing a settings management UI on@@ -12,9 +12,7 @@ similarity to @git config@ and <http://weechat.org Weechat>'s settings system, but isn't identical. .- NOTE: The API is probably not documented well enough. Sorry for that. I'm- releasing like this because I want to release packages which depend on this- library. Full docs will be written.+ To get started, see the tutorial in the "Data.Settings" module. homepage: http://rel4tion.org/projects/settings/ bug-reports: http://rel4tion.org/projects/settings/tickets/ license: PublicDomain
src/Data/Settings.hs view
@@ -13,6 +13,299 @@ - <http://creativecommons.org/publicdomain/zero/1.0/>. -} +-- | This top-level module contains just a tutorial, which you can read below.+-- It will help you figure out which of the sub-modules you need, and how to+-- use them.+--+-- = Tutorial+--+-- == Concepts+--+-- For a real usage example, see the /funbot/ package.+--+-- This library works with 2 components of your application state:+--+-- 1. Application settings value, of any type you like. Usually this is a+-- record of a type you define specifically for your application. It can be+-- a value just for settings, or, if modifiable settings are stored in+-- various parts of your state, it can be the state value itself.+-- 2. A settings tree, of type @Section@ (defined in "Data.Settings.Types").+-- This is a user interface component for accessing the settings values as a+-- tree with labeled nodes. If your settings tree never changes, you can use+-- a Haskell value directly for it. It if changes, add it to your+-- application state so that it can be modified as needed during run time.+--+-- The idea is that you freely use whatever you like for the settings values,+-- and the settings tree is a UI component added on top without interfering+-- with your program logic code. Persistence using periodic exports to JSON is+-- provided in the "Data.Settings.Persist" module, but you can use any other+-- solution as needed, e.g. the acid-state package. The persistence module is+-- unrelated to the settings tree, and should probably move into its own+-- package and get some extra features missing right now, like version control.+--+-- == Settings Tree Basics+--+-- In order to understand the layers of the API, we'll examine it bottom-up.+-- We'll start with the generic flexible parts and move towards the more+-- specific but simpler and more convenient ones. You'll likely need a bit of+-- both sides, so it's probably best to taste both.+--+-- Suppose we're writing a terminal based text editor, like /nano/ or /vim/.+-- The UI allows the user to enter commands like @get x.y.z@ or @set x.y.z val@+-- which manipulate the settings.+--+-- Let's define a type for settings. It may look like this:+--+-- > data Settings = Settings+-- > { setsTabWidth :: Int+-- > , setsFont :: String+-- > , setsTextSize :: Int+-- > , setsColorScheme :: String+-- > }+--+-- For simplicity, suppose the settings tree won't be changing, so all we need+-- in our application state is the settings. Let's use this:+--+-- > data AppState = AppState+-- > { appOpenFiles :: [String]+-- > , appUI :: Widget+-- > , appSettings :: Settings+-- > }+--+-- If we wanted to allow the settings tree structure to change, we'd have a+-- field for it too in the app state record.+--+-- This will be our monad:+--+-- > type App = StateT AppState IO+--+-- Now let's define a settings tree. A settings tree is the top-level /section/+-- of it. Each such section consists of two things: A set of settings options,+-- and a set of subsections. An empty tree looks like this:+--+-- > import Data.Settings.Section (empty)+-- >+-- > stree :: Section App+-- > stree = empty+--+-- Which is equivalent to:+--+-- > import qualified Data.HashMap.Lazy as M+-- >+-- > stree :: Section App+-- > stree = Section+-- > { secOpts = M.empty+-- > , secSubs = M.empty+-- > }+--+-- The 'secOpts' field is a map between option names and 'Option' values. The+-- 'secSubs' field is a map between subsection names and 'Section' values. We+-- can then refer to a specific tree node using period-separated syntax. For+-- example, if we have a tree with a single top-level option @\"a\"@, we can+-- refer to it in the UI simply a @\"a\"@. If we have a tree with a subsection+-- @\"s\"@ and under it an option @\"a\"@, we refer to that section as @\"a\"@+-- and to the option under it as @\"s.a\"@. And so on, we can have arbitrarily+-- deep nesting of sections and options, e.g. @\"s.t.u.v.w.x.a\"@.+--+-- The low-level flexible way to define a settings tree is by using 'Option'+-- value contructors directly. Let's define a simple flat tree with 4 options+-- and no subsections.+--+-- The 'Option' fields are monadic actions in our application monad, @App@.+--+-- > import Control.Monad.Trans.State+-- > import qualified Data.HashMap.Lazy as M+-- > import Data.Settings.Types+-- > import Text.Read (readMaybe)+-- >+-- > -- Convenience wrappers to make the code shorter+-- > -- Perhaps a good chance to use lens?+-- > getS = gets appSettings+-- > putS sets = modify $ \ app -> app { appSettings = sets }+-- > modifyS f = modify $ \ app -> app { appSettings = f $ appSettings app }+-- >+-- > stree :: Section App+-- > stree = Section+-- > { secOpts = M.fromList+-- > [ ( "tab-width"+-- > , Option+-- > { optGet = liftM (show . setsTabWidth) getS+-- > , optSet = \ val ->+-- > case readMaybe val of+-- > Just n -> do+-- > modifyS $ \ s -> s { setsTabWidth = n }+-- > return Nothing+-- > Nothing -> return $ Just $ InvalidValueForType val+-- > , optReset = modifyS $ \ s -> s { setsTabWidth = 4 }+-- > }+-- > )+-- > , ( "font"+-- > , Option {- ... similar fashion ... -}+-- > )+-- > , ( "text-size"+-- > , Option {- ... similar fashion ... -}+-- > )+-- > , ( "color-scheme"+-- > , Option {- ... similar fashion ... -}+-- > )+-- > ]+-- > , secSubs = M.empty+-- > }+--+-- == Building a Settings UI+--+-- We'll see higher level alternatives later. Let's see how to contruct the+-- settings UI now. The "Data.Settings.Iterface" provides a set of high-level+-- functions you can use on your UI code. You just need to wrap them with UI+-- actions like error message (e.g. invalid value) and feedback for successful+-- operations.+--+-- Before we can use those functions, we need to make our application monad an+-- instance of the 'MonadSettings' (multi-parameter) typeclass:+--+-- > instance MonadSettings App Settings where+-- > getSettings = getS+-- > putSettings = putS+-- > modifySettings = modifyS+-- > getSTree = return stree+--+-- Now, suppose the user enters the command @get x.y.z@ in our text editor's+-- command input line. This should return a friendly result. If @x.y.z@ is a+-- valid path in our settings tree leading to an option value, display that+-- value. If it's a section, display a list of the options and subsections it+-- contains. If it's neither, i.e. the path is invalid, report the error.+--+-- Such a UI can easily be constructed using functions in+-- "Data.Settings.Interface", e.g. see the 'Data.Settings.Interface.query'+-- function. Using the values it returns, you can construct UI strings to+-- display on the screen.+--+-- For example, in our case we'd want @get@ to display the top-level tree+-- contents, @get tab-width@ to display a number (4 by default) and @get foo@+-- to display an error /no such option or section/.+--+-- == Settings Tree Definition Tools+--+-- Let's go back to defining the settings tree. Some things we could improve:+--+-- * We defined @getS@ and related small functions, and used them when we+-- defined the 'MonadSettings' instance. Instead, we can first define the+-- instance and then just use its methods in our settings free definition if+-- needed.+-- * The usage of @readMaybe@ and @show@ allowed us to easily and quickly wrap+-- the tab width, an 'Int' value, by the string-based interface. But with+-- larger settings records and more value types, we'd want something more+-- robust and appropriate for UI. For example, if we did this for a 'Bool'+-- field, the user would have to type in @set x.y.z True@ while+-- @set x.y.z yes@ wouldn't work. Why tie the UI to the way booleans are+-- written in Haskell? We can have /true, TRUE, True, yes, Yes, 1/ etc. all+-- mean 'True'. Be flexible and user friendly.+-- * Once we write the 'MonadSettings' instance, instead of using its methods+-- directly (like we used @getS@ etc.) we can have wrappers do it for us, so+-- that we only need to write functions operating over the @Settings@ type+-- directly, making our code simple and readable and easy to tweak.+--+-- Let's start with the second point, wrapping typed settings values with UI,+-- e.g. like the example given for booleans above. The "Data.Settings.Option"+-- module provides the @mkOptionV@ function. This function wraps the type+-- details for us, if we supply instances of the @OptionValue@ class. Let's+-- define an instance for 'Int', which is the type of 2 out of the 4 fields in+-- our @Settings@ type. Generally, you'd want to define instances for all the+-- relevant field types in your settings type, e.g. perhaps also 'Bool' and+-- 'Float' and custom enum tyes and so on, depending on your requirements and+-- UI designs.+--+-- > instace OptionValue Int where+-- > readOption = readMaybe+-- > showOption = show+-- > typeName = const "Integer"+--+-- And here's an instance for 'Bool':+--+-- > instace OptionValue Bool where+-- > readOption s+-- > | sl `elem` ["true, "yes", "on", "1"] = Just True+-- > | sl `elem` ["false", "no", "off", "0"] = Just False+-- > | otherwise = Nothing+-- > where sl = map toLower s+-- > showOption = show+-- > typeName = const "Boolean"+--+-- Now, using @mkOptionV@, and this time also using the @MonadSettings@+-- functions, we can redefine the tab width option like this:+--+-- > mkOptionV+-- > (liftM setsTabWidth getSettings)+-- > (\ n -> do+-- > modifySettings $ \ s -> s { setsTabWidth = n }+-- > return True+-- > )+-- > (modifySettings $ \ s -> s { setsTabWidth = 4 })+--+-- Now let's improve further. This will be the highest level of the API. Given+-- a @MonadSettings@ instance, the repetitive parts of the code can be cleaned+-- further, by using the @mkOptionS@ function.+--+-- > mkOptionS+-- > setsTabWidth+-- > (\ n s -> Just s { setsTabWidth = n })+-- > (\ s -> (Just 4, s { setsTabWidth = 4 }))+-- > (const $ return ())+--+-- Perhaps a bit cleaner form removing duplication is this:+--+-- > mkOptionS+-- > setsTabWidth+-- > (\ n s -> Just $ set n s)+-- > (\ s -> (Just defval, set defval s))+-- > (const $ return ())+-- > where+-- > set n s = s { setsTabWidth = n }+-- > defval = 4+--+-- The last argument is a callback action to be run when a successful set or+-- reset of the value occurs.+--+-- We are done with the basics. Another 2 aspects of the library are settings+-- tree modification and persistence.+--+-- == Settings Tree Dynamic Modification+--+-- Modification simply requires holding the tree as application state, and+-- changing as needed. Removing sections, adding options and so on. There is an+-- API in "Data.Settings.Section" for working with the settings tree, and since+-- unordered maps are being used, you may also find "Data.HashMap.Lazy" useful+-- (from unordered-containers package).+--+-- == Settings Persistence+--+-- Finally, persistence. "Data.Settings.Persist" provides a function for+-- loading the settings value from a JSON file, and a function which generates+-- a safe scalable saver function. The JSON files are written using the+-- @aeson-pretty@ package, so that they are easy to read and modify manually if+-- needed.+--+-- The save function generator, @mkSaveSettings@, returns a function which+-- saves settings when called, but only at most once in @t@, the time interval+-- passed to the generator. For example, if you pass an interval of 3 seconds,+-- you can safely call the generated save function even 100 times a second, and+-- the JSON file will still get updated just once in 3 seconds, avoiding an+-- overload of file I/O.+--+-- The actual saving happens in a dedicated worker thread, so even when a save+-- does occur, it won't block the caller thread. You can, for example, call the+-- save function in the callback you pass to @mkOptionS@. The generator can be+-- called once at program start, and the returned save function saved in+-- application state.+--+-- Using the persistence module requires that you define @FromJSON@ and+-- @ToJSON@ instances for your settings type, see the @aeson@ package.+--+-- Note that while this simple periodic save-to-file method can serve a simple+-- standalone application well, it won't work if you wish your settings to be+-- shared by multiple applications and allow them to read and write settings at+-- the same time. If that's the case, check out the @acid-state@ package, and+-- other persistence related packages. module Data.Settings ( )
src/Data/Settings/Types.hs view
@@ -30,9 +30,15 @@ import qualified Data.HashMap.Lazy as M +-- | A settings option. The option value itself is held as usual in regular+-- application state, not here. What is held here is /functions/ applied to+-- that state to get or set the value. data Option m = Option- { optGet :: m String+ { -- |+ optGet :: m String+ -- | , optSet :: String -> m (Maybe SettingsError)+ -- | , optReset :: m () }