persistent-discover-0.1.0.5: src/Database/Persist/Discover/Exe.hs
-- | This module contains types, definitions, and the logic behind
-- finding persistent definitions.
--
-- In brief, we do an import of all the models defined in the current
-- directory or any subdirectories. These imports are "instances only"
-- imports. Then we splice in @$(discoverEntities)@ to
--
-- The result module is named @All@, and it's placed in the hierarchy where
-- you define this. So if you have a source file:
--
-- @
-- -- src/PersistentModels/All.hs
--
-- {-# OPTIONS_GHC -F -pgmF persistent-discover
-- @
--
-- Then it will translate to:
--
-- @
-- -- src/PersistentModels/All.hs
--
-- module PersistentModels.All where
--
-- import PersistentModels.Foo ()
-- import PersistentModels.Bar ()
-- import PersistentModels.Baz ()
--
-- allEntityDefs :: [EntityDef]
-- allEntityDefs = $(discoverEntities)
-- @
--
-- @since 0.1.0.0
module Database.Persist.Discover.Exe where
import System.FilePath
import Control.Monad.State
import Data.String
import Data.DList (DList(..))
import qualified Data.DList as DList
import Data.Foldable (for_)
import System.Directory
import Data.List
import Data.Char
import Control.Applicative
import Data.Maybe
newtype Source = Source FilePath
newtype Destination = Destination FilePath
data AllModelsFile = AllModelsFile
{ amfModuleBase :: Module
, amfModuleImports :: [Module]
}
render :: Render -> String
render action =
unlines $ DList.toList $ execState (unRender action) mempty
renderLine :: Render -> Render
renderLine action =
fromString $ mconcat $ DList.toList $ execState (unRender action) mempty
newtype Render' a = Render { unRender :: State (DList String) a }
deriving newtype
(Functor, Applicative, Monad)
type Render = Render' ()
instance (a ~ ()) => IsString (Render' a) where
fromString str =
Render (modify (\s -> s <> pure str))
indent :: Int -> Render -> Render
indent i doc =
Render do
let
new =
fmap (replicate i ' ' <>)
$ execState (unRender doc) mempty
modify (<> new)
-- |
--
-- @since 0.1.0.0
discoverModels
:: Source
-> Destination
-> IO ()
discoverModels (Source src) (Destination dest) = do
let (dir, file) = splitFileName src
files <- filter (/= file) <$> getFilesRecursive dir
let
input =
AllModelsFile
{ amfModuleBase =
fromJust $ pathToModule src
, amfModuleImports =
mapMaybe pathToModule (fmap (dir </>) files)
}
output =
renderFile input
writeFile dest output
-- | Returns a list of relative paths to all files in the given directory.
getFilesRecursive :: FilePath -- ^ The directory to search.
-> IO [FilePath]
getFilesRecursive baseDir = sort <$> go []
where
go :: FilePath -> IO [FilePath]
go dir = do
c <- map (dir </>) . filter (`notElem` [".", ".."]) <$> getDirectoryContents (baseDir </> dir)
dirs <- filterM (doesDirectoryExist . (baseDir </>)) c >>= mapM go
files <- filterM (doesFileExist . (baseDir </>)) c
return (files ++ concat dirs)
renderFile
:: AllModelsFile
-> String
renderFile amf = render do
let
modName =
moduleName $ amfModuleBase amf
renderLine do
"{-# LINE 1 "
fromString $ show modName
" #-}"
"{-# LANGUAGE TemplateHaskell #-}"
""
renderLine do
"module "
fromString $ modName
" where"
""
for_ (amfModuleImports amf) \mod' ->
renderLine do
"import "
fromString $ moduleName mod'
" ()"
""
"import Database.Persist.TH (discoverEntities)"
"import Database.Persist.Types (EntityDef)"
""
"-- | All of the entity definitions, as discovered by the @persistent-discover@ utility."
"allEntityDefs :: [EntityDef]"
"allEntityDefs = $(discoverEntities)"
-- -- | Derive module name from specified path.
-- pathToModule :: FilePath -> Module
-- pathToModule f =
-- Module
-- { moduleName =
-- intercalate "." $ mapMaybe go $ splitDirectories f
-- , modulePath =
-- f
-- }
-- where
-- go :: String -> Maybe String
-- go (c:cs) =
-- Just (toUpper c : cs)
-- fileName = last $ splitDirectories f
-- m:ms = takeWhile (/='.') fileName
data Module = Module
{ moduleName :: String
, modulePath :: FilePath
}
deriving (Eq, Show)
mkModulePieces
:: FilePath
-> [String]
mkModulePieces fp = do
let
extension =
takeExtension fp
guard (extension == ".hs" || extension == ".lhs")
reverse
. takeWhile (not . isLowerFirst)
. reverse
. filter noDots
. splitDirectories
. dropExtension
$ fp
where
noDots x =
"." /= x && ".." /= x
isLowerFirst :: String -> Bool
isLowerFirst [] = True
isLowerFirst (c:_) = isLower c
pathToModule
:: FilePath
-> Maybe Module
pathToModule file = do
case mkModulePieces file of
[] ->
empty
x : xs -> do
guard $ all isValidModuleName (x : xs)
pure (Module (intercalate "." (x:xs)) file)
-- | Returns True if the given string is a valid task module name.
-- See `Cabal.Distribution.ModuleName` (http://git.io/bj34)
isValidModuleName :: String -> Bool
isValidModuleName [] = False
isValidModuleName (c:cs) = isUpper c && all isValidModuleChar cs
-- | Returns True if the given Char is a valid taks module character.
isValidModuleChar :: Char -> Bool
isValidModuleChar c = isAlphaNum c || c == '_' || c == '\''
-- | Convert a String in camel case to snake case.
casify :: String -> String
casify str = intercalate "_" $ groupBy (\a b -> isUpper a && isLower b) str
stripSuffix :: Eq a => [a] -> [a] -> Maybe [a]
stripSuffix suffix str =
reverse <$> stripPrefix (reverse suffix) (reverse str)