packages feed

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

module Stackctl.Config.RequiredVersion
  ( RequiredVersion(..)
  , 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 Text.ParserCombinators.ReadP (readP_to_S)

data RequiredVersion = RequiredVersion
  { requiredVersionOp :: Text
  , requiredVersionCompare :: Version -> Version -> Bool
  , requiredVersionCompareWith :: Version
  }

instance Show RequiredVersion where
  show RequiredVersion {..} =
    unpack requiredVersionOp <> " " <> showVersion requiredVersionCompareWith

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

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 = do
    v <- parseVersion w

    case op of
      "=" -> Right $ RequiredVersion op (==) v
      "<" -> Right $ RequiredVersion op (<) v
      "<=" -> Right $ RequiredVersion op (<=) v
      ">" -> Right $ RequiredVersion op (>) v
      ">=" -> Right $ RequiredVersion op (>=) v
      "=~" -> Right $ RequiredVersion op (=~) v
      _ ->
        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

(=~) :: 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

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