semver-range (empty) → 0.1.0
raw patch · 5 files changed
+395/−0 lines, 5 filesdep +basedep +classy-preludedep +parsecsetup-changed
Dependencies added: base, classy-prelude, parsec, text, unordered-containers
Files
- LICENSE +21/−0
- Setup.hs +2/−0
- semver-range.cabal +37/−0
- src/Data/SemVer.hs +190/−0
- src/Data/SemVer/Parser.hs +145/−0
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2015 Allen Nelson++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ semver-range.cabal view
@@ -0,0 +1,37 @@+name: semver-range+version: 0.1.0+synopsis: An implementation of semver and semantic version ranges.+license: MIT+license-file: LICENSE+author: Allen Nelson+maintainer: anelson@narrativescience.com+build-type: Simple+cabal-version: >=1.10+bug-reports: https://github.com/adnelson/semver-range/issues++source-repository head+ type: git+ location: git://github.com/adnelson/semver-range.git++library+ Exposed-modules:+ Data.SemVer+ Data.SemVer.Parser+ other-extensions: FlexibleContexts+ , FlexibleInstances+ , LambdaCase+ , NoImplicitPrelude+ , NoMonomorphismRestriction+ , OverloadedStrings+ , QuasiQuotes+ , RecordWildCards+ , ScopedTypeVariables+ , TypeFamilies+ , TypeSynonymInstances+ build-depends: base >=4.8 && <4.9+ , classy-prelude+ , text+ , unordered-containers+ , parsec+ hs-source-dirs: src+ default-language: Haskell2010
+ src/Data/SemVer.hs view
@@ -0,0 +1,190 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE LambdaCase #-}+module Data.SemVer where++import ClassyPrelude+import qualified Prelude as P+import Data.Text (Text)+import qualified Data.Text as T++type ReleaseTag = Text++-- | A SemVer has major, minor and patch versions, and zero or more+-- pre-release version tags.+type SemVer = (Int, Int, Int, [ReleaseTag])++-- | A partially specified semantic version. Implicitly defines+-- a range of acceptable versions, as seen in @wildcardToRange@.+data Wildcard = Any+ | One Int+ | Two Int Int+ | Three Int Int Int [Text]+ deriving (Show, Eq)++-- | A range specifies bounds on a semver.+data SemVerRange+ = Eq SemVer -- ^ Exact equality+ | Gt SemVer -- ^ Greater than+ | Lt SemVer -- ^ Less than+ | Geq SemVer -- ^ Greater than or equal to+ | Leq SemVer -- ^ Less than or equal to+ | And SemVerRange SemVerRange -- ^ Conjunction+ | Or SemVerRange SemVerRange -- ^ Disjunction+ deriving (Eq, Ord)++-- | Pull all of the concrete versions out of a range.+versionsOf :: SemVerRange -> [SemVer]+versionsOf = \case+ Eq sv -> [sv]+ Geq sv -> [sv]+ Leq sv -> [sv]+ Lt sv -> [sv]+ Gt sv -> [sv]+ And svr1 svr2 -> versionsOf svr1 <> versionsOf svr2+ Or svr1 svr2 -> versionsOf svr1 <> versionsOf svr2++-- | Create a SemVer with no version tags.+semver :: Int -> Int -> Int -> SemVer+semver a b c = (a, b, c, [])++-- | Get the release tags from a semver.+releaseTags :: SemVer -> [ReleaseTag]+releaseTags (_, _, _, tags) = tags++-- | Get only the version tuple from a semver.+toTuple :: SemVer -> (Int, Int, Int)+toTuple (a, b, c, _) = (a, b, c)++-- | Get a list of tuples from a version range.+tuplesOf :: SemVerRange -> [(Int, Int, Int)]+tuplesOf = map toTuple . versionsOf++-- | Get all of the release tags from a version range.+rangeReleaseTags :: SemVerRange -> [ReleaseTag]+rangeReleaseTags = concatMap releaseTags . versionsOf++-- | Satisfies any version.+anyVersion :: SemVerRange+anyVersion = Gt $ semver 0 0 0++-- | Render a semver as Text.+renderSV :: SemVer -> Text+renderSV = pack . renderSV'++-- | Render a semver as a String.+renderSV' :: SemVer -> String+renderSV' (x, y, z, []) = show x <> "." <> show y <> "." <> show z+renderSV' (x, y, z, tags) = renderSV' (x, y, z, []) <> "-" <>+ (intercalate "." $ map unpack tags)++instance Show SemVerRange where+ show = \case+ Eq sv -> "=" <> renderSV' sv+ Gt sv -> ">" <> renderSV' sv+ Lt sv -> "<" <> renderSV' sv+ Geq sv -> ">=" <> renderSV' sv+ Leq sv -> "<=" <> renderSV' sv+ And svr1 svr2 -> show svr1 <> " " <> show svr2+ Or svr1 svr2 -> show svr1 <> " || " <> show svr2++-- | Returns whether a given semantic version matches a range.+-- Note that there are special cases when there are release tags. For detauls+-- see https://github.com/npm/node-semver#prerelease-tags.+matches :: SemVerRange -> SemVer -> Bool+matches range ver = case (rangeReleaseTags range, releaseTags ver) of+ -- This is the simple case, where neither the range nor the version has given+ -- any release tags. Then we can just do regular predicate calculus.+ ([], []) -> matchesSimple range ver+ -- If the version has release tags but the range doesn't, it's not a match.+ ([], _) -> False+ -- If the version has release tags but none of the comparators have the same+ -- (major, minor, patch) tuples, it's not a match. For now, we're being even+ -- more strict, saying that there has to be exactly one comparator as well.+ (_, _) | tuplesOf range /= [toTuple ver] -> False+ -- If they have the same tuples, we can compare the version tags.+ (rTags, vTags) -> matchesTags range rTags vTags++-- | Simple predicate calculus matching, doing AND and OR combination with+-- numerical comparison.+matchesSimple :: SemVerRange -> SemVer -> Bool+matchesSimple range ver = case range of+ Eq sv -> ver == sv+ Gt sv -> ver > sv+ Lt sv -> ver < sv+ Geq sv -> ver >= sv+ Leq sv -> ver <= sv+ And sv1 sv2 -> matchesSimple sv1 ver && matchesSimple sv2 ver+ Or sv1 sv2 -> matchesSimple sv1 ver || matchesSimple sv2 ver++-- | Given a range and two sets of tags, the first being a bound on the second,+-- uses the range to compare the tags and see if they match.+matchesTags :: SemVerRange -> [ReleaseTag] -> [ReleaseTag] -> Bool+matchesTags range rangeTags verTags = case range of+ Eq _ -> verTags == rangeTags+ Gt _ -> verTags > rangeTags+ Lt _ -> verTags < rangeTags+ Geq _ -> verTags >= rangeTags+ Leq _ -> verTags <= rangeTags+ -- Note that as we're currently doing things, these cases won't get hit.+ And svr1 svr2 -> matchesTags svr1 verTags rangeTags &&+ matchesTags svr2 verTags rangeTags+ Or svr1 svr2 -> matchesTags svr1 verTags rangeTags ||+ matchesTags svr2 verTags rangeTags++-- | Gets the highest-matching semver in a range.+bestMatch :: SemVerRange -> [SemVer] -> Either String SemVer+bestMatch range vs = case filter (matches range) vs of+ [] -> Left "No matching versions"+ vs -> Right $ P.maximum vs++-- | Fills in zeros in a wildcard.+wildcardToSemver :: Wildcard -> SemVer+wildcardToSemver Any = (0, 0, 0, [])+wildcardToSemver (One n) = (n, 0, 0, [])+wildcardToSemver (Two n m) = (n, m, 0, [])+wildcardToSemver (Three n m o tags) = (n, m, o, tags)++-- | Translates a wildcard (partially specified version) to a range.+-- Ex: 2 := >=2.0.0 <3.0.0+-- Ex: 1.2.x := 1.2 := >=1.2.0 <1.3.0+wildcardToRange :: Wildcard -> SemVerRange+wildcardToRange = \case+ Any -> Geq (0, 0, 0, [])+ One n -> Geq (n, 0, 0, []) `And` Lt (n+1, 0, 0, [])+ Two n m -> Geq (n, m, 0, []) `And` Lt (n, m + 1, 0, [])+ Three n m o tags -> Eq (n, m, o, tags)++-- | Translates a ~wildcard to a range.+-- Ex: ~1.2.3 := >=1.2.3 <1.(2+1).0 := >=1.2.3 <1.3.0+tildeToRange :: Wildcard -> SemVerRange+tildeToRange = \case+ Any -> tildeToRange (Three 0 0 0 [])+ One n -> tildeToRange (Three n 0 0 [])+ Two n m -> tildeToRange (Three n m 0 [])+ Three n m o tags -> And (Geq (n, m, o, tags)) (Lt (n, m + 1, 0, tags))++-- | Translates a ^wildcard to a range.+-- Ex: ^1.2.x := >=1.2.0 <2.0.0+caratToRange :: Wildcard -> SemVerRange+caratToRange = \case+ One n -> And (Geq (n, 0, 0, [])) (Lt (n+1, 0, 0, []))+ Two n m -> And (Geq (n, m, 0, [])) (Lt (n+1, 0, 0, []))+ Three 0 0 n tags -> Eq (0, 0, n, tags)+ Three 0 n m tags -> And (Geq (0, n, m, tags)) (Lt (0, n + 1, 0, tags))+ Three n m o tags -> And (Geq (n, m, o, tags)) (Lt (n+1, 0, 0, tags))++-- | Translates two hyphenated wildcards to an actual range.+-- Ex: 1.2.3 - 2.3.4 := >=1.2.3 <=2.3.4+-- Ex: 1.2 - 2.3.4 := >=1.2.0 <=2.3.4+-- Ex: 1.2.3 - 2 := >=1.2.3 <3.0.0+hyphenatedRange :: Wildcard -> Wildcard -> SemVerRange+hyphenatedRange wc1 wc2 = And sv1 sv2 where+ sv1 = case wc1 of Any -> Geq (0, 0, 0, [])+ One n -> Geq (n, 0, 0, [])+ Two n m -> Geq (n, m, 0, [])+ Three n m o tags -> Geq (n, m, o, tags)+ sv2 = case wc2 of Any -> Geq (0, 0, 0, []) -- Refers to "any version"+ One n -> Lt (n+1, 0, 0, [])+ Two n m -> Lt (n, m + 1, 0, [])+ Three n m o tags -> Leq (n, m, o, tags)
+ src/Data/SemVer/Parser.hs view
@@ -0,0 +1,145 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE LambdaCase #-}+module Data.SemVer.Parser (+ parseSemVer, parseSemVerRange, pSemVerRange, pSemVer,+ fromHaskellVersion+ ) where++import qualified Prelude as P+import ClassyPrelude hiding (try)+import Text.Parsec hiding ((<|>), spaces, parse, State, uncons)+import qualified Text.Parsec as Parsec++import Data.Version (Version(..))+import Data.SemVer++type Parser = ParsecT String () Identity++-- | Given a parser and a string, attempts to parse the string.+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++-- | Consumes any spaces (not other whitespace).+spaces :: Parser String+spaces = many $ char ' '++-- | Consumes at least one space (not other whitespace).+spaces1 :: Parser String+spaces1 = many1 $ char ' '++-- | Parses the given string and any trailing spaces.+sstring :: String -> Parser String+sstring = lexeme . string++-- | Parses the given character and any trailing spaces.+schar :: Char -> Parser Char+schar = lexeme . char++-- | Parses `p` and any trailing spaces.+lexeme :: Parser a -> Parser a+lexeme p = p <* spaces++-- | Parses an integer.+pInt :: Parser Int+pInt = lexeme pInt'++-- | Parses an integer without consuming trailing spaces.+pInt' :: Parser Int+pInt' = P.read <$> many1 digit++-- | Parse a string as a version range, or return an error.+parseSemVerRange :: Text -> Either ParseError SemVerRange+parseSemVerRange = parse pSemVerRange++-- | Parse a string as an explicit version, or return an error.+parseSemVer :: Text -> Either ParseError SemVer+parseSemVer = parse pSemVer++-- | Parses a semantic version.+pSemVer :: Parser SemVer+pSemVer = wildcardToSemver <$> pWildCard++pVersionComp :: Parser SemVerRange+pVersionComp = do+ comparator <- cmp+ ver <- pSemVer+ let func = case comparator of {"=" -> Eq; ">" -> Gt; "<" -> Lt;+ ">=" -> Geq; "<=" -> Leq; "==" -> Eq}+ return $ func ver++-- | Parses a comparison operator.+cmp :: Parser String+cmp = choice $ fmap (try . sstring) [">=", "<=", ">", "<", "==", "="]++-- | Parses versions with an explicit range qualifier (gt, lt, etc).+pSemVerRangeSingle :: Parser SemVerRange+pSemVerRangeSingle = choice [+ wildcardToRange <$> pWildCard,+ tildeToRange <$> pTildeRange,+ caratToRange <$> pCaratRange,+ pVersionComp+ ]++-- | Parses semantic version ranges joined with Ands and Ors.+pJoinedSemVerRange :: Parser SemVerRange+pJoinedSemVerRange = do+ first <- pSemVerRangeSingle+ option first $ do+ lookAhead (sstring "||" <|> cmp) >>= \case+ "||" -> Or first <$> (sstring "||" *> pJoinedSemVerRange)+ _ -> And first <$> pJoinedSemVerRange++-- | Parses a hyphenated range.+pHyphen :: Parser SemVerRange+pHyphen = hyphenatedRange <$> pWildCard <*> (sstring "-" *> pWildCard)++-- | Parses a "wildcard" (which is a possibly partial semantic version).+pWildCard :: Parser Wildcard+pWildCard = try $ do+ let seps = choice $ map string ["x", "X", "*"]+ let bound = choice [seps *> pure Nothing, Just <$> pInt']+ let stripNothings [Nothing] = []+ stripNothings (Just x:xs) = x : stripNothings xs+ tag = fmap pack $ many1 $ letter <|> digit <|> char '-'+ -- Versions can optionally start with the character 'v'+ optional (char 'v')+ takeWhile isJust <$> sepBy1 bound (sstring ".") >>= \case+ [] -> return Any+ [Just n] -> return $ One n+ [Just n, Just m] -> return $ Two n m+ [Just n, Just m, Just o] -> option (Three n m o []) $ do+ char '-'+ tags <- tag `sepBy1` char '.'+ return $ Three n m o tags+ w -> unexpected ("Invalid version " ++ show w)++-- | Parses a tilde range (~1.2.3).+pTildeRange :: Parser Wildcard+pTildeRange = do+ sstring "~"+ -- For some reason, including the following operators after+ -- a tilde is valid, but seems to have no effect.+ optional $ choice [try $ sstring ">=", sstring ">", sstring "="]+ pWildCard++-- | Parses a carat range (^1.2.3).+pCaratRange :: Parser Wildcard+pCaratRange = sstring "^" *> pWildCard++-- | Top-level parser. Parses a semantic version range.+pSemVerRange :: Parser SemVerRange+pSemVerRange = try pHyphen <|> pJoinedSemVerRange+++-- | Parse a semver from a haskell version. There must be exactly+-- three numbers in the versionBranch field.+fromHaskellVersion :: Version -> Either Text SemVer+fromHaskellVersion v = case versionBranch v of+ [x, y, z] -> return (x, y, z, []) -- ignoring version tags since deprecated+ bad -> do+ let badVer = intercalate "." (map show bad)+ Left $ pack ("Not a SemVer version: " <> badVer)