{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Rank2Types #-}
module Data.Versions
(
Versioning(..), isIdeal, isGeneral, isComplex
, SemVer(..)
, PVP(..)
, Version(..)
, Mess(..), messMajor, messMinor, messPatch, messPatchChunk
, VUnit(..), digits, str
, VChunk
, VSep(..)
, ParsingError
, versioning, semver, pvp, version, mess
, versioning', semver', pvp', version', mess'
, prettyV, prettySemVer, prettyPVP, prettyVer, prettyMess, errorBundlePretty
, Lens'
, Traversal'
, Semantic(..)
, _Versioning, _SemVer, _Version, _Mess
, _Ideal, _General, _Complex
, epoch
, _Digits, _Str
) where
import Control.DeepSeq
import Data.Bool (bool)
import Data.Char (isAlpha)
import Data.Hashable (Hashable)
import Data.List (intersperse)
import Data.List.NonEmpty (NonEmpty(..))
import qualified Data.List.NonEmpty as NEL
import qualified Data.Text as T
import Data.Void (Void)
import GHC.Generics (Generic)
import Text.Megaparsec hiding (chunk)
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
#if !MIN_VERSION_base(4,11,0)
import Data.Semigroup
#endif
data Versioning = Ideal SemVer | General Version | Complex Mess
deriving (Eq, Show, Generic, NFData, Hashable)
isIdeal :: Versioning -> Bool
isIdeal (Ideal _) = True
isIdeal _ = False
isGeneral :: Versioning -> Bool
isGeneral (General _) = True
isGeneral _ = False
isComplex :: Versioning -> Bool
isComplex (Complex _) = True
isComplex _ = False
instance Ord Versioning where
compare (Ideal s) (Ideal s') = compare s s'
compare (General v) (General v') = compare v v'
compare (Complex m) (Complex m') = compare m m'
compare (Ideal s) (General v) = compare (vFromS s) v
compare (General v) (Ideal s) = opposite $ compare (vFromS s) v
compare (General v) (Complex m) = compare (mFromV v) m
compare (Complex m) (General v) = opposite $ compare (mFromV v) m
compare (Ideal s) (Complex m) = semverAndMess s m
compare (Complex m) (Ideal s) = opposite $ semverAndMess s m
vFromS :: SemVer -> Version
vFromS (SemVer m i p r _) = Version Nothing [[Digits m], [Digits i], [Digits p]] r
mFromV :: Version -> Mess
mFromV (Version e v r) = maybe affix (\a -> VNode [showt a] VColon affix) e
where
affix :: Mess
affix = VNode (chunksAsT v) VHyphen $ VLeaf (chunksAsT r)
semverAndMess :: SemVer -> Mess -> Ordering
semverAndMess s@(SemVer ma mi pa _ _) m = case compare ma <$> messMajor m of
Nothing -> fallback
Just LT -> LT
Just GT -> GT
Just EQ -> case compare mi <$> messMinor m of
Nothing -> fallback
Just LT -> LT
Just GT -> GT
Just EQ -> case compare pa <$> messPatch m of
Just LT -> LT
Just GT -> GT
Just EQ -> fallback
Nothing -> case messPatchChunk m of
Nothing -> fallback
Just (Digits pa':_) -> case compare pa pa' of
LT -> LT
GT -> GT
EQ -> GT
Just _ -> fallback
where
fallback :: Ordering
fallback = compare (General $ vFromS s) (Complex m)
instance Semantic Versioning where
major f (Ideal v) = Ideal <$> major f v
major f (General v) = General <$> major f v
major f (Complex v) = Complex <$> major f v
{-# INLINE major #-}
minor f (Ideal v) = Ideal <$> minor f v
minor f (General v) = General <$> minor f v
minor f (Complex v) = Complex <$> minor f v
{-# INLINE minor #-}
patch f (Ideal v) = Ideal <$> patch f v
patch f (General v) = General <$> patch f v
patch f (Complex v) = Complex <$> patch f v
{-# INLINE patch #-}
release f (Ideal v) = Ideal <$> release f v
release f (General v) = General <$> release f v
release f (Complex v) = Complex <$> release f v
{-# INLINE release #-}
meta f (Ideal v) = Ideal <$> meta f v
meta f (General v) = General <$> meta f v
meta f (Complex v) = Complex <$> meta f v
{-# INLINE meta #-}
semantic f (Ideal v) = Ideal <$> semantic f v
semantic f (General v) = General <$> semantic f v
semantic f (Complex v) = Complex <$> semantic f v
{-# INLINE semantic #-}
_Versioning :: Traversal' T.Text Versioning
_Versioning f t = either (const (pure t)) (fmap prettyV . f) $ versioning t
{-# INLINE _Versioning #-}
_SemVer :: Traversal' T.Text SemVer
_SemVer f t = either (const (pure t)) (fmap prettySemVer . f) $ semver t
{-# INLINE _SemVer #-}
_Version :: Traversal' T.Text Version
_Version f t = either (const (pure t)) (fmap prettyVer . f) $ version t
{-# INLINE _Version #-}
_Mess :: Traversal' T.Text Mess
_Mess f t = either (const (pure t)) (fmap prettyMess . f) $ mess t
{-# INLINE _Mess #-}
_Ideal :: Traversal' Versioning SemVer
_Ideal f (Ideal s) = Ideal <$> f s
_Ideal _ v = pure v
{-# INLINE _Ideal #-}
_General :: Traversal' Versioning Version
_General f (General v) = General <$> f v
_General _ v = pure v
{-# INLINE _General #-}
_Complex :: Traversal' Versioning Mess
_Complex f (Complex m) = Complex <$> f m
_Complex _ v = pure v
{-# INLINE _Complex #-}
type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s
type Traversal' s a = forall f. Applicative f => (a -> f a) -> s -> f s
class Semantic v where
major :: Traversal' v Word
minor :: Traversal' v Word
patch :: Traversal' v Word
release :: Traversal' v [VChunk]
meta :: Traversal' v [VChunk]
semantic :: Traversal' v SemVer
instance Semantic T.Text where
major = _Versioning . major
minor = _Versioning . minor
patch = _Versioning . patch
release = _Versioning . release
meta = _Versioning . meta
semantic = _SemVer
data SemVer = SemVer
{ _svMajor :: Word
, _svMinor :: Word
, _svPatch :: Word
, _svPreRel :: [VChunk]
, _svMeta :: [VChunk] }
deriving stock (Show, Generic)
deriving anyclass (NFData, Hashable)
instance Eq SemVer where
(SemVer ma mi pa pr _) == (SemVer ma' mi' pa' pr' _) =
(ma,mi,pa,pr) == (ma',mi',pa',pr')
instance Ord SemVer where
compare (SemVer ma mi pa pr _) (SemVer ma' mi' pa' pr' _) =
case compare (ma,mi,pa) (ma',mi',pa') of
LT -> LT
GT -> GT
EQ -> case (pr,pr') of
([],[]) -> EQ
([],_) -> GT
(_,[]) -> LT
_ -> compare pr pr'
instance Semigroup SemVer where
SemVer mj mn pa p m <> SemVer mj' mn' pa' p' m' =
SemVer (mj + mj') (mn + mn') (pa + pa') (p ++ p') (m ++ m')
instance Monoid SemVer where
mempty = SemVer 0 0 0 [] []
#if !MIN_VERSION_base(4,11,0)
mappend = (<>)
#endif
instance Semantic SemVer where
major f sv = fmap (\ma -> sv { _svMajor = ma }) (f $ _svMajor sv)
{-# INLINE major #-}
minor f sv = fmap (\mi -> sv { _svMinor = mi }) (f $ _svMinor sv)
{-# INLINE minor #-}
patch f sv = fmap (\pa -> sv { _svPatch = pa }) (f $ _svPatch sv)
{-# INLINE patch #-}
release f sv = fmap (\pa -> sv { _svPreRel = pa }) (f $ _svPreRel sv)
{-# INLINE release #-}
meta f sv = fmap (\pa -> sv { _svMeta = pa }) (f $ _svMeta sv)
{-# INLINE meta #-}
semantic = ($)
{-# INLINE semantic #-}
data VUnit = Digits Word | Str T.Text
deriving stock (Eq, Show, Read, Ord, Generic)
deriving anyclass (NFData, Hashable)
instance Semigroup VUnit where
Digits n <> Digits m = Digits $ n + m
Str t <> Str s = Str $ t <> s
Digits n <> _ = Digits n
_ <> Digits n = Digits n
instance Monoid VUnit where
mempty = Str ""
#if !MIN_VERSION_base(4,11,0)
mappend = (<>)
#endif
digits :: Word -> VUnit
digits = Digits
str :: T.Text -> Maybe VUnit
str t = bool Nothing (Just $ Str t) $ T.all isAlpha t
_Digits :: Traversal' VUnit Word
_Digits f (Digits i) = Digits <$> f i
_Digits _ v = pure v
{-# INLINE _Digits #-}
_Str :: Traversal' VUnit T.Text
_Str f (Str t) = Str . (\t' -> bool t t' (T.all isAlpha t')) <$> f t
_Str _ v = pure v
{-# INLINE _Str #-}
type VChunk = [VUnit]
newtype PVP = PVP { _pComponents :: NonEmpty Word }
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData, Hashable)
instance Semigroup PVP where
PVP (m :| r) <> PVP (m' :| r') = PVP $ (m + m') :| f r r'
where
f a [] = a
f [] b = b
f (a:as) (b:bs) = (a + b) : f as bs
instance Monoid PVP where
mempty = PVP (0 :| [])
#if !MIN_VERSION_base(4,11,0)
mappend = (<>)
#endif
instance Semantic PVP where
major f (PVP (m :| rs)) = (\ma -> PVP $ ma :| rs) <$> f m
{-# INLINE major #-}
minor f (PVP (m :| mi : rs)) = (\mi' -> PVP $ m :| mi' : rs) <$> f mi
minor f (PVP (m :| [])) = (\mi' -> PVP $ m :| [mi']) <$> f 0
{-# INLINE minor #-}
patch f (PVP (m :| mi : pa : rs)) = (\pa' -> PVP $ m :| mi : pa' : rs) <$> f pa
patch f (PVP (m :| mi : [])) = (\pa' -> PVP $ m :| mi : [pa']) <$> f 0
patch f (PVP (m :| [])) = (\pa' -> PVP $ m :| 0 : [pa']) <$> f 0
{-# INLINE patch #-}
release f p = const p <$> f []
{-# INLINE release #-}
meta f p = const p <$> f []
{-# INLINE meta #-}
semantic f (PVP (m :| rs)) = (\(SemVer ma mi pa _ _) -> PVP $ ma :| [mi, pa]) <$> f s
where
s = case rs of
mi : pa : _ -> SemVer m mi pa [] []
mi : _ -> SemVer m mi 0 [] []
[] -> SemVer m 0 0 [] []
{-# INLINE semantic #-}
data Version = Version
{ _vEpoch :: Maybe Word
, _vChunks :: [VChunk]
, _vRel :: [VChunk] }
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData, Hashable)
instance Semigroup Version where
Version e c r <> Version e' c' r' = Version ((+) <$> e <*> e') (c ++ c') (r ++ r')
instance Monoid Version where
mempty = Version Nothing [] []
#if !MIN_VERSION_base(4,11,0)
mappend = (<>)
#endif
wipe :: Version -> Version
wipe v = v { _vEpoch = Nothing }
instance Ord Version where
compare (Version _ [] []) (Version _ [] []) = EQ
compare v0@(Version (Just 0) _ _) v1@(Version Nothing _ _) = compare (wipe v0) v1
compare v0@(Version Nothing _ _) v1@(Version (Just 0) _ _) = compare v0 (wipe v1)
compare (Version (Just _) _ _) (Version Nothing _ _) = GT
compare (Version Nothing _ _) (Version (Just _) _ _) = LT
compare v0@(Version (Just n) _ _) v1@(Version (Just m) _ _) | n == m = compare (wipe v0) (wipe v1)
| otherwise = compare n m
compare (Version _ [] rs) (Version _ [] rs') = compare (Version Nothing rs []) (Version Nothing rs' [])
compare Version{} (Version _ [] _) = GT
compare (Version _ [] _) Version{} = LT
compare (Version _ (a:as) rs) (Version _ (b:bs) rs') = case f a b of
EQ -> compare (Version Nothing as rs) (Version Nothing bs rs')
res -> res
where f [] [] = EQ
f [] _ = GT
f _ [] = LT
f (Digits n:ns) (Digits m:ms) | n > m = GT
| n < m = LT
| otherwise = f ns ms
f (Str n:ns) (Str m:ms) | n > m = GT
| n < m = LT
| otherwise = f ns ms
f (Digits _ :_) (Str _ :_) = GT
f (Str _ :_ ) (Digits _ :_) = LT
instance Semantic Version where
major f (Version e ([Digits n] : cs) rs) = (\n' -> Version e ([Digits n'] : cs) rs) <$> f n
major _ v = pure v
{-# INLINE major #-}
minor f (Version e (c : [Digits n] : cs) rs) = (\n' -> Version e (c : [Digits n'] : cs) rs) <$> f n
minor _ v = pure v
{-# INLINE minor #-}
patch f (Version e (c : d : [Digits n] : cs) rs) = (\n' -> Version e (c : d : [Digits n'] : cs) rs) <$> f n
patch _ v = pure v
{-# INLINE patch #-}
release f v = fmap (\vr -> v { _vRel = vr }) (f $ _vRel v)
{-# INLINE release #-}
meta _ v = pure v
{-# INLINE meta #-}
semantic f (Version _ ([Digits a] : [Digits b] : [Digits c] : _) rs) = vFromS <$> f (SemVer a b c rs [])
semantic _ v = pure v
{-# INLINE semantic #-}
epoch :: Lens' Version (Maybe Word)
epoch f v = fmap (\ve -> v { _vEpoch = ve }) (f $ _vEpoch v)
{-# INLINE epoch #-}
data Mess = VLeaf [T.Text] | VNode [T.Text] VSep Mess
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData, Hashable)
messMajor :: Mess -> Maybe Word
messMajor (VNode (m:_) _ _) = hush $ parse (digitsP <* eof) "Major" m
messMajor _ = Nothing
messMinor :: Mess -> Maybe Word
messMinor (VNode (_:m:_) _ _) = hush $ parse (digitsP <* eof) "Minor" m
messMinor _ = Nothing
messPatch :: Mess -> Maybe Word
messPatch (VNode (_:_:p:_) _ _) = hush $ parse (digitsP <* eof) "Patch" p
messPatch _ = Nothing
messPatchChunk :: Mess -> Maybe VChunk
messPatchChunk (VNode (_:_:p:_) _ _) = hush $ parse chunk "Chunk" p
messPatchChunk _ = Nothing
instance Ord Mess where
compare (VLeaf l1) (VLeaf l2) = compare l1 l2
compare (VNode t1 _ _) (VLeaf t2) = compare t1 t2
compare (VLeaf t1) (VNode t2 _ _) = compare t1 t2
compare (VNode t1 _ v1) (VNode t2 _ v2) | t1 < t2 = LT
| t1 > t2 = GT
| otherwise = compare v1 v2
instance Semantic Mess where
major f v@(VNode (t : ts) s ms) = either (const $ pure v) g $ parse digitsP "Major" t
where g n = (\n' -> VNode (showt n' : ts) s ms) <$> f n
major _ v = pure v
{-# INLINE major #-}
minor f v@(VNode (t0 : t : ts) s ms) = either (const $ pure v) g $ parse digitsP "Minor" t
where g n = (\n' -> VNode (t0 : showt n' : ts) s ms) <$> f n
minor _ v = pure v
{-# INLINE minor #-}
patch f v@(VNode (t0 : t1 : t : ts) s ms) = either (const $ pure v) g $ parse digitsP "Patch" t
where g n = (\n' -> VNode (t0 : t1 : showt n' : ts) s ms) <$> f n
patch _ v = pure v
{-# INLINE patch #-}
release _ v = pure v
{-# INLINE release #-}
meta _ v = pure v
{-# INLINE meta #-}
semantic f v@(VNode (t0 : t1 : t2 : _) _ _) = either (const $ pure v) (fmap (mFromV . vFromS)) $
(\a b c -> f $ SemVer a b c [] [])
<$> parse digitsP "Major" t0
<*> parse digitsP "Minor" t1
<*> parse digitsP "Patch" t2
semantic _ v = pure v
{-# INLINE semantic #-}
data VSep = VColon | VHyphen | VPlus | VUnder
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData, Hashable)
type ParsingError = ParseErrorBundle T.Text Void
versioning :: T.Text -> Either ParsingError Versioning
versioning = parse versioning' "versioning"
versioning' :: Parsec Void T.Text Versioning
versioning' = choice [ try (fmap Ideal semver' <* eof)
, try (fmap General version' <* eof)
, fmap Complex mess' <* eof ]
semver :: T.Text -> Either ParsingError SemVer
semver = parse (semver' <* eof) "Semantic Version"
semver' :: Parsec Void T.Text SemVer
semver' = L.lexeme space (SemVer <$> majorP <*> minorP <*> patchP <*> preRel <*> metaData)
digitsP :: Parsec Void T.Text Word
digitsP = read <$> ((T.unpack <$> string "0") <|> some digitChar)
majorP :: Parsec Void T.Text Word
majorP = digitsP <* char '.'
minorP :: Parsec Void T.Text Word
minorP = majorP
patchP :: Parsec Void T.Text Word
patchP = digitsP
preRel :: Parsec Void T.Text [VChunk]
preRel = (char '-' *> chunks) <|> pure []
metaData :: Parsec Void T.Text [VChunk]
metaData = (char '+' *> chunks) <|> pure []
chunks :: Parsec Void T.Text [VChunk]
chunks = chunk `sepBy` char '.'
chunk :: Parsec Void T.Text VChunk
chunk = try zeroWithLetters <|> oneZero <|> many (iunit <|> sunit)
where oneZero = (:[]) . Digits . read . T.unpack <$> string "0"
zeroWithLetters = do
z <- Digits . read . T.unpack <$> string "0"
s <- some sunit
c <- chunk
pure $ (z : s) ++ c
iunit :: Parsec Void T.Text VUnit
iunit = Digits . read <$> some digitChar
sunit :: Parsec Void T.Text VUnit
sunit = Str . T.pack <$> some letterChar
pvp :: T.Text -> Either ParsingError PVP
pvp = parse (pvp' <* eof) "PVP"
pvp' :: Parsec Void T.Text PVP
pvp' = L.lexeme space (PVP . NEL.fromList <$> L.decimal `sepBy` char '.')
version :: T.Text -> Either ParsingError Version
version = parse (version' <* eof) "Version"
version' :: Parsec Void T.Text Version
version' = L.lexeme space (Version <$> optional (try epochP) <*> chunks <*> preRel)
epochP :: Parsec Void T.Text Word
epochP = read <$> (some digitChar <* char ':')
mess :: T.Text -> Either ParsingError Mess
mess = parse (mess' <* eof) "Mess"
mess' :: Parsec Void T.Text Mess
mess' = L.lexeme space (try node <|> leaf)
leaf :: Parsec Void T.Text Mess
leaf = VLeaf <$> tchunks
node :: Parsec Void T.Text Mess
node = VNode <$> tchunks <*> sep <*> mess'
tchunks :: Parsec Void T.Text [T.Text]
tchunks = (T.pack <$> some (letterChar <|> digitChar)) `sepBy` char '.'
sep :: Parsec Void T.Text VSep
sep = choice [ VColon <$ char ':'
, VHyphen <$ char '-'
, VPlus <$ char '+'
, VUnder <$ char '_' ]
sepCh :: VSep -> Char
sepCh VColon = ':'
sepCh VHyphen = '-'
sepCh VPlus = '+'
sepCh VUnder = '_'
prettyV :: Versioning -> T.Text
prettyV (Ideal sv) = prettySemVer sv
prettyV (General v) = prettyVer v
prettyV (Complex m) = prettyMess m
prettySemVer :: SemVer -> T.Text
prettySemVer (SemVer ma mi pa pr me) = mconcat $ ver <> pr' <> me'
where ver = intersperse "." [ showt ma, showt mi, showt pa ]
pr' = foldable [] ("-" :) $ intersperse "." (chunksAsT pr)
me' = foldable [] ("+" :) $ intersperse "." (chunksAsT me)
prettyPVP :: PVP -> T.Text
prettyPVP (PVP (m :| rs)) = T.intercalate "." . map showt $ m : rs
prettyVer :: Version -> T.Text
prettyVer (Version ep cs pr) = ep' <> mconcat (ver <> pr')
where ver = intersperse "." $ chunksAsT cs
pr' = foldable [] ("-" :) $ intersperse "." (chunksAsT pr)
ep' = maybe "" (\e -> showt e <> ":") ep
prettyMess :: Mess -> T.Text
prettyMess (VLeaf t) = mconcat $ intersperse "." t
prettyMess (VNode t s v) = T.snoc t' (sepCh s) <> prettyMess v
where t' = mconcat $ intersperse "." t
chunksAsT :: [VChunk] -> [T.Text]
chunksAsT = map (mconcat . map f)
where f (Digits i) = showt i
f (Str s) = s
foldable :: Foldable f => f b -> (f a -> f b) -> f a -> f b
foldable d g f | null f = d
| otherwise = g f
opposite :: Ordering -> Ordering
opposite EQ = EQ
opposite LT = GT
opposite GT = LT
showt :: Show a => a -> T.Text
showt = T.pack . show
hush :: Either a b -> Maybe b
hush (Left _) = Nothing
hush (Right b) = Just b