packages feed

hix-0.7.1: lib/Hix/Data/VersionBounds.hs

module Hix.Data.VersionBounds where

import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON), Value (Object, String), object, (.:?), (.=))
import Data.Aeson.Types (typeMismatch)
import Distribution.Pretty (Pretty (pretty))
import Distribution.Version (
  Version,
  VersionRange,
  earlierVersion,
  intersectVersionRanges,
  orEarlierVersion,
  orLaterVersion,
  simplifyVersionRange,
  thisVersion,
  )

import Hix.Class.EncodeNix (EncodeNix)
import Hix.Data.Json (aesonParsec, jsonParsec)
import Hix.Data.Version (range0)
import Hix.Pretty (showP)
import Hix.Version (lowerVersion, upperVersion)

data Bound =
  BoundLower
  |
  BoundUpper
  deriving stock (Eq, Show, Generic)

instance Pretty Bound where
  pretty = \case
    BoundLower -> "lower"
    BoundUpper -> "upper"

data VersionBounds =
  VersionBounds {
    lower :: Maybe Version,
    upper :: Maybe Version
  }
  deriving stock (Eq, Show, Generic)
  deriving anyclass (EncodeNix)

instance Semigroup VersionBounds where
  left <> right = VersionBounds {lower = left.lower <|> right.lower, upper = left.upper <|> right.upper}

instance Monoid VersionBounds where
  mempty = VersionBounds {lower = Nothing, upper = Nothing}

unsafeVersionBoundsFromRange :: VersionRange -> VersionBounds
unsafeVersionBoundsFromRange range =
  VersionBounds {lower = lowerVersion range, upper = upperVersion range}

instance FromJSON VersionBounds where
  parseJSON = \case
    String s -> do
      range <- aesonParsec (toString s)
      pure (unsafeVersionBoundsFromRange range)
    Object o -> do
      lower <- fmap jsonParsec <$> o .:? "lower"
      upper <- fmap jsonParsec <$> o .:? "upper"
      pure VersionBounds {..}
    v -> do
      let
        expected =
          "a managed bound as either a string containing a Cabal-style range or an object with 'lower' and/or 'upper'"
          <>
          " as keys and simple versions as values"
      typeMismatch expected v

instance ToJSON VersionBounds where
  toJSON VersionBounds {..} =
    object [
      "lower" .= toJSON (showP @Text <$> lower),
      "upper" .= toJSON (showP @Text <$> upper)
    ]

maybeRange ::
  (Bound -> Version -> VersionRange) ->
  VersionBounds ->
  Maybe VersionRange
maybeRange mkRange bounds
  | Just l <- bounds.lower
  , Just u <- bounds.upper
  , u <= l
  = Just (thisVersion l)

  | Just l <- lower
  , Just u <- upper
  = Just (simplifyVersionRange (intersectVersionRanges l u))

  | Just l <- lower
  , Nothing <- upper
  = Just l

  | Nothing <- lower
  , Just u <- upper
  = Just u

  | Nothing <- lower
  , Nothing <- upper
  = Nothing
  where
    lower = mkRange BoundLower <$> bounds.lower
    upper = mkRange BoundUpper <$> bounds.upper

-- | Return a @==lower@ range for invalid bounds.
-- The constructors prevent this from happening, so it's maybe for tests?
-- Probably better to make the type abstract and crash.
maybeMajorRange ::
  VersionBounds ->
  Maybe VersionRange
maybeMajorRange =
  maybeRange \case
    BoundLower -> orLaterVersion
    BoundUpper -> earlierVersion

majorRange :: VersionBounds -> VersionRange
majorRange = fromMaybe range0 . maybeMajorRange

instance Pretty VersionBounds where
  pretty = pretty . majorRange

maybeInclusiveRange :: VersionBounds -> Maybe VersionRange
maybeInclusiveRange =
  maybeRange \case
    BoundLower -> orLaterVersion
    BoundUpper -> orEarlierVersion

inclusiveRange :: VersionBounds -> VersionRange
inclusiveRange = fromMaybe range0 . maybeInclusiveRange

anyBounds :: VersionBounds
anyBounds =
  VersionBounds {lower = Nothing, upper = Nothing}

versionBounds :: Version -> Version -> VersionBounds
versionBounds lower upper =
  VersionBounds {lower = Just lower, upper = Just upper}

fromLower :: Version -> VersionBounds
fromLower lower =
  VersionBounds {lower = Just lower, upper = Nothing}

fromUpper :: Version -> VersionBounds
fromUpper upper =
  VersionBounds {lower = Nothing, upper = Just upper}

exactVersion :: Version -> VersionBounds
exactVersion version = versionBounds version version

withLower :: Version -> VersionBounds -> VersionBounds
withLower lower VersionBounds {upper} =
  VersionBounds {lower = Just lower, upper = clamp =<< upper}
  where
    clamp old | old < lower = Nothing
              | otherwise = Just old

withUpper :: Version -> VersionBounds -> VersionBounds
withUpper upper VersionBounds {lower} =
  VersionBounds {lower = clamp =<< lower, upper = Just upper}
  where
    clamp old | old > upper = Nothing
              | otherwise = Just old