packages feed

cabal-dev-0.9: src/Distribution/Dev/MergeCabalConfig.hs

{-|

Merge configuration files for Cabal (and cabal-install).

-}
module Distribution.Dev.MergeCabalConfig
    ( mergeFields
    , isFlaggedForRemoval
    , filterFields
    , removeFlaggedFields
    )
where

import Data.Maybe              ( fromMaybe, mapMaybe )
import Control.Monad           ( guard )
import Control.Applicative     ( Applicative, Alternative, pure, empty, (<|>), (<$>) )
import Distribution.ParseUtils ( Field(..) )

-- |Merge two lists of fields.
--
-- The fields from the second list are merged into the first list of
-- fields.
--
-- >>> mergeFields [F 1 "foo" "bar"] [F 2 "baz" "quux"]
-- [F 2 "baz" "quux", F 1 "foo" "bar"]
-- >>> mergeFields [F 1 "foo" "bar"] [F 2 "foo" "quux"]
-- [F 2 "foo" "quux"]
mergeFields :: [Field] -- ^Starting fields
            -> [Field] -- ^New fields
            -> [Field] -- ^Merged fields (preferring fields from the
                       -- second argument)
mergeFields = foldr $ \f fs ->
              fromMaybe (f:fs) $ replace (mergeField mergeFields f) fs

-- |Filter a configuration based on a predicate. Filters at the top
-- level as well as inside of Sections.
--
-- >>> let p f = case f of { F _ _ "bar" -> False; _ -> True; }
-- >>> filterFields p [F 0 "foo" "bar", Section 1 "quux" "beep" [], F 2 "arf" "baz"]
-- [Section 1 "quux" "beep" [], Field 2 "arf" "baz"]
filterFields :: (Field -> Bool) -> [Field] -> [Field]
filterFields p = mapMaybe $ \f -> guard (p f) >> return (recurseSection f)
    where
      recurseSection :: Field -> Field
      recurseSection (Section l n a fs) = Section l n a $ filterFields p fs
      recurseSection f                  = f

-- |Look for the magic value "USE-DEFAULT" as the value of a leaf
-- (Field). This means that there is no way to set a field to the
-- value "USE-DEFAULT" in a cabal configuration for use with
-- cabal-dev.
--
-- >>> isFlaggedForRemoval (F 0 "root-cmd" "sudo")
-- False
-- >>> isFlaggedForRemoval (F 0 "root-cmd" "USE-DEFAULT")
-- True
isFlaggedForRemoval :: Field -> Bool
isFlaggedForRemoval (F _ _ "USE-DEFAULT") = True
isFlaggedForRemoval _                     = False

-- |Recursively remove all fields whose value is "USE-DEFAULT" from
-- the configuration.
--
-- >>> removeFlaggedFields [F 0 "foo" "USE-DEFAULT", Section 1 "quux" "beep" [], F 2 "arf" "baz"]
-- [Section 1 "quux" "beep" [], F 2 "arf" "baz"]
removeFlaggedFields :: [Field] -> [Field]
removeFlaggedFields = filterFields $ not . isFlaggedForRemoval

-- |Attempt to merge two fields.
--
-- * Two fields can be merged if they have the same type and the same
--   identifiers.
-- * If two sections match, their fields are merged recursively.
-- * If two fields match, the value of the first field is taken.
--
-- >>> let m = mergeField mergeFields :: Field -> Field -> Maybe Field
-- >>> let fooF1 = F 1 "foo" "bar"
-- >>> let fooF2 = F 2 "foo" "baz"
-- >>> let fooSec1 = Section 3 "foo" "bar" [F 4 "a" "b"]
-- >>> let fooSec2 = Section 5 "foo" "bar" [F 6 "x" "y"]
-- >>> m fooF1 fooF2
-- Just (F 1 "foo" "bar")
-- >>> m fooF2 fooF1
-- Just (F 2 "foo" "baz")
-- >>> m fooF1 fooSec1
-- Nothing
-- >>> m fooSec1 fooF1
-- Nothing
-- >>> m fooSec1 fooSec2
-- Just (Section "foo" "bar" [F 2 "x" "y", F 1 "a" "b"])
-- >>> m fooSec2 fooSec1
-- Just (Section "foo" "bar" [F 1 "a" "b", F 2 "x" "y"])
mergeField :: (Applicative f, Alternative f) =>
              ([Field] -> [Field] -> [Field])
           -> Field -> Field -> f Field
mergeField _ (F l k v)           (F _ k' _)
    | k == k'            = pure $ F l k v
mergeField m (Section l n a fs') (Section _ n' a' fs'')
    | n == n' && a == a' = pure $ Section l n a $ m fs'' fs'
mergeField _ _                   _
    = empty

-- | Attempt to replace a value in a list with a given a replacement
-- function.
--
-- The first value that does not yield 'empty' for the replacement
-- function is replaced by the value. At this point, the computation
-- short-circuits. If no value yields a replacement, empty is returned.
--
-- >>> replace (`lookup` [(3,5)]) [1..5] :: Maybe [Int]
-- Just [1,2,5,4,5]
-- >>> replace (`lookup` [(3,5)]) [3,3,3] :: Maybe [Int]
-- Just [5,3,3]
-- >>> replace (`lookup` [(11,5)]) [1..5] :: Maybe [Int]
-- Nothing
-- >>> replace (\x -> if x == 11 then [5,9] else []) [1..5] :: [[Int]]
-- []
-- >>> replace (\x -> if x == 3 then [5,9] else []) [1..5] :: [[Int]]
-- [[1,2,5,4,5],[1,2,9,4,5]]
-- >>> replace (const $ Just "hello") [undefined, "world"] :: Maybe [String]
-- Just ["hello","world"]
replace :: (Applicative f, Alternative f) => (a -> f a) -> [a] -> f [a]
replace f = go id
    where
      go _   []     = empty
      go acc (x:xs) = (rebuild <$> f x) <|> go (acc . (x:)) xs
          where
            rebuild x' = acc $ x' : xs