persistent-audit-0.1.0.2: src/Database/Persist/Audit/Parser.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Database.Persist.Audit.Parser where
import Control.Applicative
import Data.Attoparsec.ByteString.Char8 (isSpace)
import Data.Attoparsec.Combinator
import Data.Attoparsec.Text
import Data.List (delete,nub)
import Data.Maybe
import Data.Monoid ((<>))
import Data.Text (Text)
import qualified Data.Text as T
import Database.Persist.Audit.Types
import Database.Persist.Audit.Parser.Types
import Prelude hiding (takeWhile)
import Text.Read (readMaybe)
-- handling indented text appropriately
parseQuasiQuoterFile :: Text -> Either String PersistModelFile
parseQuasiQuoterFile = parseOnly parseEntities
parseModelsFile :: Text -> Either String PersistModelFile
parseModelsFile = parseOnly parseEntities
-- | Parse Persist Models that are in quasi-quoters. The source could be a haskell file.
parsePersistQuasiQuoters :: Parser PersistModelFile
parsePersistQuasiQuoters = do
_ <- manyTill' anyChar (string "[persistLowerCase|" <|> string "[persistUpperCase|")
manyTill' ( PersistModelFileEntity <$> parseEntity
<|> PersistModelFileWhiteSpace <$> collectWhiteSpace
<|> PersistModelFileComment <$> singleLineComment) (string "|]")
-- | Parse a Persist Model file.
parseEntities :: Parser PersistModelFile
parseEntities = do
many' ( PersistModelFileEntity <$> parseEntity
<|> PersistModelFileWhiteSpace <$> collectWhiteSpace
<|> PersistModelFileComment <$> singleLineComment)
-- | Parse a single Persist Entity
parseEntity :: Parser Entity
parseEntity = do
entityName <- haskellTypeNameWithoutPrefix
_ <- many' spaceNoNewLine
derivesJson <- (string "json" *> pure True) <|> pure False
_ <- many' spaceNoNewLine
mSqlTable <- (Just <$> parseEntitySqlTable) <|> pure Nothing
_ <- takeTill isEndOfLine
endOfLine <|> endOfInput
entityChildren <- many' ( EntityChildEntityField <$> parseEntityField
<|> EntityChildEntityDerive <$> parseEntityDerive
<|> EntityChildEntityPrimary <$> parseEntityPrimary
<|> EntityChildEntityForeign <$> parseEntityForeign
<|> EntityChildEntityUnique <$> parseEntityUnique
<|> EntityChildWhiteSpace <$> collectWhiteSpace
<|> EntityChildComment <$> singleLineComment)
return $ Entity entityName derivesJson mSqlTable entityChildren
parseEntitySqlTable :: Parser Text
parseEntitySqlTable = do
_ <- string "sql"
_ <- many' spaceNoNewLine
_ <- char '='
_ <- many' spaceNoNewLine
-- take while not space
text <- many' (digit <|> letter <|> underline)
return $ T.pack text
-- helper functions
-- | Wrap a Parser in 'Maybe' because it might fail. Useful for making choices.
maybeOption :: Parser a -> Parser (Maybe a)
maybeOption p = option Nothing (Just <$> p)
-- | Parse a lowercase 'Char'.
lowerCase :: Parser Char
lowerCase = satisfy (\c -> c >= 'a' && c <= 'z')
-- | Parse an uppercase 'Char'.
upperCase :: Parser Char
upperCase = satisfy (\c -> c >= 'A' && c <= 'Z')
-- | Parse an underline.
underline :: Parser Char
underline = satisfy (== '_')
-- | Parse strict marker "!" for haskellTypeName.
exclamationMark :: Parser Char
exclamationMark = satisfy (== '!')
-- | Parse lazy marker "~" for haskellTypeName.
tilde :: Parser Char
tilde = satisfy (== '~')
-- | Parse any space 'Char' excluding "\n".
spaceNoNewLine :: Parser Char
spaceNoNewLine = satisfy (\x -> isSpace x && not (isEndOfLine x)) <?> "spaceNoNewLine"
-- | Parse a Haskell function name. It starts with underscore or lowercase letter then
-- is followed by a combination of underscores, single quotes, letters and digits.
-- E.g., "get", "_get", "get_1", etc.
haskellFunctionName :: Parser Text
haskellFunctionName = do
first <- lowerCase <|> underline
rest <- many' (digit <|> letter <|> underline)
lookAhead ((space *> pure ()) <|> (char ']' *> pure ()) <|> endOfInput)
return $ T.pack ([first] ++ rest)
-- | Parse a Haskell type name. It starts with an uppercase letter then
-- is followed by a combination of underscores, single quotes, letters and digits.
-- E.g., "Person", "Address", "PhoneNumber", etc.
haskellTypeName :: Parser Text
haskellTypeName = do
_ <- (Just <$> exclamationMark) <|> (Just <$> tilde) <|> pure Nothing
haskellTypeNameWithoutPrefix
haskellTypeNameWithoutPrefix :: Parser Text
haskellTypeNameWithoutPrefix = do
first <- upperCase
rest <- many' (digit <|> letter <|> underline)
-- check for ']' because it could be in a list
lookAhead ((space *> pure ()) <|> (char ']' *> pure ()) <|> endOfInput)
return $ T.pack ([first] ++ rest)
-- | Parse a comment that starts with "--".
singleLineComment :: Parser Comment
singleLineComment = do
_ <- string "--"
comment <- takeTill isEndOfLine
endOfLine
return $ Comment ("--" <> comment <> "\n")
collectWhiteSpace :: Parser WhiteSpace
collectWhiteSpace = do
whiteSpace <- takeWhile (\x -> isSpace x && not (isEndOfLine x))
endOfLine -- <|> endOfInput
return $ WhiteSpace (whiteSpace <> "\n")
-- EntityName
parseEntityName :: Parser Text
parseEntityName = do
name <- haskellTypeName
_ <- takeTill isEndOfLine
endOfLine <|> endOfInput
return name
-- EntityField
parseEntityField :: Parser EntityField
parseEntityField = do
efn <- parseEntityFieldName
eft <- parseEntityFieldType
ms <- parseMigrationOnlyAndSafeToRemove [] <|> pure []
rs <- parseEntityFieldLastItem [] <|> pure []
_ <- takeTill isEndOfLine
endOfLine <|> endOfInput
return $ EntityField efn
eft
(elem MigrationOnly ms)
(elem SafeToRemove ms)
(getFieldDefault rs)
(getFieldSqlRow rs)
(getFieldSqlType rs)
(getFieldMaxLen rs)
deleteItems :: (Eq a) => [a] -> [a] -> [a]
deleteItems (x:xs) ys = deleteItems xs $ delete x ys
deleteItems _ ys = nub ys
parseMigrationOnly :: Parser MigrationOnlyAndSafeToRemoveOption
parseMigrationOnly = string "MigrationOnly" *> pure MigrationOnly
parseSafeToRemove :: Parser MigrationOnlyAndSafeToRemoveOption
parseSafeToRemove = string "SafeToRemove" *> pure SafeToRemove
getMigrationOnlyAndSafeToRemoveOption :: MigrationOnlyAndSafeToRemoveOption -> Parser MigrationOnlyAndSafeToRemoveOption
getMigrationOnlyAndSafeToRemoveOption MigrationOnly = parseMigrationOnly
getMigrationOnlyAndSafeToRemoveOption SafeToRemove = parseSafeToRemove
parseMigrationOnlyAndSafeToRemove :: [MigrationOnlyAndSafeToRemoveOption] -> Parser [MigrationOnlyAndSafeToRemoveOption]
parseMigrationOnlyAndSafeToRemove parserOps = do
_ <- many1 spaceNoNewLine
-- let parsers = [MigrationOnly,SafeToRemove] \\ parserOps
let parsers = deleteItems parserOps [MigrationOnly,SafeToRemove]
mResult <- (Just <$> choice (map getMigrationOnlyAndSafeToRemoveOption parsers)) <|> pure Nothing
case mResult of
Nothing -> return parserOps
Just result -> parseMigrationOnlyAndSafeToRemove (parserOps ++ [result]) <|> pure (parserOps ++ [result])
getFieldDefault :: [EntityFieldLastItem] -> Maybe Text
getFieldDefault (x:xs) =
case x of
(FieldDefault y) -> Just y
_ -> getFieldDefault xs
getFieldDefault _ = Nothing
getFieldSqlRow :: [EntityFieldLastItem] -> Maybe Text
getFieldSqlRow (x:xs) =
case x of
(FieldSqlRow y) -> Just y
_ -> getFieldSqlRow xs
getFieldSqlRow _ = Nothing
getFieldSqlType :: [EntityFieldLastItem] -> Maybe Text
getFieldSqlType (x:xs) =
case x of
(FieldSqlType y) -> Just y
_ -> getFieldSqlType xs
getFieldSqlType _ = Nothing
getFieldMaxLen :: [EntityFieldLastItem] -> Maybe Int
getFieldMaxLen (x:xs) =
case x of
(FieldMaxLen y) -> Just y
_ -> getFieldMaxLen xs
getFieldMaxLen _ = Nothing
getEntityFieldLastItemParser :: EntityFieldLastItem -> Parser EntityFieldLastItem
getEntityFieldLastItemParser (FieldDefault _) = parseFieldDefault
getEntityFieldLastItemParser (FieldSqlRow _) = parseFieldSqlRow
getEntityFieldLastItemParser (FieldSqlType _) = parseFieldSqlType
getEntityFieldLastItemParser (FieldMaxLen _) = parseFieldMaxLen
parseFieldDefault :: Parser EntityFieldLastItem
parseFieldDefault = do
_ <- string "default"
_ <- many' spaceNoNewLine
_ <- char '='
_ <- many' spaceNoNewLine
-- take while not space
text <- many' (digit <|> letter <|> underline)
return $ FieldDefault $ T.pack text
parseFieldSqlRow :: Parser EntityFieldLastItem
parseFieldSqlRow = do
_ <- string "sql"
_ <- many' spaceNoNewLine
_ <- char '='
_ <- many' spaceNoNewLine
-- take while not space
text <- many' (digit <|> letter <|> underline)
return $ FieldSqlRow $ T.pack text
parseFieldSqlType :: Parser EntityFieldLastItem
parseFieldSqlType = do
_ <- string "sqltype"
_ <- many' spaceNoNewLine
_ <- char '='
_ <- many' spaceNoNewLine
-- take while not space
text <- many' (digit <|> letter <|> underline)
return $ FieldSqlType $ T.pack text
parseFieldMaxLen :: Parser EntityFieldLastItem
parseFieldMaxLen = do
_ <- string "maxlen"
_ <- many' spaceNoNewLine
_ <- char '='
_ <- many' spaceNoNewLine
-- take while not space
intString <- many1 digit
case readMaybe intString :: Maybe Int of
Nothing -> fail "fieldMaxLen"
Just int -> return $ FieldMaxLen int
parseEntityFieldLastItem :: [EntityFieldLastItem] -> Parser [EntityFieldLastItem]
parseEntityFieldLastItem parserOps = do
_ <- many1 spaceNoNewLine
let parsers = deleteItems parserOps [FieldDefault "", FieldSqlType "", FieldSqlRow "", FieldMaxLen 0]
mResult <- (Just <$> choice (map getEntityFieldLastItemParser parsers)) <|> pure Nothing
case mResult of
Nothing -> return parserOps
Just result -> parseEntityFieldLastItem (parserOps ++ [result]) <|> pure (parserOps ++ [result])
parseEntityFieldName :: Parser Text
parseEntityFieldName = do
_ <- many1 spaceNoNewLine
name <- haskellFunctionName
case name == "deriving" of
True -> fail "deriving"
False -> return name
parseStrictness :: Parser Strictness
parseStrictness = do
(string "!" *> pure ExplicitStrict) <|> (string "~" *> pure Lazy) <|> pure Strict
parseEntityFieldType :: Parser EntityFieldType
parseEntityFieldType = do
_ <- many1 spaceNoNewLine
mLeftBracket <- maybeOption (char '[')
strictness <- parseStrictness
name <- haskellTypeName
case mLeftBracket of
Nothing -> do
-- _ <- many' spaceNoNewLine
-- mMaybe <- maybeOption (string "Maybe")
mybe <- (parseMaybe *> pure True) <|> pure False
return $ EntityFieldType name strictness False mybe
Just _ -> do
_ <- char ']'
-- _ <- many' spaceNoNewLine
-- mMaybe <- maybeOption (string "Maybe")
mybe <- (parseMaybe *> pure True) <|> pure False
return $ EntityFieldType name strictness True mybe
parseMaybe :: Parser ()
parseMaybe = do
_ <- many1 spaceNoNewLine
_ <- string "Maybe"
return ()
-- EntityUnique
parseEntityUnique :: Parser EntityUnique
parseEntityUnique = do
eun <- parseEntityUniqueName
euefn <- parseEntityUniqueEntityFieldName
_ <- takeTill isEndOfLine
endOfLine <|> endOfInput
return $ EntityUnique eun euefn
parseEntityUniqueName :: Parser Text
parseEntityUniqueName = do
_ <- many1 spaceNoNewLine
haskellTypeName
parseEntityUniqueEntityFieldName :: Parser [Text]
parseEntityUniqueEntityFieldName = do
_ <- many1 spaceNoNewLine
many1 haskellFunctionName
-- EntityDerive
parseEntityDerive :: Parser EntityDerive
parseEntityDerive = do
_ <- many1 spaceNoNewLine
_ <- string "deriving"
-- _ <- many1 spaceNoNewLine
names <- many1 (many1 spaceNoNewLine *> haskellTypeName)
_ <- takeTill isEndOfLine
endOfLine <|> endOfInput
return $ EntityDerive names
parseEntityPrimary :: Parser EntityPrimary
parseEntityPrimary = do
_ <- many1 spaceNoNewLine
_ <- string "Primary"
-- _ <- many1 spaceNoNewLine
names <- many1 (many1 spaceNoNewLine *> haskellFunctionName)
_ <- takeTill isEndOfLine
endOfLine <|> endOfInput
return $ EntityPrimary names
parseEntityForeign :: Parser EntityForeign
parseEntityForeign = do
_ <- many1 spaceNoNewLine
_ <- string "Foreign"
_ <- many1 spaceNoNewLine
foreignTable <- haskellTypeName
names <- many1 (many1 spaceNoNewLine *> haskellFunctionName)
_ <- takeTill isEndOfLine
endOfLine <|> endOfInput
return $ EntityForeign foreignTable names
parseForeignKeyType :: Parser () -- Text
parseForeignKeyType = do
_ <- manyTill anyChar (string "Id" *> endOfInput)
return ()