salve (empty) → 0.0.0
raw patch · 7 files changed
+1018/−0 lines, 7 filesdep +basedep +criteriondep +deepseq
Dependencies added: base, criterion, deepseq, doctest, microlens, salve
Files
- CHANGELOG.markdown +7/−0
- LICENSE.markdown +21/−0
- benchmarks/benchmark.hs +46/−0
- library/Salve.hs +82/−0
- library/Salve/Internal.hs +805/−0
- salve.cabal +53/−0
- tests/doctest.hs +4/−0
+ CHANGELOG.markdown view
@@ -0,0 +1,7 @@+# Change log++Salve uses [Semantic Versioning][].+The change log is available through the [releases on GitHub][].++[Semantic Versioning]: http://semver.org/spec/v2.0.0.html+[releases on GitHub]: https://github.com/tfausak/salve/releases
+ LICENSE.markdown view
@@ -0,0 +1,21 @@+# [The MIT License](https://opensource.org/licenses/MIT)++Copyright 2017 Taylor Fausak++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished to do+so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ benchmarks/benchmark.hs view
@@ -0,0 +1,46 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}++{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE StandaloneDeriving #-}++import Control.DeepSeq+import Criterion.Main+import GHC.Generics+import Salve.Internal++deriving instance Generic Build+deriving instance Generic Constraint+deriving instance Generic Operator+deriving instance Generic PreRelease+deriving instance Generic Version++deriving instance NFData Build+deriving instance NFData Constraint+deriving instance NFData Operator+deriving instance NFData PreRelease+deriving instance NFData Version++main :: IO ()+main = defaultMain+ [ bgroup "parseVersion" (map+ (\ x -> bench x (nf parseVersion x))+ [ "0.0.0"+ , "123.456.789-pre.release+build.metadata"+ ])+ , bgroup "parseConstraint" (map+ (\ x -> bench x (nf parseConstraint x))+ [ ">=0.0.0"+ , ">1.2.3 || =1.2.3 >=1.2.3 <1.2.3 || 1.2.3 <=1.2.3 ~1.2.3 ^1.2.3"+ ])+ , bgroup "renderVersion" (map+ (\ x -> let y = unsafeParseVersion x in bench x (nf renderVersion y))+ [ "0.0.0"+ , "123.456.789-pre.release+build.metadata"+ ])+ , bgroup "renderConstraint" (map+ (\ x -> let y = unsafeParseConstraint x in bench x (nf renderConstraint y))+ [ ">=0.0.0"+ , ">1.2.3 || =1.2.3 >=1.2.3 <1.2.3 || 1.2.3 <=1.2.3 ~1.2.3 ^1.2.3"+ ])+ ]
+ library/Salve.hs view
@@ -0,0 +1,82 @@+-- | This module defines types and functions for working with versions as+-- defined by [Semantic Versioning](http://semver.org/spec/v2.0.0.html). It+-- also provides types and functions for working with version constraints as+-- described by [npm](https://docs.npmjs.com/misc/semver#ranges).+module Salve (+-- | This module doesn't export anything that conflicts with the "Prelude", so+-- you can import it unqualified.+--+-- >>> import Salve+--+-- The 'Version' data type is the core of this module. Use 'parseVersion' to+-- make versions and 'renderVersion' to convert them into strings.+--+-- >>> renderVersion <$> parseVersion "1.2.3"+-- Just "1.2.3"+--+-- The 'Constraint' data type allows you to specify version constraints. Use+-- 'parseConstraint' to make constraints and 'renderConstraint' to convert them+-- into strings.+--+-- >>> renderConstraint <$> parseConstraint ">1.2.0"+-- Just ">1.2.0"+--+-- Use 'satisfies' to see if a version satisfies a constraint.+--+-- >>> satisfies <$> parseVersion "1.2.3" <*> parseConstraint ">1.2.0"+-- Just True++-- * Types+Version,+PreRelease,+Build,+Constraint,++-- * Parsing+parseVersion,+parsePreRelease,+parseBuild,+parseConstraint,++-- ** Unsafe+-- | These functions can be used to unsafely parse strings. Instead of+-- returning 'Nothing', they raise an exception. Only use these if you are sure+-- the string can be successfully parsed!+unsafeParseVersion,+unsafeParsePreRelease,+unsafeParseBuild,+unsafeParseConstraint,++-- * Rendering+renderVersion,+renderPreRelease,+renderBuild,+renderConstraint,++-- * Predicates+isUnstable,+isStable,++-- * Helpers+bumpMajor,+bumpMinor,+bumpPatch,+satisfies,++-- * Lenses+-- | These lenses can be used to access and modify specific parts of a+-- 'Version'.+--+-- Don't be scared by these type signatures. They are provided in full to avoid+-- the @RankNTypes@ language extension. The type signature+-- @'Functor' f => (a -> f a) -> 'Version' -> f 'Version'@ is the same as+-- @'Lens.Micro.Lens'' 'Version' a@ (from "Lens.Micro"), which you may already+-- be familiar with.+majorLens,+minorLens,+patchLens,+preReleasesLens,+buildsLens,+) where++import Salve.Internal
+ library/Salve/Internal.hs view
@@ -0,0 +1,805 @@+-- | WARNING: This module should be considered private! If you find yourself+-- wanting to import something from this module, please open an issue to get+-- that thing exported from "Salve".+module Salve.Internal where++-- $setup+-- >>> import Lens.Micro.Extras++-- * Public++-- | A semantic version number. Versions have five parts:+--+-- 1. 'majorLens': The major version number.+-- 2. 'minorLens': The minor version number.+-- 3. 'patchLens': The patch version number.+-- 4. 'preReleasesLens': A list of pre-release identifiers.+-- 5. 'buildsLens': A list of build metadata.+--+-- Use 'parseVersion' to create versions.+data Version = Version+ { versionMajor :: Word+ , versionMinor :: Word+ , versionPatch :: Word+ , versionPreReleases :: [PreRelease]+ , versionBuilds :: [Build]+ } deriving (Eq, Show)++-- | In general, 'Version's compare in the way that you would expect. First the+-- major version numbers are compared, then the minors, then the patches.+--+-- >>> compare <$> parseVersion "1.2.3" <*> parseVersion "2.0.0"+-- Just LT+-- >>> compare <$> parseVersion "1.2.3" <*> parseVersion "1.3.0"+-- Just LT+-- >>> compare <$> parseVersion "1.2.3" <*> parseVersion "1.2.4"+-- Just LT+--+-- Numbers are compared numerically, not alphabetically.+--+-- >>> compare <$> parseVersion "0.0.9" <*> parseVersion "0.0.10"+-- Just LT+--+-- If all the numbers are the same, the pre-releases are compared.+--+-- >>> compare <$> parseVersion "1.2.3-a" <*> parseVersion "1.2.3-b"+-- Just LT+--+-- A version with a pre-release is always less than a version without one as+-- long as the other parts are the same.+--+-- >>> compare <$> parseVersion "1.2.3-pre" <*> parseVersion "1.2.3"+-- Just LT+-- >>> compare <$> parseVersion "1.2.4-pre" <*> parseVersion "1.2.3"+-- Just GT+--+-- Builds are not considered when comparing versions.+--+-- >>> compare <$> parseVersion "1.2.3+a" <*> parseVersion "1.2.3+b"+-- Just EQ+-- >>> (==) <$> parseVersion "1.2.3+a" <*> parseVersion "1.2.3+b"+-- Just False+instance Ord Version where+ compare x y = mconcat+ [ comparing versionMajor x y+ , comparing versionMinor x y+ , comparing versionPatch x y+ , case both versionPreReleases (x, y) of+ ([], []) -> EQ+ ([], _) -> GT+ (_, []) -> LT+ (p, q) -> compare p q+ ]++-- | Pre-release information attached to a version. These can either be numeric+-- or textual. They must not be empty.+--+-- - Numeric: Can be any non-negative integer. Cannot have leading zeros.+--+-- - Textual: Can be any string of ASCII digits, letters, or hyphens. Cannot be+-- all digits, as that would be numeric.+--+-- In general, pre-releases must match the regular expression+-- @\/^[-0-9A-Za-z]+$\/@.+--+-- Use 'parsePreRelease' to create pre-releases.+data PreRelease+ = PreReleaseNumeric Word+ | PreReleaseTextual String+ deriving (Eq, Show)++-- | Numeric pre-releases are always less than textual pre-releases.+--+-- >>> compare <$> parsePreRelease "1" <*> parsePreRelease "a"+-- Just LT+--+-- Numeric pre-releases are compared numerically.+--+-- >>> compare <$> parsePreRelease "9" <*> parsePreRelease "10"+-- Just LT+--+-- Textual pre-releases are compared alphabetically.+--+-- >>> compare <$> parsePreRelease "p10" <*> parsePreRelease "p9"+-- Just LT+instance Ord PreRelease where+ compare x y = case (x, y) of+ (PreReleaseNumeric n, PreReleaseNumeric m) -> compare n m+ (PreReleaseNumeric _, PreReleaseTextual _) -> LT+ (PreReleaseTextual _, PreReleaseNumeric _) -> GT+ (PreReleaseTextual s, PreReleaseTextual t) -> compare s t++-- | Build metadata attached to a version. These are similar to+-- 'PreRelease's with some key differences:+--+-- 1. There is no such thing as numeric builds. Even though builds can look+-- like numbers, all builds are textual.+-- 2. As a result, builds that look numeric are allowed to have leading zeros.+-- 3. Builds cannot be compared. That is, they do not have an 'Ord' instance.+--+-- Use 'parseBuild' to create builds.+newtype Build = Build String deriving (Eq, Show)++-- | Constrains allowable version numbers.+--+-- Use 'parseConstraint' to create constraints and 'satisfies' to see if a+-- version number satisfies a constraint.+data Constraint+ = ConstraintCompare Operator Version+ | ConstraintAnd Constraint Constraint+ | ConstraintOr Constraint Constraint+ deriving (Eq, Show)++-- | Attempts to parse a version. This parser follows [SemVer's+-- BNF](https://github.com/mojombo/semver/blob/eb9aac5/semver.md#backusnaur-form-grammar-for-valid-semver-versions).+--+-- >>> parseVersion "1.2.3-p.4+b.5"+-- Just (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [PreReleaseTextual "p",PreReleaseNumeric 4], versionBuilds = [Build "b",Build "5"]})+--+-- Returns 'Nothing' if the parse fails.+--+-- >>> parseVersion "wrong"+-- Nothing+--+-- Whitespace is not allowed and will cause the parser to fail.+--+-- >>> parseVersion " 1.2.3 "+-- Nothing+parseVersion :: String -> Maybe Version+parseVersion s = parse versionP s++-- | Attempts to parse a pre-release.+--+-- >>> parsePreRelease "pre"+-- Just (PreReleaseTextual "pre")+-- >>> parsePreRelease "1"+-- Just (PreReleaseNumeric 1)+--+-- Returns 'Nothing' if the parse fails.+--+-- >>> parsePreRelease "wrong!"+-- Nothing+--+-- Numeric pre-releases cannot contain leading zeros.+--+-- >>> parsePreRelease "01"+-- Nothing+parsePreRelease :: String -> Maybe PreRelease+parsePreRelease s = parse preReleaseP s++-- | Attempts to parse a build.+--+-- >>> parseBuild "build"+-- Just (Build "build")+-- >>> parseBuild "1"+-- Just (Build "1")+--+-- Returns 'Nothing' if the parse fails.+--+-- >>> parseBuild "wrong!"+-- Nothing+--+-- Unlike pre-releases, numeric builds can have leading zeros.+--+-- >>> parseBuild "01"+-- Just (Build "01")+parseBuild :: String -> Maybe Build+parseBuild s = parse buildP s++-- | Attempts to parse a constraint. This parser follows [npm's+-- BNF](https://github.com/npm/npm/blob/d081cc6/doc/misc/semver.md#range-grammar),+-- except that neither the so-called "x-ranges" nor partial version numbers are+-- not supported. So you cannot use @1.2.x@ or @>1.2@ as version constraints.+--+-- >>> parseConstraint ">1.2.3"+-- Just (ConstraintCompare OperatorGT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))+--+-- Returns 'Nothing' if the parse fails.+--+-- >>> parseConstraint "wrong"+-- Nothing+parseConstraint :: String -> Maybe Constraint+parseConstraint s = parse constraintsP s++-- | Parses a version.+--+-- >>> unsafeParseVersion "1.2.3-p.4+b.5"+-- Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [PreReleaseTextual "p",PreReleaseNumeric 4], versionBuilds = [Build "b",Build "5"]}+--+-- Raises an exception if the parse fails.+--+-- >>> unsafeParseVersion "wrong"+-- *** Exception: invalid version: "wrong"+-- ...+--+-- See 'parseVersion' for a safe version of this function.+unsafeParseVersion :: String -> Version+unsafeParseVersion s = case parseVersion s of+ Nothing -> error ("invalid version: " ++ show s)+ Just v -> v++-- | Parses a pre-release.+--+-- >>> unsafeParsePreRelease "pre"+-- PreReleaseTextual "pre"+--+-- Raises an exception if the parse fails.+--+-- >>> unsafeParsePreRelease "wrong!"+-- *** Exception: invalid pre-release: "wrong!"+-- ...+--+-- See 'parsePreRelease' for a safe version of this function.+unsafeParsePreRelease :: String -> PreRelease+unsafeParsePreRelease s = case parsePreRelease s of+ Nothing -> error ("invalid pre-release: " ++ show s)+ Just p -> p++-- | Parses a build.+--+-- >>> unsafeParseBuild "build"+-- Build "build"+--+-- Raises an exception if the parse fails.+--+-- >>> unsafeParseBuild "wrong!"+-- Build "*** Exception: invalid build: "wrong!"+-- ...+--+-- See 'parseBuild' for a safe version of this function.+unsafeParseBuild :: String -> Build+unsafeParseBuild s = case parseBuild s of+ Nothing -> error ("invalid build: " ++ show s)+ Just b -> b++-- | Parses a constraint.+--+-- >>> unsafeParseConstraint ">1.2.3"+-- ConstraintCompare OperatorGT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})+--+-- Raises an exception if the parse fails.+--+-- >>> unsafeParseConstraint "wrong"+-- *** Exception: invalid constraint: "wrong"+-- ...+--+-- See 'parseConstraint' for a safe version of this function.+unsafeParseConstraint :: String -> Constraint+unsafeParseConstraint s = case parseConstraint s of+ Nothing -> error ("invalid constraint: " ++ show s)+ Just c -> c++-- | Renders a version.+--+-- >>> renderVersion <$> parseVersion "1.2.3-p.4+b.5"+-- Just "1.2.3-p.4+b.5"+renderVersion :: Version -> String+renderVersion v = mconcat+ [ show (versionMajor v)+ , "."+ , show (versionMinor v)+ , "."+ , show (versionPatch v)+ , renderPreReleases (versionPreReleases v)+ , renderBuilds (versionBuilds v)+ ]++-- | Renders a pre-release.+--+-- >>> renderPreRelease <$> parsePreRelease "pre"+-- Just "pre"+-- >>> renderPreRelease <$> parsePreRelease "1"+-- Just "1"+renderPreRelease :: PreRelease -> String+renderPreRelease p = case p of+ PreReleaseNumeric n -> show n+ PreReleaseTextual s -> s++-- | Renders a build.+--+-- >>> renderBuild <$> parseBuild "build"+-- Just "build"+-- >>> renderBuild <$> parseBuild "1"+-- Just "1"+renderBuild :: Build -> String+renderBuild (Build b) = b++-- | Renders a constraint.+--+-- >>> renderConstraint <$> parseConstraint ">1.2.3"+-- Just ">1.2.3"+--+-- Parsing and rendering a constraint doesn't always return what you started+-- with.+--+-- >>> renderConstraint <$> parseConstraint "=1.2.3"+-- Just "1.2.3"+renderConstraint :: Constraint -> String+renderConstraint c = case c of+ ConstraintCompare o v ->+ let s = renderVersion v+ in case o of+ OperatorLT -> '<' : s+ OperatorLE -> '<' : '=' : s+ OperatorEQ -> s+ OperatorGE -> '>' : '=' : s+ OperatorGT -> '>' : s+ ConstraintAnd l r -> join ' ' (map renderConstraint [l, r])+ ConstraintOr l r -> join ' ' [renderConstraint l, "||", renderConstraint r]++-- | Returns 'True' if the major version number is zero, 'False' otherwise.+--+-- >>> isUnstable <$> parseVersion "0.1.2"+-- Just True+-- >>> isUnstable <$> parseVersion "1.0.0"+-- Just False+isUnstable :: Version -> Bool+isUnstable v = versionMajor v == 0++-- | Returns 'True' if the major version number is not zero, 'False' otherwise.+--+-- >>> isStable <$> parseVersion "1.0.0"+-- Just True+-- >>> isStable <$> parseVersion "0.1.2"+-- Just False+isStable :: Version -> Bool+isStable v = not (isUnstable v)++-- | Increments the major version number.+--+-- >>> bumpMajor <$> parseVersion "0.0.0"+-- Just (Version {versionMajor = 1, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})+--+-- The minor and patch numbers are reset to zero.+--+-- >>> bumpMajor <$> parseVersion "1.2.3"+-- Just (Version {versionMajor = 2, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})+--+-- The pre-releases and builds are removed.+--+-- >>> bumpMajor <$> parseVersion "0.0.0-pre+build"+-- Just (Version {versionMajor = 1, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})+--+-- Consider using 'majorLens' if you want to arbitrarily change the major+-- number, or if you don't want the other parts of the version to change.+bumpMajor :: Version -> Version+bumpMajor v = v+ { versionMajor = versionMajor v + 1+ , versionMinor = 0+ , versionPatch = 0+ , versionPreReleases = []+ , versionBuilds = []+ }++-- | Increments the minor version number.+--+-- >>> bumpMinor <$> parseVersion "0.0.0"+-- Just (Version {versionMajor = 0, versionMinor = 1, versionPatch = 0, versionPreReleases = [], versionBuilds = []})+--+-- The patch number is reset to zero.+--+-- >>> bumpMinor <$> parseVersion "1.2.3"+-- Just (Version {versionMajor = 1, versionMinor = 3, versionPatch = 0, versionPreReleases = [], versionBuilds = []})+--+-- The pre-releases and builds are removed.+--+-- >>> bumpMinor <$> parseVersion "0.0.0-pre+build"+-- Just (Version {versionMajor = 0, versionMinor = 1, versionPatch = 0, versionPreReleases = [], versionBuilds = []})+--+-- Consider using 'minorLens' if you want to arbitrarily change the minor+-- number, or if you don't want the other parts of the version to change.+bumpMinor :: Version -> Version+bumpMinor v = v+ { versionMinor = versionMinor v + 1+ , versionPatch = 0+ , versionPreReleases = []+ , versionBuilds = []+ }++-- | Increments the patch number.+--+-- >>> bumpPatch <$> parseVersion "0.0.0"+-- Just (Version {versionMajor = 0, versionMinor = 0, versionPatch = 1, versionPreReleases = [], versionBuilds = []})+--+-- The major and minor numbers are not changed.+--+-- >>> bumpPatch <$> parseVersion "1.2.3"+-- Just (Version {versionMajor = 1, versionMinor = 2, versionPatch = 4, versionPreReleases = [], versionBuilds = []})+--+-- The pre-releases and builds are removed.+--+-- >>> bumpPatch <$> parseVersion "0.0.0-pre+build"+-- Just (Version {versionMajor = 0, versionMinor = 0, versionPatch = 1, versionPreReleases = [], versionBuilds = []})+--+-- Consider using 'patchLens' if you want to arbitrarily change the patch+-- number, or if you don't want the other parts of the version to change.+bumpPatch :: Version -> Version+bumpPatch v = v+ { versionPatch = versionPatch v + 1+ , versionPreReleases = []+ , versionBuilds = []+ }++-- | Returns 'True' if the version satisfies the constraint, 'False' otherwise.+--+-- >>> satisfies <$> parseVersion "1.2.3" <*> parseConstraint ">1.2.0"+-- Just True+satisfies :: Version -> Constraint -> Bool+satisfies v c = case c of+ ConstraintCompare o w -> case o of+ OperatorLE -> v <= w+ OperatorLT -> v < w+ OperatorEQ -> compare v w == EQ+ OperatorGT -> v > w+ OperatorGE -> v >= w+ ConstraintAnd l r -> satisfies v l && satisfies v r+ ConstraintOr l r -> satisfies v l || satisfies v r++-- | Focuses on the major version number.+--+-- >>> view majorLens <$> parseVersion "1.2.3-pre.4+build.5"+-- Just 1+majorLens :: Functor f => (Word -> f Word) -> Version -> f Version+majorLens f v = fmap+ (\ m -> v { versionMajor = m })+ (f (versionMajor v))++-- | Focuses on the minor version number.+--+-- >>> view minorLens <$> parseVersion "1.2.3-pre.4+build.5"+-- Just 2+minorLens :: Functor f => (Word -> f Word) -> Version -> f Version+minorLens f v = fmap+ (\ n -> v { versionMinor = n })+ (f (versionMinor v))++-- | Focuses on the patch version number.+--+-- >>> view patchLens <$> parseVersion "1.2.3-pre.4+build.5"+-- Just 3+patchLens :: Functor f => (Word -> f Word) -> Version -> f Version+patchLens f v = fmap+ (\ p -> v { versionPatch = p })+ (f (versionPatch v))++-- | Focuses on the pre-release identifiers.+--+-- >>> view preReleasesLens <$> parseVersion "1.2.3-pre.4+build.5"+-- Just [PreReleaseTextual "pre",PreReleaseNumeric 4]+preReleasesLens :: Functor f => ([PreRelease] -> f [PreRelease]) -> Version -> f Version+preReleasesLens f v = fmap+ (\ ps -> v { versionPreReleases = ps })+ (f (versionPreReleases v))++-- | Focuses on the build metadata.+--+-- >>> view buildsLens <$> parseVersion "1.2.3-pre.4+build.5"+-- Just [Build "build",Build "5"]+buildsLens :: Functor f => ([Build] -> f [Build]) -> Version -> f Version+buildsLens f v = fmap+ (\ bs -> v { versionBuilds = bs })+ (f (versionBuilds v))++-- * Private++-- ** Types++data Operator+ = OperatorLT+ | OperatorLE+ | OperatorEQ+ | OperatorGE+ | OperatorGT+ deriving (Eq, Show)++constraintLT :: Version -> Constraint+constraintLT v = ConstraintCompare OperatorLT v++constraintLE :: Version -> Constraint+constraintLE v = ConstraintCompare OperatorLE v++constraintEQ :: Version -> Constraint+constraintEQ v = ConstraintCompare OperatorEQ v++constraintGE :: Version -> Constraint+constraintGE v = ConstraintCompare OperatorGE v++constraintGT :: Version -> Constraint+constraintGT v = ConstraintCompare OperatorGT v++constraintAnd :: Constraint -> Constraint -> Constraint+constraintAnd l r = ConstraintAnd l r++constraintOr :: Constraint -> Constraint -> Constraint+constraintOr l r = ConstraintOr l r++-- ** Parsing++versionP :: Parser Version+versionP = do+ major <- numberP+ _ <- charP '.'+ minor <- numberP+ _ <- charP '.'+ patch <- numberP+ preReleases <- preReleasesP+ builds <- buildsP+ pure Version+ { versionMajor = major+ , versionMinor = minor+ , versionPatch = patch+ , versionPreReleases = preReleases+ , versionBuilds = builds+ }++preReleasesP :: Parser [PreRelease]+preReleasesP = optionP [] (do+ _ <- charP '-'+ sepBy1P (charP '.') preReleaseP)++preReleaseP :: Parser PreRelease+preReleaseP = choiceP preReleaseStringP preReleaseNumberP++preReleaseNumberP :: Parser PreRelease+preReleaseNumberP = do+ n <- numberP+ pure (PreReleaseNumeric n)++preReleaseStringP :: Parser PreRelease+preReleaseStringP = do+ s <- someP (satisfyP isIdentifier)+ if all isAsciiDigit s+ then failP+ else pure (PreReleaseTextual s)++buildsP :: Parser [Build]+buildsP = optionP [] (do+ _ <- charP '+'+ sepBy1P (charP '.') buildP)++buildP :: Parser Build+buildP = do+ b <- someP (satisfyP isIdentifier)+ pure (Build b)++numberP :: Parser Word+numberP = choiceP zeroP nonZeroP++zeroP :: Parser Word+zeroP = do+ _ <- charP '0'+ pure 0++nonZeroP :: Parser Word+nonZeroP = do+ x <- satisfyP isAsciiDigitNonZero+ ys <- manyP (satisfyP isAsciiDigit)+ pure (read (x : ys))++constraintsP :: Parser Constraint+constraintsP = do+ _ <- spacesP+ cs <- sepBy1P orP constraintP+ _ <- spacesP+ pure (foldr1 constraintOr cs)++constraintP :: Parser Constraint+constraintP = choiceP hyphenatedP simplesP++hyphenatedP :: Parser Constraint+hyphenatedP = do+ v <- versionP+ _ <- hyphenP+ w <- versionP+ pure (constraintAnd (constraintGE v) (constraintLE w))++simplesP :: Parser Constraint+simplesP = do+ cs <- sepBy1P spaceP simpleP+ pure (foldr1 constraintAnd cs)++simpleP :: Parser Constraint+simpleP = choiceP caretP (choiceP tildeP primitiveP)++caretP :: Parser Constraint+caretP = do+ _ <- charP '^'+ v <- versionP+ let w = caretUpper v+ pure (constraintAnd (constraintGE v) (constraintLT w))++caretUpper :: Version -> Version+caretUpper v =+ if versionMinor v == 0+ then bumpPatch v+ else if versionMajor v == 0+ then bumpMinor v+ else bumpMajor v++tildeP :: Parser Constraint+tildeP = do+ _ <- charP '~'+ v <- versionP+ let w = tildeUpper v+ pure (constraintAnd (constraintGE v) (constraintLT w))++tildeUpper :: Version -> Version+tildeUpper v = bumpMinor v++primitiveP :: Parser Constraint+primitiveP = do+ o <- operatorP+ v <- versionP+ pure (ConstraintCompare o v)++operatorP :: Parser Operator+operatorP = oneOfP [leP, geP, ltP, gtP, eqP, pure OperatorEQ]++leP :: Parser Operator+leP = do+ _ <- stringP "<="+ pure OperatorLE++geP :: Parser Operator+geP = do+ _ <- stringP ">="+ pure OperatorGE++ltP :: Parser Operator+ltP = do+ _ <- charP '<'+ pure OperatorLT++gtP :: Parser Operator+gtP = do+ _ <- charP '>'+ pure OperatorGT++eqP :: Parser Operator+eqP = do+ _ <- charP '='+ pure OperatorEQ++hyphenP :: Parser String+hyphenP = stringP " - "++orP :: Parser String+orP = stringP " || "++spacesP :: Parser String+spacesP = manyP spaceP++spaceP :: Parser Char+spaceP = charP ' '++-- *** Helpers++parse :: Parser a -> String -> Maybe a+parse p s = safeHead (do+ (x, "") <- runParser p s+ pure x)++newtype Parser a = Parser { runParser :: String -> [(a, String)] }++instance Functor Parser where+ fmap f p = Parser (\s -> do+ (x, t) <- runParser p s+ pure (f x, t))++instance Applicative Parser where+ pure x = Parser (\ s -> pure (x, s))++ p <*> q = Parser (\ s -> do+ (f, t) <- runParser p s+ (x, u) <- runParser q t+ pure (f x, u))++instance Monad Parser where+ fail x = Parser (\ _ -> fail x)++ p >>= f = Parser (\ s -> do+ (x, t) <- runParser p s+ runParser (f x) t)++charP :: Char -> Parser Char+charP c = satisfyP (\ d -> d == c)++choiceP :: Parser a -> Parser a -> Parser a+choiceP p q = Parser (\ s -> case runParser p s of+ [] -> runParser q s+ xs -> xs)++failP :: Parser a+failP = fail undefined++getP :: Parser Char+getP = Parser (\ s -> case s of+ "" -> []+ c : t -> pure (c, t))++manyP :: Parser a -> Parser [a]+manyP p = choiceP (someP p) (pure [])++oneOfP :: [Parser a] -> Parser a+oneOfP ps = foldr choiceP failP ps++optionP :: a -> Parser a -> Parser a+optionP x p = choiceP p (pure x)++satisfyP :: (Char -> Bool) -> Parser Char+satisfyP f = do+ c <- getP+ if f c then pure c else failP++sepByP :: Parser b -> Parser a -> Parser [a]+sepByP q p = choiceP (sepBy1P q p) (pure [])++sepBy1P :: Parser b -> Parser a -> Parser [a]+sepBy1P q p = do+ x <- p+ xs <- optionP [] (do+ _ <- q+ sepBy1P q p)+ pure (x : xs)++someP :: Parser a -> Parser [a]+someP p = do+ x <- p+ xs <- manyP p+ pure (x : xs)++stringP :: String -> Parser String+stringP s = case s of+ "" -> pure s+ c : t -> do+ _ <- charP c+ _ <- stringP t+ pure s++-- ** Rendering++renderPreReleases :: [PreRelease] -> String+renderPreReleases ps = if null ps+ then ""+ else '-' : join '.' (map renderPreRelease ps)++renderBuilds :: [Build] -> String+renderBuilds bs = if null bs+ then ""+ else '+' : join '.' (map renderBuild bs)++-- ** Helpers++both :: (a -> b) -> (a, a) -> (b, b)+both f (x, y) = (f x, f y)++comparing :: Ord b => (a -> b) -> a -> a -> Ordering+comparing f x y = compare (f x) (f y)++isAsciiAlpha :: Char -> Bool+isAsciiAlpha c = (isAsciiAlphaUpper c) || (isAsciiAlphaLower c)++isAsciiAlphaLower :: Char -> Bool+isAsciiAlphaLower c = ('a' <= c) && (c <= 'z')++isAsciiAlphaUpper :: Char -> Bool+isAsciiAlphaUpper c = ('A' <= c) && (c <= 'Z')++isAsciiDigit :: Char -> Bool+isAsciiDigit c = ('0' <= c) && (c <= '9')++isAsciiDigitNonZero :: Char -> Bool+isAsciiDigitNonZero c = isAsciiDigit c && (c /= '0')++isIdentifier :: Char -> Bool+isIdentifier c = (c == '-') || isAsciiDigit c || isAsciiAlpha c++join :: Char -> [String] -> String+join c ss = case ss of+ [] -> ""+ s : ts -> mconcat (s : map (\ t -> c : t) ts)++safeHead :: [a] -> Maybe a+safeHead xs = case xs of+ [] -> Nothing+ x : _ -> Just x
+ salve.cabal view
@@ -0,0 +1,53 @@+-- This file has been generated from package.yaml by hpack version 0.17.1.+--+-- see: https://github.com/sol/hpack++name: salve+version: 0.0.0+synopsis: Semantic version numbers and constraints.+description: Salve provides semantic version numbers and constraints.+category: Distribution+maintainer: Taylor Fausak+license: MIT+license-file: LICENSE.markdown+build-type: Simple+cabal-version: >= 1.10++extra-source-files:+ CHANGELOG.markdown++library+ hs-source-dirs:+ library+ ghc-options: -Wall+ build-depends:+ base >=4.8.2 && <4.11+ exposed-modules:+ Salve+ Salve.Internal+ default-language: Haskell2010++test-suite doctest+ type: exitcode-stdio-1.0+ main-is: doctest.hs+ hs-source-dirs:+ tests+ ghc-options: -Wall+ build-depends:+ base >=4.8.2 && <4.11+ , doctest >=0.10.1 && <0.14+ , microlens >=0.3.5 && <0.5+ default-language: Haskell2010++benchmark benchmark+ type: exitcode-stdio-1.0+ main-is: benchmark.hs+ hs-source-dirs:+ benchmarks+ ghc-options: -Wall+ build-depends:+ base >=4.8.2 && <4.11+ , criterion >=1.1.0 && <1.3+ , deepseq >=1.4.1 && <1.5+ , salve+ default-language: Haskell2010
+ tests/doctest.hs view
@@ -0,0 +1,4 @@+import Test.DocTest++main :: IO ()+main = doctest ["library"]