versions 3.1.1 → 6.0.8
raw patch · 6 files changed
Files
- CHANGELOG.md +293/−15
- Data/Versions.hs +1041/−545
- README.md +22/−19
- Setup.hs +0/−2
- test/Test.hs +234/−114
- versions.cabal +56/−56
CHANGELOG.md view
@@ -1,36 +1,314 @@-Changelog-=========+# Changelog -3.1.1------+## 6.0.8 (2025-02-03)++#### Changed++- Bumped upper bound on `base` (support for GHC 9.12).++## 6.0.7 (2024-06-03)++#### Changed++- Bumped upper bound on `base`.++## 6.0.6 (2024-03-08)++#### Fixed++- Account for large numbers when parsing on 32-bit (or smaller) systems.++## 6.0.5 (2024-01-24)++#### Fixed++- Certain illegal versions were parsing as PVP.++## 6.0.4 (2023-12-29)++#### Changed++- Bump dependencies to support GHC 9.8.++## 6.0.3 (2023-10-23)++#### Added++- `Data` instances for the various data types.+- Simple conversion functions between the main version types.+- Compile-time constructors via Template Haskell, like `versioningQ`.++## 6.0.2 (2023-10-12)++#### Added++- `Lift` instances for the various types, which allows parsing version numbers+ at compile time within Template Haskell quotes. Currently there is no exported+ function that supports this directly, but you could write one like:++```haskell+-- | Parse a `Versioning` at compile time.+thVer :: Text -> Q Exp+thVer nm =+ case versioning nm of+ Left err -> fail (errorBundlePretty err)+ Right v -> lift v+```++#### Changed++- Due to the new dependency on `template-haskell`, GHC 8.8 is now the lowest+ supported compiler version.++## 6.0.1 (2023-05-08)++#### Fixed++- Restored the ability to compile with GHC versions earlier than 9.++## 6.0.0 (2023-04-29)++A number of type changes have been made to improve parsing and comparison logic.+Doing so fixed several bugs and made the code cleaner overall.++If you're just doing basic parsing and comparisons and not actually inspecting+the types themselves, you shouldn't notice a difference.++#### Added++- New types `Release`, `Chunks`, and `Chunk`.++#### Changed++- Both `SemVer` and `Version` now contain a better-behaving `Release` type for their prerelease info.+- Similarly, `Version` now also has a better-behaving `Chunks` type for its main+ version number sections.+- The `release` traversal now yields a `Maybe Release`.+- Versions with `~` in their metadata will now parse as a `Mess`. Example: `12.0.0-3ubuntu1~20.04.5`++#### Removed++- The various `Semigroup` instances. Adding version numbers together is a+ nonsensical operation and should never have been added in the first place.+- The `VChunk` and `VUnit` types and their associated functions.++#### Fixed++- Leading zeroes are handled a little better in `SemVer` pre-release data.++## 5.0.5 (2023-03-23)++#### Changed++- Bumped `base` bound to support GHC 9.6.++## 5.0.4 (2022-10-18)++#### Changed++- Bumped `base` bound to support GHC 9.4.++## 5.0.3 (2022-02-25)++#### Fixed++- A bug in `prettyVer` that flipped the order of the `preRel` and `meta` fields.++## 5.0.2 (2022-01-21)++#### Added++- `text-2.0` support.++## 5.0.1 (2021-12-08)++#### Changed++- Support for GHC 9.2.++#### Fixed++- Remove redundant pattern match.++## 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++- Support for GHC 9.++## 4.0.2 (2021-01-23)++#### Fixed++- A bug in zero parsing within SemVer prereleases. [#42]++[#42]: https://github.com/fosskers/versions/issues/42++## 4.0.1 (2020-10-22)++#### Fixed++- An infinite loop in `Version` comparison. [aura#652]++[aura#652]: https://github.com/fosskers/aura/issues/652++## 4.0.0 (2020-10-20)++#### Changed++- **Breaking:** `VChunk` now cannot be empty.+- **Breaking:** A `Version` now guarantees `NonEmpty` chunks.+- **Breaking:** A `Mess` now guarantees `NonEmpty` chunks, and its structure has+ been significantly changed. Particularly, `Mess` values are now aware of the+ `Int` values they hold (when they do), as well as "revision" values of the+ pattern `rXYZ`.+- Comparison of `Version` values is more memory efficient.++#### Added++- `Version` now has an extra field, `_vMeta :: [VChunk]` for capturing+ "metadata" like Semver. This prevents otherwise nice-looking versions from+ being demoted to `Mess`.+- The `MChunk` type to accomodate the changes to `Mess` mentioned above.++#### Removed++- **Breaking:** `Version` no longer has a `Monoid` instance.++#### Fixed++- `""` no longer parses in any way. [#32]+- Version strings with trailing whitespace no longer parse via `versioning`. [#33]+- Particular edge cases involving `Mess` comparisons. [aura#646]+- A particular edge case involving prereleases in `Version` comparisons. [aura#586]++[#32]: https://github.com/fosskers/versions/issues/32+[#33]: https://github.com/fosskers/versions/issues/33+[aura#646]: https://github.com/fosskers/aura/issues/646+[aura#586]: https://github.com/fosskers/aura/issues/586++## 3.5.4 (2020-05-12)++#### Added++- The functions `isIdeal`, `isGeneral`, and `isComplex` for `Bool`-based+ inspection of parse results.+- `messMajor`, `messMinor`, `messPatch`, and `messPatchChunk` for improved+ introspection into `Mess` values.++#### Changed++- Improved `Mess` comparison logic.++## 3.5.3++- GHC 8.10 support.++## 3.5.2++- Added a new `PVP` type and parsers.++## 3.5.1.1++- GHC 8.8 compatibility.++## 3.5.0++- Updated to `megaparsec-7`. Our `ParsingError` type alias has changed to match+ Megaparsec's new error model, and `errorBundlePretty` is now exposed instead of+ the old `parseErrorPretty`.++## 3.4.0.1++- Enhanced the whitespace handling in `semver'`, `version'`, and `mess'`.++## 3.4.0++- Removed `ParseV` and surrounding machinery.+ Use `versioning` now instead of the `parseV` function.++## 3.3.2++- GHC 8.4.1 compatibility.++## 3.3.0++- New `Semantic` typeclass that provides Traversals for SemVer-like data out+ of all the version types. `Text` was also given an instance, so its much+ easier to manipulate directly:++```+λ "1.2.3" & minor %~ (+ 1)+"1.3.3"+```++Some Lenses and Traversals had their names changed or were removed entirely+to accomodate this new typeclass.++- `SemVer` and `Version` should never contain negative values, so their numeric+ components were changed from `Int` to `Word`.++## 3.2.0++- Updated for `megaparsec-6` and GHC 8.2.++## 3.1.1+ - Added instances for common typeclasses: `Generic`, `NFData`, and `Hashable`. This is to avoid having users define these instances themselves as orphans. If there are more instances you want added, please let me know. `Data` was left out on purpose. -3.1.0-------- Added support for *epoch* numbers in the `Version` type. These are numbers+## 3.1.0++- Added support for _epoch_ numbers in the `Version` type. These are numbers like the `1:` in `1:2.3.4`. These are used in Arch Linux in rare cases where packages change their versioning scheme, but need a reliable integer prefix to establish ordering. The `Version` type has been given a new field, `_vEpoch :: Maybe Int`, and a corresponding lens, `vEpoch`. -3.0.2------+## 3.0.2+ - Expose internal parsers so that they could be used in other parser programs that parse version numbers in larger files. -3.0.0------+## 3.0.0+ - Updated for `megaparsec-5` and `ghc-8` -2.0.0------+## 2.0.0+ - Switched to `megaparsec` to perform all parsing as `Text` - Support for legacy `String` removed - Added more Traversals and INLINE'd all Lenses/Traversals -1.1.0------+## 1.1.0+ - Added Lenses and Traversals (no `lens` dependency)
Data/Versions.hs view
@@ -1,545 +1,1041 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}---- |--- Module : Data.Versions--- Copyright : (c) Colin Woodbury, 2015 - 2017--- 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(..)- , ParsingError- -- * Parsers- , semver- , version- , mess- -- ** Wrapped Parsers- , parseV- , semverP- , versionP- , messP- -- ** Megaparsec Parsers- , semver'- , version'- , mess'- -- * Pretty Printing- , prettyV- , prettySemVer- , prettyVer- , prettyMess- , parseErrorPretty- -- * Lenses- -- ** Traversing Text- , _Versioning- , _SemVer- , _Version- -- ** Versioning Traversals- , _Ideal- , _General- , _Complex- -- ** (Ideal) SemVer Lenses- , svMajor- , svMinor- , svPatch- , svPreRel- , svMeta- -- ** (General) Version Lenses- , vEpoch- , vChunks- , vRel- -- ** Misc. Lenses / Traversals- , _Digits- , _Str ) where--import Control.DeepSeq-import Data.Hashable-import Data.List (intersperse)-import Data.Monoid-import Data.Text (Text,pack,snoc)-import GHC.Generics-import Text.Megaparsec-import Text.Megaparsec.Text--------- | 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,Generic,NFData,Hashable)---- | 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) = compare (vFromS s) v- compare (General v) (Ideal s) = opposite $ compare (vFromS 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)---- | Convert a `SemVer` to a `Version`.-vFromS :: SemVer -> Version-vFromS (SemVer m i p r _) = Version Nothing [[Digits m], [Digits i], [Digits p]] r---- | Convert a `Version` to a `Mess`.-mFromV :: Version -> Mess-mFromV (Version e v r) = maybe affix (\a -> VNode [showt a] VColon affix) e- where affix = VNode (chunksAsT v) VHyphen $ VLeaf (chunksAsT r)---- | Traverse some Text for its inner versioning.------ > _Versioning :: Traversal' Text Versioning------ > ("1.2.3" & _Versioning . _Ideal . svPatch %~ (+ 1)) == "1.2.4"-_Versioning :: Applicative f => (Versioning -> f Versioning) -> Text -> f Text-_Versioning f t = either (const (pure t)) (fmap prettyV . f) $ parseV t-{-# INLINE _Versioning #-}---- | Traverse some Text for its inner SemVer.------ > _SemVer :: Traversal' Text SemVer-_SemVer :: Applicative f => (SemVer -> f SemVer) -> Text -> f Text-_SemVer f t = either (const (pure t)) (fmap prettySemVer . f) $ semver t-{-# INLINE _SemVer #-}---- | Traverse some Text for its inner Version.------ > _Version :: Traversal' Text Version-_Version :: Applicative f => (Version -> f Version) -> Text -> f Text-_Version f t = either (const (pure t)) (fmap prettyVer . f) $ version t-{-# INLINE _Version #-}---- | > _Ideal :: Traversal' Versioning SemVer-_Ideal :: Applicative f => (SemVer -> f SemVer) -> Versioning -> f Versioning-_Ideal f (Ideal s) = Ideal <$> f s-_Ideal _ v = pure v-{-# INLINE _Ideal #-}---- | > _General :: Traversal' Versioning Version-_General :: Applicative f => (Version -> f Version) -> Versioning -> f Versioning-_General f (General v) = General <$> f v-_General _ v = pure v-{-# INLINE _General #-}---- | > _Complex :: Traversal' Versioning Mess-_Complex :: Applicative f => (Mess -> f Mess) -> Versioning -> f Versioning-_Complex f (Complex m) = Complex <$> f m-_Complex _ v = pure v-{-# INLINE _Complex #-}---- | 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,Generic,NFData,Hashable)---- | 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'--instance Monoid SemVer where- mempty = SemVer 0 0 0 [] []- SemVer mj mn pa p m `mappend` SemVer mj' mn' pa' p' m' =- SemVer (mj + mj') (mn + mn') (pa + pa') (p ++ p') (m ++ m')---- | > svMajor :: Lens' SemVer Int-svMajor :: Functor f => (Int -> f Int) -> SemVer -> f SemVer-svMajor f sv = fmap (\ma -> sv { _svMajor = ma }) (f $ _svMajor sv)-{-# INLINE svMajor #-}---- | > svMinor :: Lens' SemVer Int-svMinor :: Functor f => (Int -> f Int) -> SemVer -> f SemVer-svMinor f sv = fmap (\mi -> sv { _svMinor = mi }) (f $ _svMinor sv)-{-# INLINE svMinor #-}---- | > svPatch :: Lens' SemVer Int-svPatch :: Functor f => (Int -> f Int) -> SemVer -> f SemVer-svPatch f sv = fmap (\pa -> sv { _svPatch = pa }) (f $ _svPatch sv)-{-# INLINE svPatch #-}---- | > svPreRel :: Lens' SemVer Int-svPreRel :: Functor f => ([VChunk] -> f [VChunk]) -> SemVer -> f SemVer-svPreRel f sv = fmap (\pa -> sv { _svPreRel = pa }) (f $ _svPreRel sv)-{-# INLINE svPreRel #-}---- | > svMeta :: Lens' SemVer Int-svMeta :: Functor f => ([VChunk] -> f [VChunk]) -> SemVer -> f SemVer-svMeta f sv = fmap (\pa -> sv { _svMeta = pa }) (f $ _svMeta sv)-{-# INLINE svMeta #-}---- | 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,Generic,NFData,Hashable)---- | > _Digits :: Traversal' VUnit Int-_Digits :: Applicative f => (Int -> f Int) -> VUnit -> f VUnit-_Digits f (Digits i) = Digits <$> f i-_Digits _ v = pure v-{-# INLINE _Digits #-}---- | > _Str :: Traversal' VUnit Text-_Str :: Applicative f => (Text -> f Text) -> VUnit -> f VUnit-_Str f (Str t) = Str <$> f t-_Str _ v = pure v-{-# INLINE _Str #-}---- | 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, and may optionally have an /epoch/.--- These are prefixes marked by a colon, like in @1:2.3.4@.------ Examples of @Version@ that are not @SemVer@: 0.25-2, 8.u51-1, 20150826-1, 1:2.3.4-data Version = Version { _vEpoch :: Maybe Int- , _vChunks :: [VChunk]- , _vRel :: [VChunk] } deriving (Eq,Show,Generic,NFData,Hashable)---- | Set a `Version`'s epoch to `Nothing`.-wipe :: Version -> Version-wipe v = v { _vEpoch = Nothing }---- | Customized.-instance Ord Version where- -- | The obvious base case.- compare (Version _ [] []) (Version _ [] []) = EQ-- -- | For the purposes of Versions with epochs, `Nothing` is the same as `Just 0`,- -- so we need to compare their actual version numbers.- compare v0@(Version (Just 0) _ _) v1@(Version Nothing _ _) = compare (wipe v0) v1- compare v0@(Version Nothing _ _) v1@(Version (Just 0) _ _) = compare v0 (wipe v1)-- compare (Version (Just _) _ _) (Version Nothing _ _) = GT- compare (Version Nothing _ _) (Version (Just _) _ _) = LT-- -- | If two epochs are equal, we need to compare their actual version numbers.- -- Otherwise, the comparison of the epochs is the only thing that matters.- compare v0@(Version (Just n) _ _) v1@(Version (Just m) _ _) | n == m = compare (wipe v0) (wipe v1)- | otherwise = compare n m-- -- | If the two Versions were otherwise equal and recursed down this far,- -- we need to compare them by their "release" values.- compare (Version _ [] rs) (Version _ [] rs') = compare (Version Nothing rs []) (Version Nothing rs' [])-- -- | If one side has run out of chunks to compare but the other hasn't,- -- the other must be newer.- compare (Version _ _ _) (Version _ [] _) = GT- compare (Version _ [] _) (Version _ _ _) = LT-- -- | The usual case. If first VChunks of each Version is equal, then we- -- keep recursing. Otherwise, we don't need to check further. Consider @1.2@- -- compared to @1.1.3.4.5.6@.- compare (Version _ (a:as) rs) (Version _ (b:bs) rs') = case f a b of- EQ -> compare (Version Nothing as rs) (Version Nothing bs rs')- res -> res- where f [] [] = EQ-- -- | Opposite of the above. If we've recursed this far and one side has- -- fewer chunks, it must be the "greater" version. A Chunk break only occurs in- -- a switch from digits to letters and vice versa, so anything "extra" must be- -- an @rc@ marking or similar. Consider @1.1@ compared to @1.1rc1@.- f [] _ = GT- f _ [] = LT-- -- | The usual case.- f (Digits n:ns) (Digits m:ms) | n > m = GT- | n < m = LT- | otherwise = f ns ms- f (Str n:ns) (Str m:ms) | n > m = GT- | n < m = LT- | otherwise = f ns ms-- -- | An arbitrary decision to prioritize digits over letters.- f (Digits _ :_) (Str _ :_) = GT- f (Str _ :_ ) (Digits _ :_) = LT---- | > vEpoch :: Lens' Version (Maybe Int)-vEpoch :: Functor f => (Maybe Int -> f (Maybe Int)) -> Version -> f Version-vEpoch f v = fmap (\ve -> v { _vEpoch = ve }) (f $ _vEpoch v)---- | > vChunks :: Lens' Version [VChunk]-vChunks :: Functor f => ([VChunk] -> f [VChunk]) -> Version -> f Version-vChunks f v = fmap (\vc -> v { _vChunks = vc }) (f $ _vChunks v)-{-# INLINE vChunks #-}---- | > vRel :: Lens' Version [VChunk]-vRel :: Functor f => ([VChunk] -> f [VChunk]) -> Version -> f Version-vRel f v = fmap (\vr -> v { _vRel = vr }) (f $ _vRel v)-{-# INLINE vRel #-}---- | 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 internal tests show consistency.-data Mess = VLeaf [Text] | VNode [Text] VSep Mess deriving (Eq,Show,Generic,NFData,Hashable)--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,Generic,NFData,Hashable)---- | A synonym for the more verbose `megaparsec` error type.-type ParsingError = ParseError (Token Text) Dec---- | A wrapper for a parser function. Can be composed via their--- Monoid instance, such that a different parser can be tried--- if a previous one fails.-newtype VParser = VParser { runVP :: Text -> Either ParsingError Versioning }--instance Monoid VParser where- -- | A parser which will always fail.- mempty = VParser $ \_ -> Ideal <$> semver ""-- -- | Will attempt the right parser if the left one fails.- (VParser f) `mappend` (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 ParsingError 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 ParsingError SemVer-semver = parse (semver' <* eof) "Semantic Version"---- | Internal megaparsec parser of 'semverP'.-semver' :: Parser SemVer-semver' = 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" <|> some digitChar)--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 = chunk `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 :: Parser VChunk-chunk = try zeroWithLetters <|> oneZero <|> many (iunit <|> sunit)- where oneZero = (:[]) . Digits . read <$> string "0"- zeroWithLetters = do- z <- Digits . read <$> string "0"- s <- some sunit- c <- chunk- pure $ (z : s) ++ c--iunit :: Parser VUnit-iunit = Digits . read <$> some digitChar--sunit :: Parser VUnit-sunit = Str . pack <$> some letterChar---- | 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 ParsingError Version-version = parse (version' <* eof) "Version"---- | Internal megaparsec parser of 'versionP'.-version' :: Parser Version-version' = Version <$> optional (try epoch) <*> chunks <*> preRel--epoch :: Parser Int-epoch = read <$> (some digitChar <* char ':')---- | 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 ParsingError Mess-mess = parse (mess' <* eof) "Mess"---- | Internal megaparsec parser of 'messP'.-mess' :: Parser Mess-mess' = try node <|> leaf--leaf :: Parser Mess-leaf = VLeaf <$> tchunks--node :: Parser Mess-node = VNode <$> tchunks <*> sep <*> mess'--tchunks :: Parser [Text]-tchunks = (pack <$> some (letterChar <|> digitChar)) `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 ep cs pr) = ep' <> mconcat (ver <> pr')- where ver = intersperse "." $ chunksAsT cs- pr' = foldable [] ("-" :) $ intersperse "." (chunksAsT pr)- ep' = maybe "" (\e -> showt e <> ":") ep---- | 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---- Yes, `text-show` exists, but this reduces external dependencies.-showt :: Show a => a -> Text-showt = pack . show+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveLift #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE Rank2Types #-}++-- |+-- Module : Data.Versions+-- Copyright : (c) Colin Woodbury, 2015 - 2023+-- License : BSD3+-- Maintainer: Colin Woodbury <colin@fosskers.ca>+--+-- 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.+--+-- __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+-- `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(..), isIdeal, isGeneral, isComplex+ , SemVer(..)+ , PVP(..)+ , Version(..)+ , Mess(..), messMajor, messMinor, messPatch, messPatchChunk+ , Release(..)+ , Chunks(..)+ , Chunk(..)+ , MChunk(..)+ , VSep(..)+ -- ** Compile-time Constructors+ , versioningQ, semverQ, versionQ, messQ, pvpQ+ -- ** Conversions+ , semverToVersion, versionToMess, versionToPvp+ -- * Parsing Versions+ , ParsingError+ , versioning, semver, pvp, version, mess+ -- ** Megaparsec Parsers+ -- | For when you'd like to mix version parsing into some larger parser.+ , versioning', semver', pvp', version', mess'+ -- * Pretty Printing+ , prettyV, prettySemVer, prettyPVP, prettyVer, prettyMess, errorBundlePretty+ -- * Lenses+ , Lens'+ , Traversal'+ , Semantic(..)+ -- ** Traversing Text+ -- | When traversing `Text`, leveraging its `Semantic` instance will+ -- likely benefit you more than using these Traversals directly.+ , _Versioning, _SemVer, _Version, _Mess+ -- ** Versioning Traversals+ , _Ideal, _General, _Complex+ -- ** (General) Version Lenses+ , epoch+ ) where++import qualified Control.Applicative.Combinators.NonEmpty as PC+import Control.DeepSeq+import Control.Monad (unless, void)+import Data.Char (isAlpha, isAlphaNum)+import Data.Data (Data)+import Data.Foldable (fold)+import Data.Hashable (Hashable)+import Data.List (intersperse)+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as NEL+import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Void (Void)+import Data.Word (Word64)+import GHC.Generics (Generic)+import Language.Haskell.TH (Exp, Q)+import Language.Haskell.TH.Syntax (Lift(..))+import Text.Megaparsec hiding (chunk)+import Text.Megaparsec.Char+import qualified Text.Megaparsec.Char.Lexer as L++---++-- | 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, Generic, NFData, Hashable, Lift, Data)++-- | Short-hand for detecting a `SemVer`.+isIdeal :: Versioning -> Bool+isIdeal (Ideal _) = True+isIdeal _ = False++-- | Short-hand for detecting a `Version`.+isGeneral :: Versioning -> Bool+isGeneral (General _) = True+isGeneral _ = False++-- | Short-hand for detecting a `Mess`.+isComplex :: Versioning -> Bool+isComplex (Complex _) = True+isComplex _ = False++-- | 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) = semverAndVer s v+ compare (General v) (Ideal s) = opposite $ semverAndVer s v+ compare (General v) (Complex m) = compare (versionToMess v) m+ compare (Complex m) (General v) = opposite $ compare (versionToMess v) m+ compare (Ideal s) (Complex m) = semverAndMess s m+ compare (Complex m) (Ideal s) = opposite $ semverAndMess s m++-- | Convert a `SemVer` to a `Version`.+semverToVersion :: SemVer -> Version+semverToVersion (SemVer ma mi pa re me) =+ Version+ { _vEpoch = Nothing+ , _vChunks = Chunks $ Numeric ma :| [Numeric mi, Numeric pa]+ , _vMeta = me+ , _vRel = re }++-- | Convert a `Version` to a `Mess`.+versionToMess :: Version -> Mess+versionToMess (Version me (Chunks v) r _) = case me of+ Nothing -> f+ Just e ->+ let cs = (:| []) . MDigit e $ showt e+ in Mess cs $ Just (VColon, f)+ where+ f :: Mess+ f = Mess cs $ fmap g r+ where+ cs = NEL.map toMChunk v++ g :: Release -> (VSep, Mess)+ g (Release cs) = (VHyphen, Mess ms Nothing)+ where+ ms = NEL.map toMChunk cs++-- | Convert a `Version` to a `PVP`. Fails if there is an epoch present, but+-- otherwise ignores the `Release` and other metadata. Naturally it also fails+-- if any of the version components contain any non-digits.+versionToPvp :: Version -> Maybe PVP+versionToPvp (Version (Just _) _ _ _) = Nothing+versionToPvp (Version Nothing (Chunks cs) _ _) = PVP <$> traverse f cs+ where+ f :: Chunk -> Maybe Word+ f (Numeric w) = Just w+ f (Alphanum _) = Nothing++semverAndVer :: SemVer -> Version -> Ordering+-- A `Version` with a non-zero epoch value is automatically greater than any+-- `SemVer`.+semverAndVer _ (Version (Just e) _ _ _) | e > 0 = LT+semverAndVer (SemVer ma mi pa sr _) (Version _ (Chunks vc) vr _) =+ case compare ma <$> (nth 0 vc' >>= singleDigitLenient) of+ Nothing -> GT+ Just GT -> GT+ Just LT -> LT+ Just EQ -> case compare mi <$> (nth 1 vc' >>= singleDigitLenient) of+ Nothing -> GT+ Just GT -> GT+ Just LT -> LT+ Just EQ -> case compare pa <$> (nth 2 vc' >>= singleDigitLenient) of+ Nothing -> GT+ Just GT -> GT+ Just LT -> LT+ -- By thes point, the major/minor/patch positions have all been equal.+ -- If there is a fourth position, its type, not its value, will+ -- determine which overall version is greater.+ Just EQ -> case nth 3 vc' of+ -- 1.2.3 > 1.2.3.git+ Just (Alphanum _) -> GT+ -- 1.2.3 < 1.2.3.0+ Just (Numeric _) -> LT+ Nothing -> compare sr vr+ where+ vc' :: [Chunk]+ vc' = NEL.toList vc++ nth :: Int -> [Chunk] -> Maybe Chunk+ nth _ [] = Nothing+ nth 0 (c:_) = Just c+ nth n (_:cs) = nth (n - 1) cs++-- | Special logic for when semver-like values can be extracted from a `Mess`.+-- This avoids having to "downcast" the `SemVer` into a `Mess` before comparing,+-- and in some cases can offer better comparison results.+semverAndMess :: SemVer -> Mess -> Ordering+semverAndMess s@(SemVer ma mi pa _ _) m = case compare ma <$> messMajor m of+ Nothing -> fallback+ Just LT -> LT+ Just GT -> GT+ Just EQ -> case compare mi <$> messMinor m of+ Nothing -> fallback+ Just LT -> LT+ Just GT -> GT+ Just EQ -> case compare pa <$> messPatch m of+ Just LT -> LT+ Just GT -> GT+ -- If they've been equal up to this point, the `Mess`+ -- will by definition have more to it, meaning that+ -- it's more likely to be newer, despite its poor shape.+ Just EQ -> fallback+ -- Even if we weren't able to extract a standalone patch number, we might+ -- still be able to find a number at the head of the `Chunk` in that+ -- position.+ Nothing -> case messPatchChunk m >>= singleDigitLenient of+ -- We were very close, but in the end the `Mess` had a nonsensical value+ -- in its patch position.+ Nothing -> fallback+ Just pa' -> case compare pa pa' of+ LT -> LT+ GT -> GT+ -- This follows semver's rule that pre-releases have lower precedence.+ EQ -> GT+ where+ fallback :: Ordering+ fallback = compare (General $ semverToVersion s) (Complex m)++instance Semantic Versioning where+ major f (Ideal v) = Ideal <$> major f v+ major f (General v) = General <$> major f v+ major f (Complex v) = Complex <$> major f v+ {-# INLINE major #-}++ minor f (Ideal v) = Ideal <$> minor f v+ minor f (General v) = General <$> minor f v+ minor f (Complex v) = Complex <$> minor f v+ {-# INLINE minor #-}++ patch f (Ideal v) = Ideal <$> patch f v+ patch f (General v) = General <$> patch f v+ patch f (Complex v) = Complex <$> patch f v+ {-# INLINE patch #-}++ release f (Ideal v) = Ideal <$> release f v+ release f (General v) = General <$> release f v+ release f (Complex v) = Complex <$> release f v+ {-# INLINE release #-}++ meta f (Ideal v) = Ideal <$> meta f v+ meta f (General v) = General <$> meta f v+ meta f (Complex v) = Complex <$> meta f v+ {-# INLINE meta #-}++ semantic f (Ideal v) = Ideal <$> semantic f v+ semantic f (General v) = General <$> semantic f v+ semantic f (Complex v) = Complex <$> semantic f v+ {-# INLINE semantic #-}++-- | Traverse some Text for its inner versioning.+--+-- @+-- λ "1.2.3" & _Versioning . _Ideal . patch %~ (+ 1) -- or just: "1.2.3" & patch %~ (+ 1)+-- "1.2.4"+-- @+_Versioning :: Traversal' Text Versioning+_Versioning f t = either (const (pure t)) (fmap prettyV . f) $ versioning t+{-# INLINE _Versioning #-}++-- | Traverse some Text for its inner SemVer.+_SemVer :: Traversal' Text SemVer+_SemVer f t = either (const (pure t)) (fmap prettySemVer . f) $ semver t+{-# INLINE _SemVer #-}++-- | Traverse some Text for its inner Version.+_Version :: Traversal' Text Version+_Version f t = either (const (pure t)) (fmap prettyVer . f) $ version t+{-# INLINE _Version #-}++-- | Traverse some Text for its inner Mess.+_Mess :: Traversal' Text Mess+_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+{-# INLINE _Complex #-}++-- | Simple Lenses compatible with both lens and microlens.+type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s++-- | Simple Traversals compatible with both lens and microlens.+type Traversal' s a = forall f. Applicative f => (a -> f a) -> s -> f s++-- | Version types which sanely and safely yield `SemVer`-like information about+-- themselves. For instances other than `SemVer` itself however, these optics+-- may /not/ yield anything, depending on the actual value being traversed.+-- Hence, the optics here are all `Traversal'`s.+--+-- Consider the `Version` @1.2.3.4.5@. We can imagine wanting to increment the+-- minor number:+--+-- @+-- λ "1.2.3.4.5" & minor %~ (+ 1)+-- "1.3.3.4.5"+-- @+--+-- But of course something like this would fail:+--+-- @+-- λ "1.e.3.4.5" & minor %~ (+ 1)+-- "1.e.3.4.5"+-- @+--+-- However!+--+-- @+-- λ "1.e.3.4.5" & major %~ (+ 1)+-- "2.e.3.4.5"+-- @+class Semantic v where+ -- | @MAJOR.minor.patch-prerel+meta@+ major :: Traversal' v Word+ -- | @major.MINOR.patch-prerel+meta@+ minor :: Traversal' v Word+ -- | @major.minor.PATCH-prerel+meta@+ patch :: Traversal' v Word+ -- | @major.minor.patch-PREREL+meta@+ release :: Traversal' v (Maybe Release)+ -- | @major.minor.patch-prerel+META@+ meta :: Traversal' v (Maybe Text)+ -- | A Natural Transformation into an proper `SemVer`.+ semantic :: Traversal' v SemVer++instance Semantic Text where+ major = _Versioning . major+ minor = _Versioning . minor+ patch = _Versioning . patch+ release = _Versioning . release+ meta = _Versioning . meta+ semantic = _SemVer++--------------------------------------------------------------------------------+-- (Ideal) SemVer++-- | 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.+--+-- 3. PREREL and META strings may only contain ASCII alphanumerics and hyphens.+--+-- For more information, see http://semver.org+data SemVer = SemVer+ { _svMajor :: !Word+ , _svMinor :: !Word+ , _svPatch :: !Word+ , _svPreRel :: !(Maybe Release)+ , _svMeta :: !(Maybe Text) }+ deriving stock (Show, Generic, Lift, Data)+ deriving anyclass (NFData, Hashable)++-- | 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+ (Nothing, Nothing) -> EQ+ (Nothing, _) -> GT+ (_, Nothing) -> LT+ (Just ap, Just bp) -> compare ap bp++instance Semantic SemVer where+ major f sv = fmap (\ma -> sv { _svMajor = ma }) (f $ _svMajor sv)+ {-# INLINE major #-}++ minor f sv = fmap (\mi -> sv { _svMinor = mi }) (f $ _svMinor sv)+ {-# INLINE minor #-}++ patch f sv = fmap (\pa -> sv { _svPatch = pa }) (f $ _svPatch sv)+ {-# INLINE patch #-}++ release f sv = fmap (\pa -> sv { _svPreRel = pa }) (f $ _svPreRel sv)+ {-# INLINE release #-}++ meta f sv = fmap (\pa -> sv { _svMeta = pa }) (f $ _svMeta sv)+ {-# INLINE meta #-}++ semantic = ($)+ {-# INLINE semantic #-}++-- | `Chunk`s have comparison behaviour according to SemVer's rules for preleases.+newtype Release = Release (NonEmpty Chunk)+ deriving stock (Eq, Show, Read, Generic, Lift, Data)+ deriving anyclass (NFData, Hashable)++instance Ord Release where+ compare (Release as) (Release bs) =+ fromMaybe EQ . listToMaybe . mapMaybe f $ zipLongest (NEL.toList as) (NEL.toList bs)+ where+ f :: These Chunk Chunk -> Maybe Ordering+ f (Both a b) = case cmpSemVer a b of+ LT -> Just LT+ GT -> Just GT+ EQ -> Nothing+ f (This _) = Just GT+ f (That _) = Just LT++-- | A logical unit of a version number.+--+-- Either entirely numerical (with no leading zeroes) or entirely alphanumerical+-- (with a free mixture of numbers, letters, and hyphens.)+--+-- Groups of these (like `Release`) are separated by periods to form a full+-- section of a version number.+--+-- Examples:+--+-- @+-- 1+-- 20150826+-- r3+-- 0rc1-abc3+-- @+data Chunk = Numeric !Word | Alphanum !Text+ deriving stock (Eq, Show, Read, Generic, Lift, Data)+ deriving anyclass (NFData, Hashable)++toMChunk :: Chunk -> MChunk+toMChunk (Numeric n) = MDigit n $ showt n+toMChunk (Alphanum s) = MPlain s++-- | `Chunk` is used in multiple places but requires different comparison+-- semantics depending on the wrapping type. This function and `cmpLenient`+-- below provide this.+cmpSemVer :: Chunk -> Chunk -> Ordering+cmpSemVer (Numeric a) (Numeric b) = compare a b+cmpSemVer (Numeric _) (Alphanum _) = LT+cmpSemVer (Alphanum _) (Numeric _) = GT+cmpSemVer (Alphanum a) (Alphanum b) = compare a b++-- | Like `cmpSemVer`, but for `Version`s. We need to be mindful of comparisons+-- like @1.2.0 > 1.2.0rc1@ which normally wouldn't occur in SemVer.+cmpLenient :: Chunk -> Chunk -> Ordering+cmpLenient (Numeric a) (Numeric b) = compare a b+cmpLenient a@(Alphanum x) b@(Alphanum y) =+ case (singleDigitLenient a, singleDigitLenient b) of+ (Just i, Just j) -> compare i j+ _ -> compare x y+cmpLenient (Numeric n) b@(Alphanum _) =+ case singleDigitLenient b of+ Nothing -> GT+ Just m -> case compare n m of+ -- 1.2.0 > 1.2.0rc1+ EQ -> GT+ c -> c+cmpLenient a@(Alphanum _) (Numeric n) =+ case singleDigitLenient a of+ Nothing -> LT+ Just m -> case compare m n of+ -- 1.2.0rc1 < 1.2.0+ EQ -> LT+ c -> c++-- | Like `singleDigit` but will grab a leading `Word` even if followed by+-- letters.+singleDigitLenient :: Chunk -> Maybe Word+singleDigitLenient (Numeric n) = Just n+singleDigitLenient (Alphanum s) = hush $ parse unsignedP "Single Digit Lenient" s++--------------------------------------------------------------------------------+-- (Haskell) PVP++-- | A PVP version number specific to the Haskell ecosystem. Like SemVer this is+-- a prescriptive scheme, and follows <https://pvp.haskell.org/ the PVP spec>.+--+-- Legal PVP values are of the form: MAJOR(.MAJOR.MINOR)+--+-- Example: @1.2.3@+--+-- Extra Rules:+--+-- 1. Each component must be a number.+--+-- 2. Only the first MAJOR component is actually necessary. Otherwise, there can+-- be any number of components. @1.2.3.4.5.6.7@ is legal.+--+-- 3. Unlike SemVer there are two MAJOR components, and both indicate a breaking+-- change. The spec otherwise designates no special meaning to components+-- past the MINOR position.+newtype PVP = PVP { _pComponents :: NonEmpty Word }+ deriving stock (Eq, Ord, Show, Generic, Lift, Data)+ deriving anyclass (NFData, Hashable)++instance Semantic PVP where+ major f (PVP (m :| rs)) = (\ma -> PVP $ ma :| rs) <$> f m+ {-# INLINE major #-}++ minor f (PVP (m :| mi : rs)) = (\mi' -> PVP $ m :| mi' : rs) <$> f mi+ minor f (PVP (m :| [])) = (\mi' -> PVP $ m :| [mi']) <$> f 0+ {-# 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 :| [])) = (\pa' -> PVP $ m :| 0 : [pa']) <$> f 0+ {-# INLINE patch #-}++ release f p = p <$ f Nothing+ {-# INLINE release #-}++ 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 Nothing Nothing+ mi : _ -> SemVer m mi 0 Nothing Nothing+ [] -> SemVer m 0 0 Nothing Nothing+ {-# INLINE semantic #-}++--------------------------------------------------------------------------------+-- (General) Version++-- | A version number with decent structure and comparison logic.+--+-- This is a /descriptive/ scheme, meaning that it encapsulates the most common,+-- unconscious patterns that developers use when assigning version numbers to+-- their software. If not `SemVer`, most version numbers found in the wild will+-- parse as a `Version`. These generally conform to the @x.x.x-x@ pattern, and+-- may optionally have an /epoch/.+--+-- Epochs are prefixes marked by a colon, like in @1:2.3.4@. When comparing two+-- `Version` values, epochs take precedent. So @2:1.0.0 > 1:9.9.9@. If one of+-- the given `Version`s has no epoch, its epoch is assumed to be 0.+--+-- Examples of @Version@ that are not @SemVer@: 0.25-2, 8.u51-1, 20150826-1,+-- 1:2.3.4+data Version = Version+ { _vEpoch :: !(Maybe Word)+ , _vChunks :: !Chunks+ , _vRel :: !(Maybe Release)+ , _vMeta :: !(Maybe Text) }+ deriving stock (Eq, Show, Generic, Lift, Data)+ deriving anyclass (NFData, Hashable)++-- | Customized. As in SemVer, metadata is ignored for the purpose of+-- comparison.+instance Ord Version where+ -- If two epochs are equal, we need to compare their actual version numbers.+ -- Otherwise, the comparison of the epochs is the only thing that matters.+ compare (Version mae ac ar _) (Version mbe bc br _) =+ case compare ae be of+ EQ -> case compare ac bc of+ EQ -> compare ar br+ ord -> ord+ ord -> ord+ where+ ae = fromMaybe 0 mae+ be = fromMaybe 0 mbe++instance Semantic Version where+ major f (Version e (Chunks (Numeric n :| cs)) me rs) =+ (\n' -> Version e (Chunks $ Numeric n' :| cs) me rs) <$> f n+ major _ v = pure v+ {-# INLINE major #-}++ minor f (Version e (Chunks (c :| Numeric n : cs)) me rs) =+ (\n' -> Version e (Chunks $ c :| Numeric n' : cs) me rs) <$> f n+ minor _ v = pure v+ {-# INLINE minor #-}++ patch f (Version e (Chunks (c :| d : Numeric n : cs)) me rs) =+ (\n' -> Version e (Chunks $ c :| d : Numeric n' : cs) me rs) <$> f n+ patch _ v = pure v+ {-# INLINE patch #-}++ -- | This will always succeed.+ release f v = fmap (\vr -> v { _vRel = vr }) (f $ _vRel v)+ {-# INLINE release #-}++ -- | This will always fail.+ meta _ v = pure v+ {-# INLINE meta #-}++ semantic f (Version _ (Chunks (Numeric a :| Numeric b : Numeric c : _)) rs me) =+ semverToVersion <$> 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 #-}++-- | `Chunk`s that have a comparison behaviour specific to `Version`.+newtype Chunks = Chunks (NonEmpty Chunk)+ deriving stock (Eq, Show, Generic, Lift, Data)+ deriving anyclass (NFData, Hashable)++instance Ord Chunks where+ compare (Chunks as) (Chunks bs) =+ fromMaybe EQ . listToMaybe . mapMaybe f $ zipLongest (NEL.toList as) (NEL.toList bs)+ where+ f :: These Chunk Chunk -> Maybe Ordering+ f (Both a b) = case cmpLenient a b of+ LT -> Just LT+ GT -> Just GT+ EQ -> Nothing+ f (This _) = Just GT+ f (That _) = Just LT++--------------------------------------------------------------------------------+-- (Complex) Mess++-- | Possible values of a section of a `Mess`. A numeric value is extracted if+-- it could be, alongside the original text it came from. This preserves both+-- `Ord` and pretty-print behaviour for versions like @1.003.0@.+data MChunk+ = MDigit !Word !Text+ -- ^ A nice numeric value.+ | MRev !Word !Text+ -- ^ A numeric value preceeded by an @r@, indicating a revision.+ | MPlain !Text+ -- ^ Anything else.+ deriving stock (Eq, Show, Generic, Lift, Data)+ deriving anyclass (NFData, Hashable)++instance Ord MChunk where+ compare (MDigit a _) (MDigit b _) = compare a b+ compare (MRev a _) (MRev b _) = compare a b+ compare (MPlain a) (MPlain b) = compare a b+ compare a b = compare (mchunkText a) (mchunkText b)++-- | A total extraction of the `Text` from an `MChunk`.+mchunkText :: MChunk -> Text+mchunkText (MDigit _ t) = t+mchunkText (MRev _ t) = t+mchunkText (MPlain t) = t++-- | 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 @_-+:@+--+-- Some `Mess` values have a shape that is tantalizingly close to a `SemVer`.+-- Example: @1.6.0a+2014+m872b87e73dfb-1@. For values like these, we can extract+-- the semver-compatible values out with `messMajor`, etc.+--+-- Not guaranteed to have well-defined ordering (@Ord@) behaviour, but so far+-- internal tests show consistency. `messMajor`, etc., are used internally where+-- appropriate to enhance accuracy.+data Mess = Mess !(NonEmpty MChunk) !(Maybe (VSep, Mess))+ deriving stock (Eq, Show, Generic, Lift, Data)+ deriving anyclass (NFData, Hashable)++-- | Try to extract the "major" version number from `Mess`, as if it were a+-- `SemVer`.+messMajor :: Mess -> Maybe Word+messMajor (Mess (MDigit i _ :| _) _) = Just i+messMajor _ = Nothing++-- | Try to extract the "minor" version number from `Mess`, as if it were a+-- `SemVer`.+messMinor :: Mess -> Maybe Word+messMinor (Mess (_ :| MDigit i _ : _) _) = Just i+messMinor _ = Nothing++-- | Try to extract the "patch" version number from `Mess`, as if it were a+-- `SemVer`.+messPatch :: Mess -> Maybe Word+messPatch (Mess (_ :| _ : MDigit i _ : _) _) = Just i+messPatch _ = Nothing++-- | Okay, fine, say `messPatch` couldn't find a nice value. But some `Mess`es+-- have a "proper" patch-plus-release-candidate value in their patch position,+-- which is parsable as a `Chunk`.+--+-- Example: @1.6.0a+2014+m872b87e73dfb-1@ We should be able to extract @0a@ safely.+messPatchChunk :: Mess -> Maybe Chunk+messPatchChunk (Mess (_ :| _ : MPlain p : _) _) = hush $ parse chunkP "Chunk" p+messPatchChunk _ = Nothing++instance Ord Mess where+ compare (Mess t1 m1) (Mess t2 m2) = case compare t1 t2 of+ EQ -> case (m1, m2) of+ (Just (_, v1), Just (_, v2)) -> compare v1 v2+ (Just (_, _), Nothing) -> GT+ (Nothing, Just (_, _)) -> LT+ (Nothing, Nothing) -> EQ+ res -> res++instance Semantic Mess where+ major f (Mess (MDigit n _ :| ts) m) = (\n' -> Mess (MDigit n' (showt n') :| ts) m) <$> f n+ major _ v = pure v+ {-# INLINE major #-}++ minor f (Mess (t0 :| MDigit n _ : ts) m) = (\n' -> Mess (t0 :| MDigit n' (showt n') : ts) m) <$> f n+ minor _ v = pure v+ {-# INLINE minor #-}++ patch f (Mess (t0 :| t1 : MDigit n _ : ts) m) = (\n' -> Mess (t0 :| t1 : MDigit n' (showt n') : ts) m) <$> f n+ patch _ v = pure v+ {-# INLINE patch #-}++ -- | This will always fail.+ release _ v = pure v+ {-# INLINE release #-}++ -- | This will always fail.+ meta _ v = pure v+ {-# INLINE meta #-}++ -- | Good luck.+ semantic f (Mess (MDigit t0 _ :| MDigit t1 _ : MDigit t2 _ : _) _) =+ versionToMess . semverToVersion <$> f (SemVer t0 t1 t2 Nothing Nothing)+ semantic _ v = pure v+ {-# INLINE semantic #-}++-- | 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 tilde (~). Example: @12.0.0-3ubuntu1~20.04.5@+-- * 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 | VTilde+ deriving stock (Eq, Show, Generic, Lift, Data)+ deriving anyclass (NFData, Hashable)++-- | Parse a `Versioning` at compile time.+versioningQ :: Text -> Q Exp+versioningQ nm =+ case versioning nm of+ Left err -> fail (errorBundlePretty err)+ Right v -> lift v++-- | Parse a `SemVer` at compile time.+semverQ :: T.Text -> Q Exp+semverQ nm =+ case semver nm of+ Left err -> fail (errorBundlePretty err)+ Right v -> lift v++-- | Parse a `Version` at compile time.+versionQ :: T.Text -> Q Exp+versionQ nm =+ case version nm of+ Left err -> fail (errorBundlePretty err)+ Right v -> lift v++-- | Parse a `Mess` at compile time.+messQ :: T.Text -> Q Exp+messQ nm =+ case mess nm of+ Left err -> fail (errorBundlePretty err)+ Right v -> lift v++-- | Parse a `PVP` at compile time.+pvpQ :: T.Text -> Q Exp+pvpQ nm =+ case pvp nm of+ Left err -> fail (errorBundlePretty err)+ Right v -> lift v++--------------------------------------------------------------------------------+-- Parsing++-- | 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)+-- `Version`, or a (Complex) `Mess`.+versioning :: Text -> Either ParsingError Versioning+versioning = parse versioning' "versioning"++-- | Parse a `Versioning`. Assumes the version number is the last token in+-- the string.+versioning' :: Parsec Void Text Versioning+versioning' = choice [ try (fmap Ideal semver'' <* eof)+ , try (fmap General version'' <* eof)+ , fmap Complex mess'' <* eof ]++-- | Parse a (Ideal) Semantic Version.+semver :: Text -> Either ParsingError SemVer+semver = parse (semver'' <* eof) "Semantic Version"++-- | Internal megaparsec parser of `semver`.+semver' :: Parsec Void Text SemVer+semver' = L.lexeme space semver''++semver'' :: Parsec Void Text SemVer+semver'' = SemVer <$> majorP <*> minorP <*> patchP <*> optional releaseP <*> optional metaData++-- | Parse a group of digits, which can't be lead by a 0, unless it is 0.+unsignedP :: Parsec Void Text Word+unsignedP = asWord64 >>= convertOrFail+ where+ asWord64 :: Parsec Void Text Word64+ asWord64 = (0 <$ char '0') <|> L.decimal++ convertOrFail :: Word64 -> Parsec Void Text Word+ convertOrFail w | w > bound = fail $ "Value (" ++ show w ++ ") larger than Word size: " ++ show bound+ | otherwise = pure $ fromIntegral w+ where+ bound :: Word64+ bound = fromIntegral (maxBound :: Word)++majorP :: Parsec Void Text Word+majorP = unsignedP <* char '.'++minorP :: Parsec Void Text Word+minorP = majorP++patchP :: Parsec Void Text Word+patchP = unsignedP++releaseP :: Parsec Void Text Release+releaseP = char '-' *> fmap Release (chunkP `PC.sepBy1` char '.')++chunkP :: Parsec Void Text Chunk+chunkP = try alphanumP <|> numericP++alphanumP :: Parsec Void Text Chunk+alphanumP = do+ ids <- takeWhile1P (Just "Hyphenated Alphanums") (\c -> isAlphaNum c || c == '-')+ -- It's okay for this to `fail` like this, since this fail is caught higher up+ -- in `chunkP` and another parser which should be guaranteed to succeed is+ -- called. It's guaranteed since by this point we /did/ parse something, but+ -- the test below proves it contains only numbers. Therefore the fallback call+ -- to `numericP` should succeed.+ unless (T.any (\c -> isAlpha c || c == '-') ids) $ fail "Only numeric!"+ pure $ Alphanum ids++alphanumWithoutHyphensP :: Parsec Void Text Chunk+alphanumWithoutHyphensP = do+ ids <- takeWhile1P (Just "Unhyphenated Alphanums") isAlphaNum+ unless (T.any isAlpha ids) $ fail "Only numeric!"+ pure $ Alphanum ids++numericP :: Parsec Void Text Chunk+numericP = Numeric <$> unsignedP++chunkWithoutHyphensP :: Parsec Void Text Chunk+chunkWithoutHyphensP = try alphanumWithoutHyphensP <|> numericP++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 == '-')++-- | Parse a (Haskell) `PVP`, as defined above.+pvp :: Text -> Either ParsingError PVP+pvp = parse (pvp' <* eof) "PVP"++-- | Internal megaparsec parser of `pvp`.+pvp' :: Parsec Void Text PVP+pvp' = L.lexeme space (PVP <$> unsignedP `PC.sepBy1` char '.')++-- | Parse a (General) `Version`, as defined above.+version :: Text -> Either ParsingError Version+version = parse (version'' <* eof) "Version"++-- | Internal megaparsec parser of `version`.+version' :: Parsec Void Text Version+version' = L.lexeme space version''++version'' :: Parsec Void Text Version+version'' = Version <$> optional (try epochP) <*> chunksP <*> optional releaseP <*> optional metaData++epochP :: Parsec Void Text Word+epochP = unsignedP <* char ':'++chunksP :: Parsec Void Text Chunks+chunksP = Chunks <$> chunkWithoutHyphensP `PC.sepBy1` char '.'++-- | Parse a (Complex) `Mess`, as defined above.+mess :: Text -> Either ParsingError Mess+mess = parse (mess'' <* eof) "Mess"++-- | Internal megaparsec parser of `mess`.+mess' :: Parsec Void Text Mess+mess' = L.lexeme space mess''++mess'' :: Parsec Void Text Mess+mess'' = Mess <$> mchunks <*> optional ((,) <$> sep <*> mess')++mchunks :: Parsec Void Text (NonEmpty MChunk)+mchunks = mchunk `PC.sepBy1` char '.'++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)+ , MPlain . T.pack <$> some (letterChar <|> digitChar) ]+ where+ next :: Parsec Void Text ()+ next = lookAhead (void (single '.') <|> void sep <|> eof)++sep :: Parsec Void Text VSep+sep = choice [ VColon <$ char ':'+ , VHyphen <$ char '-'+ , VPlus <$ char '+'+ , VUnder <$ char '_'+ , VTilde <$ char '~' ]++sepCh :: VSep -> Char+sepCh VColon = ':'+sepCh VHyphen = '-'+sepCh VPlus = '+'+sepCh VUnder = '_'+sepCh VTilde = '~'++-- | 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' = maybe [] (\m -> ["-", prettyRelease m]) pr+ me' = maybe [] (\m -> ["+", m]) me++-- | Convert a `PVP` back to its textual representation.+prettyPVP :: PVP -> Text+prettyPVP (PVP (m :| rs)) = T.intercalate "." . map showt $ m : rs++-- | Convert a `Version` back to its textual representation.+prettyVer :: Version -> Text+prettyVer (Version ep cs pr me) = mconcat $ ep' <> [ver] <> pr' <> me'+ where+ ver = prettyChunks cs+ me' = maybe [] (\m -> ["+", m]) me+ pr' = maybe [] (\m -> ["-", prettyRelease m]) pr+ ep' = maybe [] (\e -> [showt e, ":"]) ep++-- | Convert a `Mess` back to its textual representation.+prettyMess :: Mess -> Text+prettyMess (Mess t m) = case m of+ Nothing -> t'+ Just (s, v) -> T.snoc t' (sepCh s) <> prettyMess v+ where+ t' :: Text+ t' = fold . NEL.intersperse "." $ NEL.map mchunkText t++prettyChunks :: Chunks -> Text+prettyChunks (Chunks cs) = T.intercalate "." . map prettyChunk $ NEL.toList cs++prettyRelease :: Release -> Text+prettyRelease (Release cs) = T.intercalate "." . map prettyChunk $ NEL.toList cs++prettyChunk :: Chunk -> Text+prettyChunk (Numeric n) = showt n+prettyChunk (Alphanum s) = s++--------------------------------------------------------------------------------+-- Utilities++-- | Flip an Ordering.+opposite :: Ordering -> Ordering+opposite EQ = EQ+opposite LT = GT+opposite GT = LT++-- Yes, `text-show` exists, but this reduces external dependencies.+showt :: Show a => a -> Text+showt = T.pack . show++hush :: Either a b -> Maybe b+hush (Left _) = Nothing+hush (Right b) = Just b++data These a b = This !a | That !b | Both !a !b++zipLongest :: [a] -> [b] -> [These a b]+zipLongest [] [] = []+zipLongest (a:as) (b:bs) = Both a b : zipLongest as bs+zipLongest (a:as) [] = This a : zipLongest as []+zipLongest [] (b:bs) = That b : zipLongest [] bs
README.md view
@@ -1,7 +1,7 @@ versions ======== -[](https://travis-ci.org/fosskers/versions)+ [](https://hackage.haskell.org/package/versions) [](http://stackage.org/nightly/package/versions) [](http://stackage.org/lts/package/versions)@@ -28,7 +28,7 @@ Usage ------In general, `parseV` is the function you want. It attempts to parse a given+In general, `versioning` 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`).@@ -40,15 +40,15 @@ ```haskell incPatch :: SemVer -> SemVer-incPatch s = s & svPatch %~ (+ 1)+incPatch s = s & patch %~ (+ 1) ``` Or, something more involved: ```haskell -- | Get all major versions of legally parsed SemVers.-majors :: [Text] -> [Int]-majors vs = vs ^.. each . to semver . _Right . svMajor+majors :: [Text] -> [Word]+majors vs = vs ^.. each . to semver . _Right . major ``` The `to semver . _Right` is clunky, so we provide some direct `Text`@@ -57,27 +57,30 @@ [lens-aeson](http://hackage.haskell.org/package/lens-aeson): ```haskell--- | Get all major versions of legally parsed SemVers.-majors :: [Text] -> [Int]-majors vs = vs ^.. each . _SemVer . svMajor-```--Note that `_SemVer` only attempts to parse as true Semantic Versioning. If-the package versions you're parsing don't agree to a standard, you can-achieve a similar result via:--```haskell--- | Get all major versions of potentially parsed SemVers.-majors :: [Text] -> [Int]-majors vs = vs ^.. each . _Versioning . _Ideal . svMajor+-- | Get the major version of any `Text` that has one.+majors :: [Text] -> [Word]+majors vs = vs ^.. each . major ``` We can also use these `Text` Traversals to increment versions, as above: ```haskell incPatch :: Text -> Text-incPatch s = s & _SemVer . svPatch %~ (+ 1)+incPatch s = s & patch %~ (+ 1) > incPatch "1.2.3" "1.2.4" ```++#### Caveats++The largest number that can be parsed as part of a version is:++``` haskell+ghci> maxBound :: Word64+18446744073709551615+```++However, on 32-bit systems (or smaller), the maximum is their `maxBound :: Word`. +A number larger than that, even if smaller than `maxBound :: Word64`,+will yield a parse error.
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
test/Test.hs view
@@ -1,42 +1,74 @@+{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-} -module Main where+{-# OPTIONS_GHC -fno-warn-orphans #-} -import Data.Monoid ((<>))-import Data.Text (Text,unpack)-import Data.Versions-import Lens.Micro-import Test.Tasty-import Test.Tasty.HUnit+module Main ( main ) where +import Data.Either (fromRight, isLeft)+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as NonEmpty+import qualified Data.Text as T+import Data.Versions+import Data.Void (Void)+import Language.Haskell.TH (recover)+import Lens.Micro+import Test.Tasty+import Test.Tasty.HUnit+import Text.Megaparsec+import Text.Megaparsec.Char+import Text.Printf (printf)+ --- -- | These don't need to parse as a SemVer.-goodVers :: [Text]+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", "9.2.1.b-debug+lol"+ , "0:1.2.3" ] -messes :: [Text]-messes = [ "10.2+0.93+1-1", "003.03-3", "002.000-7", "20.26.1_0-2" ]+badVers :: [T.Text]+badVers = ["", "1.2 "] -messComps :: [Text]+badPVP :: [T.Text]+badPVP = ["", "clc237", "abc"]++messes :: [T.Text]+messes = [ "10.2+0.93+1-1", "003.03-3", "002.000-7", "20.26.1_0-2", "1.6.0a+2014+m872b87e73dfb-1"+ , "1.3.00.16851-1", "5.2.458699.0906-1", "12.0.0-3ubuntu1~20.04.5" ]++messComps :: NonEmpty T.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"+badSemVs :: [T.Text]+badSemVs = [ -- Not enough version slots+ "1", "1.2", "a.b.c"+ -- Illegal characters+ , "1.01.1", "1.2.3+a1b!2c3.1", "", "1.2.3 "+ -- Really large version+ -- , "18446744073709551610000000000000000.0.0" ] -goodSemVs :: [Text]+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"+ , "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"+ -- Zeroes+ , "1.2.2-00a"+ -- Really large version+ , "18446744073709551610.0.0" ] -- | The exact example from `http://semver.org`-semverOrd :: [Text]+semverOrd :: NonEmpty T.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"@@ -47,144 +79,232 @@ -- 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" ]+cabalOrd :: NonEmpty T.Text+cabalOrd = [ "0", "0.2", "0.2.0", "0.2.0.0" ] -versionOrd :: [Text]+versionOrd :: NonEmpty T.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" $- testCase "1.2-5 < 1.2.3-1" (comp version "1.2-5" "1.2.3-1") :- testCase "1.0rc1 < 1.0" (comp version "1.0rc1" "1.0") :- testCase "1.0 < 1:1.0" (comp version "1.0" "1:1.0") :- testCase "1.1 < 1:1.0" (comp version "1.1" "1:1.0") :- testCase "1.1 < 1:1.1" (comp version "1.1" "1:1.1") :- 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:1.2.3-1 is Version" $ check $ isVersion <$> parseV "1:1.2.3-1"- , testCase "1.2.3+1-1 is Mess" $ check $ isMess <$> parseV "1.2.3+1-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"+suite = testGroup "Tests"+ [ testGroup "Unit Tests"+ [ testGroup "(Ideal) Semantic Versioning"+ [ testGroup "Bad Versions (shouldn't parse)" $ map bad badSemVs+ , testGroup "Good Versions (should parse)" $+ map (\s -> testCase (T.unpack s) $ isomorphSV s) goodSemVs+ , testGroup "Comparisons" $+ 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") :+ zipWith (\a b -> testCase (T.unpack $ a <> " < " <> b) $ comp semver a b) (NonEmpty.toList semverOrd) (NonEmpty.tail semverOrd)+ , testGroup "Whitespace Handling"+ [ testCase "1.2.3-1[ ]" $ parse semver' "semver whitespace" "1.2.3-1 " @?= Right (SemVer 1 2 3 (Just . Release $ Numeric 1 :| []) Nothing)+ ]+ , testGroup "Zero Handling"+ [ testCase "2.2.1-b05" $ semver "2.2.1-b05" @?= Right (SemVer 2 2 1 (Just . Release $ Alphanum "b05" :| []) Nothing)+ ] ]- , 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"- , testCase "1.2-5 < 1.2.3-1" $ comp parseV "1.2-5" "1.2.3-1"+ , testGroup "(Haskell) PVP"+ [ testGroup "Good PVPs" $+ map (\s -> testCase (T.unpack s) $ isomorphPVP s) (NonEmpty.toList cabalOrd)+ , testGroup "Bad PVP" $+ map (\s -> testCase (T.unpack s) $ assertBool "A bad PVP parsed" $ isLeft $ pvp s) badPVP+ , testGroup "Comparisons" $+ zipWith (\a b -> testCase (T.unpack $ a <> " < " <> b) $ comp pvp a b) (NonEmpty.toList cabalOrd) (NonEmpty.tail cabalOrd) ]- ]+ , testGroup "(General) Versions"+ [ testGroup "Good Versions" $+ map (\s -> testCase (T.unpack s) $ isomorphV s) goodVers+ , testGroup "Bad Versions (shouldn't parse)" $+ map (\s -> testCase (T.unpack s) $ assertBool "A bad version parsed" $ isLeft $ version s) badVers+ , testGroup "Comparisons" $+ testCase "1.2-5 < 1.2.3-1" (comp version "1.2-5" "1.2.3-1") :+ testCase "1.0rc1 < 1.0" (comp version "1.0rc1" "1.0") :+ testCase "1.0 < 1:1.0" (comp version "1.0" "1:1.0") :+ testCase "1.1 < 1:1.0" (comp version "1.1" "1:1.0") :+ testCase "1.1 < 1:1.1" (comp version "1.1" "1:1.1") :+ map (\(a,b) -> testCase (T.unpack $ a <> " < " <> b) $ comp version a b)+ (zip (NonEmpty.toList cabalOrd) (NonEmpty.tail cabalOrd) <> zip (NonEmpty.toList versionOrd) (NonEmpty.tail versionOrd))+ ]+ , testGroup "(Complex) Mess"+ [ testGroup "Good Versions" $+ map (\s -> testCase (T.unpack s) $ isomorphM s) messes+ , testGroup "Bad Versions (shouldn't parse)" $+ map (\s -> testCase (T.unpack s) $ assertBool "A bad version parsed" $ isLeft $ mess s) badVers+ , testGroup "Comparisons" $+ zipWith (\a b -> testCase (T.unpack $ a <> " < " <> b) $ comp mess a b) (NonEmpty.toList messComps) (NonEmpty.tail messComps)+ , testGroup "SemVer-like Value Extraction"+ [ testCase "messMajor" $+ (hush (mess "1.6.0a+2014+m872b87e73dfb-1") >>= messMajor) @?= Just 1+ , testCase "messMinor" $+ (hush (mess "1.6.0a+2014+m872b87e73dfb-1") >>= messMinor) @?= Just 6+ , testCase "messPatch - Good" $+ (hush (mess "1.6.0+2014+m872b87e73dfb-1") >>= messPatch) @?= Just 0+ , testCase "messPatch - Bad" $+ (hush (mess "1.6.0a+2014+m872b87e73dfb-1") >>= messPatch) @?= Nothing+ , testCase "messPatchChunk" $+ (hush (mess "1.6.0a+2014+m872b87e73dfb-1") >>= messPatchChunk) @?= Just (Alphanum "0a")+ ]+ ]+ , testGroup "Mixed Versioning"+ [ testGroup "Identification"+ [ 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: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"+ , testCase "1:3.20.1-1 is Version" $ check $ isVersion <$> versioning "1:3.20.1-1"+ ]+ , testGroup "Bad Versions" $+ map (\s -> testCase (T.unpack s) $ assertBool "A bad version parsed" $ isLeft $ versioning s) badVers+ , testGroup "Isomorphisms" $+ map (\s -> testCase (T.unpack s) $ isomorph s) $ goodSemVs ++ goodVers ++ messes+ , testGroup "Comparisons"+ [ compVer "1.2.2r1-1" "1.2.3-1"+ , compVer "1.2.3-1" "1.2.4r1-1"+ , compVer "1.2.3-1" "2+0007-1"+ , compVer "1.2.3r1-1" "2+0007-1"+ , compVer "1.2-5" "1.2.3-1"+ , compVer "1.6.0a+2014+m872b87e73dfb-1" "1.6.0-1"+ , compVer "1.11.0.git.20200404-1" "1.11.0+20200830-1"+ , compVer "0.17.0+r8+gc41db5f1-1" "0.17.0+r157+g584760cf-1"+ , compVer "0.4.8-1" "0.4.9-1"+ , compVer "7.42.13-4" "7.46.0-2"+ , compVer "1.15.2-1" "1.15.3-1"+ , compVer "2.1.16102-2" "2.1.17627-1"+ , compVer "8.64.0.81-1" "8.65.0.78-1"+ , compVer "1.3.00.16851-1" "1.3.00.25560-1"+ , compVer "10.0.4-1" "10.1.0-1"+ , compVer "1:3.20-1" "1:3.20.1-1"+ , compVer "5.2.458699.0906-1" "5.3.472687.1012-1"+ ]+ , testGroup "Equality"+ [ eqVer "1:3.20.1-1"+ , eqVer "1.3.00.25560-1"+ , eqVer "150_28-3"+ , eqVer "1.0.r15.g3fc772c-5"+ , eqVer "0.88-2"+ ]+ , testGroup "Conversions"+ [ testCase "Good Version -> PVP" $ versionToPvp $(versionQ "1.2.3") @?= Just $(pvpQ "1.2.3")+ , testCase "Bad Version -> PVP" $ versionToPvp $(versionQ "1.e.3") @?= Nothing+ ]+ ] , testGroup "Lenses and Traversals" [ testCase "SemVer - Increment Patch" incPatch , testCase "SemVer - Increment Patch from Text" incFromT , testCase "SemVer - Get patches" patches- , testCase "Traverse `General` as `Ideal`" noInc ]+ , testGroup "Template Haskell"+ [ testCase "SemVer" $ prettyV $(versioningQ "1.2.3") @?= "1.2.3"+ , testCase "Version" $ prettyV $(versioningQ "1.2.3.4") @?= "1.2.3.4"+ , testCase "Mess" $ prettyV $(versioningQ "003.03-3") @?= "003.03-3"+ , testCase "Failure" $ $(recover [| () |] (versioningQ "!!!")) @?= ()+ ]+ , 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 (Just . Release $ Alphanum "1-x86" :| []) Nothing)+ , testCase "Parser State" $ parse pvp'' "parser state" "1.2.3arst" @?= Right ($(pvpQ "1.2.3"), "arst")+ ]+ ] ] +bad :: T.Text -> TestTree+bad s = testCase (T.unpack s) $ case semver s of+ Left _ -> pure ()+ Right s' -> assertFailure $ "A bad version parsed: " ++ T.unpack (prettySemVer s')++compVer :: T.Text -> T.Text -> TestTree+compVer a b = testCase (printf "%s < %s" a b) $ comp versioning a b++eqVer :: T.Text -> TestTree+eqVer a = testCase (T.unpack a) $ equal versioning a+ -- | Does pretty-printing return a Versioning to its original form?-isomorph :: Text -> Assertion-isomorph t = Right t @=? (prettyV <$> parseV t)+isomorph :: T.Text -> Assertion+isomorph t = case prettyV <$> versioning t of+ Right t' -> t @?= t'+ Left e -> assertBool (errorBundlePretty e) False -- | Does pretty-printing return a Version to its original form?-isomorphV :: Text -> Assertion-isomorphV t = Right t @=? (prettyVer <$> version t)+isomorphV :: T.Text -> Assertion+isomorphV t = case prettyVer <$> version t of+ Right t' -> t @?= t'+ Left e -> assertBool (errorBundlePretty e) False -- | Does pretty-printing return a SemVer to its original form?-isomorphSV :: Text -> Assertion-isomorphSV t = Right t @=? (prettySemVer <$> semver t)+isomorphSV :: T.Text -> Assertion+isomorphSV t = case prettySemVer <$> semver t of+ Right t' -> t @?= t'+ Left e -> assertBool (errorBundlePretty e) False -isomorphM :: Text -> Assertion-isomorphM t = Right t @=? (prettyMess <$> mess t)+isomorphPVP :: T.Text -> Assertion+isomorphPVP t = case prettyPVP <$> pvp t of+ Right t' -> t @?= t'+ Left e -> assertBool (errorBundlePretty e) False -comp :: Ord b => (Text -> Either a b) -> Text -> Text -> Assertion+isomorphM :: T.Text -> Assertion+isomorphM t = case prettyMess <$> mess t of+ Right t' -> t @?= t'+ Left e -> assertBool (errorBundlePretty e) False++comp :: Ord b => (T.Text -> Either a b) -> T.Text -> T.Text -> Assertion comp f a b = check $ (<) <$> f a <*> f b +equal :: Ord r => (T.Text -> Either l r) -> T.Text -> Assertion+equal f a = check $ (\r -> r == r) <$> f a+ check :: Either a Bool -> Assertion-check = assert . either (const False) id+check = assertBool "Some Either-based assertion failed" . fromRight False isSemVer :: Versioning -> Bool isSemVer (Ideal _) = True-isSemVer _ = False+isSemVer _ = False isVersion :: Versioning -> Bool isVersion (General _) = True-isVersion _ = False+isVersion _ = False isMess :: Versioning -> Bool isMess (Complex _) = True-isMess _ = False+isMess _ = False incPatch :: Assertion-incPatch = (v1 & _Ideal . svPatch %~ (+ 1)) @?= v2- where v1 = Ideal $ SemVer 1 2 3 [] []- v2 = Ideal $ SemVer 1 2 4 [] []---- | Nothing should happen.-noInc :: Assertion-noInc = (v & _Ideal . svPatch %~ (+ 1)) @?= v- where v = General $ Version Nothing [] []+incPatch = (v1 & patch %~ (+ 1)) @?= v2+ where v1 = Ideal $ SemVer 1 2 3 Nothing Nothing+ v2 = Ideal $ SemVer 1 2 4 Nothing Nothing incFromT :: Assertion-incFromT = ("1.2.3" & _Versioning . _Ideal . svPatch %~ (+ 1)) @?= "1.2.4"+incFromT = (("1.2.3" :: T.Text) & patch %~ (+ 1)) @?= "1.2.4" patches :: Assertion patches = ps @?= [3,4,5]- where ps = ["1.2.3","2.3.4","3.4.5"] ^.. each . _SemVer . svPatch--isLeft :: Either t1 t -> Bool-isLeft (Left _) = True-isLeft _ = False+ where ps = (["1.2.3","2.3.4","3.4.5"] :: [T.Text]) ^.. each . patch -{-}--- 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+main :: IO ()+main = defaultMain suite -instance Assertable (Either a b) where- assert = assertRight ""+nameGrab :: Parsec Void T.Text T.Text+nameGrab = T.pack <$> manyTill anySingle (try finished)+ where finished = char '-' *> lookAhead digitChar -assertMaybe :: String -> Maybe a -> Assertion-assertMaybe _ (Just _) = return ()-assertMaybe msg Nothing = assertFailure msg+versionGrab :: Parsec Void T.Text Versioning+versionGrab = manyTill anySingle (try finished) *> ver+ where finished = char '-' *> lookAhead digitChar+ ver = fmap Ideal semver' <|> fmap General version' <|> fmap Complex mess' -instance Assertable (Maybe a) where- assert = assertMaybe ""--}+hush :: Either a b -> Maybe b+hush (Left _) = Nothing+hush (Right b) = Just b -main :: IO ()-main = defaultMain suite+-- | An attempt to squeeze out the remaining parser state.+pvp'' :: Parsec Void T.Text (PVP, T.Text)+pvp'' = do+ v <- pvp'+ s <- getParserState+ pure (v, stateInput s)
versions.cabal view
@@ -1,66 +1,66 @@--- This file has been generated from package.yaml by hpack version 0.17.1.------ see: https://github.com/sol/hpack--name: versions-version: 3.1.1-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- 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-category: Data-build-type: Simple-cabal-version: >= 1.10+cabal-version: 2.2+name: versions+version: 6.0.8+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 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.+ .+ This library implements version @2.0.0@ of the SemVer spec. +category: Data+homepage: https://github.com/fosskers/versions+author: Colin Woodbury+maintainer: colin@fosskers.ca+license: BSD-3-Clause+license-file: LICENSE+build-type: Simple extra-source-files:- CHANGELOG.md- README.md+ CHANGELOG.md+ README.md -source-repository head- type: git- location: git://github.com/fosskers/haskell-versions.git+common commons+ default-language: Haskell2010+ ghc-options:+ -Wall -Wcompat -Wincomplete-record-updates+ -Wincomplete-uni-patterns + build-depends:+ , base >=4.10 && <4.22+ , megaparsec >=7+ , text ^>=1.2 || >= 2.0 && < 2.2+ , template-haskell >= 2.15+ library- exposed-modules:- Data.Versions+ import: commons+ exposed-modules: Data.Versions build-depends:- base >=4.8 && <4.10- , text >=1.2 && <1.3- , megaparsec >=4 && <6- , deepseq >= 1.4 && < 1.5- , hashable >= 1.2 && < 1.3- default-language: Haskell2010- ghc-options: -fwarn-unused-imports -fwarn-unused-binds -fwarn-name-shadowing -fwarn-unused-matches+ , deepseq >=1.4+ , hashable >=1.2+ , parser-combinators >= 1.0 test-suite versions-test- type: exitcode-stdio-1.0+ import: commons+ type: exitcode-stdio-1.0+ main-is: Test.hs+ hs-source-dirs: test+ ghc-options: -threaded -with-rtsopts=-N build-depends:- base >=4.8 && <4.10- , text >=1.2 && <1.3- , microlens >=0.4 && <0.5- , tasty >=0.10.1.2- , tasty-hunit >=0.9.2+ , microlens >=0.4+ , tasty >=0.10.1.2+ , tasty-hunit >=0.9.2 , versions- hs-source-dirs:- test- main-is: Test.hs- default-language: Haskell2010- ghc-options: -fwarn-unused-imports -fwarn-unused-binds -fwarn-name-shadowing -fwarn-unused-matches -threaded