packages feed

mole-0.0.7: src/Main.hs

{-# LANGUAGE OverloadedStrings #-}

module Main where

import           Control.Concurrent.STM
import           Control.Monad
import           Control.Monad.Reader
import           Control.Monad.Trans.Except
import           Control.Applicative

import           Data.Map (Map)
import qualified Data.Map as M

import qualified Data.Set as S

import qualified Data.ByteString as BS

import           Data.Text (Text)
import qualified Data.Text as T

import           Data.List hiding (stripPrefix)

import           Network.URI

import           System.FilePath
import           System.FilePath.Find as FPF
import           System.Directory

import           Data.Mole.Types
import           Data.Mole.Core
import           Data.Mole.Server
import           Data.Mole.Watcher
import           Data.Mole.Builder
import           Data.Mole.Builder.External
import           Data.Mole.Builder.Binary

import           Options.Applicative
import           Options.Applicative.Types



main :: IO ()
main = do
    opt <- execParser (parseOptions `withInfo` "mole")
    run opt (optCommand opt)


data Options = Options
    { optCommand :: Command

    , optPaths :: [FilePath]
      -- ^ The paths where auto-discovery looks for assets.

    , optAssets :: [(AssetId, Options -> String -> IO AssetDefinition)]
      -- ^ Additional assets that are specified on the commandline.
    }

data Command
    = Version
    | Build !FilePath
    | Serve !Int !(Maybe FilePath)


run :: Options -> Command -> IO ()
run _ Version = putStrLn "HEAD"

run opt (Build outputDir) = do
    config <- mkConfig opt outputDir
    h <- newHandle config

    forM_ (entryPoints config) $ \aId ->
        markDirty h aId

    _ <- require h (S.fromList $ entryPoints config)

    aids <- atomically $ do
        s <- readTVar (state h)
        let assetIds = M.keys $ M.filter (\ars -> case arsState ars of Completed _ -> True; Failed _ -> True; _ -> False) (M.restrictKeys (assets s) (S.fromList $ entryPoints config))
        if length assetIds /= (length (entryPoints config)) then retry else pure assetIds

    atomically $ do
        e <- isEmptyTQueue (messages h)
        unless e retry

    atomically $ do
        e <- isEmptyTQueue (emitStream h)
        unless e retry

run opt (Serve port mbSocketPath) = do
    config <- mkConfig opt ""
    h <- newHandle config

    attachFileWatcher h

    -- Mark all entry points as dirty to have the assets ready as soon as
    -- possible, hopefully even before the user opens the web server.
    forM_ (entryPoints config) $ \aId ->
        markDirty h aId

    serveFiles h port mbSocketPath


collectAssetDefinitions :: FilePath -> FilePath -> IO (Map AssetId AssetDefinition)
collectAssetDefinitions outputDir basePath = do
    FPF.fold (pure True) f M.empty basePath
  where
    f m fi = if takeExtension p == ".html"
        then M.insert
            (AssetId $ T.pack $ drop (length basePath + 1) $ p)
            (AssetDefinition (builderForFile basePath p) transformPublicIdentifierDef (emitResultDef outputDir))
            m
        else m
      where p = infoPath fi


transformPublicIdentifierDef :: PublicIdentifier -> PublicIdentifier
transformPublicIdentifierDef pubId = case T.uncons $ unPublicIdentifier pubId of
    Nothing       -> pubId
    Just ('/', _) -> pubId
    _             -> PublicIdentifier $ T.cons '/' $ unPublicIdentifier pubId


locateSource :: Options -> AssetId -> IO (Maybe (String, String))
locateSource opt (AssetId aId) = do
    res <- concat <$> mapM (\basePath -> do
            paths <- FPF.find (pure True) f basePath
            return $ zip (repeat basePath) paths
        ) (optPaths opt)
    case sortOn (length . snd) res of
        (basePath, x):_ -> return $ Just (basePath, x)
        _               -> return Nothing

  where
    f = do
        p <- filePath
        t <- fileType
        return $ t == RegularFile && isSuffixOf (T.unpack aId) p


defAutoDiscovery :: Options -> FilePath -> Handle -> AssetId -> IO (Maybe AssetDefinition)
defAutoDiscovery opt outputDir _ (AssetId aId)
    | aId == "" = do
        return $ Just $ AssetDefinition (externalBuilder $ PublicIdentifier aId) id (emitResultDef outputDir)
    | isURI $ T.unpack aId = do
        return $ Just $ AssetDefinition (externalBuilder $ PublicIdentifier aId) id (emitResultDef outputDir)
    -- | head aId == '/' = do
        -- logMessage h (AssetId aId) $ "Starts with a slash, treating as external!"
        -- return $ Just $ AssetDefinition (externalBuilder aId) id (emitResultDef outputDir)
    | otherwise = do
        mbSource <- locateSource opt (AssetId aId)
        case mbSource of
            Nothing -> return Nothing
            Just (basePath, x) -> do
                -- logMessage h (AssetId aId) $ "Found asset at " ++ x
                return $ Just $ AssetDefinition (builderForFile basePath x)
                    transformPublicIdentifierDef (emitResultDef outputDir)


emitResultDef :: FilePath -> Handle -> AssetId -> Result -> IO ()
emitResultDef dist _ _ (Result pubId mbRes) = do
    case mbRes of
        Nothing -> return ()
        Just (body, _) -> when (dist /= "") $ do
            -- putStrLn $ "emit " ++ pubId
            createDirectoryIfMissing True $ dist `joinDrive` (takeDirectory $ T.unpack $ unPublicIdentifier pubId)
            BS.writeFile (dist `joinDrive` T.unpack (unPublicIdentifier pubId)) body


mkConfig :: Options -> FilePath -> IO Config
mkConfig opt outputDir = do
    otherAssets <- mconcat <$> mapM (collectAssetDefinitions outputDir) (optPaths opt)

    oAssets <- forM (optAssets opt) $ \(aId, m) -> do
        ad <- m opt outputDir
        return (aId, ad)

    let allAssets = M.fromList oAssets <> otherAssets
    let allEntryPoints = filter (\(AssetId a) -> T.isSuffixOf ".html" a) $ M.keys allAssets
    -- print $ M.keys allAssets

    -- let otherAssets = M.fromList $ (flip map) (optEntryPoints opt) $ \(AssetId aId) ->
    --         ( AssetId aId
    --         , AssetDefinition (rawBuilder aId "application/octet-stream" ("_site/" ++ aId)) transformPublicIdentifierDef (emitResultDef outputDir)
    --         )

    return $ Config (allAssets <> otherAssets) (defAutoDiscovery opt outputDir)
        (allEntryPoints ++ (map fst $ optAssets opt))




parseOptions :: Parser Options
parseOptions = (\cmd paths extAssets rawAssets -> Options cmd paths (extAssets <> rawAssets))
    <$> parseCommand
    <*> parsePaths
    <*> many (parseAsset Ext)
    <*> many (parseAsset Raw)

parseCommand :: Parser Command
parseCommand = subparser $ mconcat
    [ command "version"
        (parseVersion `withInfo` "Print the version and exit")
    , command "build"
        (parseBuild `withInfo` "Build the website")
    , command "serve"
        (parseServe `withInfo` "Serve the website")
    ]

parseVersion :: Parser Command
parseVersion = pure Version

parseBuild :: Parser Command
parseBuild = Build
    <$> strArgument (metavar "OUTPUT-DIRECTORY" <> help "Where to write the files to.")

parseServe :: Parser Command
parseServe = Serve
    <$> option auto (long "port" <> short 'p' <> metavar "PORT" <> value 8000)
    <*> option (Just <$> str) (long "socket-path" <> short 'u' <> metavar "SOCKET-PATH" <> value Nothing)

askText :: ReaderT String (Except ParseError) Text
askText = T.pack <$> ask

assetIdRead :: ReadM AssetId
assetIdRead = ReadM $ AssetId <$> askText

data AssetType = Ext | Raw

assetRead :: AssetType -> ReadM (AssetId, Options -> String -> IO AssetDefinition)
assetRead at = ReadM $ do
    v <- askText
    case T.splitOn "=" v of
        [aId, p] -> return $ ad aId $ PublicIdentifier p
        _ -> lift $ throwE $ ErrorMsg "ASSET=DEFINITION"

  where
    ad aId p = case at of
        Ext -> (AssetId aId, \_ _ -> return $ AssetDefinition (externalBuilder p) id (\_ _ _ -> return ()))
        Raw ->
            ( AssetId aId
            , \opt outputDir -> do
                mbSource <- locateSource opt (AssetId aId)
                case mbSource of
                    Nothing -> error $ "Could not find asset " ++ T.unpack aId
                    Just (_, p') -> do
                        return $ AssetDefinition (rawBuilder p p' "application/octet-stream") transformPublicIdentifierDef (emitResultDef outputDir)
            )


parseAsset :: AssetType -> Parser (AssetId, Options -> String -> IO AssetDefinition)
parseAsset at = option (assetRead at)
    ( long (prefix <> "-asset") <> metavar "ASSET=DEFINITION" )
  where
    prefix = case at of
        Ext -> "external"
        Raw -> "raw"


parsePaths :: Parser [FilePath]
parsePaths = option pathRead
    ( long "paths" <> short 'p' <> metavar "SEARCH:PATH:DIRS:..."<> value ["assets/"] )
  where
    pathRead = ReadM $ do
        v <- askText
        case map T.unpack $ T.splitOn ":" v of
            (x:xs) -> return $ x:xs
            _ ->  lift $ throwE $ ErrorMsg "SEARCH:PATH:DIRS:..."


withInfo :: Parser a -> String -> ParserInfo a
withInfo opts desc = info (helper <*> opts) $ progDesc desc