{-# LANGUAGE BangPatterns, CPP #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE StandaloneDeriving, UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
module Database.Persist.Quasi
( parse
, PersistSettings (..)
, upperCaseSettings
, lowerCaseSettings
, nullable
#if TEST
, Token (..)
, Line' (..)
, preparse
, tokenize
, parseFieldType
, empty
, removeSpaces
, associateLines
, skipEmpty
, LinesWithComments(..)
#endif
) where
import Prelude hiding (lines)
import qualified Data.List.NonEmpty as NEL
import Data.List.NonEmpty (NonEmpty(..))
import Control.Arrow ((&&&))
import Control.Monad (msum, mplus)
import Data.Char
import Data.List (find, foldl')
import qualified Data.Map as M
import Data.Maybe (mapMaybe, fromMaybe, maybeToList, listToMaybe)
import Data.Monoid (mappend)
import Data.Text (Text)
import qualified Data.Text as T
import Database.Persist.Types
data ParseState a = PSDone | PSFail String | PSSuccess a Text deriving Show
parseFieldType :: Text -> Either String FieldType
parseFieldType t0 =
case parseApplyFT t0 of
PSSuccess ft t'
| T.all isSpace t' -> Right ft
PSFail err -> Left $ "PSFail " ++ err
other -> Left $ show other
where
parseApplyFT t =
case goMany id t of
PSSuccess (ft:fts) t' -> PSSuccess (foldl' FTApp ft fts) t'
PSSuccess [] _ -> PSFail "empty"
PSFail err -> PSFail err
PSDone -> PSDone
parseEnclosed :: Char -> (FieldType -> FieldType) -> Text -> ParseState FieldType
parseEnclosed end ftMod t =
let (a, b) = T.break (== end) t
in case parseApplyFT a of
PSSuccess ft t' -> case (T.dropWhile isSpace t', T.uncons b) of
("", Just (c, t'')) | c == end -> PSSuccess (ftMod ft) (t'' `Data.Monoid.mappend` t')
(x, y) -> PSFail $ show (b, x, y)
x -> PSFail $ show x
parse1 t =
case T.uncons t of
Nothing -> PSDone
Just (c, t')
| isSpace c -> parse1 $ T.dropWhile isSpace t'
| c == '(' -> parseEnclosed ')' id t'
| c == '[' -> parseEnclosed ']' FTList t'
| isUpper c ->
let (a, b) = T.break (\x -> isSpace x || x `elem` ("()[]"::String)) t
in PSSuccess (getCon a) b
| otherwise -> PSFail $ show (c, t')
getCon t =
case T.breakOnEnd "." t of
(_, "") -> FTTypeCon Nothing t
("", _) -> FTTypeCon Nothing t
(a, b) -> FTTypeCon (Just $ T.init a) b
goMany front t =
case parse1 t of
PSSuccess x t' -> goMany (front . (x:)) t'
PSFail err -> PSFail err
PSDone -> PSSuccess (front []) t
data PersistSettings = PersistSettings
{ psToDBName :: !(Text -> Text)
, psStrictFields :: !Bool
, psIdName :: !Text
}
defaultPersistSettings, upperCaseSettings, lowerCaseSettings :: PersistSettings
defaultPersistSettings = PersistSettings
{ psToDBName = id
, psStrictFields = True
, psIdName = "id"
}
upperCaseSettings = defaultPersistSettings
lowerCaseSettings = defaultPersistSettings
{ psToDBName =
let go c
| isUpper c = T.pack ['_', toLower c]
| otherwise = T.singleton c
in T.dropWhile (== '_') . T.concatMap go
}
parse :: PersistSettings -> Text -> [EntityDef]
parse ps = parseLines ps . preparse
preparse :: Text -> [Line]
preparse =
removeSpaces
. filter (not . empty)
. map tokenize
. T.lines
data Token = Spaces !Int
| Token Text
| DocComment Text
deriving (Show, Eq)
tokenize :: Text -> [Token]
tokenize t
| T.null t = []
| "-- | " `T.isPrefixOf` t = [DocComment t]
| "--" `T.isPrefixOf` t = []
| "#" `T.isPrefixOf` t = []
| T.head t == '"' = quotes (T.tail t) id
| T.head t == '(' = parens 1 (T.tail t) id
| isSpace (T.head t) =
let (spaces, rest) = T.span isSpace t
in Spaces (T.length spaces) : tokenize rest
| Just (beforeEquals, afterEquals) <- findMidToken t
, not (T.any isSpace beforeEquals)
, Token next : rest <- tokenize afterEquals =
Token (T.concat [beforeEquals, "=", next]) : rest
| otherwise =
let (token, rest) = T.break isSpace t
in Token token : tokenize rest
where
findMidToken t' =
case T.break (== '=') t' of
(x, T.drop 1 -> y)
| "\"" `T.isPrefixOf` y || "(" `T.isPrefixOf` y -> Just (x, y)
_ -> Nothing
quotes t' front
| T.null t' = error $ T.unpack $ T.concat $
"Unterminated quoted string starting with " : front []
| T.head t' == '"' = Token (T.concat $ front []) : tokenize (T.tail t')
| T.head t' == '\\' && T.length t' > 1 =
quotes (T.drop 2 t') (front . (T.take 1 (T.drop 1 t'):))
| otherwise =
let (x, y) = T.break (`elem` ['\\','\"']) t'
in quotes y (front . (x:))
parens count t' front
| T.null t' = error $ T.unpack $ T.concat $
"Unterminated parens string starting with " : front []
| T.head t' == ')' =
if count == (1 :: Int)
then Token (T.concat $ front []) : tokenize (T.tail t')
else parens (count - 1) (T.tail t') (front . (")":))
| T.head t' == '(' =
parens (count + 1) (T.tail t') (front . ("(":))
| T.head t' == '\\' && T.length t' > 1 =
parens count (T.drop 2 t') (front . (T.take 1 (T.drop 1 t'):))
| otherwise =
let (x, y) = T.break (`elem` ['\\','(',')']) t'
in parens count y (front . (x:))
empty :: [Token] -> Bool
empty [] = True
empty [Spaces _] = True
empty _ = False
data Line' f
= Line
{ lineIndent :: Int
, tokens :: f Text
}
deriving instance Show (f Text) => Show (Line' f)
deriving instance Eq (f Text) => Eq (Line' f)
mapLine :: (forall x. f x -> g x) -> Line' f -> Line' g
mapLine k (Line i t) = Line i (k t)
traverseLine :: Functor t => (forall x. f x -> t (g x)) -> Line' f -> t (Line' g)
traverseLine k (Line i xs) = Line i <$> k xs
type Line = Line' []
removeSpaces :: [[Token]] -> [Line]
removeSpaces =
map toLine
where
toLine (Spaces i:rest) = toLine' i rest
toLine xs = toLine' 0 xs
toLine' i = Line i . mapMaybe fromToken
fromToken (Token t) = Just t
fromToken (DocComment t) = Just t
fromToken Spaces{} = Nothing
parseLines :: PersistSettings -> [Line] -> [EntityDef]
parseLines ps lines =
fixForeignKeysAll $ toEnts lines
where
toEnts :: [Line] -> [UnboundEntityDef]
toEnts =
map mk
. associateLines
. skipEmpty
mk :: LinesWithComments -> UnboundEntityDef
mk lwc =
let Line _ (name :| entAttribs) :| rest = lwcLines lwc
in setComments (lwcComments lwc) $ mkEntityDef ps name entAttribs (map (mapLine NEL.toList) rest)
isComment :: Text -> Maybe Text
isComment xs =
T.stripPrefix "-- | " xs
data LinesWithComments = LinesWithComments
{ lwcLines :: NonEmpty (Line' NonEmpty)
, lwcComments :: [Text]
} deriving (Eq, Show)
appendLwc :: LinesWithComments -> LinesWithComments -> LinesWithComments
appendLwc a b =
LinesWithComments (foldr NEL.cons (lwcLines b) (lwcLines a)) (lwcComments a `mappend` lwcComments b)
newLine :: Line' NonEmpty -> LinesWithComments
newLine l = LinesWithComments (pure l) []
firstLine :: LinesWithComments -> Line' NonEmpty
firstLine = NEL.head . lwcLines
consLine :: Line' NonEmpty -> LinesWithComments -> LinesWithComments
consLine l lwc = lwc { lwcLines = NEL.cons l (lwcLines lwc) }
consComment :: Text -> LinesWithComments -> LinesWithComments
consComment l lwc = lwc { lwcComments = l : lwcComments lwc }
associateLines :: [Line' NonEmpty] -> [LinesWithComments]
associateLines lines =
foldr combine [] $
foldr toLinesWithComments [] lines
where
toLinesWithComments line linesWithComments =
case linesWithComments of
[] ->
[newLine line]
(lwc : lwcs) ->
case isComment (NEL.head (tokens line)) of
Just comment
| lineIndent line == lowestIndent ->
consComment comment lwc : lwcs
_ ->
if lineIndent line <= lineIndent (firstLine lwc)
then
consLine line lwc : lwcs
else
newLine line : lwc : lwcs
lowestIndent = minimum . fmap lineIndent $ lines
combine :: LinesWithComments -> [LinesWithComments] -> [LinesWithComments]
combine lwc [] =
[lwc]
combine lwc (lwc' : lwcs) =
let minIndent = minimumIndentOf lwc
otherIndent = minimumIndentOf lwc'
in
if minIndent < otherIndent then
appendLwc lwc lwc' : lwcs
else
lwc : lwc' : lwcs
minimumIndentOf = minimum . fmap lineIndent . lwcLines
skipEmpty :: [Line' []] -> [Line' NonEmpty]
skipEmpty = mapMaybe (traverseLine NEL.nonEmpty)
setComments :: [Text] -> UnboundEntityDef -> UnboundEntityDef
setComments [] = id
setComments comments =
overUnboundEntityDef (\ed -> ed { entityComments = Just (T.unlines comments) })
fixForeignKeysAll :: [UnboundEntityDef] -> [EntityDef]
fixForeignKeysAll unEnts = map fixForeignKeys unEnts
where
ents = map unboundEntityDef unEnts
entLookup = M.fromList $ map (\e -> (entityHaskell e, e)) ents
fixForeignKeys :: UnboundEntityDef -> EntityDef
fixForeignKeys (UnboundEntityDef foreigns ent) =
ent { entityForeigns = map (fixForeignKey ent) foreigns }
fixForeignKey :: EntityDef -> UnboundForeignDef -> ForeignDef
fixForeignKey ent (UnboundForeignDef foreignFieldTexts fdef) =
let pentError =
error $ "could not find table " ++ show (foreignRefTableHaskell fdef)
++ " fdef=" ++ show fdef ++ " allnames="
++ show (map (unHaskellName . entityHaskell . unboundEntityDef) unEnts)
++ "\n\nents=" ++ show ents
pent =
fromMaybe pentError $ M.lookup (foreignRefTableHaskell fdef) entLookup
in
case entityPrimary pent of
Just pdef ->
if length foreignFieldTexts /= length (compositeFields pdef)
then
lengthError pdef
else
let
fds_ffs =
zipWith (toForeignFields pent)
foreignFieldTexts
(compositeFields pdef)
dbname =
unDBName (entityDB pent)
oldDbName =
unDBName (foreignRefTableDBName fdef)
in fdef
{ foreignFields = map snd fds_ffs
, foreignNullable = setNull $ map fst fds_ffs
, foreignRefTableDBName =
DBName dbname
, foreignConstraintNameDBName =
DBName
. T.replace oldDbName dbname . unDBName
$ foreignConstraintNameDBName fdef
}
Nothing ->
error $ "no explicit primary key fdef="++show fdef++ " ent="++show ent
where
setNull :: [FieldDef] -> Bool
setNull [] = error "setNull: impossible!"
setNull (fd:fds) = let nullSetting = isNull fd in
if all ((nullSetting ==) . isNull) fds then nullSetting
else error $ "foreign key columns must all be nullable or non-nullable"
++ show (map (unHaskellName . fieldHaskell) (fd:fds))
isNull = (NotNullable /=) . nullable . fieldAttrs
toForeignFields pent fieldText pfd =
case chktypes fd haskellField (entityFields pent) pfh of
Just err -> error err
Nothing -> (fd, ((haskellField, fieldDB fd), (pfh, pfdb)))
where
fd = getFd (entityFields ent) haskellField
haskellField = HaskellName fieldText
(pfh, pfdb) = (fieldHaskell pfd, fieldDB pfd)
chktypes :: FieldDef -> HaskellName -> [FieldDef] -> HaskellName -> Maybe String
chktypes ffld _fkey pflds pkey =
if fieldType ffld == fieldType pfld then Nothing
else Just $ "fieldType mismatch: " ++ show (fieldType ffld) ++ ", " ++ show (fieldType pfld)
where
pfld = getFd pflds pkey
entName = entityHaskell ent
getFd [] t = error $ "foreign key constraint for: " ++ show (unHaskellName entName)
++ " unknown column: " ++ show t
getFd (f:fs) t
| fieldHaskell f == t = f
| otherwise = getFd fs t
lengthError pdef = error $ "found " ++ show (length foreignFieldTexts) ++ " fkeys and " ++ show (length (compositeFields pdef)) ++ " pkeys: fdef=" ++ show fdef ++ " pdef=" ++ show pdef
data UnboundEntityDef = UnboundEntityDef
{ _unboundForeignDefs :: [UnboundForeignDef]
, unboundEntityDef :: EntityDef
}
overUnboundEntityDef
:: (EntityDef -> EntityDef) -> UnboundEntityDef -> UnboundEntityDef
overUnboundEntityDef f ubed =
ubed { unboundEntityDef = f (unboundEntityDef ubed) }
lookupKeyVal :: Text -> [Text] -> Maybe Text
lookupKeyVal key = lookupPrefix $ key `mappend` "="
lookupPrefix :: Text -> [Text] -> Maybe Text
lookupPrefix prefix = msum . map (T.stripPrefix prefix)
mkEntityDef :: PersistSettings
-> Text
-> [Attr]
-> [Line]
-> UnboundEntityDef
mkEntityDef ps name entattribs lines =
UnboundEntityDef foreigns $
EntityDef
{ entityHaskell = entName
, entityDB = DBName $ getDbName ps name' entattribs
, entityId = (setComposite primaryComposite $ fromMaybe autoIdField idField)
, entityAttrs = entattribs
, entityFields = cols
, entityUniques = uniqs
, entityForeigns = []
, entityDerives = derives
, entityExtra = extras
, entitySum = isSum
, entityComments = comments
}
where
comments = Nothing
entName = HaskellName name'
(isSum, name') =
case T.uncons name of
Just ('+', x) -> (True, x)
_ -> (False, name)
(attribs, extras) = splitExtras lines
attribPrefix = flip lookupKeyVal entattribs
idName | Just _ <- attribPrefix "id" = error "id= is deprecated, ad a field named 'Id' and use sql="
| otherwise = Nothing
(idField, primaryComposite, uniqs, foreigns) = foldl' (\(mid, mp, us, fs) attr ->
let (i, p, u, f) = takeConstraint ps name' cols attr
squish xs m = xs `mappend` maybeToList m
in (just1 mid i, just1 mp p, squish us u, squish fs f)) (Nothing, Nothing, [],[]) attribs
derives = concat $ mapMaybe takeDerives attribs
cols :: [FieldDef]
cols = reverse . fst . foldr k ([], []) $ reverse attribs
k x (!acc, !comments) =
case isComment =<< listToMaybe x of
Just comment ->
(acc, comment : comments)
Nothing ->
( maybe id (:) (setFieldComments comments <$> takeColsEx ps x) acc
, []
)
setFieldComments [] x = x
setFieldComments xs fld =
fld { fieldComments = Just (T.unlines xs) }
autoIdField = mkAutoIdField ps entName (DBName `fmap` idName) idSqlType
idSqlType = maybe SqlInt64 (const $ SqlOther "Primary Key") primaryComposite
setComposite Nothing fd = fd
setComposite (Just c) fd = fd { fieldReference = CompositeRef c }
just1 :: (Show x) => Maybe x -> Maybe x -> Maybe x
just1 (Just x) (Just y) = error $ "expected only one of: "
`mappend` show x `mappend` " " `mappend` show y
just1 x y = x `mplus` y
mkAutoIdField :: PersistSettings -> HaskellName -> Maybe DBName -> SqlType -> FieldDef
mkAutoIdField ps entName idName idSqlType = FieldDef
{ fieldHaskell = HaskellName "Id"
, fieldDB = fromMaybe (DBName $ psIdName ps) idName
, fieldType = FTTypeCon Nothing $ keyConName $ unHaskellName entName
, fieldSqlType = idSqlType
, fieldReference = ForeignRef entName defaultReferenceTypeCon
, fieldAttrs = []
, fieldStrict = True
, fieldComments = Nothing
}
defaultReferenceTypeCon :: FieldType
defaultReferenceTypeCon = FTTypeCon (Just "Data.Int") "Int64"
keyConName :: Text -> Text
keyConName entName = entName `mappend` "Id"
splitExtras :: [Line] -> ([[Text]], M.Map Text [[Text]])
splitExtras [] = ([], M.empty)
splitExtras (Line indent [name]:rest)
| not (T.null name) && isUpper (T.head name) =
let (children, rest') = span ((> indent) . lineIndent) rest
(x, y) = splitExtras rest'
in (x, M.insert name (map tokens children) y)
splitExtras (Line _ ts:rest) =
let (x, y) = splitExtras rest
in (ts:x, y)
takeColsEx :: PersistSettings -> [Text] -> Maybe FieldDef
takeColsEx =
takeCols
(\ft perr -> error $ "Invalid field type " ++ show ft ++ " " ++ perr)
takeCols
:: (Text -> String -> Maybe FieldDef)
-> PersistSettings
-> [Text]
-> Maybe FieldDef
takeCols _ _ ("deriving":_) = Nothing
takeCols onErr ps (n':typ:rest)
| not (T.null n) && isLower (T.head n) =
case parseFieldType typ of
Left err -> onErr typ err
Right ft -> Just FieldDef
{ fieldHaskell = HaskellName n
, fieldDB = DBName $ getDbName ps n rest
, fieldType = ft
, fieldSqlType = SqlOther $ "SqlType unset for " `mappend` n
, fieldAttrs = rest
, fieldStrict = fromMaybe (psStrictFields ps) mstrict
, fieldReference = NoReference
, fieldComments = Nothing
}
where
(mstrict, n)
| Just x <- T.stripPrefix "!" n' = (Just True, x)
| Just x <- T.stripPrefix "~" n' = (Just False, x)
| otherwise = (Nothing, n')
takeCols _ _ _ = Nothing
getDbName :: PersistSettings -> Text -> [Text] -> Text
getDbName ps n [] = psToDBName ps n
getDbName ps n (a:as) = fromMaybe (getDbName ps n as) $ T.stripPrefix "sql=" a
takeConstraint :: PersistSettings
-> Text
-> [FieldDef]
-> [Text]
-> (Maybe FieldDef, Maybe CompositeDef, Maybe UniqueDef, Maybe UnboundForeignDef)
takeConstraint ps tableName defs (n:rest) | not (T.null n) && isUpper (T.head n) = takeConstraint'
where
takeConstraint'
| n == "Unique" = (Nothing, Nothing, Just $ takeUniq ps tableName defs rest, Nothing)
| n == "Foreign" = (Nothing, Nothing, Nothing, Just $ takeForeign ps tableName defs rest)
| n == "Primary" = (Nothing, Just $ takeComposite defs rest, Nothing, Nothing)
| n == "Id" = (Just $ takeId ps tableName (n:rest), Nothing, Nothing, Nothing)
| otherwise = (Nothing, Nothing, Just $ takeUniq ps "" defs (n:rest), Nothing)
takeConstraint _ _ _ _ = (Nothing, Nothing, Nothing, Nothing)
takeId :: PersistSettings -> Text -> [Text] -> FieldDef
takeId ps tableName (n:rest) = fromMaybe (error "takeId: impossible!") $ setFieldDef $
takeCols (\_ _ -> addDefaultIdType) ps (field:rest `mappend` setIdName)
where
field = case T.uncons n of
Nothing -> error "takeId: empty field"
Just (f, ield) -> toLower f `T.cons` ield
addDefaultIdType = takeColsEx ps (field : keyCon : rest `mappend` setIdName)
setFieldDef = fmap (\fd ->
let refFieldType = if fieldType fd == FTTypeCon Nothing keyCon
then defaultReferenceTypeCon
else fieldType fd
in fd { fieldReference = ForeignRef (HaskellName tableName) $ refFieldType
})
keyCon = keyConName tableName
setIdName = ["sql=" `mappend` psIdName ps]
takeId _ tableName _ = error $ "empty Id field for " `mappend` show tableName
takeComposite :: [FieldDef]
-> [Text]
-> CompositeDef
takeComposite fields pkcols
= CompositeDef
(map (getDef fields) pkcols)
attrs
where
(_, attrs) = break ("!" `T.isPrefixOf`) pkcols
getDef [] t = error $ "Unknown column in primary key constraint: " ++ show t
getDef (d:ds) t
| fieldHaskell d == HaskellName t =
if nullable (fieldAttrs d) /= NotNullable
then error $ "primary key column cannot be nullable: " ++ show t
else d
| otherwise = getDef ds t
takeUniq :: PersistSettings
-> Text
-> [FieldDef]
-> [Text]
-> UniqueDef
takeUniq ps tableName defs (n:rest)
| not (T.null n) && isUpper (T.head n)
= UniqueDef
(HaskellName n)
dbName
(map (HaskellName &&& getDBName defs) fields)
attrs
where
isAttr a =
"!" `T.isPrefixOf` a
isSqlName a =
"sql=" `T.isPrefixOf` a
isNonField a =
isAttr a
|| isSqlName a
(fields, nonFields) =
break isNonField rest
attrs = filter isAttr nonFields
usualDbName =
DBName $ psToDBName ps (tableName `T.append` n)
sqlName :: Maybe DBName
sqlName =
case find isSqlName nonFields of
Nothing ->
Nothing
(Just t) ->
case drop 1 $ T.splitOn "=" t of
(x : _) -> Just (DBName x)
_ -> Nothing
dbName = fromMaybe usualDbName sqlName
getDBName [] t =
error $ "Unknown column in unique constraint: " ++ show t
++ " " ++ show defs ++ show n ++ " " ++ show attrs
getDBName (d:ds) t
| fieldHaskell d == HaskellName t = fieldDB d
| otherwise = getDBName ds t
takeUniq _ tableName _ xs =
error $ "invalid unique constraint on table["
++ show tableName
++ "] expecting an uppercase constraint name xs="
++ show xs
data UnboundForeignDef = UnboundForeignDef
{ _unboundFields :: [Text]
, _unboundForeignDef :: ForeignDef
}
takeForeign :: PersistSettings
-> Text
-> [FieldDef]
-> [Text]
-> UnboundForeignDef
takeForeign ps tableName _defs (refTableName:n:rest)
| not (T.null n) && isLower (T.head n)
= UnboundForeignDef fields $ ForeignDef
{ foreignRefTableHaskell =
HaskellName refTableName
, foreignRefTableDBName =
DBName $ psToDBName ps refTableName
, foreignConstraintNameHaskell =
HaskellName n
, foreignConstraintNameDBName =
DBName $ psToDBName ps (tableName `T.append` n)
, foreignFields =
[]
, foreignAttrs =
attrs
, foreignNullable =
False
}
where
(fields,attrs) = break ("!" `T.isPrefixOf`) rest
takeForeign _ tableName _ xs = error $ "invalid foreign key constraint on table[" ++ show tableName ++ "] expecting a lower case constraint name xs=" ++ show xs
takeDerives :: [Text] -> Maybe [Text]
takeDerives ("deriving":rest) = Just rest
takeDerives _ = Nothing
nullable :: [Text] -> IsNullable
nullable s
| "Maybe" `elem` s = Nullable ByMaybeAttr
| "nullable" `elem` s = Nullable ByNullableAttr
| otherwise = NotNullable