packages feed

pubgrub-0.1.0.0: test/Cookbook.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies      #-}

module Cookbook where

import           Colog.Core            (LogAction (..))
import           Control.Monad         (guard)
import           Data.Functor.Identity (Identity (..))
import           Data.List             (find, sortOn)
import           Data.Map.Strict       (Map)
import qualified Data.Map.Strict       as Map
import           Data.Maybe            (isJust)
import           Data.Ord              (Down (..))
import           Data.Semigroup        (First (..))
import qualified Data.Set              as Set
import           Data.Text             (Text)
import qualified Data.Text             as T
import           PubGrub
import           PubGrub.SetOps        (isSubsetOf)

newtype Name = Name Text
    deriving (Eq, Ord, Show)

data Said
    = Requested Name VersionSet
    | Constrained Name VersionSet
    | DependsOn Name VersionSet Name VersionSet
    | Unusable Name VersionSet Text

instance Domain Name where

    type ValueOf Name = Version
    type SetOf Name = VersionSet
    type FactOf Name = Said
    singleton _ = singletonVersionSet
    mergeRule = sameDependency

range :: [Int] -> [Int] -> VersionSet
range lo hi = halfOpenVersionSet (mkVersion lo) (Just (mkVersion hi))

request :: Name -> VersionSet -> Incompatibility Name
request p s = seed (Requested p s) p (Negative s)

constrain :: Name -> VersionSet -> Incompatibility Name
constrain p allowed = seed (Constrained p allowed) p (Positive (complementVersionSet allowed))

data Release = Release
    { needs  :: [(Name, VersionSet)]
    , broken :: Maybe Text
    }

type Index = Map Name [(Version, Release)]

slot :: [Version] -> Version -> VersionSet
slot versions x
    | all (>= x) versions = belowVersionSet next
    | otherwise = halfOpenVersionSet x next
  where
    next = find (> x) versions

dependsOn :: Name -> VersionSet -> Name -> VersionSet -> Incompatibility Name
dependsOn p ps q qs = fact (DependsOn p ps q qs) [(p, Positive ps), (q, Negative qs)]

provider :: Index -> Provider Name Identity
provider index = Provider $ \p offered ->
    let releases = Map.findWithDefault [] p index
        versions = map fst releases
        newestFirst = sortOn (Down . fst) [r | r@(x, _) <- releases, offered `contains` x]
        (unusable, usable) = span (isJust . broken . snd) newestFirst
        reasons =
            [ fact (Unusable p (slot versions x) reason) [(p, Positive (slot versions x))]
            | (x, Release{broken = Just reason}) <- unusable
            ]
     in Identity $ case usable of
            (x, release) : _ -> (Just x, reasons ++ [dependsOn p (slot versions x) q qs | (q, qs) <- needs release])
            []               -> (Nothing, reasons)

app, lib, util, extra :: Name
app = Name "app"
lib = Name "lib"
util = Name "util"
extra = Name "extra"

example :: Index
example =
    Map.fromList
        [ (app, [(mkVersion [1], Release [(lib, range [1] [2, 1])] Nothing)])
        , ( lib
          , [ (mkVersion [1], Release [(util, range [1] [3])] Nothing)
            , (mkVersion [2], Release [(util, range [1] [3])] Nothing)
            , (mkVersion [2, 1], Release [] (Just "it does not build here"))
            ]
          )
        , (util, [(mkVersion [1], Release [] Nothing), (mkVersion [2], Release [] Nothing)])
        , (extra, [(mkVersion [1], Release [] Nothing), (mkVersion [2], Release [] Nothing)])
        ]

plan :: Index -> [Incompatibility Name] -> Maybe (Map Name Version)
plan index seeds = case runIdentity (solve mempty (provider index) seeds) of
    Right solution -> Just solution
    Left _         -> Nothing

sameDependency :: MergeRule Name
sameDependency =
    MergeRule
        { mergeParts = \i -> case incompCause i of
            Fact (DependsOn p ps q qs) -> Just ((p, q, qs), ps)
            _                          -> Nothing
        , mergeBuild = \(p, q, qs) ps -> dependsOn p ps q qs
        }

sameTerms :: (Domain v, Ord (SetOf v)) => MergeRule v
sameTerms =
    MergeRule
        { mergeParts = \i -> case incompCause i of
            Fact _ -> Just (incompTerms i, First i)
            _      -> Nothing
        , mergeBuild = \_ (First i) -> i
        }

name :: Name -> VersionSet -> Text
name (Name p) s = p <> " " <> pretty s

formatter :: Formatter Name
formatter = (mkFormatter says hole name){clauseRules = [whichDependsOn]}
  where
    says said _ = case said of
        Requested p s       -> name p s <> " is requested"
        Constrained p s     -> name p s <> " is required by a constraint"
        DependsOn p ps q qs -> name p ps <> " depends on " <> name q qs
        Unusable p s reason -> name p s <> " cannot be used: " <> reason
    hole p s = "no version of " <> name p s <> " exists"

whichDependsOn :: Rule (Incompatibility Name, Incompatibility Name) Text
whichDependsOn = symmetric $ \(first, second) -> do
    DependsOn p ps q qs <- asFact first
    DependsOn q' qs' r rs <- asFact second
    guard (q == q' && qs `isSubsetOf` qs')
    pure (name p ps <> " depends on " <> name q qs <> " which depends on " <> name r rs)

why :: Index -> [Incompatibility Name] -> Text
why index seeds = case runIdentity (solve mempty (provider index) seeds) of
    Right _                -> "There is a plan."
    Left (NoSolution root) -> reportConflict formatter root
    Left _                 -> "The solver failed unexpectedly."

searchLog :: Index -> [Incompatibility Name] -> [Text]
searchLog index seeds = fst (solve logger (Provider expandLogged) seeds)
  where
    logger = LogAction $ \msg -> (describe msg, ())
    expandLogged p s = ([], runIdentity (expand (provider index) p s))

describe :: SolverMsg Name -> [Text]
describe msg = case msg of
    Chose (Name p) x            -> [p <> " " <> pretty x]
    NoVersionFor (Name p) s     -> ["no " <> p <> " in " <> pretty s]
    Learned i                   -> ["learned: " <> sayTerms formatter (incompTerms i)]
    Backtracked level           -> ["back to level " <> T.pack (show level)]
    _                           -> []

solveWithLock :: Index -> Map Name Version -> [Incompatibility Name] -> Either (SolveError Name) (Map Name Version)
solveWithLock index lock requests = case attempt lock of
    Left (NoSolution root)
        | let involved = Map.keysSet lock `Set.intersection` conflictSet root
        , not (Set.null involved) ->
            attempt (Map.withoutKeys lock involved)
    result -> result
  where
    attempt held =
        runIdentity . solve mempty (provider index) $
            requests ++ [constrain p (singletonVersionSet x) | (p, x) <- Map.toList held]