packages feed

ihp-ide-1.4.0: IHP/IDE/CodeGen/Controller.hs

module IHP.IDE.CodeGen.Controller where

import IHP.ControllerPrelude hiding (appPort)
import IHP.IDE.ToolServer.Types
import IHP.IDE.CodeGen.View.Generators
import IHP.IDE.CodeGen.View.NewController
import IHP.IDE.CodeGen.View.NewScript
import IHP.IDE.CodeGen.View.NewView
import IHP.IDE.CodeGen.View.NewMail
import IHP.IDE.CodeGen.View.NewAction
import IHP.IDE.CodeGen.View.NewApplication
import IHP.IDE.CodeGen.View.NewJob
import IHP.IDE.CodeGen.Types
import IHP.IDE.CodeGen.ControllerGenerator as ControllerGenerator
import IHP.IDE.CodeGen.ScriptGenerator as ScriptGenerator
import IHP.IDE.CodeGen.ViewGenerator as ViewGenerator
import IHP.IDE.CodeGen.MailGenerator as MailGenerator
import IHP.IDE.CodeGen.ActionGenerator as ActionGenerator
import IHP.IDE.CodeGen.ApplicationGenerator as ApplicationGenerator
import IHP.IDE.CodeGen.JobGenerator as JobGenerator
import IHP.IDE.ToolServer.Helper.Controller
import qualified System.Process as Process
import qualified System.Directory as Directory
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import qualified Text.Inflections as Inflector
import System.Directory

instance Controller CodeGenController where
    action GeneratorsAction = do
        render GeneratorsView

    action NewControllerAction = do
        let controllerName = paramOrDefault "" "name"
        let applicationName = paramOrDefault "Web" "applicationName"
        let pagination = paramOrDefault False "pagination"
        controllerAlreadyExists <- doesControllerExist controllerName applicationName
        applications <- findApplications
        when controllerAlreadyExists do
            setErrorMessage "Controller with this name does already exist."
            redirectTo NewControllerAction
        plan <- ControllerGenerator.buildPlan controllerName applicationName pagination
        render NewControllerView { .. }
        where
            doesControllerExist controllerName applicationName = doesFileExist $ cs applicationName <> "/Controller/" <> cs controllerName <> ".hs"

    action CreateControllerAction = do
        let controllerName = param "name"
        let applicationName = param "applicationName"
        let pagination = paramOrDefault False "pagination"
        (Right plan) <- ControllerGenerator.buildPlan controllerName applicationName pagination
        executePlan plan
        setSuccessMessage "Controller generated"
        redirectTo GeneratorsAction

    action NewScriptAction = do
        let scriptName = paramOrDefault "" "name"
        scriptAlreadyExists <- doesFileExist $ "Application/Script/" <> cs scriptName <> ".hs"
        when scriptAlreadyExists do
            setErrorMessage "Script with this name already exists."
            redirectTo NewScriptAction
        let plan = ScriptGenerator.buildPlan scriptName
        render NewScriptView { .. }

    action CreateScriptAction = do
        let scriptName = paramOrDefault "" "name"
        let (Right plan) = ScriptGenerator.buildPlan scriptName
        executePlan plan
        setSuccessMessage "Script generated"
        redirectTo GeneratorsAction

    action NewViewAction = do
        let viewName = paramOrDefault "" "name"
        let applicationName = paramOrDefault "Web" "applicationName"
        let controllerName = paramOrDefault "" "controllerName"
        viewAlreadyExists <- doesFileExist $ (cs applicationName) <> "/View/" <> (cs controllerName) <> "/" <> (cs viewName) <>".hs"
        when viewAlreadyExists do
            setErrorMessage "View with this name already exists."
            redirectTo NewViewAction
        controllers <- findControllers applicationName
        applications <- findApplications
        plan <- ViewGenerator.buildPlan viewName applicationName controllerName
        render NewViewView { .. }

    action CreateViewAction = do
        let viewName = paramOrDefault "" "name"
        let applicationName = paramOrDefault "Web" "applicationName"
        let controllerName = paramOrDefault "" "controllerName"
        (Right plan) <- ViewGenerator.buildPlan viewName applicationName controllerName
        executePlan plan
        setSuccessMessage "View generated"
        redirectTo GeneratorsAction

    action NewMailAction = do
        let mailName = paramOrDefault "" "name"
        let applicationName = paramOrDefault "Web" "applicationName"
        let controllerName = paramOrDefault "" "controllerName"
        mailAlreadyExists <- doesFileExist $ (cs applicationName) <> "/Mail/" <> (cs controllerName) <> "/" <> (cs mailName) <>".hs"
        when mailAlreadyExists do
            setErrorMessage "Mail with this name already exists."
            redirectTo NewMailAction
        controllers <- findControllers applicationName
        applications <- findApplications
        plan <- MailGenerator.buildPlan mailName applicationName controllerName
        render NewMailView { .. }

    action CreateMailAction = do
        let mailName = paramOrDefault "" "name"
        let applicationName = paramOrDefault "Web" "applicationName"
        let controllerName = paramOrDefault "" "controllerName"
        (Right plan) <- MailGenerator.buildPlan mailName applicationName controllerName
        executePlan plan
        setSuccessMessage "Mail generated"
        redirectTo GeneratorsAction

    action NewActionAction = do
        let actionName = paramOrDefault "" "name"
        let applicationName = paramOrDefault "Web" "applicationName"
        let controllerName = paramOrDefault "" "controllerName"
        let doGenerateView = paramOrDefault False "doGenerateView"
        controllers <- findWebControllers
        applications <- findApplications
        plan <- ActionGenerator.buildPlan actionName applicationName controllerName doGenerateView
        render NewActionView { .. }

    action CreateActionAction = do
        let actionName = paramOrDefault "" "name"
        let applicationName = paramOrDefault "Web" "applicationName"
        let controllerName = paramOrDefault "" "controllerName"
        let doGenerateView = paramOrDefault False "doGenerateView"
        (Right plan) <- ActionGenerator.buildPlan actionName applicationName controllerName doGenerateView
        executePlan plan
        setSuccessMessage $ "Action" ++ (if doGenerateView then " and View " else "") ++ " generated"
        redirectTo GeneratorsAction

    action NewApplicationAction = do
        let applicationName = paramOrDefault "" "name"
        plan <- ApplicationGenerator.buildPlan applicationName
        render NewApplicationView { .. }

    action CreateApplicationAction = do
        let applicationName = paramOrDefault "" "name"
        (Right plan) <- ApplicationGenerator.buildPlan applicationName
        executePlan plan
        setSuccessMessage "Application generated"
        redirectTo GeneratorsAction

    action NewJobAction = do
        let jobName = paramOrDefault "" "name"
        let applicationName = paramOrDefault "Web" "applicationName"
        controllers <- findControllers applicationName
        applications <- findApplications
        plan <- JobGenerator.buildPlan jobName applicationName
        render NewJobView { .. }

    action CreateJobAction = do
        let jobName = paramOrDefault "" "name"
        let applicationName = paramOrDefault "Web" "applicationName"
        (Right plan) <- JobGenerator.buildPlan jobName applicationName
        executePlan plan
        setSuccessMessage "Job generated"
        redirectTo GeneratorsAction

    action OpenControllerAction = do
        let name = param "name"
        case name |> Inflector.toCamelCased True of
            Left error -> renderPlain "Failed to transform name to camel case"
            Right indexActionName-> do
                appPort <- theAppPort
                redirectToUrl ("http://localhost:" <> tshow appPort <> "/" <> indexActionName)


executePlan :: [GeneratorAction] -> IO ()
executePlan actions = forEach actions evalAction
    where
        evalAction CreateFile { filePath, fileContent } = do
            Text.writeFile (cs filePath) (cs fileContent)
            putStrLn ("+ " <> filePath)
        evalAction AppendToFile { filePath, fileContent } = do
            Text.appendFile (cs filePath) fileContent
            putStrLn ("* " <> filePath)
        evalAction AppendToMarker { marker, filePath, fileContent } = do
            content <- Text.readFile (cs filePath)
            let newContent = Text.replace marker (marker <> "\n" <> cs fileContent) (cs content)
            Text.writeFile (cs filePath) (cs newContent)
            putStrLn ("* " <> filePath <> " (import)")
        evalAction AddImport { filePath, fileContent } = do
            addImport filePath fileContent
            putStrLn ("* " <> filePath <> " (import)")
        evalAction AddAction { filePath, fileContent } = do
            addAction filePath [fileContent]
            putStrLn ("* " <> filePath <> " (AddAction)")
        evalAction AddMountToFrontController { filePath, applicationName } = do
            addMountControllerStatement filePath applicationName
            putStrLn ("* " <> filePath <> " (AddMountToFrontController)")
        evalAction AddToDataConstructor { dataConstructor, filePath, fileContent } = do
            content <- Text.readFile (cs filePath)
            case addToDataConstructor content dataConstructor fileContent of
                Just newContent -> do
                    Text.writeFile (cs filePath) (cs newContent)
                    putStrLn ("* " <> filePath <> " (AddToDataConstructor)")
                Nothing -> putStrLn ("Could not automatically add " <> tshow content <> " to " <> filePath)
        evalAction EnsureDirectory { directory } = do
            Directory.createDirectoryIfMissing True (cs directory)
        evalAction RunShellCommand { shellCommand } = do
            _ <- Process.system (cs shellCommand)
            putStrLn ("* " <> shellCommand)

undoPlan :: [GeneratorAction] -> IO()
undoPlan actions = forEach actions evalAction
    where
        evalAction CreateFile { filePath, fileContent } = do
            (Directory.removeFile (cs filePath)) `catch` handleError
            putStrLn ("- " <> filePath)
        evalAction AppendToFile { filePath, fileContent } = do
            deleteTextFromFile (cs filePath) fileContent `catch` handleError
            putStrLn ("* " <> filePath)
        evalAction AppendToMarker { marker, filePath, fileContent } = do
            (deleteTextFromFile (cs filePath) (fileContent <> "\n")) `catch` handleError
            putStrLn ("* " <> filePath <> " (import)")
        evalAction AddImport { filePath, fileContent } = do
            (deleteTextFromFile (cs filePath) (fileContent <> "\n")) `catch` handleError
            putStrLn ("* " <> filePath <> " (import)")
        evalAction AddAction { filePath, fileContent } = do
            (deleteTextFromFile (cs filePath) (fileContent <> "\n")) `catch` handleError
            putStrLn ("* " <> filePath <> " (RemoveAction)")
        evalAction AddToDataConstructor { dataConstructor, filePath, fileContent } = do
            (deleteTextFromFile (cs filePath) (fileContent <> "\n")) `catch` handleError
            putStrLn ("* " <> filePath <> " (RemoveFromDataConstructor)")
        evalAction EnsureDirectory { directory } = do
            (Directory.removeDirectory (cs directory)) `catch` handleError
        evalAction RunShellCommand { shellCommand } = pure ()
        handleError :: SomeException -> IO ()
        handleError ex = putStrLn (tshow ex)

deleteTextFromFile :: Text -> Text -> IO ()
deleteTextFromFile filePath lineContent = do
    fileContent <- Text.readFile (cs filePath)
    let replacedContent = Text.replace lineContent "" fileContent
    Text.writeFile (cs filePath) replacedContent

addImport :: Text -> Text -> IO ()
addImport file importStatement = do
    content :: Text <- Text.readFile (cs file)
    case addImport' content importStatement of
        Just newContent -> Text.writeFile (cs file) (cs newContent)
        Nothing -> pure ()
    pure ()

addImport' :: Text -> Text -> Maybe Text
addImport' content importStatement = do
    if importStatement `isInfixOf` content
        then Nothing
        else appendLineAfter content ("import" `isPrefixOf`) [importStatement]

addAction :: Text -> [Text] -> IO ()
addAction filePath fileContent = do
    content <- Text.readFile (cs filePath)
    case addAction' content fileContent of
        Just newContent -> Text.writeFile (cs filePath) (cs newContent)
        Nothing -> putStrLn ("Could not automatically add " <> tshow content <> " to " <> filePath)
    pure ()

addAction' :: Text -> [Text] -> Maybe Text
addAction' fileContent = appendLineAfter fileContent ("instance Controller" `isPrefixOf`)

addMountControllerStatement :: Text -> Text -> IO ()
addMountControllerStatement file applicationName = do
    content :: Text <- Text.readFile (cs file)
    case addMountControllerStatement' applicationName content of
        Just newContent -> Text.writeFile (cs file) (cs newContent)
        Nothing -> putStrLn ("Could not automatically add " <> tshow applicationName <> " to " <> file)
    pure ()

addMountControllerStatement' :: Text -> Text -> Maybe Text
addMountControllerStatement' applicationName file =
    let withMaybeMountedFrontController = appendLineAfter file ("mountFrontController" `isInfixOf`) ["            , mountFrontController " <> applicationName <> "Application"]
    in
        case withMaybeMountedFrontController of
            Just result -> Just result
            Nothing -> Just (Text.replace needle replacement file)
                where
                    needle =  "    controllers = []"
                    replacement = "    controllers = [\n            mountFrontController " <> applicationName <> "Application" <> "\n        ]"

-- | Gets content of a Types.hs, a existent data constructor and a type which should be added to it
--   and returns fileContent with the type in it.
addToDataConstructor :: Text -> Text -> Text -> Maybe Text
addToDataConstructor fileContent dataConstructor content = do
    lineOfDataConstructor <- lines fileContent
        |> zip [1..]
        |> filter (\(n, line) -> dataConstructor `isInfixOf` line)
        |> lastMay
        |> fmap fst
    lineOfDerivingStatement <- ((drop lineOfDataConstructor $ lines fileContent) :: [Text])
        |> zip [lineOfDataConstructor..]
        |> filter (\(n, line) -> "deriving" `isInfixOf` line)
        |> headMay
        |> fmap fst
    Just $ unlines $ ((take lineOfDerivingStatement $ lines fileContent) <> [content] <> (drop lineOfDerivingStatement $ lines fileContent))

appendLineAfter :: Text -> (Text -> Bool) -> [Text] -> Maybe Text
appendLineAfter file isRelevantLine newLines =
    let content :: [Text] = lines file
        lastImportLine = content
            |> zip [1..]
            |> filter (\(n, line) -> isRelevantLine line)
            |> lastMay
            |> fmap fst
    in fmap (\lastImportLine -> unlines $ (take lastImportLine content) <> newLines <> (drop lastImportLine content)) lastImportLine