semver 0.2.1 → 0.3.0
raw patch · 4 files changed
+325/−277 lines, 4 files
Files
- semver.cabal +5/−1
- src/Data/SemVer.hs +60/−276
- src/Data/SemVer/Delimited.hs +120/−0
- src/Data/SemVer/Internal.hs +140/−0
semver.cabal view
@@ -1,5 +1,5 @@ name: semver-version: 0.2.1+version: 0.3.0 synopsis: Representation, manipulation, and de/serialisation of Semantic Versions. homepage: https://github.com/brendanhay/semver license: OtherLicense@@ -29,6 +29,10 @@ exposed-modules: Data.SemVer+ , Data.SemVer.Delimited++ other-modules:+ Data.SemVer.Internal build-depends: attoparsec >= 0.10 && < 0.13
src/Data/SemVer.hs view
@@ -1,6 +1,3 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}- -- Module : Data.SemVer -- Copyright : (c) 2014 Brendan Hay <brendan.g.hay@gmail.com> -- License : This Source Code Form is subject to the terms of@@ -25,12 +22,13 @@ , version , initial -- ** Lenses- , versionMajor- , versionMinor- , versionPatch- , versionRelease- , versionMeta+ , major+ , minor+ , patch+ , release+ , metadata -- ** Incrementing+ -- $incrementing , incrementMajor , incrementMinor , incrementPatch@@ -55,102 +53,17 @@ -- ** Prisms , _Numeric , _Textual-- -- * Delimiters- -- $delimiters- , Delimiters- -- ** Constructor- , delimiters- -- ** Lenses- , delimMinor- , delimPatch- , delimRelease- , delimMeta- , delimIdent- -- ** Encoding- , toDelimitedBuilder- -- ** Decoding- , delimitedParser ) where import Control.Applicative-import Control.DeepSeq-import Control.Monad import Data.Attoparsec.Text-import Data.Char-import Data.Function (on)-import Data.List (intersperse)-import Data.Monoid-import Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Lazy as LText-import Data.Text.Lazy.Builder (Builder)-import qualified Data.Text.Lazy.Builder as Build-import qualified Data.Text.Lazy.Builder.Int as Build-import Prelude hiding (takeWhile)---- | A type representing an individual identifier from the release--- or metadata components of a 'Version'.------ * The 'Ord' instance implements precedence according to the semantic version--- specification, with numeric identifiers being of /lower/ precedence than--- textual identifiers, otherwise lexicographic ordering is used.------ The functions 'numeric' and 'textual' can be used to construct an 'Identifier'.-data Identifier- = INum !Int- | IText !Text- deriving (Eq, Show)---- | Safely construct a numeric identifier.-numeric :: Int -> Identifier-numeric = INum-{-# INLINE numeric #-}---- | Construct an identifier from the given 'Text', returning 'Nothing' if--- neither a numeric or valid textual input is supplied.-textual :: Text -> Maybe Identifier-textual = either (const Nothing) Just- . parseOnly (identifierParser endOfInput <* endOfInput)-{-# INLINE textual #-}--_Numeric :: Applicative f => (Int -> f Int) -> Identifier -> f Identifier-_Numeric f (INum x) = INum <$> f x-_Numeric _ x = pure x-{-# INLINE _Numeric #-}--_Textual :: Applicative f => (Text -> f Text) -> Identifier -> f Identifier-_Textual f (IText x) = IText <$> f x-_Textual _ x = pure x-{-# INLINE _Textual #-}--instance Ord Identifier where- compare a b = case (a, b) of- (INum x, INum y) -> x `compare` y- (IText x, IText y) -> x `compare` y- (INum _, _) -> LT- (IText _, _) -> GT--instance NFData Identifier where- rnf (INum n) = rnf n- rnf (IText t) = rnf t---- | An opaque type representing a successfully decoded or constructed--- semantic version. See the related functions and lenses for modification and--- update.------ * The 'Eq' instance represents exhaustive equality with all--- components considered.------ * The 'Ord' instance implements the precedence rules from the semantic--- version specification with metadata being ignored.-data Version = Version- { _versionMajor :: !Int- , _versionMinor :: !Int- , _versionPatch :: !Int- , _versionRelease :: [Identifier]- , _versionMeta :: [Identifier]- } deriving (Eq, Show)+import qualified Data.SemVer.Delimited as Delim+import Data.SemVer.Internal+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Lazy as LText+import Data.Text.Lazy.Builder (Builder)+import qualified Data.Text.Lazy.Builder as Build -- | Smart constructor fully specifying all available version components. version :: Int -- ^ Major version component.@@ -168,143 +81,44 @@ initial :: Version initial = version 0 0 0 [] [] -instance Ord Version where- compare a b = on compare versions a b <> on compare _versionRelease a b- where- versions Version{..} =- [ _versionMajor- , _versionMinor- , _versionPatch- ]--instance NFData Version where- rnf Version{..} =- rnf _versionMajor- `seq` rnf _versionMinor- `seq` rnf _versionPatch- `seq` rnf _versionRelease- `seq` rnf _versionMeta- -- | Lens for the major version component.-versionMajor :: Functor f => (Int -> f Int) -> Version -> f Version-versionMajor f x = (\y -> x { _versionMajor = y }) <$> f (_versionMajor x)-{-# INLINE versionMajor #-}+major :: Functor f => (Int -> f Int) -> Version -> f Version+major f x = (\y -> x { _versionMajor = y }) <$> f (_versionMajor x)+{-# INLINE major #-} -- | Lens for minor version component.-versionMinor :: Functor f => (Int -> f Int) -> Version -> f Version-versionMinor f x = (\y -> x { _versionMinor = y }) <$> f (_versionMinor x)-{-# INLINE versionMinor #-}+minor :: Functor f => (Int -> f Int) -> Version -> f Version+minor f x = (\y -> x { _versionMinor = y }) <$> f (_versionMinor x)+{-# INLINE minor #-} -- | Lens for the patch version component.-versionPatch :: Functor f => (Int -> f Int) -> Version -> f Version-versionPatch f x = (\y -> x { _versionPatch = y }) <$> f (_versionPatch x)-{-# INLINE versionPatch #-}+patch :: Functor f => (Int -> f Int) -> Version -> f Version+patch f x = (\y -> x { _versionPatch = y }) <$> f (_versionPatch x)+{-# INLINE patch #-} -- | Lens for the list of release identifiers.-versionRelease :: Functor f+release :: Functor f => ([Identifier] -> f [Identifier]) -> Version -> f Version-versionRelease f x = (\y -> x { _versionRelease = y }) <$> f (_versionRelease x)-{-# INLINE versionRelease #-}+release f x = (\y -> x { _versionRelease = y }) <$> f (_versionRelease x)+{-# INLINE release #-} -- | Lens for the list of metadata identifiers.-versionMeta :: Functor f+metadata :: Functor f => ([Identifier] -> f [Identifier]) -> Version -> f Version-versionMeta f x = (\y -> x { _versionMeta = y }) <$> f (_versionMeta x)-{-# INLINE versionMeta #-}---- $delimiters------ A set of delimiters is used to encode/decode a 'Version' and specify--- alternative serialisation strategies.------ Lenses can be used to modify the default delimiter set, as in the following--- example - using alpha characters to encode the version as a valid--- DNS CNAME:------ @--- let Right v = fromText "1.2.3+40"--- let alpha = delimiters & delimMajor .= \'m\' & delimPatch .= \'p\' & delimRelease .= \'r\' & delimMeta .= \'d\' & delimIdent .= \'i\'------ Data.Text.Lazy.Builder.toLazyText (\"app01-\" <> toDelimitedBuilder alpha v <> \".dmz.internal\")--- @------ Would result in the following 'LText.Text':------ @--- app01-1m2p3d40.dmz.internal--- @------ Using the same 'Delimiters' set with 'delimitedParser' would ensure--- correct decoding behaviour.---- | An opaque set representing the seperators used to delimit semantic--- version components.-data Delimiters = Delimiters- { _delimMinor :: !Char- , _delimPatch :: !Char- , _delimRelease :: !Char- , _delimMeta :: !Char- , _delimIdent :: !Char- } deriving (Eq, Ord, Show)+metadata f x = (\y -> x { _versionMeta = y }) <$> f (_versionMeta x)+{-# INLINE metadata #-} --- | The default set of delimiters used in the semantic version specification.------ Example: Given exhaustive version components would result in the--- following hypothetical version:+-- $incrementing ----- @--- 1.2.3-alpha.1+sha.exp.12ab3d9--- @-delimiters :: Delimiters-delimiters = Delimiters- { _delimMinor = '.'- , _delimPatch = '.'- , _delimRelease = '-'- , _delimMeta = '+'- , _delimIdent = '.'- }--instance NFData Delimiters where- rnf Delimiters{..} =- rnf _delimMinor- `seq` rnf _delimPatch- `seq` rnf _delimRelease- `seq` rnf _delimMeta- `seq` rnf _delimIdent---- | Lens for the minor version delimiter. Default: @.@-delimMinor :: Functor f => (Char -> f Char) -> Delimiters -> f Delimiters-delimMinor f x = (\y -> x { _delimMinor = y }) <$> f (_delimMinor x)-{-# INLINE delimMinor #-}---- | Lens for the patch version delimiter. Default: @.@-delimPatch :: Functor f => (Char -> f Char) -> Delimiters -> f Delimiters-delimPatch f x = (\y -> x { _delimPatch = y }) <$> f (_delimPatch x)-{-# INLINE delimPatch #-}---- | Lens for the release component delimiter. Default: @-@-delimRelease :: Functor f => (Char -> f Char) -> Delimiters -> f Delimiters-delimRelease f x = (\y -> x { _delimRelease = y }) <$> f (_delimRelease x)-{-# INLINE delimRelease #-}---- | Lens for the metadata component delimiter. Default: @+@-delimMeta :: Functor f => (Char -> f Char) -> Delimiters -> f Delimiters-delimMeta f x = (\y -> x { _delimMeta = y }) <$> f (_delimMeta x)-{-# INLINE delimMeta #-}---- | Lens for the individual identifier delimiter. Default: @.@-delimIdent :: Functor f => (Char -> f Char) -> Delimiters -> f Delimiters-delimIdent f x = (\y -> x { _delimIdent = y }) <$> f (_delimIdent x)-{-# INLINE delimIdent #-}---- $increments+-- The following increment functions are used to ensure that the related+-- version components are reset according to the specification. ----- Mention the usage of increments to correctly adjust version components--- according to spec.+-- See the individual function documentation for specifics regarding each+-- version component. -- | Increment the major component of a 'Version' by 1, resetting the minor -- and patch components.@@ -375,10 +189,10 @@ -- | Convert a 'Version' to it's readable 'String' representation. ----- Note: This is optimised for cases where you wish to use a 'String' and+-- Note: This is optimised for cases where you require 'String' output, and -- as such is faster than the semantically equivalent @unpack . toLazyText@. toString :: Version -> String-toString = toMonoid (:[]) show Text.unpack delimiters+toString = toMonoid (:[]) show Text.unpack Delim.semantic -- | Convert a 'Version' to a strict 'Text' representation. --@@ -397,35 +211,7 @@ -- | Convert a 'Version' to a 'Builder'. toBuilder :: Version -> Builder-toBuilder = toDelimitedBuilder delimiters---- | Convert a 'Version' to a 'Builder' using the specified 'Delimiters' set.-toDelimitedBuilder :: Delimiters -> Version -> Builder-toDelimitedBuilder = toMonoid Build.singleton Build.decimal Build.fromText--toMonoid :: Monoid m- => (Char -> m)- -> (Int -> m)- -> (Text -> m)- -> Delimiters- -> Version- -> m-toMonoid del int txt Delimiters{..} Version{..} = mconcat- [ int _versionMajor- , del _delimMinor- , int _versionMinor- , del _delimPatch- , int _versionPatch- , f _delimRelease _versionRelease- , f _delimMeta _versionMeta- ]- where- f _ [] = mempty- f c xs = del c <> mconcat (intersperse (del _delimIdent) (map g xs))-- g (INum n) = int n- g (IText t) = txt t-{-# INLINE toMonoid #-}+toBuilder = Delim.toBuilder Delim.semantic -- | Parse a 'Version' from 'Text', returning an attoparsec error message -- in the 'Left' case on failure.@@ -440,33 +226,31 @@ fromLazyText :: LText.Text -> Either String Version fromLazyText = fromText . LText.toStrict --- | A greedy attoparsec 'Parser' which requires the entire 'Text' input to match.+-- | A greedy attoparsec 'Parser' which requires the entire 'Text'+-- input to match. parser :: Parser Version-parser = delimitedParser delimiters+parser = Delim.parser Delim.semantic --- | A greedy attoparsec 'Parser' using the specified 'Delimiters' set--- which requires the entire 'Text' input to match.-delimitedParser :: Delimiters -> Parser Version-delimitedParser Delimiters{..} = Version- <$> (nonNegative <* char _delimMinor)- <*> (nonNegative <* char _delimPatch)- <*> nonNegative- <*> option [] (try (char _delimRelease) *> identifiers)- <*> option [] (try (char _delimMeta) *> identifiers)- <* endOfInput- where- identifiers :: Parser [Identifier]- identifiers = many (identifierParser $ void (char _delimIdent))+-- | Safely construct a numeric identifier.+numeric :: Int -> Identifier+numeric = INum+{-# INLINE numeric #-} -identifierParser :: Parser () -> Parser Identifier-identifierParser end = num <|> text- where- num = INum <$> nonNegative <* (end <|> endOfInput)- text = IText <$> takeWhile1 (inClass "0-9A-Za-z-") <* optional end+-- | Construct an identifier from the given 'Text', returning 'Nothing' if+-- neither a numeric or valid textual input is supplied.+textual :: Text -> Maybe Identifier+textual = either (const Nothing) (Just . IText)+ . parseOnly (textualParser endOfInput <* endOfInput)+{-# INLINE textual #-} -nonNegative :: (Show a, Integral a) => Parser a-nonNegative = do- n <- decimal- when (n < 0) $- fail ("Numeric value must be non-negative: " ++ show n)- return n+-- | A prism into the numeric branch of an 'Identifier'.+_Numeric :: Applicative f => (Int -> f Int) -> Identifier -> f Identifier+_Numeric f (INum x) = INum <$> f x+_Numeric _ x = pure x+{-# INLINE _Numeric #-}++-- | A prism into the textual branch of an 'Identifier'.+_Textual :: Applicative f => (Text -> f Text) -> Identifier -> f (Maybe Identifier)+_Textual f (IText x) = textual <$> f x+_Textual _ x = pure (Just x)+{-# INLINE _Textual #-}
+ src/Data/SemVer/Delimited.hs view
@@ -0,0 +1,120 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- Module : Data.SemVer.Delimited+-- Copyright : (c) 2014 Brendan Hay <brendan.g.hay@gmail.com>+-- License : This Source Code Form is subject to the terms of+-- the Mozilla Public License, v. 2.0.+-- A copy of the MPL can be found in the LICENSE file or+-- you can obtain it at http://mozilla.org/MPL/2.0/.+-- Maintainer : Brendan Hay <brendan.g.hay@gmail.com>+-- Stability : experimental+-- Portability : non-portable (GHC extensions)++-- | A set of delimiters can be used to encode/decode a 'Version' and specify+-- alternative serialisation strategies.+--+-- Lenses can be used to modify the default delimiter set, as in the following+-- example - using alpha characters to encode the version as a valid+-- DNS CNAME (assuming operators from lens or lens-family-core):+--+-- @+-- let Right v = fromText "1.2.3+40"+-- let alpha = semantic & major .~ \'m\' & patch .~ \'p\' & release .~ \'r\' & metadata .~ \'d\' & identifier .~ \'i\'+--+-- Data.Text.Lazy.Builder.toLazyText (\"app01-\" <> toDelimitedBuilder alpha v <> \".dmz.internal\")+-- @+--+-- Would result in the following 'LText.Text':+--+-- @+-- app01-1m2p3d40.dmz.internal+-- @+--+-- Using the same 'Delimiters' set with 'delimitedParser' would ensure+-- correct decoding behaviour.+module Data.SemVer.Delimited+ (+ -- * Delimiters+ Delimiters+ -- ** Constructor+ , semantic+ -- ** Lenses+ , minor+ , patch+ , release+ , metadata+ , identifier+ -- ** Encoding+ , toBuilder+ -- ** Decoding+ , parser+ ) where++import Control.Applicative+import Control.Monad+import Data.Attoparsec.Text+import Data.SemVer.Internal+import Data.Text.Lazy.Builder (Builder)+import qualified Data.Text.Lazy.Builder as Build+import qualified Data.Text.Lazy.Builder.Int as Build++-- | The default set of delimiters used in the semantic version specification.+--+-- Example: Given exhaustive version components would result in the+-- following hypothetical version:+--+-- @+-- 1.2.3-alpha.1+sha.exp.12ab3d9+-- @+semantic :: Delimiters+semantic = Delimiters+ { _delimMinor = '.'+ , _delimPatch = '.'+ , _delimRelease = '-'+ , _delimMeta = '+'+ , _delimIdent = '.'+ }++-- | Lens for the minor version delimiter. Default: @.@+minor :: Functor f => (Char -> f Char) -> Delimiters -> f Delimiters+minor f x = (\y -> x { _delimMinor = y }) <$> f (_delimMinor x)+{-# INLINE minor #-}++-- | Lens for the patch version delimiter. Default: @.@+patch :: Functor f => (Char -> f Char) -> Delimiters -> f Delimiters+patch f x = (\y -> x { _delimPatch = y }) <$> f (_delimPatch x)+{-# INLINE patch #-}++-- | Lens for the release component delimiter. Default: @-@+release :: Functor f => (Char -> f Char) -> Delimiters -> f Delimiters+release f x = (\y -> x { _delimRelease = y }) <$> f (_delimRelease x)+{-# INLINE release #-}++-- | Lens for the metadata component delimiter. Default: @+@+metadata :: Functor f => (Char -> f Char) -> Delimiters -> f Delimiters+metadata f x = (\y -> x { _delimMeta = y }) <$> f (_delimMeta x)+{-# INLINE metadata #-}++-- | Lens for the individual identifier delimiter. Default: @.@+identifier :: Functor f => (Char -> f Char) -> Delimiters -> f Delimiters+identifier f x = (\y -> x { _delimIdent = y }) <$> f (_delimIdent x)+{-# INLINE identifier #-}++-- | Convert a 'Version' to a 'Builder' using the specified 'Delimiters' set.+toBuilder :: Delimiters -> Version -> Builder+toBuilder = toMonoid Build.singleton Build.decimal Build.fromText++-- | A greedy attoparsec 'Parser' using the specified 'Delimiters' set+-- which requires the entire 'Text' input to match.+parser :: Delimiters -> Parser Version+parser Delimiters{..} = Version+ <$> (nonNegative <* char _delimMinor)+ <*> (nonNegative <* char _delimPatch)+ <*> nonNegative+ <*> option [] (try (char _delimRelease) *> identifiers)+ <*> option [] (try (char _delimMeta) *> identifiers)+ <* endOfInput+ where+ identifiers :: Parser [Identifier]+ identifiers = many (identifierParser $ void (char _delimIdent))
+ src/Data/SemVer/Internal.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- Module : Data.SemVer.Internal+-- Copyright : (c) 2014 Brendan Hay <brendan.g.hay@gmail.com>+-- License : This Source Code Form is subject to the terms of+-- the Mozilla Public License, v. 2.0.+-- A copy of the MPL can be found in the LICENSE file or+-- you can obtain it at http://mozilla.org/MPL/2.0/.+-- Maintainer : Brendan Hay <brendan.g.hay@gmail.com>+-- Stability : experimental+-- Portability : non-portable (GHC extensions)++module Data.SemVer.Internal where++import Control.Applicative+import Control.DeepSeq+import Control.Monad+import Data.Attoparsec.Text+import Data.Function (on)+import Data.List (intersperse)+import Data.Monoid+import Data.Text (Text)++-- | An opaque type representing a successfully decoded or constructed+-- semantic version. See the related functions and lenses for modification and+-- update.+--+-- * The 'Eq' instance represents exhaustive equality with all+-- components considered.+--+-- * The 'Ord' instance implements the precedence rules from the semantic+-- version specification with metadata being ignored.+data Version = Version+ { _versionMajor :: !Int+ , _versionMinor :: !Int+ , _versionPatch :: !Int+ , _versionRelease :: [Identifier]+ , _versionMeta :: [Identifier]+ } deriving (Eq, Show)++instance Ord Version where+ compare a b = on compare versions a b <> on compare _versionRelease a b+ where+ versions Version{..} =+ [ _versionMajor+ , _versionMinor+ , _versionPatch+ ]++instance NFData Version where+ rnf Version{..} =+ rnf _versionMajor+ `seq` rnf _versionMinor+ `seq` rnf _versionPatch+ `seq` rnf _versionRelease+ `seq` rnf _versionMeta++-- | A type representing an individual identifier from the release+-- or metadata components of a 'Version'.+--+-- * The 'Ord' instance implements precedence according to the semantic version+-- specification, with numeric identifiers being of /lower/ precedence than+-- textual identifiers, otherwise lexicographic ordering is used.+--+-- The functions 'numeric' and 'textual' can be used to construct an 'Identifier'.+data Identifier+ = INum !Int+ | IText !Text+ deriving (Eq, Show)++instance Ord Identifier where+ compare a b = case (a, b) of+ (INum x, INum y) -> x `compare` y+ (IText x, IText y) -> x `compare` y+ (INum _, _) -> LT+ (IText _, _) -> GT++instance NFData Identifier where+ rnf (INum n) = rnf n+ rnf (IText t) = rnf t++identifierParser :: Parser () -> Parser Identifier+identifierParser p =+ either INum IText <$> eitherP (numericParser p) (textualParser p)++numericParser :: Parser () -> Parser Int+numericParser p = nonNegative <* (p <|> endOfInput)++textualParser :: Parser () -> Parser Text+textualParser p = takeWhile1 (inClass "0-9A-Za-z-") <* optional p++nonNegative :: (Show a, Integral a) => Parser a+nonNegative = do+ n <- decimal+ when (n < 0) $+ fail ("Numeric value must be non-negative: " ++ show n)+ return n++-- | An opaque set representing the seperators used to delimit semantic+-- version components.+data Delimiters = Delimiters+ { _delimMinor :: !Char+ , _delimPatch :: !Char+ , _delimRelease :: !Char+ , _delimMeta :: !Char+ , _delimIdent :: !Char+ } deriving (Eq, Ord, Show)++instance NFData Delimiters where+ rnf Delimiters{..} =+ rnf _delimMinor+ `seq` rnf _delimPatch+ `seq` rnf _delimRelease+ `seq` rnf _delimMeta+ `seq` rnf _delimIdent++toMonoid :: Monoid m+ => (Char -> m)+ -> (Int -> m)+ -> (Text -> m)+ -> Delimiters+ -> Version+ -> m+toMonoid del int txt Delimiters{..} Version{..} = mconcat+ [ int _versionMajor+ , del _delimMinor+ , int _versionMinor+ , del _delimPatch+ , int _versionPatch+ , f _delimRelease _versionRelease+ , f _delimMeta _versionMeta+ ]+ where+ f _ [] = mempty+ f c xs = del c <> mconcat (intersperse (del _delimIdent) (map g xs))++ g (INum n) = int n+ g (IText t) = txt t+{-# INLINE toMonoid #-}