packages feed

mallard-0.5.0.1: lib/Database/Mallard/Parser.hs

{-# LANGUAGE CPP               #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}

module Database.Mallard.Parser
    ( ParserException (..)
    , parseMigration
    , parseMigrations
    ) where

import           Control.Exception
import           Control.Lens               hiding (noneOf)
import           Control.Monad
import           Crypto.Hash
import           Data.HashMap.Strict        (HashMap)
import qualified Data.HashMap.Strict        as Map
import qualified Data.List.NonEmpty         as NonEmpty
import           Data.String.Interpolation
import           Data.Text                  (Text)
import qualified Data.Text                  as T
import qualified Data.Text.Encoding         as T
import           Data.Text.Lens
import           Database.Mallard.Types
import           Path
import           Text.Megaparsec            hiding (tab)
import           Text.Megaparsec.ByteString
import qualified Text.Megaparsec.Lexer      as L

type Description = Text
type Requires = MigrationId

data FieldValue
    = TextField Text
    | ListField [Text]

spaceConsumer :: Parser ()
spaceConsumer = L.space (void spaceChar) (L.skipLineComment "#") (L.skipBlockComment "##" "##")

symbol :: String -> Parser String
symbol = L.symbol spaceConsumer

brackets :: Parser a -> Parser a
brackets = between (symbol "[") (symbol "]")

comma :: Parser ()
comma = char ','  >> space

parseMigrations :: Parser [Migration]
parseMigrations = manyTill (space >> parseMigration) eof

parseMigration :: Parser Migration
parseMigration = do
    (name, description, requires) <- parseHeader
    content <- T.pack <$> manyTill anyChar (string "--/")
    return $ Migration
        { _migrationName = name
        , _migrationDescription = description
        , _migrationRequires = requires
        , _migrationChecksum = hash (T.encodeUtf8 content)
        , _migrationScript = content
        }

parseHeader :: Parser (MigrationId, Description, [Requires])
parseHeader = do
    fields <- parseHeaderFields
    case Map.lookup "name" fields of
        Nothing -> fail "The name field was not provided in the header."
        Just (ListField _) -> fail "The name field cannot be a list."
        Just (TextField name) ->
            case Map.lookup "description" fields of
                Nothing -> fail "The description field was not provided in the header."
                Just (ListField _) -> fail "The description field cannot be a list."
                Just (TextField description) ->
                    case Map.lookup "requires" fields of
                        Nothing                   -> return (MigrationId name, description, [])
                        Just (TextField requires) -> return (MigrationId name, description, [MigrationId requires])
                        Just (ListField requires) -> return (MigrationId name, description, fmap MigrationId requires)

parseFieldValue :: Parser FieldValue
parseFieldValue = parseTextValue <|> parseListValue
    where
        parseTextValue = TextField <$> parseQuotedText
        parseListValue = ListField <$> brackets (parseQuotedText `sepBy` comma)

parseHeaderFields :: Parser (HashMap Text FieldValue)
parseHeaderFields = Map.fromList <$> between (symbol "--/") (symbol "--|") (field `sepBy` newline)
    where
        field :: Parser (Text, FieldValue)
        field = do
            string "-- "
            name <- many (noneOf (" :"::String))
            space
            char ':'
            space
            value <- parseFieldValue
            return (T.pack name, value)

parseQuotedText :: Parser Text
parseQuotedText = do
    char '"'
    val <- many (noneOf ("\""::String))
    char '"'
    return $ val ^. packed

-- Exceptions

data ParserException
    = ParserException
        { _peFile  :: (Path Abs File)
        , _peError :: ParseError Char Dec
        }
    deriving (Show)

instance Exception ParserException where
    displayException e = [str|
        Unable to parse file: $:toFilePath (_peFile e)$

        $tab$Line: $:sourceLine (NonEmpty.head (errorPos (_peError e)))$
        $tab$Column: $:sourceColumn (NonEmpty.head (errorPos (_peError e)))$
        $tab$Expected: $:errorExpected (_peError e)$
        $tab$Occurred: $:errorUnexpected (_peError e)$
    |]