packages feed

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 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)