From 9ffc115fcdd3be75ab5492023656896a42dfc7d9 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Thu, 15 Jun 2023 15:44:52 +0200 Subject: [PATCH] Actions are now generated. --- .../cli/src/Wasp/Cli/Command/AI/CodeAgent.hs | 1 + .../Wasp/Cli/Command/AI/GenerateNewProject.hs | 37 +---- .../Command/AI/GenerateNewProject/Action.hs | 149 ++++++++++++++++++ .../Command/AI/GenerateNewProject/Common.hs | 32 +++- .../Command/AI/GenerateNewProject/Entity.hs | 28 ++++ .../Cli/Command/AI/GenerateNewProject/Plan.hs | 54 +++---- .../Command/AI/GenerateNewProject/Skeleton.hs | 47 +++--- waspc/src/Wasp/Util.hs | 18 +++ waspc/waspc.cabal | 2 + 9 files changed, 279 insertions(+), 89 deletions(-) create mode 100644 waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Action.hs create mode 100644 waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Entity.hs diff --git a/waspc/cli/src/Wasp/Cli/Command/AI/CodeAgent.hs b/waspc/cli/src/Wasp/Cli/Command/AI/CodeAgent.hs index 4fd9b5a29..1975954bb 100644 --- a/waspc/cli/src/Wasp/Cli/Command/AI/CodeAgent.hs +++ b/waspc/cli/src/Wasp/Cli/Command/AI/CodeAgent.hs @@ -56,6 +56,7 @@ getFile path = gets $ H.lookup path . _files getAllFiles :: CodeAgent [(FilePath, Text)] getAllFiles = gets $ H.toList . _files +-- TODO: Make it so that if ChatGPT replies with being too busy, we try again. queryChatGPT :: ChatGPTParams -> [ChatMessage] -> CodeAgent Text queryChatGPT params messages = do key <- asks _openAIApiKey diff --git a/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject.hs b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject.hs index 4423d3d9d..24aa3b7c8 100644 --- a/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject.hs +++ b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject.hs @@ -10,7 +10,9 @@ import Data.Text (Text) import qualified Data.Text as T import NeatInterpolation (trimming) import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeToLog) +import Wasp.Cli.Command.AI.GenerateNewProject.Action (Action, generateAndWriteAction) import Wasp.Cli.Command.AI.GenerateNewProject.Common (NewProjectDetails (..)) +import Wasp.Cli.Command.AI.GenerateNewProject.Entity (writeEntitiesToWaspFile) import Wasp.Cli.Command.AI.GenerateNewProject.Plan (generatePlan) import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan import Wasp.Cli.Command.AI.GenerateNewProject.Skeleton (generateAndWriteProjectSkeleton) @@ -25,18 +27,18 @@ import Wasp.Cli.Command.AI.GenerateNewProject.Skeleton (generateAndWriteProjectS -- and also contain description of what happened (or maybe that is separate message). generateNewProject :: NewProjectDetails -> CodeAgent () generateNewProject newProjectDetails = do - waspFilePath <- generateAndWriteProjectSkeleton newProjectDetails + (waspFilePath, planRules) <- generateAndWriteProjectSkeleton newProjectDetails writeToLog "Generated project skeleton." writeToLog "Generating plan..." - plan <- generatePlan newProjectDetails + plan <- generatePlan newProjectDetails planRules writeToLog $ "Plan generated!\n" <> summarizePlan plan writeEntitiesToWaspFile waspFilePath (Plan.entities plan) writeToLog "Added entities to wasp file." writeToLog "Generating actions..." - actions <- forM (Plan.actions plan) $ generateAndWriteAction waspFilePath plan + actions <- forM (Plan.actions plan) $ generateAndWriteAction newProjectDetails waspFilePath plan writeToLog "Generating queries..." queries <- forM (Plan.queries plan) $ generateAndWriteQuery waspFilePath plan @@ -70,16 +72,9 @@ generateNewProject newProjectDetails = do showT :: Show a => a -> Text showT = T.pack . show - generateAndWriteAction waspFilePath plan actionPlan = do - action <- generateAction newProjectDetails (Plan.entities plan) actionPlan - writeActionToFile action - writeActionToWaspFile waspFilePath action - writeToLog $ "Generated action: " <> T.pack (Plan.actionName actionPlan) - return action - generateAndWriteQuery waspFilePath plan queryPlan = do query <- generateQuery newProjectDetails (Plan.entities plan) queryPlan - writeQueryToFile query + writeQueryToFile query -- TODO: Do we need to convert @server/ from path into src/server/? writeQueryToWaspFile waspFilePath query writeToLog $ "Generated query: " <> T.pack (Plan.queryName queryPlan) return query @@ -91,26 +86,6 @@ generateNewProject newProjectDetails = do writeToLog $ "Generated page: " <> T.pack (Plan.pageName pagePlan) return page -writeEntitiesToWaspFile :: FilePath -> [Plan.Entity] -> CodeAgent () -writeEntitiesToWaspFile waspFilePath entities = do - -- TODO: assemble code for each entity and write it to wasp file. - undefined - -generateAction :: NewProjectDetails -> [Plan.Entity] -> Plan.Action -> CodeAgent Action -generateAction = undefined - -writeActionToFile :: Action -> CodeAgent () -writeActionToFile = undefined - -writeActionToWaspFile :: FilePath -> Action -> CodeAgent () -writeActionToWaspFile waspFilePath action = undefined - -data Action = Action - { _actionWaspDecl :: String, - _actionJsImpl :: String, - _actionPlan :: Plan.Action - } - generateQuery :: NewProjectDetails -> [Plan.Entity] -> Plan.Query -> CodeAgent Query generateQuery = undefined diff --git a/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Action.hs b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Action.hs new file mode 100644 index 000000000..83ce1239c --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Action.hs @@ -0,0 +1,149 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Wasp.Cli.Command.AI.GenerateNewProject.Action + ( generateAndWriteAction, + Action, + ) +where + +import Data.Aeson (FromJSON) +import Data.List (isPrefixOf) +import Data.Maybe (fromMaybe) +import qualified Data.Text as T +import GHC.Generics (Generic) +import NeatInterpolation (trimming) +import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeToFile, writeToLog) +import Wasp.Cli.Command.AI.GenerateNewProject.Common + ( NewProjectDetails (..), + defaultChatGPTParams, + queryChatGPTForJSON, + ) +import Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts (appDescriptionStartMarkerLine) +import qualified Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts as Prompts +import Wasp.Cli.Command.AI.GenerateNewProject.Entity (entityPlanToWaspDecl) +import Wasp.Cli.Command.AI.GenerateNewProject.Plan (Plan) +import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan +import Wasp.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..)) + +generateAndWriteAction :: NewProjectDetails -> FilePath -> Plan -> Plan.Action -> CodeAgent Action +generateAndWriteAction newProjectDetails waspFilePath plan actPlan = do + action <- generateAction newProjectDetails (Plan.entities plan) actPlan + writeActionToJsFile action + writeActionToWaspFile waspFilePath action + writeToLog $ "Generated action: " <> T.pack (Plan.actionName actPlan) + return action + +generateAction :: NewProjectDetails -> [Plan.Entity] -> Plan.Action -> CodeAgent Action +generateAction newProjectDetails entityPlans plan = do + impl <- queryChatGPTForJSON defaultChatGPTParams chatMessages + return Action {actionImpl = impl, actionPlan = plan} + where + chatMessages = + [ ChatMessage {role = System, content = Prompts.systemPrompt}, + ChatMessage {role = User, content = planPrompt} + ] + appName = T.pack $ _projectAppName newProjectDetails + appDesc = T.pack $ _projectDescription newProjectDetails + basicWaspLangInfoPrompt = Prompts.basicWaspLangInfo + actionName = T.pack $ Plan.actionName plan + actionFnPath = T.pack $ Plan.actionFnPath plan + actionDesc = T.pack $ Plan.actionDesc plan + entityDecls = T.intercalate "\n\n" $ entityPlanToWaspDecl <$> entityPlans + planPrompt = + [trimming| + ${basicWaspLangInfoPrompt} + + ${actionDocPrompt} + + We are implementing a Wasp app (check bottom for description). + + This app has following entities: + ${entityDecls} + + Let's now implement the following Wasp action: + - name: ${actionName} + - fn: ${actionFnPath} + - description: ${actionDesc} + + Please, respond ONLY with a valid JSON, of following format: + { "actionWaspDecl": "action ${actionName} {\n ... }", + "actionJsImpl": "export const {$actionName} ... ", + "actionJsImports": "import foo from \"bar.js\"\n ..."" + } + "waspDeclaration" and "jsImplementation" are required, "jsImports" you can skip if none are needed. + There should be no other text in the response. + + ${appDescriptionStartMarkerLine} + + App name: ${appName} + ${appDesc} + |] + actionDocPrompt = + [trimming| + Action is implemented via Wasp declaration and corresponding NodeJS implementation. + + Example of Wasp declaration: + + ```wasp + action updateTaskIsDone { + fn: import { updateTaskIsDone } from "@server/taskActions.js", + entities: [Task] // Entities that action uses. + } + ``` + + Example of NodeJS implementation: + + ```js + import HttpError from '@wasp/core/HttpError.js' + + export const updateTaskIsDone = (args, context) => { + if (!context.user) { throw new HttpError(403) } + + return context.entities.Task.update({ // prisma object + where: { args.id }, + data: { args.isDone } + }) + } + ``` + + Action can then be easily called from the client, via Wasp's RPC mechanism. + |] + +writeActionToJsFile :: Action -> CodeAgent () +writeActionToJsFile action = + -- TODO: An issue we have here is that if other action already did the same import, + -- we don't know and we import it again. + -- One thing we can do it supply chatGPT with a list of imports that are already there. + -- Second thing we can do is to look for same lines at the start of the file, but that sounds fragile. + -- Maybe best to read and pass previous imports (we would have to do that above somewhere). + -- Or even the whole file? Hmmmmm. + writeToFile path $ + ((jsImports <> "\n") <>) . (<> "\n\n" <> jsImpl) . (fromMaybe "") + where + path = resolvePath $ Plan.actionFnPath $ actionPlan action + jsImpl = T.pack $ actionJsImpl $ actionImpl action + jsImports = T.pack $ actionJsImports $ actionImpl action + resolvePath p | "@server/" `isPrefixOf` p = "src/server/" <> drop (length ("@server/" :: String)) p + resolvePath _ = error "path incorrectly formatted, should start with @server." + +writeActionToWaspFile :: FilePath -> Action -> CodeAgent () +writeActionToWaspFile waspFilePath action = + writeToFile waspFilePath $ + (<> "\n\n" <> waspDeclCode) . fromMaybe (error "wasp file shouldn't be empty") + where + waspDeclCode = T.pack $ actionWaspDecl $ actionImpl action + +data Action = Action + { actionImpl :: ActionImpl, + actionPlan :: Plan.Action + } + deriving (Show) + +data ActionImpl = ActionImpl + { actionWaspDecl :: String, + actionJsImpl :: String, + actionJsImports :: String + } + deriving (Generic, Show) + +instance FromJSON ActionImpl diff --git a/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Common.hs b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Common.hs index e2f1df419..9693972d8 100644 --- a/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Common.hs @@ -2,10 +2,19 @@ module Wasp.Cli.Command.AI.GenerateNewProject.Common ( NewProjectDetails (..), File, AuthProvider (..), + queryChatGPTForJSON, + defaultChatGPTParams, + writeToWaspFileEnd, ) where +import Data.Aeson (FromJSON) +import qualified Data.Aeson as Aeson +import Data.Maybe (fromMaybe) import Data.Text (Text) +import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, queryChatGPT, writeToFile) +import Wasp.OpenAI.ChatGPT (ChatGPTParams (..), ChatMessage (..), Model (GPT_3_5_turbo_16k)) +import Wasp.Util (naiveTrimJSON, textToLazyBS) data NewProjectDetails = NewProjectDetails { _projectAppName :: !String, @@ -13,8 +22,29 @@ data NewProjectDetails = NewProjectDetails _projectAuth :: !AuthProvider } --- TODO: Make these relative to WaspProjectDir? +-- TODO: Make these relative to WaspProjectDir, via StrongPath? type File = (FilePath, Text) -- TODO: Support more methods. data AuthProvider = UsernameAndPassword + +queryChatGPTForJSON :: FromJSON a => ChatGPTParams -> [ChatMessage] -> CodeAgent a +queryChatGPTForJSON chatGPTParams chatMessages = do + responseJSONText <- naiveTrimJSON <$> queryChatGPT chatGPTParams chatMessages + case Aeson.eitherDecode $ textToLazyBS responseJSONText of + Right plan -> return plan + Left _errMsg -> + -- TODO: Handle this better. + -- Try sending response back to chatGPT and ask it to fix it -> hey it is not valid JSON, fix it. + -- Write to log, to let user know. + error "Failed to parse plan" + +-- TODO: Test more for the optimal temperature (possibly higher). +-- TODO: Should we make sure we have max_tokens set to high enough? +defaultChatGPTParams :: ChatGPTParams +defaultChatGPTParams = ChatGPTParams {_model = GPT_3_5_turbo_16k, _temperature = Just 1.0} + +writeToWaspFileEnd :: FilePath -> Text -> CodeAgent () +writeToWaspFileEnd waspFilePath text = do + writeToFile waspFilePath $ + (<> "\n" <> text) . fromMaybe (error "wasp file shouldn't be empty") diff --git a/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Entity.hs b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Entity.hs new file mode 100644 index 000000000..6751762aa --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Entity.hs @@ -0,0 +1,28 @@ +module Wasp.Cli.Command.AI.GenerateNewProject.Entity + ( writeEntitiesToWaspFile, + entityPlanToWaspDecl, + ) +where + +import Data.Text (Text) +import qualified Data.Text as T +import NeatInterpolation (trimming) +import Wasp.Cli.Command.AI.CodeAgent (CodeAgent) +import Wasp.Cli.Command.AI.GenerateNewProject.Common (writeToWaspFileEnd) +import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan + +writeEntitiesToWaspFile :: FilePath -> [Plan.Entity] -> CodeAgent () +writeEntitiesToWaspFile waspFilePath entityPlans = do + writeToWaspFileEnd waspFilePath $ "\n" <> entitiesCode + where + entitiesCode = T.intercalate "\n\n" $ entityPlanToWaspDecl <$> entityPlans + +entityPlanToWaspDecl :: Plan.Entity -> Text +entityPlanToWaspDecl plan = + let name = T.pack $ Plan.entityName plan + pslBody = T.pack $ Plan.entityBodyPsl plan + in [trimming| + entity ${name} {=psl + ${pslBody} + psl=} + |] diff --git a/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Plan.hs b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Plan.hs index 9d8a5465e..27e01c23b 100644 --- a/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Plan.hs +++ b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Plan.hs @@ -7,38 +7,32 @@ module Wasp.Cli.Command.AI.GenerateNewProject.Plan Action (..), Page (..), generatePlan, + PlanRule, ) where import Data.Aeson (FromJSON) -import qualified Data.Aeson as Aeson -import Data.ByteString.Lazy.UTF8 (ByteString) -import Data.Text (Text) import qualified Data.Text as T -import qualified Data.Text.Lazy as TL -import qualified Data.Text.Lazy.Encoding as TLE import GHC.Generics (Generic) import NeatInterpolation (trimming) -import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, queryChatGPT) -import Wasp.Cli.Command.AI.GenerateNewProject.Common (NewProjectDetails (_projectAppName, _projectDescription)) +import Wasp.Cli.Command.AI.CodeAgent (CodeAgent) +import Wasp.Cli.Command.AI.GenerateNewProject.Common + ( AuthProvider (UsernameAndPassword), + NewProjectDetails (..), + defaultChatGPTParams, + queryChatGPTForJSON, + ) import Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts (appDescriptionStartMarkerLine) import qualified Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts as Prompts -import Wasp.OpenAI.ChatGPT (ChatGPTParams (..), ChatMessage (..), ChatRole (..), Model (..)) +import Wasp.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..)) -generatePlan :: NewProjectDetails -> CodeAgent Plan -generatePlan newProjectDetails = do - responseJSONText <- naiveTrimJSON <$> queryChatGPT chatGPTParams chatMessages - case Aeson.eitherDecode $ textToLazyBS responseJSONText of - Right plan -> return plan - Left _errMsg -> - -- TODO: Handle this better. - -- Try sending response back to chatGPT and ask it to fix it -> hey it is not valid JSON, fix it. - -- Write to log, to let user know. - error "Failed to parse plan" +-- | Additional rule to follow while generating plan. +type PlanRule = String + +generatePlan :: NewProjectDetails -> [PlanRule] -> CodeAgent Plan +generatePlan newProjectDetails planRules = do + queryChatGPTForJSON defaultChatGPTParams chatMessages where - -- TODO: Try configuring temperature. - -- TODO: Make sure we have max_tokens set to high enough. - chatGPTParams = ChatGPTParams {_model = GPT_3_5_turbo_16k, _temperature = Just 1.0} chatMessages = [ ChatMessage {role = System, content = Prompts.systemPrompt}, ChatMessage {role = User, content = planPrompt} @@ -47,6 +41,7 @@ generatePlan newProjectDetails = do appDesc = T.pack $ _projectDescription newProjectDetails basicWaspLangInfoPrompt = Prompts.basicWaspLangInfo waspFileExamplePrompt = Prompts.waspFileExample + rulesText = T.pack . unlines $ "Rules:" : map (" - " ++) planRules planPrompt = [trimming| ${basicWaspLangInfoPrompt} @@ -80,9 +75,10 @@ generatePlan newProjectDetails = do }] } - Make sure to generate at least one page with routePath "/". + ${rulesText} - We will later use this plan to implement all of these parts of Wasp app. + We will later use this plan to implement all of these parts of Wasp app, + so make sure descriptions are detailed enough to guide implementing them. Please, respond ONLY with a valid JSON that is a plan. There should be no other text in the response. @@ -171,15 +167,3 @@ data Page = Page deriving (Generic, Show) instance FromJSON Page - --- | Given a text containing a single instance of JSON and some text around it but no { or }, trim --- it until just JSON is left. --- Examples --- naiveTrimJson "some text { \"a\": 5 } yay" == "{\"a\": 5 }" --- naiveTrimJson "some {text} { \"a\": 5 }" -> won't work correctly. -naiveTrimJSON :: Text -> Text -naiveTrimJSON textContainingJson = - T.reverse . T.dropWhile (/= '}') . T.reverse . T.dropWhile (/= '{') $ textContainingJson - -textToLazyBS :: Text -> ByteString -textToLazyBS = TLE.encodeUtf8 . TL.fromStrict diff --git a/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Skeleton.hs b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Skeleton.hs index 73cbe5423..76d0c5ba8 100644 --- a/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Skeleton.hs +++ b/waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Skeleton.hs @@ -10,15 +10,16 @@ import NeatInterpolation (trimming) import qualified StrongPath as SP import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeNewFile) import Wasp.Cli.Command.AI.GenerateNewProject.Common (AuthProvider (..), File, NewProjectDetails (..)) +import Wasp.Cli.Command.AI.GenerateNewProject.Plan (PlanRule) import Wasp.Cli.Command.CreateNewProject (readCoreWaspProjectFiles) import qualified Wasp.Version -generateAndWriteProjectSkeleton :: NewProjectDetails -> CodeAgent FilePath +generateAndWriteProjectSkeleton :: NewProjectDetails -> CodeAgent (FilePath, [PlanRule]) generateAndWriteProjectSkeleton newProjectDetails = do coreFiles <- liftIO $ map (first SP.fromRelFile) <$> readCoreWaspProjectFiles mapM_ writeNewFile coreFiles - let waspFile@(waspFilePath, _) = generateBaseWaspFile newProjectDetails + let (waspFile@(waspFilePath, _), planRules) = generateBaseWaspFile newProjectDetails writeNewFile waspFile case _projectAuth newProjectDetails of @@ -28,28 +29,36 @@ generateAndWriteProjectSkeleton newProjectDetails = do writeNewFile generateDotEnvServerFile - return waspFilePath + return (waspFilePath, planRules) -generateBaseWaspFile :: NewProjectDetails -> File -generateBaseWaspFile newProjectDetails = (path, content) +generateBaseWaspFile :: NewProjectDetails -> (File, [PlanRule]) +generateBaseWaspFile newProjectDetails = ((path, content), planRules) where path = "main.wasp" appName = T.pack $ _projectAppName newProjectDetails appTitle = appName waspVersion = T.pack $ show Wasp.Version.waspVersion - appAuth = case _projectAuth newProjectDetails of - -- NOTE: We assume here that there will be a page with route "/". + (appAuth, authPlanRules) = case _projectAuth newProjectDetails of + -- NOTE: We assume two things here: + -- - that there will be a page with route "/". + -- - that there will be a User entity with 'id', 'username' and 'password'. + -- We later that those will be added to Plan. UsernameAndPassword -> - [trimming| - auth: { - userEntity: User, - methods: { - usernameAndPassword: {} + ( [trimming| + auth: { + userEntity: User, + methods: { + usernameAndPassword: {} + }, + onAuthFailedRedirectTo: "/login", + onAuthSucceededRedirectTo: "/" }, - onAuthFailedRedirectTo: "/login", - onAuthSucceededRedirectTo: "/" - }, - |] + |], + [ "Generate a User entity, with at least the following fields: 'id', 'username', 'password'.", + "There should be a page with route path \"/\"." + ] + ) + planRules = authPlanRules <> ["Don't generate the Login or Signup page."] content = [trimming| app ${appName} { @@ -60,12 +69,6 @@ generateBaseWaspFile newProjectDetails = (path, content) ${appAuth} } - entity User {=psl - id Int @id @default(autoincrement()) - username String @unique - password String - psl=} - route LoginRoute { path: "/login", to: LoginPage } page LoginPage { component: import Login from "@client/Login.jsx" diff --git a/waspc/src/Wasp/Util.hs b/waspc/src/Wasp/Util.hs index fecf935f1..ed937959a 100644 --- a/waspc/src/Wasp/Util.hs +++ b/waspc/src/Wasp/Util.hs @@ -33,6 +33,8 @@ module Wasp.Util kebabToCamelCase, maybeToEither, whenM, + naiveTrimJSON, + textToLazyBS, ) where @@ -41,6 +43,7 @@ import Control.Monad (unless, when) import qualified Crypto.Hash.SHA256 as SHA256 import qualified Data.Aeson as Aeson import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.UTF8 as BSU import Data.Char (isSpace, isUpper, toLower, toUpper) import qualified Data.HashMap.Strict as M @@ -48,8 +51,11 @@ import Data.List (intercalate) import Data.List.Split (splitOn, wordsBy) import Data.Maybe (fromMaybe) import Data.Text (Text) +import qualified Data.Text as T import qualified Data.Text as Text import qualified Data.Text.Encoding as TextEncoding +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as TLE import StrongPath (File, Path') import qualified StrongPath as SP import Text.Printf (printf) @@ -230,3 +236,15 @@ maybeToEither leftValue = maybe (Left leftValue) Right getEnvVarDefinition :: (String, String) -> String getEnvVarDefinition (name, value) = concat [name, "=", value] + +-- | Given a text containing a single instance of JSON and some text around it but no { or }, trim +-- it until just JSON is left. +-- Examples +-- naiveTrimJson "some text { \"a\": 5 } yay" == "{\"a\": 5 }" +-- naiveTrimJson "some {text} { \"a\": 5 }" -> won't work correctly. +naiveTrimJSON :: Text -> Text +naiveTrimJSON textContainingJson = + T.reverse . T.dropWhile (/= '}') . T.reverse . T.dropWhile (/= '{') $ textContainingJson + +textToLazyBS :: Text -> BSL.ByteString +textToLazyBS = TLE.encodeUtf8 . TL.fromStrict diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index a024c0e94..20c54b038 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -372,8 +372,10 @@ library cli-lib Wasp.Cli.FileSystem Wasp.Cli.Command.AI.CodeAgent Wasp.Cli.Command.AI.GenerateNewProject + Wasp.Cli.Command.AI.GenerateNewProject.Action Wasp.Cli.Command.AI.GenerateNewProject.Common Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts + Wasp.Cli.Command.AI.GenerateNewProject.Entity Wasp.Cli.Command.AI.GenerateNewProject.Plan Wasp.Cli.Command.AI.GenerateNewProject.Skeleton Wasp.Cli.Command.AI.New