packages feed

Configurable-0.1.0.0: src/Control/Configurable.hs

{-# LANGUAGE ExistentialQuantification,
             DeriveDataTypeable,
             MultiParamTypeClasses,
             FunctionalDependencies,
             ScopedTypeVariables,
             TupleSections #-}

module Control.Configurable (
  Configurable (..),
  Configuration,
  ConfiguredBy (..),
  ConfigMethod,
  doConfig, unsafeDoConfig, toConfigure, configureDefault
  ) where

import Data.Data
import Data.Monoid

-- | A 'Configurable' type is any type which should be specialized
-- from runtime configuration information. For instance, a record type
-- containing usernames and passwords for a database is probably
-- 'Configurable'. In principle, 'Configurable' types should be
-- distinguished from the functions/types/'ConfiguredBy' instances
-- they provide for.
class (Typeable a, Show a, Monoid a) => Configurable a where
  toConfiguration :: a -> Configuration
  toConfiguration = Configuration
  fromConfiguration :: Configuration -> Maybe a
  fromConfiguration (Configuration a) = cast a

  -- | A wildcard deconstructor on lists of 'Configuration's. After a
  -- list of 'Configuration's has been generated by either 'doConfig'
  -- or 'unsafeDoConfig', 'getConfig' is a typesafe destructor which
  -- can be used to filter the result for application to other
  -- functions. The default implementation relies on the 'Monoid'
  -- instance of the 'Configurable' to combine configurations.
  getConfig :: [Configuration] -> Maybe a
  getConfig cs = mconcat (map fromConfiguration cs)

-- | A 'Configuration' is an opaque wrapper around an existentially
-- typed 'Configurable'. 
data Configuration = forall a. Configurable a => Configuration a
                   deriving Typeable

instance Show Configuration where
  showsPrec p (Configuration a) = showsPrec p a

instance Configurable ()

-- | The default 'Monoid' instance of 'Configuration' is simply
-- left-annihilation. The 'mempty' just wraps the '()' type.
instance Monoid Configuration where
  mempty      = Configuration ()
  mappend a _ = a

-- | 'ConfiguredBy' is a convenience class allowing specification of
-- which kinds of 'Configurable's are needed for other types.
class Configurable b => ConfiguredBy a b | a -> b where
  -- | The default implementation relies on the functional dependency
  -- in the type and the 'Monoid' instance method 'mempty' of the
  -- 'Configurable'.
  confs :: a -> [Configuration]
  confs _ = [toConfiguration (mempty :: b)]

-- | 'ConfigMethod' is an opaque wrapper for methods to specialize
-- 'Configurations'.
data ConfigMethod = ConfigMethod { unConfigM :: [(Configuration, Bool)] }

-- | 'toConfigure' allows you to specify a specialization function for
-- any particular 'Configurable'. These are combinated together into a
-- large specialization routine which is then run via 'doConfig' or
-- 'unsafeDoConfig'.
toConfigure :: Configurable a => (a -> a) -> ConfigMethod -> ConfigMethod
toConfigure f = ConfigMethod . map ff . unConfigM
  where ff (z, configured) =
          case fromConfiguration z of
            Just a  -> (toConfiguration (f a), True)
            Nothing -> (z, configured)

-- | Introduce a new 'Configurable' based on its 'mempty' method.
configureDefault :: Configurable a => (a -> a) -> Configuration
configureDefault f = toConfiguration (f mempty)

-- | Given some 'Configuration' goals, use 'ConfigMethod's to
-- specialize a '[Configuration]'
doConfig :: [[Configuration]]
            -> (ConfigMethod -> ConfigMethod)
            -> Either [Configuration] [Configuration]
doConfig = flip runConfig

-- | An auxilary function for definine 'doConfig'.
runConfig :: (ConfigMethod -> ConfigMethod)
            -> [[Configuration]]
            -> Either [Configuration] [Configuration]
runConfig f = ret . unConfigM . f . ConfigMethod .  map (,False) . concat
  where ret cs = let unconfigured = filter (not . snd) cs
                 in if null unconfigured
                    then Right (map fst cs)
                    else Left (map fst unconfigured)

-- | Given some 'Configuration' goals, use 'ConfigMethod's to
-- specialize a '[Configuration]', but if any goals are never
-- specialized then fail immediately!
unsafeDoConfig :: [[Configuration]] -> (ConfigMethod -> ConfigMethod) -> [Configuration]
unsafeDoConfig cs f = go cs
  where
    go = map check . unConfigM . f . ConfigMethod .  map (,False) . concat
    check (c, True) = c
    check (c, False) = error $ "Required " ++ show c