persistent-2.17.1.0: Database/Persist/Quasi/Internal/ModelParser.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Database.Persist.Quasi.Internal.ModelParser
( SourceLoc (..)
, Attribute (..)
, attribute
, attributeContent
, Directive (..)
, directiveContent
, EntityField (..)
, entityField
, entityFieldContent
, FieldName (..)
, fieldName
, ParsedEntityDef (..)
, parseSource
, memberEntityFields
, ParserWarning
, parserWarningMessage
, ParseResult
, CumulativeParseResult
, toCumulativeParseResult
, renderErrors
, runConfiguredParser
, ParserErrorLevel (..)
, initialExtraState
) where
import Control.Applicative (Alternative)
import Control.Monad (MonadPlus, void)
import Control.Monad.Reader (MonadReader, ReaderT, asks, runReaderT)
import Control.Monad.State
import Control.Monad.Writer
import Data.Char (isSpace)
import Data.Either (partitionEithers)
import Data.Foldable (fold)
import Data.Functor.Identity
import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NEL
import qualified Data.Map as M
import Data.Maybe
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Void
import Database.Persist.Quasi.Internal.TypeParser
import Database.Persist.Quasi.PersistSettings.Internal
import Database.Persist.Types
import Database.Persist.Types.SourceSpan
import Language.Haskell.TH.Syntax (Lift)
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
import qualified Text.Megaparsec.Stream as TMS
-- We'll augment the parser with extra state to accumulate comments seen during parsing.
-- Comments are lexed as whitespace, but will be used to generate documentation later.
data ExtraState = ExtraState
{ esPositionedCommentTokens :: [(SourcePos, CommentToken)]
, esLastDocumentablePosition :: Maybe SourcePos
}
-- @since 2.16.0.0
initialExtraState :: ExtraState
initialExtraState =
ExtraState
{ esPositionedCommentTokens = []
, esLastDocumentablePosition = Nothing
}
newtype Parser a = Parser
{ unParser
:: ReaderT
PersistSettings
( StateT
ExtraState
( ParsecT
Void
String
( Writer
(Set ParserWarning)
)
)
)
a
}
deriving newtype
( Functor
, Applicative
, Monad
, Alternative
, MonadPlus
, MonadState ExtraState
, MonadReader PersistSettings
, MonadParsec Void String
, MonadFail
)
type EntityParseError = ParseErrorBundle String Void
-- | Result of parsing a single source text.
--
-- @since 2.16.0.0
type ParseResult a =
(Set ParserWarning, Either (ParseErrorBundle String Void) a)
type InternalParseResult a = ParseResult (a, ExtraState)
-- | Cumulative result of parsing multiple source texts.
--
-- @since 2.16.0.0
type CumulativeParseResult a = (Set ParserWarning, Either [EntityParseError] a)
toCumulativeParseResult
:: (Monoid a) => [ParseResult a] -> CumulativeParseResult a
toCumulativeParseResult prs = do
let
(warnings, eithers) = sequence prs
case partitionEithers eithers of
([], results) -> (warnings, Right $ fold results)
(errs, _) -> (warnings, Left errs)
-- | Run a parser using provided PersistSettings and ExtraState
-- @since 2.16.0.0
runConfiguredParser
:: PersistSettings
-> ExtraState
-> Parser a
-> String
-> String
-> InternalParseResult a
runConfiguredParser ps acc parser fp s = (warnings, either)
where
sm = runReaderT (unParser parser) ps
pm = runStateT sm acc
wm = runParserT' pm initialInternalState
((_is, either), warnings) = runWriter wm
initialSourcePos =
SourcePos
{ sourceName = fp
, sourceLine = pos1
, sourceColumn = pos1
}
initialPosState =
PosState
{ pstateInput = s
, pstateOffset = 0
, pstateSourcePos = initialSourcePos
, -- for legacy compatibility, we treat each tab as a single unit of whitespace
pstateTabWidth = pos1
, pstateLinePrefix = ""
}
initialInternalState =
State
{ stateInput = s
, stateOffset = 0
, statePosState = initialPosState
, stateParseErrors = []
}
reportWarnings :: Set ParserWarning -> Parser ()
#if MIN_VERSION_megaparsec(9,5,0)
reportWarnings = Parser . tell
#else
reportWarnings _pw = pure ()
#endif
-- | Renders a list of EntityParseErrors as a String using `errorBundlePretty`,
-- separated by line breaks.
-- @since 2.16.0.0
renderErrors :: [EntityParseError] -> String
renderErrors errs = intercalate "\n" $ fmap errorBundlePretty errs
-- | Attempts to parse with a provided parser. If it fails with an error matching
-- the provided predicate, it registers a warning with the provided message and falls
-- back to the second provided parser.
tryOrWarn
:: String
-> (ParseError String Void -> Bool)
-> Parser a
-> Parser a
-> Parser a
tryOrWarn msg p l r = do
parserState <- getParserState
withRecovery (warnAndRetry $ statePosState parserState) l
where
warnAndRetry posState err = do
if p err
then do
let
(pairs, _) = attachSourcePos errorOffset [err] posState
reportWarnings . Set.fromList $
map
( \(e, _pos) ->
ParserWarning
{ parserWarningExtraMessage = msg <> "\n"
, parserWarningUnderlyingError = e
, parserWarningPosState = posState
}
)
pairs
r
else parseError err
-- | Attempts to parse with a provided parser. If it fails with an error matching
-- the provided predicate, it registers a delayed error and falls
-- back to the second provided parser.
--
-- This is useful when registering errors in space consumers and other parsers that are called
-- with `try`, since a non-delayed error in this context will cause backtracking and not
-- get reported to the user.
tryOrRegisterError
:: (ParseError String Void -> Bool)
-> Parser a
-> Parser a
-> Parser a
tryOrRegisterError p l r = do
parserState <- getParserState
withRecovery (delayedError $ statePosState parserState) l
where
delayedError posState err = do
if p err
then do
let
(pairs, _) = attachSourcePos errorOffset [err] posState
registerParseError err
r
else parseError err
tryOrReport
:: Maybe ParserErrorLevel
-> String
-> (ParseError String Void -> Bool)
-> Parser a
-> Parser a
-> Parser a
tryOrReport level msg p l r = case level of
Just LevelError -> tryOrRegisterError p l r
Just LevelWarning -> tryOrWarn msg p l r
Nothing -> r
-- | Source location: file and line/col information. This is half of a 'SourceSpan'.
--
-- @since 2.16.0.0
data SourceLoc = SourceLoc
{ locFile :: Text
, locStartLine :: Int
, locStartCol :: Int
}
deriving (Show, Lift)
-- | An attribute of an entity field definition or a directive.
--
-- @since 2.17.1.0
data Attribute
= Assignment Text Text
| Parenthetical Text
| PText Text
| -- | Quoted field attributes are deprecated since 2.17.1.0.
Quotation Text
deriving (Eq, Ord, Show)
-- | The name of an entity block or extra block.
--
-- @since 2.17.1.0
newtype BlockKey = BlockKey Text
deriving (Show)
-- | A parsed comment or doc comment.
--
-- @since 2.16.0.0
data CommentToken
= DocComment Text
| Comment Text
deriving (Eq, Ord, Show)
-- | Converts an attribute into a Text representation for second-stage parsing or
-- presentation to the user
--
-- @since 2.16.0.0
attributeContent :: Attribute -> Text
attributeContent = \case
Assignment l r -> mconcat [l, "=", r]
Parenthetical s -> s
PText s -> s
Quotation s -> s
-- | Converts a directive into a Text representation for second-stage parsing or
-- presentation to the user
--
-- @since 2.17.1.0
directiveContent :: Directive -> [Text]
directiveContent d =
[directiveNameContent $ directiveName d]
<> (attributeContent <$> directiveAttributes d)
entityFieldContent :: EntityField -> [Text]
entityFieldContent f =
[ fieldNameAndStrictnessAsText f
, (typeExprContent . entityFieldType) f
]
++ fmap attributeContent (entityFieldAttributes f)
blockKeyContent :: BlockKey -> Text
blockKeyContent (BlockKey t) = t
directiveNameContent :: DirectiveName -> Text
directiveNameContent (DirectiveName t) = t
-- | Generates the field name of an EntityField, accompanied by
-- its strictness sigil, if one is present.
-- This is only needed temporarily, and can eventually be refactored away.
--
-- @since 2.17.1.0
fieldNameAndStrictnessAsText :: EntityField -> Text
fieldNameAndStrictnessAsText f =
let
s = case entityFieldStrictness f of
Just Strict -> "!"
Just Lazy -> "~"
Nothing -> ""
(FieldName fn) = entityFieldName f
in
s <> fn
commentContent :: CommentToken -> Text
commentContent = \case
Comment s -> s
DocComment s -> s
quotedAttributeErrorMessage :: String
quotedAttributeErrorMessage = "Unexpected quotation mark in field or directive attribute"
attribute :: Parser Attribute
attribute = do
quotedFieldAttributeErrorLevel <- asks psQuotedArgumentErrorLevel
tryOrReport
quotedFieldAttributeErrorLevel
"Quoted field attributes are deprecated since 2.17.1.0, and will be removed in or after 2.18.0.0"
isQuotedAttributeError
attribute'
(Quotation . Text.pack <$> quotation)
where
isQuotedAttributeError (FancyError _ s) = s == Set.singleton (ErrorFail quotedAttributeErrorMessage)
isQuotedAttributeError _ = False
attribute' :: Parser Attribute
attribute' = do
q <- lookAhead (optional $ char '"')
case q of
Just _ -> fail quotedAttributeErrorMessage
Nothing ->
choice
[ try assignment
, parenthetical
, ptext
]
docComment :: Parser (SourcePos, CommentToken)
docComment = do
pos <- getSourcePos
content <-
string "-- |" *> validHSpace *> takeWhileP (Just "character") (/= '\n')
pure (pos, DocComment (Text.pack content))
comment :: Parser (SourcePos, CommentToken)
comment = do
pos <- getSourcePos
content <-
(string "--" <|> string "#")
*> validHSpace
*> takeWhileP (Just "character") (/= '\n')
pure (pos, Comment (Text.pack content))
skipComment :: Parser ()
skipComment = do
content <- docComment <|> comment
void $ appendCommentToState content
isValidHSpace :: Bool -> Char -> Bool
isValidHSpace allowTabs c =
if allowTabs
then isSpace c && c /= '\n'
else isSpace c && c /= '\n' && c /= '\t'
isValidSpace :: Bool -> Char -> Bool
isValidSpace allowTabs c =
if allowTabs
then isSpace c
else isSpace c && c /= '\t'
validSpaceParser
:: (Maybe String -> (TMS.Token String -> Bool) -> Parser (Tokens String))
-> (Bool -> Char -> Bool)
-> Parser ()
validSpaceParser taker validator = do
tabErrorLevel <- asks psTabErrorLevel
void $
tryOrReport
tabErrorLevel
"use spaces instead of tabs"
isUnexpectedTabError
(taker (Just "valid whitespace") (validator False))
(taker (Just "valid whitespace") (validator True))
isUnexpectedTabError :: ParseError String Void -> Bool
isUnexpectedTabError (TrivialError _ ue l) =
ue == Just (Tokens ('\t' :| ""))
&& l == Set.singleton (Label ('v' :| "alid whitespace"))
isUnexpectedTabError _ = False
someValidHSpace :: Parser ()
someValidHSpace = validSpaceParser takeWhile1P isValidHSpace
someValidSpace :: Parser ()
someValidSpace = validSpaceParser takeWhile1P isValidSpace
validHSpace :: Parser ()
validHSpace = validSpaceParser takeWhileP isValidHSpace
spaceConsumer :: Parser ()
spaceConsumer =
L.space
someValidHSpace
skipComment
empty
spaceConsumerN :: Parser ()
spaceConsumerN =
L.space
someValidSpace
skipComment
empty
-- This catch-all character class is used in a variety of places, and includes characters
-- which have syntactic function. As we continue to iterate on the parser, we may want to consider
-- shrinking or eliminating `contentChar`.
contentChar :: Parser Char
contentChar =
choice
[ alphaNumChar
, char '.'
, char '['
, char ']'
, char '_'
, char '\''
, char '"'
, char '!'
, char '~'
, char '-'
, char ':'
, char ','
, do
backslash <- char '\\'
nextChar <- lookAhead anySingle
if nextChar == '(' || nextChar == ')'
then single nextChar
else pure backslash
]
nonLineSpaceChar :: Parser Char
nonLineSpaceChar = choice [char ' ', char '\t']
-- This is a replacement for `Text.Megaparsec.Char.Lexer.charLiteral`;
-- it does nearly the same thing but additionally supports escaped parentheses.
charLiteral :: Parser Char
charLiteral = label "literal character" $ do
char1 <- anySingle
case char1 of
'\\' -> do
char2 <- anySingle
case char2 of
'(' -> pure '('
')' -> pure ')'
'\\' -> pure '\\'
'\"' -> pure '\"'
'\'' -> pure '\''
_ -> unexpected (Tokens $ char2 :| [])
_ -> pure char1
assignment :: Parser Attribute
assignment = label "assignment expression" $ do
L.lexeme spaceConsumer $ do
lhs <- some contentChar
_ <- char '='
rhs <-
choice
[ quotation
, sqlLiteral
, parentheticalInner
, try sqlFunctionApplication
, some $ contentChar <|> char '(' <|> char ')'
]
pure $ Assignment (Text.pack lhs) (Text.pack rhs)
where
parentheticalInner = do
str <- parenthetical'
pure . init . drop 1 $ str
sqlFunctionApplication = do
fn <- some contentChar
argString <- parentheticalInner
pure $ mconcat [fn, "(", argString, ")"]
sqlTypeName :: Parser String
sqlTypeName =
some $
choice
[ alphaNumChar
, char '_'
]
sqlLiteral :: Parser String
sqlLiteral = label "SQL literal" $ do
quote <- L.lexeme spaceConsumer $ char '\'' *> manyTill charLiteral (char '\'')
st <- optional $ do
colons <- string "::"
tn <- sqlTypeName
pure $ colons <> tn
pure $
mconcat
[ "'"
, quote
, "'"
, fromMaybe "" st
]
quotation :: Parser String
quotation = char '"' *> manyTill charLiteral (char '"')
parenthetical :: Parser Attribute
parenthetical = label "parenthetical" $ do
str <- L.lexeme spaceConsumer parenthetical'
pure . Parenthetical . Text.pack . init . drop 1 $ str
parenthetical' :: Parser String
parenthetical' = do
str <- between (char '(') (char ')') q
pure $ "(" ++ str ++ ")"
where
q = mconcat <$> some (c <|> parenthetical')
c = (: []) <$> choice [contentChar, nonLineSpaceChar, char '"']
blockKey :: Parser BlockKey
blockKey = label "block key" $ do
fl <- upperChar
rl <- many alphaNumChar
pure . BlockKey . Text.pack $ fl : rl
fieldStrictness :: Parser FieldStrictness
fieldStrictness =
label "strictness sigil" $
(Strict <$ char '!') <|> (Lazy <$ char '~')
fieldName :: Parser FieldName
fieldName = label "field name" $ do
fl <- lowerChar
rl <- many fieldNameChar
pure . FieldName . Text.pack $ fl : rl
where
fieldNameChar =
choice
[ alphaNumChar
, char '_'
]
ptext :: Parser Attribute
ptext = label "plain attribute" $ do
str <- L.lexeme spaceConsumer $ some contentChar
pure . PText . Text.pack $ str
data ParsedEntityDef = ParsedEntityDef
{ parsedEntityDefComments :: [Text]
, parsedEntityDefEntityName :: EntityNameHS
, parsedEntityDefIsSum :: Bool
, parsedEntityDefEntityAttributes :: [Attribute]
, parsedEntityDefFields :: [(EntityField, Maybe Text)]
, parsedEntityDefDirectives :: [(Directive, Maybe Text)]
, parsedEntityDefExtras :: M.Map Text [ExtraLine]
, parsedEntityDefSpan :: Maybe SourceSpan
}
deriving (Show)
data DocCommentBlock = DocCommentBlock
{ docCommentBlockLines :: [Text]
, docCommentBlockPos :: SourcePos
}
deriving (Show)
data EntityHeader = EntityHeader
{ entityHeaderSum :: Bool
, entityHeaderTableName :: Text
, entityHeaderRemainingAttributes :: [Attribute]
, entityHeaderPos :: SourcePos
}
deriving (Show)
data EntityBlock = EntityBlock
{ entityBlockDocCommentBlock :: Maybe DocCommentBlock
, entityBlockEntityHeader :: EntityHeader
, entityBlockMembers :: [Member]
}
deriving (Show)
entityBlockFirstPos :: EntityBlock -> SourcePos
entityBlockFirstPos = entityHeaderPos . entityBlockEntityHeader
entityBlockLastPos :: EntityBlock -> SourcePos
entityBlockLastPos eb = case entityBlockMembers eb of
[] -> entityBlockFirstPos eb
members -> maximum $ fmap memberEndPos members
entityBlockEntityFields :: EntityBlock -> [EntityField]
entityBlockEntityFields = foldMap f <$> entityBlockMembers
where
f m = case m of
MemberExtraBlock _ -> []
MemberEntityField ba -> [ba]
MemberDirective _ -> []
entityBlockExtraBlocks :: EntityBlock -> [ExtraBlock]
entityBlockExtraBlocks = foldMap f <$> entityBlockMembers
where
f m = case m of
MemberExtraBlock eb -> [eb]
MemberEntityField _ -> []
MemberDirective _ -> []
entityBlockDirectives :: EntityBlock -> [Directive]
entityBlockDirectives = foldMap f <$> entityBlockMembers
where
f m = case m of
MemberExtraBlock _ -> []
MemberEntityField _ -> []
MemberDirective bd -> [bd]
data ExtraBlockHeader = ExtraBlockHeader
{ extraBlockHeaderKey :: Text
, extraBlockHeaderRemainingAttributes :: [Attribute]
, extraBlockHeaderPos :: SourcePos
}
deriving (Show)
data ExtraBlock = ExtraBlock
{ extraBlockDocCommentBlock :: Maybe DocCommentBlock
, extraBlockExtraBlockHeader :: ExtraBlockHeader
, extraBlockLines :: NonEmpty ExtraBlockLine
}
deriving (Show)
data FieldStrictness = Strict | Lazy
deriving (Show)
newtype FieldName = FieldName Text
deriving (Show, Eq)
newtype DirectiveName = DirectiveName Text
deriving (Show, Eq)
data EntityField = EntityField
{ entityFieldDocCommentBlock :: Maybe DocCommentBlock
, entityFieldStrictness :: Maybe FieldStrictness
, entityFieldName :: FieldName
, entityFieldType :: TypeExpr
, entityFieldAttributes :: [Attribute]
, entityFieldPos :: SourcePos
}
deriving (Show)
data Directive = Directive
{ directiveDocCommentBlock :: Maybe DocCommentBlock
, directiveName :: DirectiveName
, directiveAttributes :: [Attribute]
, directivePos :: SourcePos
}
deriving (Show)
data Member
= MemberExtraBlock ExtraBlock
| MemberEntityField EntityField
| MemberDirective Directive
deriving (Show)
data ExtraBlockLine = ExtraBlockLine
{ extraBlockLineDocCommentBlock :: Maybe DocCommentBlock
, extraBlockLineTokens :: [String]
, extraBlockLinePos :: SourcePos
}
deriving (Show)
-- | The source position at the beginning of the member's final line.
memberEndPos :: Member -> SourcePos
memberEndPos (MemberEntityField fs) = entityFieldPos fs
memberEndPos (MemberDirective d) = directivePos d
memberEndPos (MemberExtraBlock ex) = extraBlockLinePos . NEL.last . extraBlockLines $ ex
-- | Represents an entity member as a list of EntityFields
--
-- @since 2.16.0.0
memberEntityFields :: Member -> [EntityField]
memberEntityFields (MemberEntityField fs) = [fs]
memberEntityFields (MemberDirective _) = []
memberEntityFields (MemberExtraBlock _) = []
extraBlocksAsMap :: [ExtraBlock] -> M.Map Text [ExtraLine]
extraBlocksAsMap exs = M.fromList $ fmap asPair exs
where
asPair ex =
( extraBlockHeaderKey . extraBlockExtraBlockHeader $ ex
, NEL.toList (extraLines ex)
)
extraLines :: ExtraBlock -> NonEmpty [Text]
extraLines ex = fmap Text.pack . extraBlockLineTokens <$> extraBlockLines ex
entityHeader :: Parser EntityHeader
entityHeader = do
pos <- getSourcePos
plus <- optional (char '+')
en <- validHSpace *> L.lexeme spaceConsumer blockKey
rest <- L.lexeme spaceConsumer (many attribute)
_ <- setLastDocumentablePosition
pure
EntityHeader
{ entityHeaderSum = isJust plus
, entityHeaderTableName = blockKeyContent en
, entityHeaderRemainingAttributes = rest
, entityHeaderPos = pos
}
appendCommentToState :: (SourcePos, CommentToken) -> Parser ()
appendCommentToState ptok =
modify $ \es ->
let
comments = esPositionedCommentTokens es
in
es{esPositionedCommentTokens = ptok : comments}
setLastDocumentablePosition :: Parser ()
setLastDocumentablePosition = do
pos <- getSourcePos
modify $ \es -> es{esLastDocumentablePosition = Just pos}
getDcb :: Parser (Maybe DocCommentBlock)
getDcb = do
es <- get
let
comments = reverse $ esPositionedCommentTokens es
_ <- put es{esPositionedCommentTokens = []}
let
candidates = dropWhile (\(_sp, ct) -> not (isDocComment ct)) comments
filteredCandidates = dropWhile (commentIsIncorrectlyPositioned es) candidates
pure $ docCommentBlockFromPositionedAttributes filteredCandidates
where
commentIsIncorrectlyPositioned
:: ExtraState -> (SourcePos, CommentToken) -> Bool
commentIsIncorrectlyPositioned es ptok = case esLastDocumentablePosition es of
Nothing -> False
Just lastDocumentablePos -> (sourceLine . fst) ptok <= sourceLine lastDocumentablePos
extraBlock :: Parser Member
extraBlock = L.indentBlock spaceConsumerN innerParser
where
mkExtraBlockMember dcb (header, extraBlockLines) =
MemberExtraBlock
ExtraBlock
{ extraBlockExtraBlockHeader = header
, extraBlockLines = ensureNonEmpty extraBlockLines
, extraBlockDocCommentBlock = dcb
}
ensureNonEmpty lines = case NEL.nonEmpty lines of
Just nel -> nel
Nothing -> error "unreachable" -- lines is known to be non-empty
innerParser = do
dcb <- getDcb
header <- extraBlockHeader
pure $
L.IndentSome
Nothing
(return . mkExtraBlockMember dcb . (header,))
extraBlockLine
extraBlockHeader :: Parser ExtraBlockHeader
extraBlockHeader = do
pos <- getSourcePos
tn <- L.lexeme spaceConsumer blockKey
rest <- L.lexeme spaceConsumer (many attribute)
_ <- setLastDocumentablePosition
pure $
ExtraBlockHeader
{ extraBlockHeaderKey = blockKeyContent tn
, extraBlockHeaderRemainingAttributes = rest
, extraBlockHeaderPos = pos
}
extraBlockLine :: Parser ExtraBlockLine
extraBlockLine = do
dcb <- getDcb
pos <- getSourcePos
tokens <- some $ L.lexeme spaceConsumer (some contentChar)
_ <- setLastDocumentablePosition
pure $
ExtraBlockLine
{ extraBlockLineDocCommentBlock = dcb
, extraBlockLineTokens = tokens
, extraBlockLinePos = pos
}
entityField :: Parser Member
entityField = do
dcb <- getDcb
pos <- getSourcePos
ss <- optional fieldStrictness
fn <- L.lexeme spaceConsumer fieldName
ft <- L.lexeme spaceConsumer typeExpr -- Note that `typeExpr` consumes outer parentheses.
fa <- L.lexeme spaceConsumer (many attribute)
_ <- setLastDocumentablePosition
lookAhead (void newline <|> eof)
pure $
MemberEntityField
EntityField
{ entityFieldDocCommentBlock = dcb
, entityFieldStrictness = ss
, entityFieldName = fn
, entityFieldType = ft
, entityFieldAttributes = fa
, entityFieldPos = pos
}
directiveNameP :: Parser DirectiveName
directiveNameP =
label "directive name" $
DirectiveName . Text.pack
<$> choice
[ string "deriving"
, directiveName'
]
where
directiveName' = do
fl <- upperChar
rl <- many alphaNumChar
pure (fl : rl)
directive :: Parser Member
directive = do
dcb <- getDcb
pos <- getSourcePos
dn <- L.lexeme spaceConsumer directiveNameP
args <- many $ L.lexeme spaceConsumer attribute
_ <- setLastDocumentablePosition
lookAhead (void newline <|> eof)
pure $
MemberDirective
Directive
{ directiveDocCommentBlock = dcb
, directiveName = dn
, directiveAttributes = args
, directivePos = pos
}
member :: Parser Member
member =
choice
[ try extraBlock
, directive
, entityField
]
entityBlock :: Parser EntityBlock
entityBlock = do
L.indentBlock spaceConsumerN innerParser
where
mkEntityBlock dcb (header, members) =
EntityBlock
{ entityBlockEntityHeader = header
, entityBlockMembers = members
, entityBlockDocCommentBlock = dcb
}
innerParser = do
dcb <- getDcb
header <- entityHeader
pure $ L.IndentMany Nothing (return . mkEntityBlock dcb . (header,)) member
entitiesFromDocument :: Parser [EntityBlock]
entitiesFromDocument = many entityBlock
docCommentBlockText :: DocCommentBlock -> Text
docCommentBlockText dcb = Text.unlines $ docCommentBlockLines dcb
isDocComment :: CommentToken -> Bool
isDocComment tok = case tok of
DocComment _ -> True
_ -> False
docCommentBlockFromPositionedAttributes
:: [(SourcePos, CommentToken)] -> Maybe DocCommentBlock
docCommentBlockFromPositionedAttributes ptoks =
case NEL.nonEmpty ptoks of
Nothing -> Nothing
Just nel ->
Just $
DocCommentBlock
{ docCommentBlockLines = NEL.toList $ fmap (commentContent . snd) nel
, docCommentBlockPos = fst $ NEL.head nel
}
parseEntities
:: PersistSettings
-> Text
-> String
-> ParseResult [EntityBlock]
parseEntities ps fp s = do
let
(warnings, res) =
runConfiguredParser ps initialExtraState entitiesFromDocument (Text.unpack fp) s
case res of
Left peb ->
(warnings, Left peb)
Right (entities, _comments) ->
(warnings, pure entities)
toParsedEntityDef :: Maybe SourceLoc -> EntityBlock -> ParsedEntityDef
toParsedEntityDef mSourceLoc eb =
ParsedEntityDef
{ parsedEntityDefComments = comments
, parsedEntityDefEntityName = entityNameHS
, parsedEntityDefIsSum = isSum
, parsedEntityDefEntityAttributes = entityAttributes
, parsedEntityDefFields = parsedFields
, parsedEntityDefDirectives = parsedDirectives
, parsedEntityDefExtras = extras
, parsedEntityDefSpan = mSpan
}
where
comments =
maybe
[]
docCommentBlockLines
(entityBlockDocCommentBlock eb)
entityAttributes = entityHeaderRemainingAttributes . entityBlockEntityHeader $ eb
isSum = entityHeaderSum . entityBlockEntityHeader $ eb
entityNameHS = EntityNameHS . entityHeaderTableName . entityBlockEntityHeader $ eb
fieldPair a = (a, docCommentBlockText <$> entityFieldDocCommentBlock a)
parsedFields = fmap fieldPair (entityBlockEntityFields eb)
directivePair d = (d, docCommentBlockText <$> directiveDocCommentBlock d)
parsedDirectives = fmap directivePair (entityBlockDirectives eb)
extras = extraBlocksAsMap (entityBlockExtraBlocks eb)
filepath = maybe "" locFile mSourceLoc
relativeStartLine = maybe 0 locStartLine mSourceLoc
relativeStartCol = maybe 0 locStartCol mSourceLoc
mSpan =
Just
SourceSpan
{ spanFile = filepath
, spanStartLine =
relativeStartLine + (unPos . sourceLine $ entityBlockFirstPos eb)
, spanEndLine = relativeStartLine + (unPos . sourceLine $ entityBlockLastPos eb)
, spanStartCol =
relativeStartCol + (unPos . sourceColumn $ entityBlockFirstPos eb)
, spanEndCol = unPos . sourceColumn $ entityBlockLastPos eb
}
parseSource
:: PersistSettings
-> Maybe SourceLoc
-> Text
-> ParseResult [ParsedEntityDef]
parseSource ps mSourceLoc source =
fmap (fmap (toParsedEntityDef mSourceLoc))
<$> parseEntities ps filepath (Text.unpack source)
where
filepath = maybe "" locFile mSourceLoc