versions 4.0.3 → 5.0.0
raw patch · 4 files changed
+122/−62 lines, 4 files
Files
- CHANGELOG.md +28/−0
- Data/Versions.hs +71/−42
- test/Test.hs +20/−19
- versions.cabal +3/−1
CHANGELOG.md view
@@ -1,5 +1,33 @@ # Changelog +## 5.0.0 (2021-04-14)++This release brings `versions` in line with version `2.0.0` of the SemVer spec.+The main addition to the spec is the allowance of hyphens in both the prerelease+and metadata sections. As such, **certain versions like `1.2.3+1-1` which+previously would not parse as SemVer now do.**++To accomodate this and other small spec updates, the `SemVer` and `Version`+types have received breaking changes here.++#### Changed++- **Breaking:** The `_svMeta` field of `SemVer` is now parsed as a dumber `Maybe+ Text` instead of `[VChunk]`, due to metadata now being allowed to possess+ leading zeroes.+- **Breaking:** Like the above, the `_vMeta` field of `Version` is now `Maybe Text`.+- **Breaking: The `_vRel` and `_vMeta` fields of `Version` have had their order+ flipped.** Further, the prelease and meta sections are now expected in the+ same order as `SemVer` when parsing (prerel first, meta second). `Version` is+ thus now a quite similar to `SemVer`, except allowing letters in more+ permissive positions.+- **Breaking:** The `meta` traversal has been altered to accomodate the metadata+ field changes.++#### Fixed++- Parsing certain legal SemVers specified in the spec.+ ## 4.0.3 (2021-02-23) #### Changed
Data/Versions.hs view
@@ -7,7 +7,7 @@ -- | -- Module : Data.Versions--- Copyright : (c) Colin Woodbury, 2015 - 2020+-- Copyright : (c) Colin Woodbury, 2015 - 2021 -- License : BSD3 -- Maintainer: Colin Woodbury <colin@fosskers.ca> --@@ -29,6 +29,8 @@ -- currently using it. It provides consistency in version incrementing and has -- the best constraints on comparisons. --+-- __This library implements version @2.0.0@ of the SemVer spec.__+-- -- == Using the Parsers -- In general, `versioning` is the function you want. It attempts to parse a -- given `Text` using the three individual parsers, `semver`, `version` and@@ -74,7 +76,7 @@ import Control.DeepSeq import Control.Monad (void) import Data.Bool (bool)-import Data.Char (isAlpha)+import Data.Char (isAlpha, isAlphaNum) import Data.Foldable (fold) import Data.Hashable (Hashable) import Data.List (intersperse)@@ -140,19 +142,23 @@ -- | Convert a `SemVer` to a `Version`. vFromS :: SemVer -> Version vFromS (SemVer ma mi pa re me) =- Version Nothing ((Digits ma :| []) :| [(Digits mi :| []), Digits pa :| []]) re me+ Version+ { _vEpoch = Nothing+ , _vChunks = (Digits ma :| []) :| [Digits mi :| [], Digits pa :| []]+ , _vMeta = me+ , _vRel = re } -- | Convert a `Version` to a `Mess`. mFromV :: Version -> Mess-mFromV (Version e v m r) = maybe affix (\a -> Mess (MDigit a (showt a) :| []) $ Just (VColon, affix)) e+mFromV (Version e v r m) = maybe affix (\a -> Mess (MDigit a (showt a) :| []) $ Just (VColon, affix)) e where affix :: Mess affix = Mess (chunksAsM v) m' m' :: Maybe (VSep, Mess)- m' = case NEL.nonEmpty m of+ m' = case m of Nothing -> r'- Just m'' -> Just (VPlus, Mess (chunksAsM m'') r')+ Just m'' -> Just (VPlus, Mess (MPlain m'' :| []) r') r' :: Maybe (VSep, Mess) r' = case NEL.nonEmpty r of@@ -246,16 +252,19 @@ _Mess f t = either (const (pure t)) (fmap prettyMess . f) $ mess t {-# INLINE _Mess #-} +-- | Possibly extract a `SemVer` from a `Versioning`. _Ideal :: Traversal' Versioning SemVer _Ideal f (Ideal s) = Ideal <$> f s _Ideal _ v = pure v {-# INLINE _Ideal #-} +-- | Possibly extract a `Version` from a `Versioning`. _General :: Traversal' Versioning Version _General f (General v) = General <$> f v _General _ v = pure v {-# INLINE _General #-} +-- | Possibly extract a `Mess` from a `Versioning`. _Complex :: Traversal' Versioning Mess _Complex f (Complex m) = Complex <$> f m _Complex _ v = pure v@@ -303,7 +312,7 @@ -- | @major.minor.patch-PREREL+meta@ release :: Traversal' v [VChunk] -- | @major.minor.patch-prerel+META@- meta :: Traversal' v [VChunk]+ meta :: Traversal' v (Maybe Text) -- | A Natural Transformation into an proper `SemVer`. semantic :: Traversal' v SemVer @@ -331,7 +340,7 @@ -- -- 2. Build metadata does not affect version precedence. ----- 3. PREREL and META strings may only contain ASCII alphanumerics.+-- 3. PREREL and META strings may only contain ASCII alphanumerics and hyphens. -- -- For more information, see http://semver.org data SemVer = SemVer@@ -339,7 +348,7 @@ , _svMinor :: !Word , _svPatch :: !Word , _svPreRel :: ![VChunk]- , _svMeta :: ![VChunk] }+ , _svMeta :: !(Maybe Text) } deriving stock (Show, Generic) deriving anyclass (NFData, Hashable) @@ -362,10 +371,10 @@ 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')+ SemVer (mj + mj') (mn + mn') (pa + pa') (p ++ p') (m <> m') instance Monoid SemVer where- mempty = SemVer 0 0 0 [] []+ mempty = SemVer 0 0 0 [] Nothing #if !MIN_VERSION_base(4,11,0) mappend = (<>)@@ -418,11 +427,13 @@ str :: Text -> Maybe VUnit str t = bool Nothing (Just $ Str t) $ T.all isAlpha t +-- | Possibly traverse the inner digit value of a `VUnit`. _Digits :: Traversal' VUnit Word _Digits f (Digits i) = Digits <$> f i _Digits _ v = pure v {-# INLINE _Digits #-} +-- | Possibly traverse the inner text of a `VUnit`. _Str :: Traversal' VUnit Text _Str f (Str t) = Str . (\t' -> bool t t' (T.all isAlpha t')) <$> f t _Str _ v = pure v@@ -479,22 +490,22 @@ {-# 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 :| [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 []+ release f p = p <$ f [] {-# INLINE release #-} - meta f p = const p <$> f []+ meta f p = p <$ f Nothing {-# 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 [] []+ mi : pa : _ -> SemVer m mi pa [] Nothing+ mi : _ -> SemVer m mi 0 [] Nothing+ [] -> SemVer m 0 0 [] Nothing {-# INLINE semantic #-} --------------------------------------------------------------------------------@@ -506,27 +517,28 @@ -- -- Generally conforms to the @a.b.c-p@ pattern, and may optionally have an -- /epoch/ and /metadata/. Epochs are prefixes marked by a colon, like in--- @1:2.3.4@. Metadata is prefixed by @+@, and unlike SemVer can appear before+-- @1:2.3.4@. Metadata is prefixed by @+@, and like SemVer must appear after -- the "prerelease" (the @-p@). -- -- Examples of @Version@ that are not @SemVer@: 0.25-2, 8.u51-1, 20150826-1,--- 1:2.3.4, 1.11.0+20200830-1+-- 1:2.3.4 data Version = Version { _vEpoch :: !(Maybe Word) , _vChunks :: !(NonEmpty VChunk)- , _vMeta :: ![VChunk]- , _vRel :: ![VChunk] }+ , _vRel :: ![VChunk]+ , _vMeta :: !(Maybe Text) } deriving stock (Eq, Show, Generic) deriving anyclass (NFData, Hashable) instance Semigroup Version where Version e c m r <> Version e' c' m' r' = Version ((+) <$> e <*> e') (c <> c') (m <> m') (r <> r') --- | Customized.+-- | Customized. As in SemVer, metadata is ignored for the purpose of+-- comparison. instance Ord Version where -- | For the purposes of Versions with epochs, `Nothing` is the same as `Just 0`, -- so we need to compare their actual version numbers.- compare (Version ae as _ rs) (Version be bs _ rs') = case compare (fromMaybe 0 ae) (fromMaybe 0 be) of+ compare (Version ae as rs _) (Version be bs rs' _) = case compare (fromMaybe 0 ae) (fromMaybe 0 be) of EQ -> case g (NEL.toList as) (NEL.toList bs) of -- If the two Versions were otherwise equal and recursed down this far, -- we need to compare them by their "release" values.@@ -601,11 +613,12 @@ meta _ v = pure v {-# INLINE meta #-} - semantic f (Version _ ((Digits a:|[]) :| (Digits b:|[]) : (Digits c:|[]) : _) me rs) =- vFromS <$> f (SemVer a b c me rs)+ semantic f (Version _ ((Digits a:|[]) :| (Digits b:|[]) : (Digits c:|[]) : _) rs me) =+ vFromS <$> f (SemVer a b c rs me) semantic _ v = pure v {-# INLINE semantic #-} +-- | A `Version`'s inner epoch `Word`. epoch :: Lens' Version (Maybe Word) epoch f v = fmap (\ve -> v { _vEpoch = ve }) (f $ _vEpoch v) {-# INLINE epoch #-}@@ -679,7 +692,7 @@ -- -- Example: @1.6.0a+2014+m872b87e73dfb-1@ We should be able to extract @0a@ safely. messPatchChunk :: Mess -> Maybe VChunk-messPatchChunk (Mess (_ :| _ : MPlain p : _) _) = hush $ parse chunk "Chunk" p+messPatchChunk (Mess (_ :| _ : MPlain p : _) _) = hush $ parse (chunkWith unit) "Chunk" p messPatchChunk _ = Nothing instance Ord Mess where@@ -715,7 +728,7 @@ -- | Good luck. semantic f (Mess (MDigit t0 _ :| MDigit t1 _ : MDigit t2 _ : _) _) =- mFromV . vFromS <$> (f $ SemVer t0 t1 t2 [] [])+ mFromV . vFromS <$> f (SemVer t0 t1 t2 [] Nothing) semantic _ v = pure v {-# INLINE semantic #-} @@ -733,7 +746,7 @@ -------------------------------------------------------------------------------- -- Parsing --- | A synonym for the more verbose `megaparsec` error type.+-- | A synonym for the more verbose 'megaparsec' error type. type ParsingError = ParseErrorBundle Text Void -- | Parse a piece of `Text` into either an (Ideal) `SemVer`, a (General)@@ -757,7 +770,7 @@ semver' = L.lexeme space semver'' semver'' :: Parsec Void Text SemVer-semver'' = SemVer <$> majorP <*> minorP <*> patchP <*> preRel <*> metaData+semver'' = SemVer <$> majorP <*> minorP <*> patchP <*> preRel <*> optional metaData -- | Parse a group of digits, which can't be lead by a 0, unless it is 0. digitsP :: Parsec Void Text Word@@ -775,19 +788,24 @@ preRel :: Parsec Void Text [VChunk] preRel = (char '-' *> chunks) <|> pure [] -metaData :: Parsec Void Text [VChunk]-metaData = (char '+' *> chunks) <|> pure []+metaData :: Parsec Void Text Text+metaData = do+ void $ char '+'+ fold . NEL.intersperse "." <$> section `PC.sepBy1` char '.'+ where+ section :: Parsec Void Text Text+ section = takeWhile1P (Just "Metadata char") (\c -> isAlphaNum c || c == '-') chunksNE :: Parsec Void Text (NonEmpty VChunk)-chunksNE = chunk `PC.sepBy1` char '.'+chunksNE = chunkWith unit' `PC.sepBy1` char '.' chunks :: Parsec Void Text [VChunk]-chunks = chunk `sepBy` char '.'+chunks = chunkWith unit `sepBy` char '.' -- | Handling @0@ is a bit tricky. We can't allow runs of zeros in a chunk, -- since a version like @1.000.1@ would parse as @1.0.1@.-chunk :: Parsec Void Text VChunk-chunk = try zeroWithLetters <|> oneZero <|> PC.some (iunit <|> sunit)+chunkWith :: Parsec Void Text VUnit -> Parsec Void Text VChunk+chunkWith u = try zeroWithLetters <|> oneZero <|> PC.some u where oneZero :: Parsec Void Text (NonEmpty VUnit) oneZero = (Digits 0 :| []) <$ single '0'@@ -796,17 +814,28 @@ zeroWithLetters = do z <- Digits 0 <$ single '0' s <- PC.some sunit- c <- optional chunk+ c <- optional (chunkWith u) case c of Nothing -> pure $ NEL.cons z s Just c' -> pure $ NEL.cons z s <> c' +unit :: Parsec Void Text VUnit+unit = iunit <|> sunit++unit' :: Parsec Void Text VUnit+unit' = iunit <|> sunit'+ iunit :: Parsec Void Text VUnit iunit = Digits <$> ((0 <$ single '0') <|> (read <$> some digitChar)) sunit :: Parsec Void Text VUnit-sunit = Str . T.pack <$> some letterChar+sunit = Str . T.pack <$> some (letterChar <|> single '-') +-- | Same as `sunit`, but don't allow hyphens. Intended for the main body of+-- `Version`.+sunit' :: Parsec Void Text VUnit+sunit' = Str . T.pack <$> some letterChar+ -- | Parse a (Haskell) `PVP`, as defined above. pvp :: Text -> Either ParsingError PVP pvp = parse (pvp' <* eof) "PVP"@@ -824,7 +853,7 @@ version' = L.lexeme space version'' version'' :: Parsec Void Text Version-version'' = Version <$> optional (try epochP) <*> chunksNE <*> metaData <*> preRel+version'' = Version <$> optional (try epochP) <*> chunksNE <*> preRel <*> optional metaData epochP :: Parsec Void Text Word epochP = read <$> (some digitChar <* char ':')@@ -845,7 +874,7 @@ mchunk :: Parsec Void Text MChunk mchunk = choice [ try $ (\(t, i) -> MDigit i t) <$> match (L.decimal <* next)- , try $ (\(t, i) -> MRev i t) <$> (match (single 'r' *> L.decimal <* next))+ , try $ (\(t, i) -> MRev i t) <$> match (single 'r' *> L.decimal <* next) , MPlain . T.pack <$> some (letterChar <|> digitChar) ] where next :: Parsec Void Text ()@@ -875,7 +904,7 @@ where ver = intersperse "." [ showt ma, showt mi, showt pa ] pr' = foldable [] ("-" :) $ intersperse "." (chunksAsT pr)- me' = foldable [] ("+" :) $ intersperse "." (chunksAsT me)+ me' = maybe [] (\m -> ["+",m]) me -- | Convert a `PVP` back to its textual representation. prettyPVP :: PVP -> Text@@ -883,10 +912,10 @@ -- | Convert a `Version` back to its textual representation. prettyVer :: Version -> Text-prettyVer (Version ep cs me pr) = ep' <> mconcat (ver <> me' <> pr')+prettyVer (Version ep cs pr me) = ep' <> mconcat (ver <> me' <> pr') where ver = intersperse "." . chunksAsT $ NEL.toList cs- me' = foldable [] ("+" :) $ intersperse "." (chunksAsT me)+ me' = maybe [] (\m -> ["+",m]) me pr' = foldable [] ("-" :) $ intersperse "." (chunksAsT pr) ep' = maybe "" (\e -> showt e <> ":") ep
test/Test.hs view
@@ -6,7 +6,7 @@ module Main ( main ) where import Data.Char (chr)-import Data.Either (isLeft)+import Data.Either (fromRight, isLeft) import Data.Foldable (fold) import Data.List (groupBy) import qualified Data.List.NonEmpty as NEL@@ -25,7 +25,7 @@ --- instance Arbitrary SemVer where- arbitrary = SemVer <$> arbitrary <*> arbitrary <*> arbitrary <*> chunks <*> chunks+ arbitrary = SemVer <$> arbitrary <*> arbitrary <*> arbitrary <*> chunks <*> pure Nothing -- | Sane generation of VChunks. chunks :: Gen [VChunk]@@ -51,13 +51,14 @@ arbitrary = Letter . chr <$> choose (97, 122) instance Arbitrary Version where- arbitrary = Version <$> arbitrary <*> chunksNE <*> chunks <*> chunks+ arbitrary = Version <$> arbitrary <*> chunksNE <*> chunks <*> pure Nothing -- | These don't need to parse as a SemVer. goodVers :: [T.Text] goodVers = [ "1", "1.2", "1.0rc0", "1.0rc1", "1.1rc1", "1.58.0-3", "44.0.2403.157-1" , "0.25-2", "8.u51-1", "21-2", "7.1p1-1", "20150826-1", "1:0.10.16-3"- , "1.11.0.git.20200404-1", "1.11.0+20200830-1", "1:3.20" ]+ , "1.11.0.git.20200404-1", "1.11.0+20200830-1", "1:3.20"+ ] badVers :: [T.Text] badVers = ["", "1.2 "]@@ -72,13 +73,16 @@ ] badSemVs :: [T.Text]-badSemVs = [ "1", "1.2", "1.2.3+a1b2bc3.1-alpha.2", "a.b.c", "1.01.1"- , "1.2.3+a1b!2c3.1", "", "1.2.3 "+badSemVs = [ "1", "1.2", "a.b.c", "1.01.1", "1.2.3+a1b!2c3.1", "", "1.2.3 " ] goodSemVs :: [T.Text] goodSemVs = [ "0.1.0", "1.2.3", "1.2.3-1", "1.2.3-alpha", "1.2.3-alpha.2" , "1.2.3+a1b2c3.1", "1.2.3-alpha.2+a1b2c3.1", "2.2.1-b05"+ -- Weird Pre-releases+ , "1.0.0-x-y-z.-"+ -- Weird metadata+ , "1.0.0-alpha+001", "1.0.0+21AF26D3---117B344092BD" ] -- | The exact example from `http://semver.org`@@ -117,13 +121,12 @@ testCase "1.2.3-alpha.2 == 1.2.3-alpha.2+a1b2c3.1" (assertBool "Equality test of two complicated SemVers failed" $ semver "1.2.3-alpha.2" == semver "1.2.3-alpha.2+a1b2c3.1") :- map (\(a,b) -> testCase (T.unpack $ a <> " < " <> b) $ comp semver a b)- (zip semverOrd $ tail semverOrd)+ zipWith (\a b -> testCase (T.unpack $ a <> " < " <> b) $ comp semver a b) semverOrd (tail semverOrd) , testGroup "Whitespace Handling"- [ testCase "1.2.3-1[ ]" $ parse semver' "semver whitespace" "1.2.3-1 " @?= Right (SemVer 1 2 3 [[Digits 1]] [])+ [ testCase "1.2.3-1[ ]" $ parse semver' "semver whitespace" "1.2.3-1 " @?= Right (SemVer 1 2 3 [[Digits 1]] Nothing) ] , testGroup "Zero Handling"- [ testCase "2.2.1-b05" $ semver "2.2.1-b05" @?= Right (SemVer 2 2 1 [[Str "b", Digits 0, Digits 5]] [])+ [ testCase "2.2.1-b05" $ semver "2.2.1-b05" @?= Right (SemVer 2 2 1 [[Str "b", Digits 0, Digits 5]] Nothing) ] ]@@ -131,8 +134,7 @@ [ testGroup "Good PVPs" $ map (\s -> testCase (T.unpack s) $ isomorphPVP s) cabalOrd , testGroup "Comparisons" $- map (\(a,b) -> testCase (T.unpack $ a <> " < " <> b) $ comp pvp a b)- (zip cabalOrd $ tail cabalOrd)+ zipWith (\a b -> testCase (T.unpack $ a <> " < " <> b) $ comp pvp a b) cabalOrd (tail cabalOrd) ] , testGroup "(General) Versions" [ testGroup "Good Versions" $@@ -154,8 +156,7 @@ , testGroup "Bad Versions (shouldn't parse)" $ map (\s -> testCase (T.unpack s) $ assertBool "A bad version parsed" $ isLeft $ mess s) badVers , testGroup "Comparisons" $- map (\(a,b) -> testCase (T.unpack $ a <> " < " <> b) $ comp mess a b) $- zip messComps (tail messComps)+ zipWith (\a b -> testCase (T.unpack $ a <> " < " <> b) $ comp mess a b) messComps (tail messComps) , testGroup "SemVer-like Value Extraction" [ testCase "messMajor" $ (hush (mess "1.6.0a+2014+m872b87e73dfb-1") >>= messMajor) @?= Just 1@@ -174,10 +175,10 @@ [ testCase "1.2.3 is SemVer" $ check $ isSemVer <$> versioning "1.2.3" , testCase "1.2.3-1 is SemVer" $ check $ isSemVer <$> versioning "1.2.3-1" , testCase "1.2.3-1+1 is SemVer" $ check $ isSemVer <$> versioning "1.2.3-1+1"+ , testCase "1.2.3+1-1 is SemVer" $ check $ isSemVer <$> versioning "1.2.3+1-1" , testCase "1.2.3r1 is Version" $ check $ isVersion <$> versioning "1.2.3r1" , testCase "0.25-2 is Version" $ check $ isVersion <$> versioning "0.25-2" , testCase "1:1.2.3-1 is Version" $ check $ isVersion <$> versioning "1:1.2.3-1"- , testCase "1.2.3+1-1 is Version" $ check $ isVersion <$> versioning "1.2.3+1-1" , testCase "1:3.20.1-1 is Version" $ check $ isVersion <$> versioning "1:3.20.1-1" , testCase "000.007-1 is Mess" $ check $ isMess <$> versioning "000.007-1" , testCase "20.26.1_0-2 is Mess" $ check $ isMess <$> versioning "20.26.1_0-2"@@ -221,7 +222,7 @@ ] , testGroup "Megaparsec Behaviour" [ testCase "manyTill" $ parse nameGrab "manyTill" "linux-firmware-3.2.14-1-x86_64.pkg.tar.xz" @?= Right "linux-firmware"- , testCase "Extracting version" $ parse versionGrab "extraction" "linux-firmware-3.2.14-1-x86_64.pkg.tar.xz" @?= Right(Ideal $ SemVer 3 2 14 [[Digits 1]] [])+ , testCase "Extracting version" $ parse versionGrab "extraction" "linux-firmware-3.2.14-1-x86_64.pkg.tar.xz" @?= Right(Ideal $ SemVer 3 2 14 [[Digits 1, Str "-x", Digits 86]] Nothing) ] ] ]@@ -267,7 +268,7 @@ equal f a = check $ (\r -> r == r) <$> f a check :: Either a Bool -> Assertion-check = assertBool "Some Either-based assertion failed" . either (const False) id+check = assertBool "Some Either-based assertion failed" . fromRight False isSemVer :: Versioning -> Bool isSemVer (Ideal _) = True@@ -283,8 +284,8 @@ incPatch :: Assertion incPatch = (v1 & patch %~ (+ 1)) @?= v2- where v1 = Ideal $ SemVer 1 2 3 [] []- v2 = Ideal $ SemVer 1 2 4 [] []+ where v1 = Ideal $ SemVer 1 2 3 [] Nothing+ v2 = Ideal $ SemVer 1 2 4 [] Nothing incFromT :: Assertion incFromT = (("1.2.3" :: T.Text) & patch %~ (+ 1)) @?= "1.2.4"
versions.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: versions-version: 4.0.3+version: 5.0.0 synopsis: Types and parsers for software version numbers. description: A library for parsing and comparing software version numbers. We like to give@@ -19,6 +19,8 @@ Please switch to <http://semver.org Semantic Versioning> if you aren't currently using it. It provides consistency in version incrementing and has the best constraints on comparisons.+ .+ This library implements version @2.0.0@ of the SemVer spec. category: Data homepage: https://github.com/fosskers/versions