module IHP.IDE.CodeGen.ControllerGenerator (buildPlan, buildPlan') where import ClassyPrelude import IHP.NameSupport import IHP.HaskellSupport import qualified Data.Text as Text import qualified Data.Char as Char import qualified IHP.IDE.SchemaDesigner.Parser as SchemaDesigner import IHP.IDE.SchemaDesigner.Types import IHP.IDE.CodeGen.Types import qualified IHP.IDE.CodeGen.ViewGenerator as ViewGenerator import Text.Countable (singularize, pluralize) buildPlan :: Text -> Text -> Bool -> IO (Either Text [GeneratorAction]) buildPlan rawControllerName applicationName paginationEnabled = do schema <- SchemaDesigner.parseSchemaSql >>= \case Left parserError -> pure [] Right statements -> pure statements let controllerName = tableNameToControllerName rawControllerName let modelName = tableNameToModelName rawControllerName pure $ Right $ buildPlan' schema applicationName controllerName modelName paginationEnabled buildPlan' schema applicationName controllerName modelName paginationEnabled = let config = ControllerConfig { modelName, controllerName, applicationName, paginationEnabled } viewPlans = generateViews schema applicationName controllerName paginationEnabled in [ CreateFile { filePath = applicationName <> "/Controller/" <> controllerName <> ".hs", fileContent = (generateController schema config) } , AppendToFile { filePath = applicationName <> "/Routes.hs", fileContent = "\n" <> (controllerInstance config) } , AppendToFile { filePath = applicationName <> "/Types.hs", fileContent = (generateControllerData config) } , AppendToMarker { marker = "-- Controller Imports", filePath = applicationName <> "/FrontController.hs", fileContent = ("import " <> applicationName <> ".Controller." <> controllerName) } , AppendToMarker { marker = "-- Generator Marker", filePath = applicationName <> "/FrontController.hs", fileContent = (" , parseRoute @" <> controllerName <> "Controller") } ] <> viewPlans data ControllerConfig = ControllerConfig { controllerName :: Text , applicationName :: Text , modelName :: Text , paginationEnabled :: Bool } deriving (Eq, Show) controllerInstance :: ControllerConfig -> Text controllerInstance ControllerConfig { controllerName, modelName, applicationName } = "instance AutoRoute " <> controllerName <> "Controller\n\n" data HaskellModule = HaskellModule { moduleName :: Text, body :: Text } generateControllerData :: ControllerConfig -> Text generateControllerData config = let name = config.controllerName pluralName = config.controllerName |> lcfirst |> pluralize |> ucfirst singularName = config.modelName |> lcfirst |> singularize |> ucfirst idFieldName = lcfirst singularName <> "Id" idType = "Id " <> singularName in "\n" <> "data " <> name <> "Controller\n" <> " = " <> pluralName <> "Action\n" <> " | New" <> singularName <> "Action\n" <> " | Show" <> singularName <> "Action { " <> idFieldName <> " :: !(" <> idType <> ") }\n" <> " | Create" <> singularName <> "Action\n" <> " | Edit" <> singularName <> "Action { " <> idFieldName <> " :: !(" <> idType <> ") }\n" <> " | Update" <> singularName <> "Action { " <> idFieldName <> " :: !(" <> idType <> ") }\n" <> " | Delete" <> singularName <> "Action { " <> idFieldName <> " :: !(" <> idType <> ") }\n" <> " deriving (Eq, Show, Data)\n" generateController :: [Statement] -> ControllerConfig -> Text generateController schema config = let applicationName = config.applicationName name = config.controllerName pluralName = config.controllerName |> lcfirst |> pluralize |> ucfirst singularName = config.modelName |> lcfirst |> singularize |> ucfirst moduleName = applicationName <> ".Controller." <> name controllerName = name <> "Controller" importStatements = [ "import " <> applicationName <> ".Controller.Prelude" , "import " <> qualifiedViewModuleName config "Index" , "import " <> qualifiedViewModuleName config "New" , "import " <> qualifiedViewModuleName config "Edit" , "import " <> qualifiedViewModuleName config "Show" ] modelVariablePlural = lcfirst name modelVariableSingular = lcfirst singularName idFieldName = lcfirst singularName <> "Id" model = ucfirst singularName paginationEnabled = config.paginationEnabled indexAction = "" <> " action " <> pluralName <> "Action = do\n" <> (if paginationEnabled then " (" <> modelVariablePlural <> "Q, pagination) <- query @" <> model <> " |> paginate\n" <> " " <> modelVariablePlural <> " <- " <> modelVariablePlural <> "Q |> fetch\n" else " " <> modelVariablePlural <> " <- query @" <> model <> " |> fetch\n" ) <> " render IndexView { .. }\n" newAction = "" <> " action New" <> singularName <> "Action = do\n" <> " let " <> modelVariableSingular <> " = newRecord\n" <> " render NewView { .. }\n" showAction = "" <> " action Show" <> singularName <> "Action { " <> idFieldName <> " } = do\n" <> " " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n" <> " render ShowView { .. }\n" editAction = "" <> " action Edit" <> singularName <> "Action { " <> idFieldName <> " } = do\n" <> " " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n" <> " render EditView { .. }\n" modelFields :: [Text] modelFields = [ modelNameToTableName modelVariableSingular, modelVariableSingular ] |> mapMaybe (fieldsForTable schema) |> headMay |> fromMaybe [] updateAction = "" <> " action Update" <> singularName <> "Action { " <> idFieldName <> " } = do\n" <> " " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n" <> " " <> modelVariableSingular <> "\n" <> " |> build" <> singularName <> "\n" <> " |> ifValid \\case\n" <> " Left " <> modelVariableSingular <> " -> render EditView { .. }\n" <> " Right " <> modelVariableSingular <> " -> do\n" <> " " <> modelVariableSingular <> " <- " <> modelVariableSingular <> " |> updateRecord\n" <> " setSuccessMessage \"" <> model <> " updated\"\n" <> " redirectTo Edit" <> singularName <> "Action { .. }\n" createAction = "" <> " action Create" <> singularName <> "Action = do\n" <> " let " <> modelVariableSingular <> " = newRecord @" <> model <> "\n" <> " " <> modelVariableSingular <> "\n" <> " |> build" <> singularName <> "\n" <> " |> ifValid \\case\n" <> " Left " <> modelVariableSingular <> " -> render NewView { .. } \n" <> " Right " <> modelVariableSingular <> " -> do\n" <> " " <> modelVariableSingular <> " <- " <> modelVariableSingular <> " |> createRecord\n" <> " setSuccessMessage \"" <> model <> " created\"\n" <> " redirectTo " <> pluralName <> "Action\n" deleteAction = "" <> " action Delete" <> singularName <> "Action { " <> idFieldName <> " } = do\n" <> " " <> modelVariableSingular <> " <- fetch " <> idFieldName <> "\n" <> " deleteRecord " <> modelVariableSingular <> "\n" <> " setSuccessMessage \"" <> model <> " deleted\"\n" <> " redirectTo " <> pluralName <> "Action\n" fromParams = "" <> "build" <> singularName <> " " <> modelVariableSingular <> " = " <> modelVariableSingular <> "\n" <> " |> fill " <> toTypeLevelList modelFields <> "\n" toTypeLevelList values = "@'" <> (values |> tshow |> Text.replace "," ", ") in "" <> "module " <> moduleName <> " where" <> "\n" <> "\n" <> intercalate "\n" importStatements <> "\n\n" <> "instance Controller " <> controllerName <> " where\n" <> indexAction <> "\n" <> newAction <> "\n" <> showAction <> "\n" <> editAction <> "\n" <> updateAction <> "\n" <> createAction <> "\n" <> deleteAction <> "\n" <> fromParams -- E.g. qualifiedViewModuleName config "Edit" == "Web.View.Users.Edit" qualifiedViewModuleName :: ControllerConfig -> Text -> Text qualifiedViewModuleName config viewName = config.applicationName <> ".View." <> config.controllerName <> "." <> viewName pathToModuleName :: Text -> Text pathToModuleName moduleName = Text.replace "." "/" moduleName generateViews :: [Statement] -> Text -> Text -> Bool -> [GeneratorAction] generateViews schema applicationName controllerName' paginationEnabled = if null controllerName' then [] else do let indexPlan = ViewGenerator.buildPlan' schema (config "IndexView") let newPlan = ViewGenerator.buildPlan' schema (config "NewView") let showPlan = ViewGenerator.buildPlan' schema (config "ShowView") let editPlan = ViewGenerator.buildPlan' schema (config "EditView") indexPlan <> newPlan <> showPlan <> editPlan where config viewName = do let modelName = tableNameToModelName controllerName' let controllerName = tableNameToControllerName controllerName' ViewGenerator.ViewConfig { .. } isAlphaOnly :: Text -> Bool isAlphaOnly text = Text.all (\c -> Char.isAlpha c || c == '_') text