packages feed

megaparsec-utils-0.1.1.2: src/Text/Megaparsec/Utils.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

-- |
-- Module      : Text.Megaparsec.Utils
-- Description : Various generic parsers and combinators.
-- Copyright   : (c) drlkf, 2024
-- License     : GPL-3
-- Maintainer  : drlkf@drlkf.net
-- Stability   : experimental
--
-- Generic utilities and common parsers.
module Text.Megaparsec.Utils (
  -- * Scalar parsers
  boolParser,
  numParser,
  posDecNumParser,
  posNumParser,
  uuidParser,

  -- * Combinators
  commaSeparated,
  occurrence,
  occurrences,

  -- * Compatibility utilities
  boundedEnumShowParser,
  parsecToReadsPrec,
  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 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)

-- | 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"])

-- | Parse a 'Bounded' 'Enum' type that has a 'Show' instance, trying all
-- possibilities, case-insensitive, in the 'Enum' order.
boundedEnumShowParser
  :: forall a e
   . Ord e
  => Bounded a
  => 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

-- | 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)

-- | 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)

-- | Parse all occurrences of a given parser.
occurrences
  :: Ord e
  => Parsec e String a
  -> Parsec e String [a]
occurrences = some . try . occurrence . try

-- | 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)

-- | 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) 

-- | Parse an integer, without any space between minus sign and digits.
numParser
  :: Ord e
  => Num a
  => Read a
  => Parsec e String a
numParser = (char '-' >> negate <$> posNumParser) <|> posNumParser

-- | Convert a 'Parsec' parser into a 'Parser' suited for 'Data.Aeson.FromJSON'
-- instances.
parsecToJSONParser
  :: ShowErrorComponent e
  => String
  -- ^ Parser name.
  -> Parsec e String a
  -- ^ Parser.
  -> (Value -> Parser a)
parsecToJSONParser n p =
  withText n (either (fail . errorBundlePretty) pure . runParser p n . T.unpack)

-- | Convert a 'Parsec' parser into a 'ReadS' parser. Useful for defining 'Read'
-- instances with 'Text.Megaparsec'.
parsecToReadsPrec
  :: Parsec e String a
  -> ReadS a
parsecToReadsPrec p = either (const []) (\x -> [(x, "")]) . runParser p "string"

-- | Parse a RFC4122-compliant UUID.
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])))