{-# 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