packages feed

stackctl-1.6.1.0: src/Stackctl/Config/RequiredVersion.hs

module Stackctl.Config.RequiredVersion
  ( RequiredVersion (..)
  , RequiredVersionOp (..)
  , requiredVersionToText
  , requiredVersionFromText
  , isRequiredVersionSatisfied

    -- * Exported for testing
  , (=~)
  ) where

import Stackctl.Prelude

import Data.Aeson
import Data.List (uncons)
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import Data.Version hiding (parseVersion)
import qualified Data.Version as Version
import Test.QuickCheck
import Text.ParserCombinators.ReadP (readP_to_S)

data RequiredVersion = RequiredVersion
  { requiredVersionOp :: RequiredVersionOp
  , requiredVersionCompareWith :: Version
  }
  deriving stock (Eq, Ord, Show)

instance Arbitrary RequiredVersion where
  arbitrary = RequiredVersion <$> arbitrary <*> arbitrary

instance FromJSON RequiredVersion where
  parseJSON =
    withText "RequiredVersion" $ either fail pure . requiredVersionFromText

instance ToJSON RequiredVersion where
  toJSON = toJSON . requiredVersionToText
  toEncoding = toEncoding . requiredVersionToText

requiredVersionToText :: RequiredVersion -> Text
requiredVersionToText RequiredVersion {..} =
  requiredVersionOpToText requiredVersionOp
    <> " "
    <> pack
      (showVersion requiredVersionCompareWith)

requiredVersionFromText :: Text -> Either String RequiredVersion
requiredVersionFromText = fromWords . T.words
 where
  fromWords :: [Text] -> Either String RequiredVersion
  fromWords = \case
    [w] -> parseRequiredVersion "=" w
    [op, w] -> parseRequiredVersion op w
    ws ->
      Left
        $ show (unpack $ T.unwords ws)
        <> " did not parse as optional operator and version string"

  parseRequiredVersion :: Text -> Text -> Either String RequiredVersion
  parseRequiredVersion op w = RequiredVersion <$> parseOp op <*> parseVersion w

  parseOp :: Text -> Either String RequiredVersionOp
  parseOp = \case
    "=" -> Right RequiredVersionEQ
    "==" -> Right RequiredVersionEQ
    "<" -> Right RequiredVersionLT
    "<=" -> Right RequiredVersionLTE
    ">" -> Right RequiredVersionGT
    ">=" -> Right RequiredVersionGTE
    "=~" -> Right RequiredVersionIsh
    op ->
      Left
        $ "Invalid comparison operator ("
        <> unpack op
        <> "), may only be =, <, <=, >, >=, or =~"

  parseVersion :: Text -> Either String Version
  parseVersion t =
    fmap (fst . NE.last)
      $ note ("Failed to parse as a version " <> s)
      $ NE.nonEmpty
      $ readP_to_S Version.parseVersion s
   where
    s = unpack t

isRequiredVersionSatisfied :: RequiredVersion -> Version -> Bool
isRequiredVersionSatisfied RequiredVersion {..} =
  (`requiredVersionCompare` requiredVersionCompareWith)
 where
  requiredVersionCompare = requiredVersionOpCompare requiredVersionOp

data RequiredVersionOp
  = RequiredVersionEQ
  | RequiredVersionLT
  | RequiredVersionLTE
  | RequiredVersionGT
  | RequiredVersionGTE
  | RequiredVersionIsh
  deriving stock (Eq, Ord, Show, Bounded, Enum)

instance Arbitrary RequiredVersionOp where
  arbitrary = arbitraryBoundedEnum

requiredVersionOpToText :: RequiredVersionOp -> Text
requiredVersionOpToText = \case
  RequiredVersionEQ -> "=="
  RequiredVersionLT -> "<"
  RequiredVersionLTE -> "<="
  RequiredVersionGT -> ">"
  RequiredVersionGTE -> ">="
  RequiredVersionIsh -> "=~"

requiredVersionOpCompare :: RequiredVersionOp -> Version -> Version -> Bool
requiredVersionOpCompare = \case
  RequiredVersionEQ -> (==)
  RequiredVersionLT -> (<)
  RequiredVersionLTE -> (<=)
  RequiredVersionGT -> (>)
  RequiredVersionGTE -> (>=)
  RequiredVersionIsh -> (=~)

(=~) :: Version -> Version -> Bool
a =~ b = a >= b && a < incrementVersion b
 where
  incrementVersion = onVersion $ backwards $ onHead (+ 1)
  onVersion f = makeVersion . f . versionBranch
  backwards f = reverse . f . reverse
  onHead f as = maybe as (uncurry (:) . first f) $ uncons as