{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Backend.Rust.Validator (run, RustValidator (..), RustValidatorEnv (..)) where
import Control.Monad.Reader
import Convex.Validator
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.Exit (ExitCode (..))
import System.FilePath ((</>))
import System.IO (hGetContents)
import System.Process (CreateProcess (..), StdStream (..), createProcess, proc, waitForProcess)
newtype RustValidator a = RustValidator
{ runRustValidator :: ReaderT RustValidatorEnv IO a
}
deriving (Functor, Applicative, Monad, MonadIO, MonadReader RustValidatorEnv, MonadFail)
data RustValidatorEnv = RustValidatorEnv {projectPath :: FilePath}
run :: RustValidatorEnv -> RustValidator a -> IO a
run renv action = runReaderT (runRustValidator action) renv
instance Validator RustValidator where
-- \| Sets up the Rust validation sandbox project.
setup = do
config <- ask
let rustProjectPath = projectPath config
-- Create the directory structure.
liftIO $ createDirectoryIfMissing True (rustProjectPath </> "src")
-- Write the Cargo.toml file if it doesn't exist.
let cargoTomlPath = rustProjectPath </> "Cargo.toml"
cargoTomlExists <- liftIO $ doesFileExist cargoTomlPath
if not cargoTomlExists
then liftIO $ writeFile cargoTomlPath cargoTomlContent
else return ()
-- Write the src/lib.rs file if it doesn't exist.
let libRsPath = rustProjectPath </> "src" </> "lib.rs"
libRsExists <- liftIO $ doesFileExist libRsPath
if not libRsExists
then liftIO $ writeFile libRsPath libRsContent
else return ()
-- \| Validates the generated Rust code.
validate generatedCode = do
rustProjectPath <- asks projectPath
let generatedFilePath = rustProjectPath </> "src" </> "generated_api.rs"
-- Write the generated code to the sandbox project.
liftIO $ writeFile generatedFilePath generatedCode
-- Run `cargo fmt` to format the code.
liftIO $ putStrLn "[Validator] Running 'cargo fmt'..."
let fmtCmd = (proc "cargo" ["fmt"]) {cwd = Just rustProjectPath}
(_, _, _, fmtHandle) <- liftIO $ createProcess fmtCmd
fmtExitCode <- liftIO $ waitForProcess fmtHandle
if fmtExitCode /= ExitSuccess
then do
liftIO $ putStrLn "[Validator] Error: 'cargo fmt' failed. The generated code has syntax errors."
return Nothing
else do
-- Run `cargo check` to validate types and ownership.
liftIO $ putStrLn "[Validator] Running 'cargo check'..."
let checkCmd = (proc "cargo" ["check"]) {cwd = Just rustProjectPath, std_err = CreatePipe}
(_, _, Just stdErr, checkHandle) <- liftIO $ createProcess checkCmd
checkExitCode <- liftIO $ waitForProcess checkHandle
if checkExitCode == ExitSuccess
then do
liftIO $ putStrLn "[Validator] Validation successful."
-- Read file content from the checked and formatted file.
content <- liftIO $ readFile generatedFilePath
return $ Just content
else do
liftIO $ putStrLn "[Validator] Error: 'cargo check' failed. The generated code is invalid."
liftIO $ hGetContents stdErr >>= putStrLn
return Nothing
-- | The content for the validation project's Cargo.toml file.
cargoTomlContent :: String
cargoTomlContent =
unlines
[ "[package]",
"name = \"validation-project\"",
"version = \"0.1.0\"",
"edition = \"2021\"",
"",
"[dependencies]",
"convex = \"0.9.0\"",
"serde = { version = \"1\", features = [\"derive\"] }",
"serde_json = \"1\"",
"thiserror = \"1.0\"",
"anyhow = \"1.0\"",
"futures-util = { version = \"0.3\" }",
"tokio = { version = \"1\", features = [\"full\"] }"
]
-- | The content for the validation project's src/lib.rs file.
libRsContent :: String
libRsContent = "pub mod generated_api;\n"