From e4e73e77f5a6fbc4e7f7d7cc9e9d2f723413b654 Mon Sep 17 00:00:00 2001 From: drlkf Date: Fri, 28 Aug 2026 22:39:20 +0200 Subject: [PATCH 1/4] chore: update to ghc 9.10 --- .github/workflows/stack-build.yml | 4 ++-- package.yaml | 2 +- stack.yaml | 2 +- stack.yaml.lock | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/stack-build.yml b/.github/workflows/stack-build.yml index 60055f4..9871f5f 100644 --- a/.github/workflows/stack-build.yml +++ b/.github/workflows/stack-build.yml @@ -18,8 +18,8 @@ jobs: matrix: os: [ubuntu] stack: - - lts: &default-lts 21.25 - ghc: &default-ghc 9.4.8 + - lts: &default-lts 24.56 + ghc: &default-ghc 9.10.3 - lts: 22.44 ghc: 9.6.7 diff --git a/package.yaml b/package.yaml index 1ff7045..2a4227c 100644 --- a/package.yaml +++ b/package.yaml @@ -11,9 +11,9 @@ homepage: https://github.com/haskell-github-trust/megaparsec-utils bug-reports: https://github.com/haskell-github-trust/megaparsec-utils/issues category: Parsing tested-with: + - GHC == 9.10 - GHC == 9.8 - GHC == 9.6 - - GHC == 9.4 extra-source-files: - README.md diff --git a/stack.yaml b/stack.yaml index e859074..6aee391 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,5 +1,5 @@ --- -resolver: lts-21.25 +resolver: lts-24.56 packages: - . ... diff --git a/stack.yaml.lock b/stack.yaml.lock index ffcc1f2..a7a5ca1 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -6,7 +6,7 @@ packages: [] snapshots: - completed: - sha256: a81fb3877c4f9031e1325eb3935122e608d80715dc16b586eb11ddbff8671ecd - size: 640086 - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/21/25.yaml - original: lts-21.25 + sha256: 121a2b65e6842f67819409330694d068e6276f64df87faaf2a66c0016ddf277b + size: 732456 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/56.yaml + original: lts-24.56 From cbb1e7992897d6e5a8299e88966978e8a7af7c85 Mon Sep 17 00:00:00 2001 From: drlkf Date: Fri, 28 Aug 2026 23:23:33 +0200 Subject: [PATCH 2/4] feat: add bytestring support --- package.yaml | 1 + src/Text/Megaparsec/Utils.hs | 71 ++-------- src/Text/Megaparsec/Utils/Byte.hs | 120 +++++++++++++++++ src/Text/Megaparsec/Utils/Char.hs | 111 ++++++++++++++++ src/Text/Megaparsec/Utils/Common.hs | 164 ++++++++++++++++++++++++ test/Text/Megaparsec/UtilsStreamSpec.hs | 66 ++++++++++ 6 files changed, 476 insertions(+), 57 deletions(-) create mode 100644 src/Text/Megaparsec/Utils/Byte.hs create mode 100644 src/Text/Megaparsec/Utils/Char.hs create mode 100644 src/Text/Megaparsec/Utils/Common.hs create mode 100644 test/Text/Megaparsec/UtilsStreamSpec.hs diff --git a/package.yaml b/package.yaml index 2a4227c..e1b516f 100644 --- a/package.yaml +++ b/package.yaml @@ -25,6 +25,7 @@ description: | dependencies: - base >= 4.7 && < 5 - aeson >= 2.0 && < 3 + - bytestring >= 0.11 && < 0.13 - megaparsec >= 9.0 && < 10 - parser-combinators >= 1.0 && < 2 - text >= 2.0 && < 3 diff --git a/src/Text/Megaparsec/Utils.hs b/src/Text/Megaparsec/Utils.hs index 0e2f8a5..bbb4acc 100644 --- a/src/Text/Megaparsec/Utils.hs +++ b/src/Text/Megaparsec/Utils.hs @@ -10,7 +10,8 @@ -- Maintainer : drlkf@drlkf.net -- Stability : experimental -- --- Generic utilities and common parsers. +-- String-based shims over 'Text.Megaparsec.Utils.Char'. New code should use +-- 'Text.Megaparsec.Utils.Char' or 'Text.Megaparsec.Utils.Byte' directly. module Text.Megaparsec.Utils ( -- * Scalar parsers boolParser, @@ -30,43 +31,25 @@ module Text.Megaparsec.Utils ( parsecToJSONParser, ) where -import Control.Applicative (many, some, (<|>)) -import Control.Applicative.Combinators (choice) -import Control.Monad (replicateM) -import Control.Monad.Combinators (optional) import Data.Aeson.Types (Parser, Value, withText) -import Data.Functor (($>)) -import Data.List (intercalate, sortOn) -import Data.List.NonEmpty (NonEmpty ((:|))) -import Data.Maybe (fromJust) +import Data.List.NonEmpty (NonEmpty) import qualified Data.Text as T (unpack) import Data.UUID (UUID) -import qualified Data.UUID as U (fromString) import Text.Megaparsec ( Parsec, ShowErrorComponent, - anySingle, errorBundlePretty, runParser, - try, ) -import Text.Megaparsec.Char ( - char, - digitChar, - hexDigitChar, - string', - ) -import Text.Read (readMaybe) +import qualified Text.Megaparsec.Utils.Char as Char +import qualified Text.Megaparsec.Utils.Common as Common -- | Parse a case-insensitive human-readable boolean, including C-style numbers, -- English yes-no and @on@ / @off@. boolParser :: Ord e => Parsec e String Bool -boolParser = true <|> false - where - true = True <$ choice (map string' ["true", "y", "yes", "on", "1"]) - false = False <$ choice (map string' ["false", "n", "no", "off", "0"]) +boolParser = Char.boolParser -- | Parse a 'Bounded' 'Enum' type that has a 'Show' instance, trying all -- possibilities, case-insensitive, in the 'Enum' order. @@ -77,58 +60,42 @@ boundedEnumShowParser => Enum a => Show a => Parsec e String a -boundedEnumShowParser = - choice . map parseShow $ sortOn (negate . length . show) [(minBound :: a) ..] - where - parseShow a = string' (show a) $> a +boundedEnumShowParser = Char.boundedEnumShowParser -- | Parse a comma-separated list of items. commaSeparated :: Ord e => Parsec e String a -> Parsec e String (NonEmpty a) -commaSeparated p = (:|) <$> p <*> many (char ',' >> p) +commaSeparated = Char.commaSeparated -- | Parse any occurrence of a given parser. Consumes any input before occurrence. occurrence :: Ord e => Parsec e String a -> Parsec e String a -occurrence p = go - where - go = p <|> (anySingle >> go) +occurrence = Common.occurrence -- | Parse all occurrences of a given parser. occurrences :: Ord e => Parsec e String a -> Parsec e String [a] -occurrences = some . try . occurrence . try +occurrences = Common.occurrences -- | Parse a positive number, with or without decimals prefixed by a @.@. posDecNumParser :: Ord e => Read a => Parsec e String a -posDecNumParser = do - num <- some digitChar - dec <- maybe "" ("." <>) <$> optional (char '.' >> some digitChar) - - let str = num <> dec - - maybe (fail ("could not read from input: " <> str)) pure (readMaybe str) +posDecNumParser = Char.posDecNumParser -- | Parse a positive integer. posNumParser :: Ord e => Read a => Parsec e String a -posNumParser = do - digits <- some digitChar - maybe - (fail ("could not read from digits: " <> digits)) - pure - (readMaybe digits) +posNumParser = Char.posNumParser -- | Parse an integer, without any space between minus sign and digits. numParser @@ -136,7 +103,7 @@ numParser => Num a => Read a => Parsec e String a -numParser = (char '-' >> negate <$> posNumParser) <|> posNumParser +numParser = Char.numParser -- | Convert a 'Parsec' parser into a 'Parser' suited for 'Data.Aeson.FromJSON' -- instances. @@ -161,14 +128,4 @@ parsecToReadsPrec p = either (const []) (\x -> [(x, "")]) . runParser p "string" uuidParser :: Ord e => Parsec e String UUID -uuidParser = do - part1 <- replicateM 8 hexDigitChar <* char '-' - part2 <- replicateM 4 hexDigitChar <* char '-' - part3 <- replicateM 4 hexDigitChar <* char '-' - part4 <- replicateM 4 hexDigitChar <* char '-' - part5 <- replicateM 12 hexDigitChar - - pure - (fromJust - (U.fromString - (intercalate "-" [part1, part2, part3, part4, part5]))) +uuidParser = Char.uuidParser diff --git a/src/Text/Megaparsec/Utils/Byte.hs b/src/Text/Megaparsec/Utils/Byte.hs new file mode 100644 index 0000000..7333ef1 --- /dev/null +++ b/src/Text/Megaparsec/Utils/Byte.hs @@ -0,0 +1,120 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +-- | +-- Module : Text.Megaparsec.Utils.Byte +-- Description : Parsers for streams of 'Word8'. +-- Copyright : (c) drlkf, 2026 +-- License : GPL-3 +-- Maintainer : drlkf@drlkf.net +-- Stability : experimental +-- +-- Generic parsers for streams of 'Word8' (strict and lazy 'ByteString'). +module Text.Megaparsec.Utils.Byte ( + -- * Scalar parsers + boolParser, + numParser, + posDecNumParser, + posNumParser, + uuidParser, + + -- * Combinators + commaSeparated, + + -- * Compatibility utilities + boundedEnumShowParser, +) where + +import Data.List.NonEmpty (NonEmpty) +import Data.UUID (UUID) +import Data.Word (Word8) +import Text.Megaparsec (MonadParsec, Token) +import Text.Megaparsec.Byte (char, char', digitChar, hexDigitChar) +import Text.Megaparsec.Utils.Common ( + mkBoolParser, + mkBoundedEnumShowParser, + mkCommaSeparated, + mkNumParser, + mkPosDecNumParser, + mkPosNumParser, + mkUuidParser, + ) + +-- | Parse a case-insensitive human-readable boolean, including C-style numbers, +-- English yes-no and @on@ / @off@. +-- +-- @since UNRELEASED@ +boolParser + :: MonadParsec e s m + => Token s ~ Word8 + => m Bool +boolParser = mkBoolParser char' (toEnum . fromEnum) + +-- | Parse a 'Bounded' 'Enum' type that has a 'Show' instance, trying all +-- possibilities, case-insensitive, in the 'Enum' order. +-- +-- @since UNRELEASED@ +boundedEnumShowParser + :: forall a e s m + . MonadParsec e s m + => Token s ~ Word8 + => Bounded a + => Enum a + => Show a + => m a +boundedEnumShowParser = mkBoundedEnumShowParser char' (toEnum . fromEnum) + +-- | Parse a comma-separated list of items. +-- +-- @since UNRELEASED@ +commaSeparated + :: MonadParsec e s m + => Token s ~ Word8 + => m a + -> m (NonEmpty a) +commaSeparated = mkCommaSeparated (char 44) + +-- | Parse a positive number, with or without decimals prefixed by a @.@. +-- +-- @since UNRELEASED@ +posDecNumParser + :: MonadFail m + => MonadParsec e s m + => Read a + => Token s ~ Word8 + => m a +posDecNumParser = mkPosDecNumParser digitChar (char 46) (toEnum . fromIntegral) + +-- | Parse a positive integer. +-- +-- @since UNRELEASED@ +posNumParser + :: MonadFail m + => MonadParsec e s m + => Read a + => Token s ~ Word8 + => m a +posNumParser = mkPosNumParser digitChar (toEnum . fromIntegral) + +-- | Parse an integer, without any space between minus sign and digits. +-- +-- @since UNRELEASED@ +numParser + :: MonadFail m + => MonadParsec e s m + => Num a + => Read a + => Token s ~ Word8 + => m a +numParser = mkNumParser (char 45) posNumParser + +-- | Parse a RFC4122-compliant UUID. +-- +-- @since UNRELEASED@ +uuidParser + :: MonadParsec e s m + => Token s ~ Word8 + => m UUID +uuidParser = mkUuidParser hexDigitChar (char 45) (toEnum . fromIntegral) diff --git a/src/Text/Megaparsec/Utils/Char.hs b/src/Text/Megaparsec/Utils/Char.hs new file mode 100644 index 0000000..84a08ce --- /dev/null +++ b/src/Text/Megaparsec/Utils/Char.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +-- | +-- Module : Text.Megaparsec.Utils.Char +-- Description : Parsers for streams of 'Char'. +-- Copyright : (c) drlkf, 2024 +-- License : GPL-3 +-- Maintainer : drlkf@drlkf.net +-- Stability : experimental +-- +-- Generic parsers for streams of 'Char' ('String', 'Text', lazy 'Text'). +module Text.Megaparsec.Utils.Char ( + -- * Scalar parsers + boolParser, + numParser, + posDecNumParser, + posNumParser, + uuidParser, + + -- * Combinators + commaSeparated, + + -- * Compatibility utilities + boundedEnumShowParser, + parsecToJSONParser, +) where + +import Data.Aeson.Types (Parser, Value, withText) +import Data.List.NonEmpty (NonEmpty) +import Data.Text (Text) +import Data.UUID (UUID) +import Text.Megaparsec ( + MonadParsec, + Parsec, + ShowErrorComponent, + Token, + errorBundlePretty, + runParser, + ) +import Text.Megaparsec.Char (char, char', digitChar, hexDigitChar) +import Text.Megaparsec.Utils.Common ( + mkBoolParser, + mkBoundedEnumShowParser, + mkCommaSeparated, + mkNumParser, + mkPosDecNumParser, + mkPosNumParser, + mkUuidParser, + ) + +-- | Parse a case-insensitive human-readable boolean, including C-style numbers, +-- English yes-no and @on@ / @off@. +boolParser :: (MonadParsec e s m, Token s ~ Char) => m Bool +boolParser = mkBoolParser char' id + +-- | Parse a 'Bounded' 'Enum' type that has a 'Show' instance, trying all +-- possibilities, case-insensitive, in the 'Enum' order. +boundedEnumShowParser + :: forall a e s m + . (MonadParsec e s m, Token s ~ Char) + => Bounded a + => Enum a + => Show a + => m a +boundedEnumShowParser = mkBoundedEnumShowParser char' id + +-- | Parse a comma-separated list of items. +commaSeparated + :: (MonadParsec e s m, Token s ~ Char) + => m a + -> m (NonEmpty a) +commaSeparated = mkCommaSeparated (char ',') + +-- | Parse a positive number, with or without decimals prefixed by a @.@. +posDecNumParser + :: (MonadFail m, MonadParsec e s m, Read a, Token s ~ Char) + => m a +posDecNumParser = mkPosDecNumParser digitChar (char '.') id + +-- | Parse a positive integer. +posNumParser + :: (MonadFail m, MonadParsec e s m, Read a, Token s ~ Char) + => m a +posNumParser = mkPosNumParser digitChar id + +-- | Parse an integer, without any space between minus sign and digits. +numParser + :: (MonadFail m, MonadParsec e s m, Num a, Read a, Token s ~ Char) + => m a +numParser = mkNumParser (char '-') posNumParser + +-- | Convert a 'Parsec' parser on 'Text' into a 'Parser' suited for +-- 'Data.Aeson.FromJSON' instances. +parsecToJSONParser + :: ShowErrorComponent e + => String + -- ^ Parser name. + -> Parsec e Text a + -- ^ Parser. + -> (Value -> Parser a) +parsecToJSONParser n p = + withText n (either (fail . errorBundlePretty) pure . runParser p n) + +-- | Parse a RFC4122-compliant UUID. +uuidParser + :: (MonadParsec e s m, Token s ~ Char) + => m UUID +uuidParser = mkUuidParser hexDigitChar (char '-') id \ No newline at end of file diff --git a/src/Text/Megaparsec/Utils/Common.hs b/src/Text/Megaparsec/Utils/Common.hs new file mode 100644 index 0000000..b752ae3 --- /dev/null +++ b/src/Text/Megaparsec/Utils/Common.hs @@ -0,0 +1,164 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} + +-- | +-- Module : Text.Megaparsec.Utils.Common +-- Description : Stream-agnostic combinators. +-- Copyright : (c) drlkf, 2024 +-- License : GPL-3 +-- Maintainer : drlkf@drlkf.net +-- Stability : experimental +-- +-- Combinators that work on any stream. +module Text.Megaparsec.Utils.Common ( + occurrence, + occurrences, + + -- * Shared parser builders + mkBoolParser, + mkBoundedEnumShowParser, + mkCommaSeparated, + mkNumParser, + mkPosDecNumParser, + mkPosNumParser, + mkUuidParser, +) where + +import Control.Applicative (many, some, (<|>)) +import Control.Applicative.Combinators (choice) +import Control.Monad (replicateM) +import Control.Monad.Combinators (optional) +import Data.Foldable (traverse_) +import Data.Functor (($>)) +import Data.List (intercalate, sortOn) +import Data.List.NonEmpty (NonEmpty ((:|))) +import Data.Maybe (fromJust) +import Data.UUID (UUID) +import qualified Data.UUID as U (fromString) +import Text.Megaparsec (MonadParsec, Token, anySingle, try) +import Text.Read (readMaybe) + +-- | Parse any occurrence of a given parser. Consumes any input before occurrence. +occurrence :: MonadParsec e s m => m a -> m a +occurrence p = go + where + go = p <|> (anySingle >> go) + +-- | Parse all occurrences of a given parser. +occurrences :: MonadParsec e s m => m a -> m [a] +occurrences = some . try . occurrence . try + +-- | Build a case-insensitive human-readable boolean parser, including C-style +-- numbers, English yes-no and @on@ / @off@. +mkBoolParser + :: MonadParsec e s m + => (Token s -> m (Token s)) + -- ^ Case-insensitive single-token parser. + -> (Char -> Token s) + -- ^ Convert a literal 'Char' to a token. + -> m Bool +mkBoolParser char' fromChar = true <|> false + where + true = True <$ choice (map (try . traverse_ (char' . fromChar)) ["true", "y", "yes", "on", "1"]) + false = False <$ choice (map (try . traverse_ (char' . fromChar)) ["false", "n", "no", "off", "0"]) + +-- | Build a parser for a 'Bounded' 'Enum' type that has a 'Show' instance, +-- trying all possibilities, case-insensitive, in the 'Enum' order. +mkBoundedEnumShowParser + :: forall a e s m + . (MonadParsec e s m) + => Bounded a + => Enum a + => Show a + => (Token s -> m (Token s)) + -- ^ Case-insensitive single-token parser. + -> (Char -> Token s) + -- ^ Convert a literal 'Char' to a token. + -> m a +mkBoundedEnumShowParser char' fromChar = + choice . map parseShow $ sortOn (negate . length . show) [(minBound :: a) ..] + where + parseShow a = try (traverse_ (char' . fromChar) (show a)) $> a + +-- | Build a parser for a comma-separated list of items. +mkCommaSeparated + :: MonadParsec e s m + => m (Token s) + -- ^ Comma parser. + -> m a + -> m (NonEmpty a) +mkCommaSeparated comma p = (:|) <$> p <*> many (comma >> p) + +-- | Build a parser for a positive number, with or without decimals prefixed by +-- a @.@. +mkPosDecNumParser + :: (MonadFail m, MonadParsec e s m, Read a) + => m (Token s) + -- ^ Digit parser. + -> m (Token s) + -- ^ Decimal-point parser. + -> (Token s -> Char) + -- ^ Convert a token to a 'Char'. + -> m a +mkPosDecNumParser digitChar dot toChar = do + num <- some digitChar + dec <- optional (dot >> some digitChar) + + let toStr = map toChar + str = toStr num <> maybe "" (('.' :) . toStr) dec + + maybe (fail ("could not read from input: " <> str)) pure (readMaybe str) + +-- | Build a parser for a positive integer. +mkPosNumParser + :: (MonadFail m, MonadParsec e s m, Read a) + => m (Token s) + -- ^ Digit parser. + -> (Token s -> Char) + -- ^ Convert a token to a 'Char'. + -> m a +mkPosNumParser digitChar toChar = do + digits <- some digitChar + let str = map toChar digits + maybe + (fail ("could not read from digits: " <> str)) + pure + (readMaybe str) + +-- | Build a parser for an integer, without any space between minus sign and +-- digits. +mkNumParser + :: (MonadFail m, MonadParsec e s m, Num a, Read a) + => m (Token s) + -- ^ Minus-sign parser. + -> m a + -- ^ Positive-number parser. + -> m a +mkNumParser minus posNumParser = (minus >> negate <$> posNumParser) <|> posNumParser + +-- | Build a parser for a RFC4122-compliant UUID. +mkUuidParser + :: MonadParsec e s m + => m (Token s) + -- ^ Hex-digit parser. + -> m (Token s) + -- ^ Dash parser. + -> (Token s -> Char) + -- ^ Convert a token to a 'Char'. + -> m UUID +mkUuidParser hexDigitChar dash toChar = do + part1 <- replicateM 8 hexDigitChar <* dash + part2 <- replicateM 4 hexDigitChar <* dash + part3 <- replicateM 4 hexDigitChar <* dash + part4 <- replicateM 4 hexDigitChar <* dash + part5 <- replicateM 12 hexDigitChar + + let toStr = map toChar + + pure + ( fromJust + ( U.fromString + (intercalate "-" (map toStr [part1, part2, part3, part4, part5])) + ) + ) \ No newline at end of file diff --git a/test/Text/Megaparsec/UtilsStreamSpec.hs b/test/Text/Megaparsec/UtilsStreamSpec.hs new file mode 100644 index 0000000..1041a92 --- /dev/null +++ b/test/Text/Megaparsec/UtilsStreamSpec.hs @@ -0,0 +1,66 @@ +{-# LANGUAGE TypeApplications #-} + +module Text.Megaparsec.UtilsStreamSpec ( + spec, +) where + +import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as B +import Data.Text (Text) +import qualified Data.Text as T +import Data.Void (Void) +import Test.Hspec (Spec, describe, it, shouldBe) +import Test.QuickCheck (property) +import Text.Megaparsec (ParseErrorBundle, Parsec, runParser) +import qualified Text.Megaparsec.Utils.Byte as Byte +import qualified Text.Megaparsec.Utils.Char as Char +import Text.Megaparsec.Utils.Common (occurrence) + +parseText :: Parsec Void Text a -> Text -> Either (ParseErrorBundle Text Void) a +parseText p = runParser p "test" + +parseBytes + :: Parsec Void ByteString a + -> ByteString + -> Either (ParseErrorBundle ByteString Void) a +parseBytes p = runParser p "test" + +charNumParser :: Parsec Void Text Int +charNumParser = Char.numParser + +byteNumParser :: Parsec Void ByteString Int +byteNumParser = Byte.numParser + +spec :: Spec +spec = do + describe "Char parsers on Text" $ do + it "numParser" . property $ \v -> + parseText charNumParser (T.pack (show (v :: Int))) `shouldBe` Right v + + it "boolParser" $ do + parseText Char.boolParser (T.pack "yes") `shouldBe` Right True + parseText Char.boolParser (T.pack "OFF") `shouldBe` Right False + + it "uuidParser" $ do + parseText Char.uuidParser (T.pack "123e4567-e89b-12d3-a456-426614174000") + `shouldBe` Right (read "123e4567-e89b-12d3-a456-426614174000") + + it "occurrence" . property $ \v -> + parseText (occurrence charNumParser) (T.pack ("abc " <> show (v :: Int))) + `shouldBe` Right v + + describe "Byte parsers on ByteString" $ do + it "numParser" . property $ \v -> + parseBytes byteNumParser (B.pack (show (v :: Int))) `shouldBe` Right v + + it "boolParser" $ do + parseBytes Byte.boolParser (B.pack "yes") `shouldBe` Right True + parseBytes Byte.boolParser (B.pack "OFF") `shouldBe` Right False + + it "uuidParser" $ do + parseBytes Byte.uuidParser (B.pack "123e4567-e89b-12d3-a456-426614174000") + `shouldBe` Right (read "123e4567-e89b-12d3-a456-426614174000") + + it "occurrence" . property $ \v -> + parseBytes (occurrence byteNumParser) (B.pack ("abc " <> show (v :: Int))) + `shouldBe` Right v From c513180a77d4f6a3f9ab8416f59248458ecf3e6d Mon Sep 17 00:00:00 2001 From: drlkf Date: Sun, 25 Oct 2026 17:51:29 +0100 Subject: [PATCH 3/4] ci: use haddock-since --- .github/workflows/semantic-release.yml | 4 +- .releaserc.mjs | 64 ++++++++++++++------------ src/Text/Megaparsec/Utils/Byte.hs | 14 ------ 3 files changed, 37 insertions(+), 45 deletions(-) diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml index e65a25c..6093625 100644 --- a/.github/workflows/semantic-release.yml +++ b/.github/workflows/semantic-release.yml @@ -32,7 +32,9 @@ jobs: ci: ${{ github.ref == github.event.repository.default_branch }} extra_plugins: | conventional-changelog-conventionalcommits - semantic-release-mirror-version + @semantic-release/changelog + @semantic-release/git + github:haskell-github-trust/semantic-release-haddock-since semantic-release-stack-upload - name: Semantic release output diff --git a/.releaserc.mjs b/.releaserc.mjs index a878c1f..5701a50 100644 --- a/.releaserc.mjs +++ b/.releaserc.mjs @@ -2,34 +2,38 @@ * @type {import('semantic-release').GlobalConfig} */ export default { - branches: ["main"], - tagFormat: "${version}", - plugins: [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { "type": "docs", "release": "patch" } - ] - } - ], - "@semantic-release/release-notes-generator", - [ - "semantic-release-mirror-version", - { - "fileGlob": "@(package.yaml|megaparsec-utils.cabal)", - "placeholderRegExp": "0.0.0-dev" - } - ], - [ - "@semantic-release/changelog", - { - changelogFile: "CHANGELOG.md", - changelogTitle: "Changelog" - } - ], - "@semantic-release/github", - "semantic-release-stack-upload", - ] + branches: ["main"], + tagFormat: "${version}", + plugins: [ + [ + "@semantic-release/commit-analyzer", + { + preset: "conventionalcommits", + releaseRules: [{ type: "docs", release: "patch" }], + }, + ], + "@semantic-release/release-notes-generator", + [ + "semantic-release-haddock-since", + { + versionFiles: ["package.yaml"], + baseline: "api/megaparsec-utils.api", + }, + ], + [ + "@semantic-release/changelog", + { + changelogFile: "CHANGELOG.md", + changelogTitle: "Changelog", + }, + ], + [ + "@semantic-release/git", + { + assets: ["package.yaml", "CHANGELOG.md", "api/*.api", "src/**/*.hs"], + }, + ], + "@semantic-release/github", + "semantic-release-stack-upload", + ], } diff --git a/src/Text/Megaparsec/Utils/Byte.hs b/src/Text/Megaparsec/Utils/Byte.hs index 7333ef1..db86b3b 100644 --- a/src/Text/Megaparsec/Utils/Byte.hs +++ b/src/Text/Megaparsec/Utils/Byte.hs @@ -44,8 +44,6 @@ import Text.Megaparsec.Utils.Common ( -- | Parse a case-insensitive human-readable boolean, including C-style numbers, -- English yes-no and @on@ / @off@. --- --- @since UNRELEASED@ boolParser :: MonadParsec e s m => Token s ~ Word8 @@ -54,8 +52,6 @@ boolParser = mkBoolParser char' (toEnum . fromEnum) -- | Parse a 'Bounded' 'Enum' type that has a 'Show' instance, trying all -- possibilities, case-insensitive, in the 'Enum' order. --- --- @since UNRELEASED@ boundedEnumShowParser :: forall a e s m . MonadParsec e s m @@ -67,8 +63,6 @@ boundedEnumShowParser boundedEnumShowParser = mkBoundedEnumShowParser char' (toEnum . fromEnum) -- | Parse a comma-separated list of items. --- --- @since UNRELEASED@ commaSeparated :: MonadParsec e s m => Token s ~ Word8 @@ -77,8 +71,6 @@ commaSeparated commaSeparated = mkCommaSeparated (char 44) -- | Parse a positive number, with or without decimals prefixed by a @.@. --- --- @since UNRELEASED@ posDecNumParser :: MonadFail m => MonadParsec e s m @@ -88,8 +80,6 @@ posDecNumParser posDecNumParser = mkPosDecNumParser digitChar (char 46) (toEnum . fromIntegral) -- | Parse a positive integer. --- --- @since UNRELEASED@ posNumParser :: MonadFail m => MonadParsec e s m @@ -99,8 +89,6 @@ posNumParser posNumParser = mkPosNumParser digitChar (toEnum . fromIntegral) -- | Parse an integer, without any space between minus sign and digits. --- --- @since UNRELEASED@ numParser :: MonadFail m => MonadParsec e s m @@ -111,8 +99,6 @@ numParser numParser = mkNumParser (char 45) posNumParser -- | Parse a RFC4122-compliant UUID. --- --- @since UNRELEASED@ uuidParser :: MonadParsec e s m => Token s ~ Word8 From 0e272e0edb8b05ca6382a4f9b9f72d6cf8ffa716 Mon Sep 17 00:00:00 2001 From: drlkf Date: Sun, 25 Oct 2026 18:25:12 +0100 Subject: [PATCH 4/4] ci: use `freckle/stack-action` --- .github/workflows/stack-build.yml | 91 ++++++----------------------- src/Text/Megaparsec/Utils/Common.hs | 2 +- stack-lts-22.yaml | 5 ++ stack-lts-22.yaml.lock | 12 ++++ stack-lts-23.yaml | 5 ++ stack-lts-23.yaml.lock | 12 ++++ 6 files changed, 54 insertions(+), 73 deletions(-) create mode 100644 stack-lts-22.yaml create mode 100644 stack-lts-22.yaml.lock create mode 100644 stack-lts-23.yaml create mode 100644 stack-lts-23.yaml.lock diff --git a/.github/workflows/stack-build.yml b/.github/workflows/stack-build.yml index 9871f5f..2263b95 100644 --- a/.github/workflows/stack-build.yml +++ b/.github/workflows/stack-build.yml @@ -10,84 +10,31 @@ permissions: contents: read jobs: + generate: + name: Generate matrix + runs-on: ubuntu-latest + outputs: + stack-yamls: ${{ steps.generate.outputs.stack-yamls }} + steps: + - uses: actions/checkout@v4 + - id: generate + uses: freckle/stack-action/generate-matrix@v5 + build: - name: lts-${{ matrix.stack.lts }} / ghc-${{ matrix.stack.ghc }} / ${{ matrix.os }} - runs-on: ${{ matrix.os }}-latest + name: ${{ matrix.stack-yaml }} / ${{ matrix.os }} + needs: generate + runs-on: ${{ matrix.os == 'macos' && 'macos-15' || format('{0}-latest', matrix.os) }} strategy: fail-fast: false matrix: - os: [ubuntu] - stack: - - lts: &default-lts 24.56 - ghc: &default-ghc 9.10.3 - - - lts: 22.44 - ghc: 9.6.7 - - - lts: 23.28 - ghc: 9.8.4 - - include: - - os: windows - stack: - lts: *default-lts - ghc: *default-ghc - - - os: macos - stack: - lts: *default-lts - ghc: *default-ghc - + os: [ubuntu, macos, windows] + stack-yaml: ${{ fromJSON(needs.generate.outputs.stack-yamls) }} + env: + STACK_YAML: ${{ matrix.stack-yaml }} steps: - - uses: actions/checkout@v3 - - - name: Set up Stack LTS ${{ matrix.stack-lts }} - uses: haskell-actions/setup@v2 - id: setup - with: - ghc-version: ${{ matrix.stack.ghc }} - enable-stack: true - stack-setup-ghc: true - stack-no-global: true - - - name: Configure the build - run: | - echo "with key: ${{ runner.os }}-lts-${{ matrix.stack.lts }}" - echo "stack root: ${{ steps.setup.outputs.stack-root }}" - - stack config set resolver lts-${{ matrix.stack.lts }} - stack build --dry-run - # The last step generates dist-newstyle/cache/plan.json for the cache key. - - - name: Restore cached dependencies - uses: actions/cache/restore@v3 - id: cache - env: - key: ${{ runner.os }}-lts-${{ matrix.stack.lts }} - - with: - path: ${{ steps.setup.outputs.stack-root }} - key: ${{ env.key }}-plan-${{ hashFiles('dist-newstyle/cache/plan.json') }} - restore-keys: ${{ env.key }}- - - - name: Install dependencies - run: stack build --only-dependencies - - # Cache dependencies already here, so that we do not have to rebuild them - # should the subsequent steps fail. - - name: Save cached dependencies - uses: actions/cache/save@v3 - # Caches are immutable, trying to save with the same key would error. - if: ${{ steps.cache.outputs.cache-primary-key != steps.cache.outputs.cache-matched-key }} - with: - path: ${{ steps.setup.outputs.stack-root }} - key: ${{ steps.cache.outputs.cache-primary-key }} - - - name: Build - run: stack build + - uses: actions/checkout@v4 - - name: Run tests - run: stack test + - uses: freckle/stack-action@v5 - name: Check cabal file run: stack exec -- cabal check diff --git a/src/Text/Megaparsec/Utils/Common.hs b/src/Text/Megaparsec/Utils/Common.hs index b752ae3..58b0108 100644 --- a/src/Text/Megaparsec/Utils/Common.hs +++ b/src/Text/Megaparsec/Utils/Common.hs @@ -129,7 +129,7 @@ mkPosNumParser digitChar toChar = do -- | Build a parser for an integer, without any space between minus sign and -- digits. mkNumParser - :: (MonadFail m, MonadParsec e s m, Num a, Read a) + :: (MonadFail m, MonadParsec e s m, Num a) => m (Token s) -- ^ Minus-sign parser. -> m a diff --git a/stack-lts-22.yaml b/stack-lts-22.yaml new file mode 100644 index 0000000..909e8a7 --- /dev/null +++ b/stack-lts-22.yaml @@ -0,0 +1,5 @@ +--- +resolver: lts-22.44 +packages: + - . +... diff --git a/stack-lts-22.yaml.lock b/stack-lts-22.yaml.lock new file mode 100644 index 0000000..8d134eb --- /dev/null +++ b/stack-lts-22.yaml.lock @@ -0,0 +1,12 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/topics/lock_files + +packages: [] +snapshots: +- completed: + sha256: 238fa745b64f91184f9aa518fe04bdde6552533d169b0da5256670df83a0f1a9 + size: 721141 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/44.yaml + original: lts-22.44 diff --git a/stack-lts-23.yaml b/stack-lts-23.yaml new file mode 100644 index 0000000..19b4f08 --- /dev/null +++ b/stack-lts-23.yaml @@ -0,0 +1,5 @@ +--- +resolver: lts-23.28 +packages: + - . +... diff --git a/stack-lts-23.yaml.lock b/stack-lts-23.yaml.lock new file mode 100644 index 0000000..a5db08c --- /dev/null +++ b/stack-lts-23.yaml.lock @@ -0,0 +1,12 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/topics/lock_files + +packages: [] +snapshots: +- completed: + sha256: 7e724f347d5969cb5e8dde9f9aae30996e3231c29d1dafd45f21f1700d4c4fcb + size: 684460 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/23/28.yaml + original: lts-23.28