packages feed

versions (empty) → 1.0.0

raw patch · 5 files changed

+604/−0 lines, 5 filesdep +basedep +eitherdep +extrasetup-changed

Dependencies added: base, either, extra, parsec, semigroups, tasty, tasty-hunit, text, text-show, versions

Files

+ Data/Versions.hs view
@@ -0,0 +1,347 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Module    : Data.Versions+-- Copyright : (c) Colin Woodbury, 2015+-- License   : BSD3+-- Maintainer: Colin Woodbury <colingw@gmail.com>+--+-- A library for parsing and comparing software version numbers.+--+-- We like to give version numbers to our software in a myriad of different+-- ways. Some ways follow strict guidelines for incrementing and comparison.+-- Some follow conventional wisdom and are generally self-consistent.+-- Some are just plain asinine. This library provides a means of parsing+-- and comparing /any/ style of versioning, be it a nice Semantic Version+-- like this:+--+-- > 1.2.3-r1+git123+--+-- ...or a monstrosity like this:+--+-- > 2:10.2+0.0093r3+1-1+--+-- 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.+--+-- == Using the Parsers+-- In general, `parseV` is the function you want. It attempts to parse+-- a given @Text@ using the three individual parsers, `semver`, `version`+-- and `mess`. If one fails, it tries the next. If you know you only want+-- to parse one specific version type, use that parser directly+-- (e.g. `semver`).++module Data.Versions+    (+      -- * Types+      Versioning(..)+    , SemVer(..)+    , Version(..)+    , Mess(..)+    , VUnit(..)+    , VChunk+    , VSep(..)+    , VParser(..)+      -- * Parsers+    , semver+    , semver'+    , version+    , version'+    , mess+    , mess'+      -- ** Wrapped Parsers+    , parseV+    , semverP+    , versionP+    , messP+      -- * Pretty Printing+    , prettyV+    , prettySemVer+    , prettyVer+    , prettyMess ) where++import Data.List (intersperse)+import Data.Semigroup+import Data.Text (Text,pack,unpack,snoc)+import Text.ParserCombinators.Parsec+import TextShow (showt)++---++-- | A top-level Versioning type. Acts as a wrapper for the more specific+-- types. This allows each subtype to have its own parser, and for said+-- parsers to be composed. This is useful for specifying custom behaviour+-- for when a certain parser fails.+data Versioning = Ideal SemVer | General Version | Complex Mess+                deriving (Eq,Show)++-- | Comparison of @Ideal@s is always well defined.+--+-- If comparison of @General@s is well-defined, then comparison+-- of @Ideal@ and @General@ is well-defined, as there exists a perfect+-- mapping from @Ideal@ to @General@.+--+-- If comparison of @Complex@es is well-defined, then comparison of @General@+-- and @Complex@ is well defined for the same reason.+-- This implies comparison of @Ideal@ and @Complex@ is also well-defined.+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)   = cmpSV s v+  compare (General v)   (Ideal s)     = opposite $ cmpSV s v+  compare (General v)   (Complex m)   = compare (mFromV v) m+  compare (Complex m)   (General v)   = opposite $ compare (mFromV v) m+  compare (Ideal s)     m@(Complex _) = compare (General $ vFromS s) m+  compare m@(Complex _) (Ideal s)     = compare m (General $ vFromS s)++cmpSV :: SemVer -> Version -> Ordering+cmpSV s (Version cs re) = compare (cs' ++ re') $ cs ++ re+  where (Version cs' re') = vFromS s++-- | Convert a `SemVer` to a `Version`.+vFromS :: SemVer -> Version+vFromS (SemVer m i p r _) = Version [[Digits m], [Digits i], [Digits p]] r++-- | Convert a `Version` to a `Mess`.+mFromV :: Version -> Mess+mFromV (Version v r) = VNode (chunksAsT v) VHyphen $ VLeaf (chunksAsT r)++-- | An (Ideal) version number that conforms to Semantic Versioning.+-- This is a /prescriptive/ parser, meaning it follows the SemVer standard.+--+-- Legal semvers are of the form: MAJOR.MINOR.PATCH-PREREL+META+--+-- Example: 1.2.3-r1+commithash+--+-- Extra Rules:+--+-- 1. Pre-release versions have /lower/ precedence than normal versions.+--+-- 2. Build metadata does not affect version precedence.+--+-- For more information, see http://semver.org+data SemVer = SemVer { svMajor  :: Int+                     , svMinor  :: Int+                     , svPatch  :: Int+                     , svPreRel :: [VChunk]+                     , svMeta   :: [VChunk] } deriving (Show)++-- | Two SemVers are equal if all fields except metadata are equal.+instance Eq SemVer where+  (SemVer ma mi pa pr _) == (SemVer ma' mi' pa' pr' _) =+    (ma,mi,pa,pr) == (ma',mi',pa',pr')++-- | Build metadata does not affect version precedence.+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'++-- | A single unit of a Version. May be digits or a string of characters.+-- Groups of these are called `VChunk`s, and are the identifiers separated+-- by periods in the source.+data VUnit = Digits Int | Str Text deriving (Eq,Show,Read,Ord)++-- | A logical unit of a version number. Can consist of multiple letters+-- and numbers.+type VChunk = [VUnit]++-- | A (General) Version.+-- Not quite as ideal as a `SemVer`, but has some internal consistancy+-- from version to version.+-- Generally conforms to the @x.x.x-x@ pattern.+--+-- Examples of @Version@ that are not @SemVer@: 0.25-2, 8.u51-1, 20150826-1+data Version = Version { vChunks :: [VChunk]+                       , vRel    :: [VChunk] } deriving (Eq,Ord,Show)++-- | A (Complex) Mess.+-- This is a /descriptive/ parser, based on examples of stupidly+-- crafted version numbers used in the wild.+--+-- Groups of letters/numbers, separated by a period, can be+-- further separated by the symbols @_-+:@+--+-- Unfortunately, @VChunk@s cannot be used here, as some developers have+-- numbers like @1.003.04@ which make parsers quite sad.+--+-- Not guaranteed to have well-defined ordering (@Ord@) behaviour,+-- but so far interal tests show consistency.+data Mess = VLeaf [Text] | VNode [Text] VSep Mess deriving (Eq,Show)++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++-- | Developers use a number of symbols to seperate groups of digits/letters+-- in their version numbers. These are:+--+-- * A colon (:). Often denotes an "epoch".+-- * A hyphen (-).+-- * A plus (+). Stop using this outside of metadata if you are. Example: @10.2+0.93+1-1@+-- * An underscore (_). Stop using this if you are.+data VSep = VColon | VHyphen | VPlus | VUnder deriving (Eq,Show)++-- | A wrapper for a parser function. Can be composed via their+-- Semigroup instance, such that a different parser can be tried+-- if a previous one fails.+newtype VParser = VParser { runVP :: Text -> Either ParseError Versioning }++instance Semigroup VParser where+  (VParser f) <> (VParser g) = VParser h+    where h t = either (const (g t)) Right $ f t++-- | Parse a piece of @Text@ into either an (Ideal) SemVer, a (General)+-- Version, or a (Complex) Mess.+parseV :: Text -> Either ParseError Versioning+parseV = runVP $ semverP <> versionP <> messP++-- | A wrapped `SemVer` parser. Can be composed with other parsers.+semverP :: VParser+semverP = VParser $ fmap Ideal . semver++-- | Parse a (Ideal) Semantic Version.+semver :: Text -> Either ParseError SemVer+semver = semver' . unpack++-- | Parse a Semantic Version, as a legacy String.+semver' :: String -> Either ParseError SemVer+semver' = parse semanticVersion "Semantic Version"++semanticVersion :: Parser SemVer+semanticVersion = p <* eof+  where p = SemVer <$> major <*> minor <*> patch <*> preRel <*> metaData++-- | Parse a group of digits, which can't be lead by a 0, unless it is 0.+digits :: Parser Int+digits = read <$> (string "0" <|> many1 digit)++major :: Parser Int+major = digits <* char '.'++minor :: Parser Int+minor = major++patch :: Parser Int+patch = digits++preRel :: Parser [VChunk]+preRel = (char '-' *> chunks) <|> pure []++metaData :: Parser [VChunk]+metaData = (char '+' *> chunks) <|> pure []++chunks :: Parser [VChunk]+chunks = (oneZero <|> many (iunit <|> sunit)) `sepBy` char '.'+  where oneZero = (:[]) . Digits . read <$> string "0"++iunit :: Parser VUnit+iunit = Digits . read <$> many1 digit++sunit :: Parser VUnit+sunit = Str . pack <$> many1 letter++-- | A wrapped `Version` parser. Can be composed with other parsers.+versionP :: VParser+versionP = VParser $ fmap General . version++-- | Parse a (General) `Version`, as defined above.+version :: Text -> Either ParseError Version+version = version' . unpack++-- | Parse a `Version`, where the input is a legacy String.+version' :: String -> Either ParseError Version+version' = parse versionNum "Version"++versionNum :: Parser Version+versionNum = Version <$> chunks <*> preRel <* eof++-- | A wrapped `Mess` parser. Can be composed with other parsers.+messP :: VParser+messP = VParser $ fmap Complex . mess++-- | Parse a (Complex) `Mess`, as defined above. +mess :: Text -> Either ParseError Mess+mess = mess' . unpack++-- | Parse a `Mess`, where the input is a legacy String.+mess' :: String -> Either ParseError Mess+mess' = parse messNumber "Mess"++messNumber :: Parser Mess+messNumber = try node <|> leaf++leaf :: Parser Mess+leaf = VLeaf <$> tchunks <* eof++node :: Parser Mess+node = VNode <$> tchunks <*> sep <*> messNumber++tchunks :: Parser [Text]+tchunks = (pack <$> many1 (letter <|> digit)) `sepBy` char '.'++sep :: Parser VSep+sep = choice [ VColon  <$ char ':'+             , VHyphen <$ char '-'+             , VPlus   <$ char '+'+             , VUnder  <$ char '_' ]++sepCh :: VSep -> Char+sepCh VColon  = ':'+sepCh VHyphen = '-'+sepCh VPlus   = '+'+sepCh VUnder  = '_'++-- | Convert any parsed Versioning type to its textual representation.+prettyV :: Versioning -> Text+prettyV (Ideal sv)  = prettySemVer sv+prettyV (General v) = prettyVer v+prettyV (Complex m) = prettyMess m++-- | Convert a `SemVer` back to its textual representation.+prettySemVer :: SemVer -> 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)++-- | Convert a `Version` back to its textual representation.+prettyVer :: Version -> Text+prettyVer (Version cs pr) = mconcat $ ver <> pr'+  where ver = intersperse "." $ chunksAsT cs+        pr' = foldable [] ("-" :) $ intersperse "." (chunksAsT pr)++-- | Convert a `Mess` back to its textual representation.+prettyMess :: Mess -> Text+prettyMess (VLeaf t)     = mconcat $ intersperse "." t+prettyMess (VNode t s v) = snoc t' (sepCh s) <> prettyMess v+  where t' = mconcat $ intersperse "." t++chunksAsT :: [VChunk] -> [Text]+chunksAsT = map (mconcat . map f)+  where f (Digits i) = showt i+        f (Str s)    = s++-- | Analogous to `maybe` and `either`. If a given Foldable is empty,+-- a default value is returned. Else, a function is applied to that Foldable.+foldable :: Foldable f => f b -> (f a -> f b) -> f a -> f b+foldable d g f | null f    = d+               | otherwise = g f++-- | Flip an Ordering.+opposite :: Ordering -> Ordering+opposite EQ = EQ+opposite LT = GT+opposite GT = LT
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2015, Colin Woodbury++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Colin Woodbury nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ test/Test.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE OverloadedStrings #-}++module Test where++import Data.Either+import Data.Monoid ((<>))+import Data.Text (Text,unpack)+import Data.Versions+import Test.Tasty+import Test.Tasty.HUnit++---++-- | These don't need to parse as a SemVer.+goodVers :: [Text]+goodVers = [ "1", "1.2", "1.58.0-3",  "44.0.2403.157-1"+           , "0.25-2",  "8.u51-1", "21-2", "7.1p1-1", "20150826-1"+           ]++messes :: [Text]+messes = [ "10.2+0.93+1-1", "003.03-3", "002.000-7", "1:0.10.16-3"+         , "20.26.1_0-2"+         ]++messComps :: [Text]+messComps = [ "10.2+0.93+1-1", "10.2+0.93+1-2", "10.2+0.93+2-1"+            , "10.2+0.94+1-1", "10.3+0.93+1-1", "11.2+0.93+1-1", "12"+            ]++badSemVs :: [Text]+badSemVs = [ "1", "1.2", "1.2.3+a1b2bc3.1-alpha.2", "a.b.c", "1.01.1"+           , "1.2.3+a1b!2c3.1"+           ]++goodSemVs :: [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"+            ]++-- | The exact example from `http://semver.org`+semverOrd :: [Text]+semverOrd = [ "1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-alpha.beta"+            , "1.0.0-beta", "1.0.0-beta.2", "1.0.0-beta.11", "1.0.0-rc.1"+            , "1.0.0"+            ]++-- | Cabal makes this distinction: 0.2 < 0.2.0 < 0.2.0.0+-- Apparently there are only 5 packages on Hackage that actually+-- make this necessary, meaning `cabal` can't be simplified to ignore it.+-- Logically, these are the same package, but for those 5 packages, they+-- aren't.+cabalOrd :: [Text]+cabalOrd = [ "0.2", "0.2.0", "0.2.0.0" ]++versionOrd :: [Text]+versionOrd = [ "0.9.9.9", "1.0.0.0", "1.0.0.1", "2" ]++suite :: TestTree+suite = testGroup "Unit Tests"+  [ testGroup "(Ideal) Semantic Versioning"+    [ testGroup "Bad Versions (shouldn't parse)" $+      map (\s -> testCase (unpack s) $ assert $ isLeft $ semver s) badSemVs+    , testGroup "Good Versions (should parse)" $+      map (\s -> testCase (unpack s) $ isomorphSV s) goodSemVs+    , testGroup "Comparisons" $+      testCase "1.2.3-alpha.2 == 1.2.3-alpha.2+a1b2c3.1"+      (assert $ semver "1.2.3-alpha.2" == semver "1.2.3-alpha.2+a1b2c3.1") :+      map (\(a,b) -> testCase (unpack $ a <> " < " <> b) $ comp semver a b)+      (zip semverOrd $ tail semverOrd)+    ]+  , testGroup "(General) Versions"+    [ testGroup "Good Versions" $+      map (\s -> testCase (unpack s) $ isomorphV s) goodVers+    , testGroup "Comparisons" $+      map (\(a,b) -> testCase (unpack $ a <> " < " <> b) $ comp version a b) $+      zip cabalOrd (tail cabalOrd) <> zip versionOrd (tail versionOrd)+    ]+  , testGroup "(Complex) Mess"+    [ testGroup "Good Versions" $+      map (\s -> testCase (unpack s) $ isomorphM s) messes+    , testGroup "Comparisons" $+      map (\(a,b) -> testCase (unpack $ a <> " < " <> b) $ comp mess a b) $+      zip messComps (tail messComps)+    ]+  , testGroup "Mixed Versioning" $+    [ testGroup "Identification" $+      [ testCase "1.2.3 is SemVer" $ check $ isSemVer <$> parseV "1.2.3"+      , testCase "1.2.3-1 is SemVer" $ check $ isSemVer <$> parseV "1.2.3-1"+      , testCase "1.2.3-1+1 is SemVer" $ check $ isSemVer <$> parseV "1.2.3-1+1"+      , testCase "1.2.3r1 is Version" $ check $ isVersion <$> parseV "1.2.3r1"+      , testCase "0.25-2 is Version" $ check $ isVersion <$> parseV "0.25-2" +      , testCase "1.2.3+1-1 is Mess" $ check $ isMess <$> parseV "1.2.3+1-1"+      , testCase "1:1.2.3-1 is Mess" $ check $ isMess <$> parseV "1:1.2.3-1"+      , testCase "000.007-1 is Mess" $ check $ isMess <$> parseV "000.007-1"+      , testCase "20.26.1_0-2 is Mess" $ check $ isMess <$> parseV "20.26.1_0-2"+      ]+    , testGroup "Isomorphisms" $+      map (\s -> testCase (unpack s) $ isomorph s) $ goodSemVs ++ goodVers ++ messes+    , testGroup "Comparisons" $+      [ testCase "1.2.2r1-1 < 1.2.3-1"   $ comp parseV "1.2.2r1-1" "1.2.3-1"+      , testCase "1.2.3-1   < 1.2.4r1-1" $ comp parseV "1.2.3-1" "1.2.4r1-1"+      , testCase "1.2.3-1   < 2+0007-1"  $ comp parseV "1.2.3-1" "2+0007-1"+      , testCase "1.2.3r1-1 < 2+0007-1"  $ comp parseV "1.2.3r1-1" "2+0007-1"+      ]+    ]+  ]++-- | Does pretty-printing return a Versioning to its original form?+isomorph :: Text -> Assertion+isomorph t = Right t @=? (prettyV <$> parseV t)++-- | Does pretty-printing return a Version to its original form?+isomorphV :: Text -> Assertion+isomorphV t = Right t @=? (prettyVer <$> version t)++-- | Does pretty-printing return a SemVer to its original form?+isomorphSV :: Text -> Assertion+isomorphSV t = Right t @=? (prettySemVer <$> semver t)++isomorphM :: Text -> Assertion+isomorphM t =  Right t @=? (prettyMess <$> mess t)++comp :: Ord b => (Text -> Either a b) -> Text -> Text -> Assertion+comp f a b = check $ (<) <$> f a <*> f b++check :: Either a Bool -> Assertion+check = assert . either (const False) id++isSemVer :: Versioning -> Bool+isSemVer (Ideal _) = True+isSemVer _ = False++isVersion :: Versioning -> Bool+isVersion (General _) = True+isVersion _ = False++isMess :: Versioning -> Bool+isMess (Complex _) = True+isMess _ = False+++{-}+-- Need to submit patch for these, as well as Maybe instance.+assertRight :: String -> Either a b -> Assertion+assertRight _ (Right _)  = return ()+assertRight msg (Left _) = assertFailure msg++instance Assertable (Either a b) where+  assert = assertRight ""++assertMaybe :: String -> Maybe a -> Assertion+assertMaybe _ (Just _)  = return ()+assertMaybe msg Nothing = assertFailure msg++instance Assertable (Maybe a) where+  assert = assertMaybe ""+-}++main :: IO ()+main = defaultMain suite
+ versions.cabal view
@@ -0,0 +1,65 @@+name:                versions+version:             1.0.0+synopsis:            Types and parsers for software version numbers.+description:         A library for parsing and comparing software version numbers.+                     .+                     We like to give version numbers to our software in a myriad of different+                     ways. Some ways follow strict guidelines for incrementing and comparison.+                     Some follow conventional wisdom and are generally self-consistent.+                     Some are just plain asinine. This library provides a means of parsing+                     and comparing /any/ style of versioning, be it a nice Semantic Version+                     like this:+                     .+                     > 1.2.3-r1+git123+                     .+                     ...or a monstrosity like this:+                     .+                     > 2:10.2+0.0093r3+1-1+                     .+                     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.++license:             BSD3+license-file:        LICENSE+author:              Colin Woodbury+maintainer:          colingw@gmail.com+-- copyright:           +category:            Data+build-type:          Simple+-- extra-source-files:  +cabal-version:       >=1.10++source-repository head+  type:     git+  location: git://github.com/fosskers/haskell-versions.git++library+  exposed-modules:     Data.Versions+  -- other-modules:       +  other-extensions:    OverloadedStrings+  build-depends:       base >=4.8 && <4.9+                     , parsec >=3.1 && <3.2+                     , semigroups >= 0.16.2.2+                     , text >=1.2 && <1.3+                     , text-show >= 2+  -- hs-source-dirs:      +  default-language:    Haskell2010++test-suite versions-test+  type:                exitcode-stdio-1.0+  other-extensions:    OverloadedStrings++  build-depends:       base >=4.8 && <4.9+                     , either >= 4.4.1+                     , extra >= 1.4.1+                     , semigroups >= 0.16.2.2+                     , tasty >= 0.10.1.2+                     , tasty-hunit >= 0.9.2+                     , text >=1.2 && <1.3+                     , text-show >= 2+                     , versions++  hs-source-dirs:      test+  main-is:             Test.hs+  default-language:    Haskell2010