{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
module Data.SemVer.Parser (
parseSemVer, parseSemVerRange, pSemVerRange, pSemVer,
fromHaskellVersion, matchText, splitWS
) where
import qualified Prelude as P
import ClassyPrelude hiding (try, many)
import Text.Parsec hiding ((<|>), spaces, parse, State, uncons, optional)
import qualified Text.Parsec as Parsec
import qualified Data.Text as T
import Text.Read (readMaybe)
import Data.Version (Version(..))
import Data.SemVer.Types
type Parser = ParsecT String () Identity
splitWS :: Text -> [Text]
splitWS = filter (/= "") . T.split (flip elem (" \t\n\r" :: String))
data Wildcard = Any
| Maj Int
| Min Int Int
| Full SemVer
deriving (Show, Eq)
wildcardToSemver :: Wildcard -> SemVer
wildcardToSemver Any = semver 0 0 0
wildcardToSemver (Maj n) = semver n 0 0
wildcardToSemver (Min n m) = semver n m 0
wildcardToSemver (Full sv) = sv
wildcardToRange :: Wildcard -> SemVerRange
wildcardToRange = \case
Any -> Geq $ semver 0 0 0
Maj n -> Geq (semver n 0 0) `And` Lt (semver (n+1) 0 0)
Min n m -> Geq (semver n m 0) `And` Lt (semver n (m+1) 0)
Full sv -> Eq sv
tildeToRange :: Wildcard -> SemVerRange
tildeToRange = \case
Any -> tildeToRange (Full $ semver 0 0 0)
Maj n -> Geq (semver n 0 0) `And` Lt (semver (n+1) 0 0)
Min n m -> Geq (semver n m 0) `And` Lt (semver n (m+1) 0)
Full (SemVer n m o [] _) -> Geq (semver n m o) `And` Lt (semver n (m+1) 0)
Full (SemVer n m o tags _) -> Geq (semver' n m o tags) `And` Lt (semver n (m+1) 0)
caratToRange :: Wildcard -> SemVerRange
caratToRange = \case
Maj n -> Geq (semver n 0 0) `And` Lt (semver (n+1) 0 0)
Min n m -> Geq (semver n m 0) `And` Lt (semver (n+1) 0 0)
Full (SemVer 0 n m tags _) -> Geq (semver' 0 n m tags) `And` Lt (semver' 0 (n+1) 0 tags)
Full (SemVer n m o tags _) -> Geq (semver' n m o tags) `And` Lt (semver' (n+1) 0 0 tags)
hyphenatedRange :: Wildcard -> Wildcard -> SemVerRange
hyphenatedRange wc1 wc2 = And sv1 sv2 where
sv1 = case wc1 of Any -> anyVersion
Maj n -> Geq (semver n 0 0)
Min n m -> Geq (semver n m 0)
Full sv -> Geq sv
sv2 = case wc2 of Any -> anyVersion
Maj n -> Lt (semver (n+1) 0 0)
Min n m -> Lt (semver n (m+1) 0)
Full sv -> Lt sv
parse :: Parser a -> Text -> Either ParseError a
parse p = Parsec.parse p "" . unpack
parseFull :: Parser a -> Text -> Either ParseError a
parseFull p = Parsec.parse (p <* eof) "" . unpack
spaces :: Parser String
spaces = many $ oneOf [' ', '\t', '\n', '\r']
spaces1 :: Parser String
spaces1 = many1 $ oneOf [' ', '\t', '\n', '\r']
sstring :: String -> Parser String
sstring = lexeme . string
schar :: Char -> Parser Char
schar = lexeme . char
lexeme :: Parser a -> Parser a
lexeme p = p <* spaces
pInt :: Parser Int
pInt = lexeme pInt'
pInt' :: Parser Int
pInt' = P.read <$> many1 digit
parseSemVerRange :: Text -> Either ParseError SemVerRange
parseSemVerRange text = case T.strip text of
"" -> return anyVersion
"||" -> return anyVersion
t -> parse (pSemVerRange <* eof) t
parseSemVer :: Text -> Either ParseError SemVer
parseSemVer = parse pSemVer . T.strip
pSemVer :: Parser SemVer
pSemVer = do
optional (char '=')
wildcardToSemver <$> pWildCard
pVersionComp :: Parser SemVerRange
pVersionComp = cmp >>= \case
"=" -> wildcardToRange <$> pWildCard
"==" -> wildcardToRange <$> pWildCard
"<=" -> Leq . topOf <$> pWildCard
">=" -> Geq <$> pSemVer
">" -> Gt <$> pSemVer
"<" -> Lt <$> pSemVer
where
topOf = \case
Any -> semver 0 0 0
Maj n -> semver (n+1) 0 0
Min n m -> semver n (m+1) 0
Full sv -> sv
cmp :: Parser String
cmp = choice (try . sstring <$> [">=", "<=", ">", "<", "==", "="])
pSemVerRangeSingle :: Parser SemVerRange
pSemVerRangeSingle = choice [
wildcardToRange <$> pWildCard,
pTildeRange,
pCaratRange,
pVersionComp
]
pJoinedSemVerRange :: Parser SemVerRange
pJoinedSemVerRange = do
first <- pSemVerRangeSingle
option first $ do
let next = choice [sstring "||", sstring "&&", map singleton anyChar]
lookAhead next >>= \case
"||" -> Or first <$> (sstring "||" *> pJoinedSemVerRange)
"&&" -> And first <$> (sstring "&&" *> pJoinedSemVerRange)
_ -> And first <$> pJoinedSemVerRange
pHyphen :: Parser SemVerRange
pHyphen = hyphenatedRange <$> pWildCard <*> (sstring "-" *> pWildCard)
pWildCard :: Parser Wildcard
pWildCard = try $ do
let seps = choice $ map string ["x", "X", "*"]
let bound = choice [seps *> pure Nothing, Just <$> pInt']
let getTag t = case readMaybe t of
Just i -> IntTag i
_ -> TextTag $ pack t
let tag = getTag <$> many1 (letter <|> digit <|> char '-')
optional (char 'v')
res <- takeWhile isJust <$> sepBy1 bound (sstring ".") >>= \case
[] -> return Any
[Just n] -> return $ Maj n
[Just n, Just m] -> return $ Min n m
[Just n, Just m, Just o] -> option (Full $ semver n m o) $ do
tags <- option [] $ do
optional (char '-')
PrereleaseTags <$> (tag `sepBy1` char '.')
metadata <- option [] $ do
char '+'
many1 (letter <|> digit <|> char '-') `sepBy1` char '.'
return $ Full $ semver'' n m o tags (map pack metadata)
w -> unexpected ("Invalid version " ++ show w)
spaces *> return res
pTildeRange :: Parser SemVerRange
pTildeRange = do
sstring "~"
optional $ choice [try $ sstring ">=", sstring ">", sstring "="]
tildeToRange <$> pWildCard
pCaratRange :: Parser SemVerRange
pCaratRange = sstring "^" *> map caratToRange pWildCard
pSemVerRange :: Parser SemVerRange
pSemVerRange = try pHyphen <|> pJoinedSemVerRange
fromHaskellVersion :: Version -> Either Text SemVer
fromHaskellVersion v = case versionBranch v of
[x, y, z] -> return (semver x y z)
bad -> do
let badVer = intercalate "." (map show bad)
Left $ pack ("Not a SemVer version: " <> badVer)
matchText :: Text -> Text -> Either Text Bool
matchText rangeTxt verTxt = case parseSemVerRange rangeTxt of
Left err -> Left ("Could not parse range: " <> pack (show err))
Right range -> case parseSemVer verTxt of
Left err -> Left ("Could not parse version: " <> pack (show err))
Right version -> Right $ matches range version