{-# LANGUAGE OverloadedStrings #-} module Text.Parsec.Erd.Parser ( AST(..), GlobalOptions(..), document, globalOptions, entity, rel, attr, comment ) where import Erd.ER import Control.Monad (liftM2, void, when) import Data.Char (isAlphaNum, isControl, isSpace) import qualified Data.Map as M import Data.Maybe import Data.Text.Lazy import Text.Parsec import Text.Parsec.Text.Lazy import Text.Printf (printf) data AST = E Entity | A Attribute | R Relation deriving (Show, Eq) data GlobalOptions = GlobalOptions { gtoptions :: Options , ghoptions :: Options , geoptions :: Options , groptions :: Options } deriving (Show, Eq) emptyGlobalOptions :: GlobalOptions emptyGlobalOptions = GlobalOptions M.empty M.empty M.empty M.empty document :: Parser (GlobalOptions, [AST]) document = do skipMany (comment <|> blanks) opts <- globalOptions emptyGlobalOptions ast <- catMaybes <$> manyTill top eof return (opts, ast) where top = (entity "entity declaration") <|> (try rel "relationship") -- must come before attr <|> (try attr "attribute") <|> (comment "comment") <|> blanks blanks = many1 (space "whitespace") >> return Nothing entity :: Parser (Maybe AST) entity = do n <- between (char '[') (char ']') ident spacesNoNew opts <- options eolComment return $ Just $ E Entity { name = n, attribs = [], hoptions = opts, eoptions = opts } attr :: Parser (Maybe AST) attr = do keys <- many $ oneOf "*+ \t" let (ispk, isfk) = ('*' `elem` keys, '+' `elem` keys) n <- ident opts <- options eolComment return $ Just $ A Attribute {field = n, pk = ispk, fk = isfk, aoptions = opts <> defaultAttrOpts} rel :: Parser (Maybe AST) rel = do let ops = "?1*+" e1 <- ident op1 <- oneOf ops _ <- string "--" op2 <- oneOf ops e2 <- ident opts <- options let getCard op = case cardByName op of Just t -> return t Nothing -> unexpected (printf "Cardinality '%s' does not exist." op) t1 <- getCard op1 t2 <- getCard op2 return $ Just $ R Relation { entity1 = e1, entity2 = e2 , card1 = t1, card2 = t2, roptions = opts } globalOptions :: GlobalOptions -> Parser GlobalOptions globalOptions gopts = option gopts $ try $ do n <- ident opts <- options case n of "title" -> emptiness >> globalOptions (gopts { gtoptions = opts}) "header" -> emptiness >> globalOptions (gopts { ghoptions = opts}) "entity" -> emptiness >> globalOptions (gopts { geoptions = opts}) "relationship" -> emptiness >> globalOptions (gopts { groptions = opts}) _ -> fail "not a valid directive" options :: Parser (M.Map String Option) options = option M.empty $ fmap M.fromList $ try $ between (char '{' >> emptiness) (emptiness >> char '}') $ opt `sepEndBy` (emptiness >> char ',' >> emptiness) opt :: Parser (String, Option) opt = do optName <- liftM2 (:) letter (manyTill (letter <|> char '-') (char ':')) "option name" emptiness value <- between (char '"') (char '"') (many $ noneOf "\"") "option value" case optionByName optName value of Left err -> fail err Right o' -> emptiness >> return (optName, o') comment :: Parser (Maybe AST) comment = do _ <- char '#' _ <- manyTill anyChar $ try eol return Nothing ident :: Parser Text ident = do spacesNoNew n <- identQuoted <|> identNoSpace spacesNoNew return n identQuoted :: Parser Text identQuoted = do quote <- oneOf "'\"`" let p = satisfy (\c -> c /= quote && not (isControl c) ) "any character except " ++ [quote] ++ " or control characters" n <- fmap pack (many1 p) _ <- char quote return n identNoSpace :: Parser Text identNoSpace = do let p = satisfy (\c -> c == '_' || isAlphaNum c) "letter, digit or underscore" fmap pack (many1 p) emptiness :: Parser () emptiness = skipMany (void (many1 space) <|> eolComment) eolComment :: Parser () eolComment = spacesNoNew >> (eol <|> void comment) spacesNoNew :: Parser () spacesNoNew = skipMany $ satisfy $ \c -> c /= '\n' && c /= '\r' && isSpace c eol :: Parser () eol = eof <|> do c <- oneOf "\n\r" when (c == '\r') $ optional $ char '\n'