packages feed

simplexmq-1.0.0: src/Simplex/Messaging/Version.hs

{-# LANGUAGE ConstrainedClassMethods #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}

module Simplex.Messaging.Version
  ( Version,
    VersionRange (minVersion, maxVersion),
    pattern VersionRange,
    VersionI (..),
    VersionRangeI (..),
    Compatible,
    pattern Compatible,
    mkVersionRange,
    safeVersionRange,
    isCompatible,
    proveCompatible,
    compatibleVersion,
  )
where

import Control.Applicative (optional)
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Word (Word16)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util ((<$?>))

pattern VersionRange :: Word16 -> Word16 -> VersionRange
pattern VersionRange v1 v2 <- VRange v1 v2

{-# COMPLETE VersionRange #-}

type Version = Word16

data VersionRange = VRange
  { minVersion :: Version,
    maxVersion :: Version
  }
  deriving (Eq, Show)

-- | construct valid version range, to be used in constants
mkVersionRange :: Version -> Version -> VersionRange
mkVersionRange v1 v2
  | v1 <= v2 = VRange v1 v2
  | otherwise = error "invalid version range"

safeVersionRange :: Version -> Version -> Maybe VersionRange
safeVersionRange v1 v2
  | v1 <= v2 = Just $ VRange v1 v2
  | otherwise = Nothing

instance Encoding VersionRange where
  smpEncode (VRange v1 v2) = smpEncode (v1, v2)
  smpP =
    maybe (fail "invalid version range") pure
      =<< safeVersionRange <$> smpP <*> smpP

instance StrEncoding VersionRange where
  strEncode (VRange v1 v2)
    | v1 == v2 = strEncode v1
    | otherwise = strEncode v1 <> "-" <> strEncode v2
  strP = do
    v1 <- strP
    v2 <- maybe (pure v1) (const strP) =<< optional (A.char '-')
    maybe (fail "invalid version range") pure $ safeVersionRange v1 v2

instance ToJSON VersionRange where
  toJSON (VRange v1 v2) = toJSON (v1, v2)
  toEncoding (VRange v1 v2) = toEncoding (v1, v2)

instance FromJSON VersionRange where
  parseJSON v =
    (\(v1, v2) -> maybe (Left "bad VersionRange") Right $ safeVersionRange v1 v2)
      <$?> parseJSON v

class VersionI a where
  type VersionRangeT a
  version :: a -> Version
  toVersionRangeT :: a -> VersionRange -> VersionRangeT a

class VersionRangeI a where
  type VersionT a
  versionRange :: a -> VersionRange
  toVersionT :: a -> Version -> VersionT a

instance VersionI Version where
  type VersionRangeT Version = VersionRange
  version = id
  toVersionRangeT _ vr = vr

instance VersionRangeI VersionRange where
  type VersionT VersionRange = Version
  versionRange = id
  toVersionT _ v = v

newtype Compatible a = Compatible_ a

pattern Compatible :: a -> Compatible a
pattern Compatible a <- Compatible_ a

{-# COMPLETE Compatible #-}

isCompatible :: VersionI a => a -> VersionRange -> Bool
isCompatible x (VRange v1 v2) = let v = version x in v1 <= v && v <= v2

isCompatibleRange :: VersionRangeI a => a -> VersionRange -> Bool
isCompatibleRange x (VRange min2 max2) = min1 <= max2 && min2 <= max1
  where
    VRange min1 max1 = versionRange x

proveCompatible :: VersionI a => a -> VersionRange -> Maybe (Compatible a)
proveCompatible x vr = x `mkCompatibleIf` (x `isCompatible` vr)

compatibleVersion :: VersionRangeI a => a -> VersionRange -> Maybe (Compatible (VersionT a))
compatibleVersion x vr =
  toVersionT x (min max1 max2) `mkCompatibleIf` isCompatibleRange x vr
  where
    max1 = maxVersion $ versionRange x
    max2 = maxVersion vr

mkCompatibleIf :: a -> Bool -> Maybe (Compatible a)
x `mkCompatibleIf` cond = if cond then Just $ Compatible_ x else Nothing