DEV Community

Discussion on: Haskell - Enforcing Naming Convention with Parsec

Collapse
 
sshine profile image
Simon Shine

For comparison, this is how it might look with Megaparsec:

{-# LANGUAGE RecordWildCards #-}

module MigratumMegaparsec
  ( MigrationFile
  , prettyMigrationFile
  , parseMigrationFile
  ) where

import Control.Applicative (some)
import Data.Bifunctor (first)
import Data.Char (isAlphaNum)
import Data.Version (Version, makeVersion, showVersion)
import Data.Void (Void)

import Text.Megaparsec (Parsec, parse, chunk, takeWhile1P, (<?>))
import Text.Megaparsec.Char (char)
import Text.Megaparsec.Char.Lexer (decimal)
import Text.Megaparsec.Error (errorBundlePretty)

data MigrationFile = MigrationFile
  { mfVersion :: Version
  , mfName    :: FilePath
  } deriving (Eq, Show)

type Parser a = Parsec Void String a

prettyMigrationFile :: MigrationFile -> String
prettyMigrationFile MigrationFile{..} =
  "V" <> showVersion mfVersion <> "__" <> mfName <> ".sql"

parseMigrationFile :: String -> Either String MigrationFile
parseMigrationFile = first errorBundlePretty . parse migrationFile ""

-- V<version number>__<file name>.sql
migrationFile :: Parser MigrationFile
migrationFile =
  MigrationFile
    <$> (char 'V' *> version)
    <*> (chunk "__" *> filename <* chunk ".sql")

version :: Parser Version
version = fmap (makeVersion . pure) decimal <?> "version"

filename :: Parser FilePath
filename = takeWhile1P (Just "filename") isAlphaNum

And in action you see one benefit of using Megaparsec over Parsec or regex-applicative: Superior error messages.

λ> prettyMigrationFile <$> parseMigrationFile "V123__hello.sql"
Right "V123__hello.sql"

λ> let derp = either putStrLn (putStrLn . prettyMigrationFile) . parseMigrationFile

λ> derp ""
1:1:
  |
1 | <empty line>
  | ^
unexpected end of input
expecting 'V'

λ> derp "V"
1:2:
  |
1 | V
  |  ^
unexpected end of input
expecting version

λ> derp "V1"
1:3:
  |
1 | V1
  |   ^
unexpected end of input
expecting "__" or digit

λ> derp "V1_"
1:3:
  |
1 | V1_
  |   ^
unexpected '_'
expecting "__" or digit

λ> derp "V1__"
1:5:
  |
1 | V1__
  |     ^
unexpected end of input
expecting filename

λ> derp "V1__foo"
1:8:
  |
1 | V1__foo
  |        ^
unexpected end of input
expecting ".sql" or filename

λ> derp "V1__.sql"
1:5:
  |
1 | V1__.sql
  |     ^
unexpected '.'
expecting filename

The error messages could be improved significantly by simply altering the ”version” and ”filename” hints in the source code, e.g. by specifying in human-readable terms what a filename is, and what a version is. Parsec does have the <?> operator, too, but Parsec does not provide this very graphical demonstration of where parsing went wrong, it only reports the label itself, and, IIRC, the Char offset in the input String!

Another advantage of Megaparsec is native Text support; I don't find that it's particularly useful in this usecase, since FilePath is aliased to String.