{-# LANGUAGE ConstrainedClassMethods #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TypeFamilies #-} module Simplex.Messaging.Version ( Version, VersionRange (minVersion, maxVersion), VersionScope, pattern VersionRange, VersionI (..), VersionRangeI (..), Compatible, pattern Compatible, mkVersionRange, safeVersionRange, versionToRange, isCompatible, isCompatibleRange, proveCompatible, compatibleVersion, compatibleVRange, compatibleVRange', ) where import Control.Applicative (optional) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import Data.Aeson.Types ((.:), (.=)) import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Version.Internal (Version (..)) pattern VersionRange :: Version v -> Version v -> VersionRange v pattern VersionRange v1 v2 <- VRange v1 v2 {-# COMPLETE VersionRange #-} data VersionRange v = VRange { minVersion :: Version v, maxVersion :: Version v } deriving (Eq, Show) instance J.FromJSON (VersionRange v) where parseJSON (J.Object v) = do minVersion <- v .: "minVersion" maxVersion <- v .: "maxVersion" pure VRange {minVersion, maxVersion} parseJSON invalid = JT.prependFailure "bad VersionRange, " (JT.typeMismatch "Object" invalid) instance J.ToJSON (VersionRange v) where toEncoding VRange {minVersion, maxVersion} = JE.pairs $ ("minVersion" .= minVersion) <> ("maxVersion" .= maxVersion) toJSON VRange {minVersion, maxVersion} = J.object ["minVersion" .= minVersion, "maxVersion" .= maxVersion] class VersionScope v -- | construct valid version range, to be used in constants mkVersionRange :: Version v -> Version v -> VersionRange v mkVersionRange v1 v2 | v1 <= v2 = VRange v1 v2 | otherwise = error "invalid version range" safeVersionRange :: Version v -> Version v -> Maybe (VersionRange v) safeVersionRange v1 v2 | v1 <= v2 = Just $ VRange v1 v2 | otherwise = Nothing versionToRange :: Version v -> VersionRange v versionToRange v = VRange v v instance VersionScope v => Encoding (VersionRange v) where smpEncode (VRange v1 v2) = smpEncode (v1, v2) smpP = maybe (fail "invalid version range") pure =<< safeVersionRange <$> smpP <*> smpP instance VersionScope v => StrEncoding (VersionRange v) 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 class VersionScope v => VersionI v a | a -> v where type VersionRangeT v a version :: a -> Version v toVersionRangeT :: a -> VersionRange v -> VersionRangeT v a class VersionScope v => VersionRangeI v a | a -> v where type VersionT v a versionRange :: a -> VersionRange v toVersionRange :: a -> VersionRange v -> a toVersionT :: a -> Version v -> VersionT v a instance VersionScope v => VersionI v (Version v) where type VersionRangeT v (Version v) = VersionRange v version = id toVersionRangeT _ vr = vr instance VersionScope v => VersionRangeI v (VersionRange v) where type VersionT v (VersionRange v) = Version v versionRange = id toVersionRange _ vr = vr toVersionT _ v = v newtype Compatible a = Compatible_ a pattern Compatible :: a -> Compatible a pattern Compatible a <- Compatible_ a {-# COMPLETE Compatible #-} isCompatible :: VersionI v a => a -> VersionRange v -> Bool isCompatible x (VRange v1 v2) = let v = version x in v1 <= v && v <= v2 isCompatibleRange :: VersionRangeI v a => a -> VersionRange v -> Bool isCompatibleRange x (VRange min2 max2) = min1 <= max2 && min2 <= max1 where VRange min1 max1 = versionRange x proveCompatible :: VersionI v a => a -> VersionRange v -> Maybe (Compatible a) proveCompatible x vr = x `mkCompatibleIf` (x `isCompatible` vr) compatibleVersion :: VersionRangeI v a => a -> VersionRange v -> Maybe (Compatible (VersionT v a)) compatibleVersion x vr = toVersionT x (min max1 max2) `mkCompatibleIf` isCompatibleRange x vr where max1 = maxVersion $ versionRange x max2 = maxVersion vr -- | intersection of version ranges compatibleVRange :: VersionRangeI v a => a -> VersionRange v -> Maybe (Compatible a) compatibleVRange x vr = compatibleVRange_ x (max min1 min2) (min max1 max2) where VRange min1 max1 = versionRange x VRange min2 max2 = vr -- | version range capped by compatible version compatibleVRange' :: VersionRangeI v a => a -> Version v -> Maybe (Compatible a) compatibleVRange' x v | v <= max1 = compatibleVRange_ x min1 v | otherwise = Nothing where VRange min1 max1 = versionRange x compatibleVRange_ :: VersionRangeI v a => a -> Version v -> Version v -> Maybe (Compatible a) compatibleVRange_ x v1 v2 = Compatible_ . toVersionRange x <$> safeVersionRange v1 v2 mkCompatibleIf :: a -> Bool -> Maybe (Compatible a) x `mkCompatibleIf` cond = if cond then Just $ Compatible_ x else Nothing