packages feed

convex-schema-parser-0.1.3.0: app/Main.hs

module Main (main) where

import qualified Backend.Python as Python
import qualified Backend.Rust as Rust
import qualified Config
import qualified Convex.Parser as P
import Data.List (intercalate)
import Dev (runDevMode)
import qualified Dev
import qualified Init
import Options.Applicative
import System.Directory (doesFileExist)
import System.Exit (exitFailure)

data Command
  = Generate GenerateOptions
  | Dev DevCliOptions
  | Init InitOptions

data GenerateOptions = GenerateOptions
  { schemaPath :: FilePath,
    declarationsDir :: FilePath,
    target :: Config.GenTarget,
    outputFile :: Maybe FilePath
  }

data DevCliOptions = DevCliOptions
  { devCliConfigPath :: FilePath
  }

data InitOptions = InitOptions
  { initConfigPath :: FilePath
  }

-- We can reuse the GenTarget Read instance from Config.hs if it were exported,
-- but redefining it here keeps Main self-contained for parsing.
instance Read Config.GenTarget where
  readsPrec _ "Python" = [(Config.Python, "")]
  readsPrec _ "Rust" = [(Config.Rust, "")]
  readsPrec _ _ = []

availableTargets :: String
availableTargets = "Available: " ++ intercalate ", " (map show [(minBound :: Config.GenTarget) ..])

generateOptionsParser :: Parser GenerateOptions
generateOptionsParser =
  GenerateOptions
    <$> strOption
      ( long "schema"
          <> metavar "SCHEMA_PATH"
          <> help "Path to the source schema.ts file"
      )
    <*> strOption
      ( long "declarations"
          <> metavar "DECL_DIR"
          <> help "Path to the root of the tsc-generated declarations directory"
      )
    <*> option
      auto
      ( long "target"
          <> metavar "TARGET"
          <> value Config.Python
          <> showDefault
          <> help ("Target language for code generation. " ++ availableTargets)
      )
    <*> optional
      ( strOption
          ( long "output"
              <> short 'o'
              <> metavar "OUTPUT_FILE"
              <> help "File to write the generated code to (prints to stdout if omitted)"
          )
      )

devCliOptionsParser :: Parser DevCliOptions
devCliOptionsParser =
  DevCliOptions
    <$> strOption
      ( long "config"
          <> metavar "CONFIG_FILE"
          <> value "convex-parser.yaml"
          <> showDefault
          <> help "Path to the YAML config file"
      )

initOptionsParser :: Parser InitOptions
initOptionsParser =
  InitOptions
    <$> strOption
      ( long "config"
          <> metavar "CONFIG_FILE"
          <> value "convex-parser.yaml"
          <> showDefault
          <> help "Path where the default YAML config file will be created"
      )

commandParser :: Parser Command
commandParser =
  subparser
    ( command "generate" (info (Generate <$> generateOptionsParser <**> helper) (progDesc "Generate a client once and exit"))
        <> command "dev" (info (Dev <$> devCliOptionsParser <**> helper) (progDesc "Watch for changes and regenerate clients from a config file"))
        <> command "init" (info (Init <$> initOptionsParser <**> helper) (progDesc "Initialize a descriptive default config"))
    )

main :: IO ()
main = do
  cmd <- customExecParser p optsParserInfo
  case cmd of
    Generate opts -> runGenerate opts
    Dev opts -> runDev opts
    Init opts -> Init.runInit (initConfigPath opts)
  where
    optsParserInfo =
      info
        (commandParser <**> helper)
        ( fullDesc
            <> progDesc "Generate typed clients from a Convex schema and function definitions."
            <> header "convex-parser - A code generator for Convex"
        )
    p = prefs showHelpOnError

runGenerate :: GenerateOptions -> IO ()
runGenerate opts = do
  projectResult <- P.parseProject (schemaPath opts) (declarationsDir opts)
  case projectResult of
    Left err -> do
      putStrLn $ "Error parsing project: " ++ err
      exitFailure
    Right project ->
      let generatedCode = case target opts of
            Config.Python -> Python.generatePythonCode project
            Config.Rust -> Rust.generateRustCode project
       in case outputFile opts of
            Just path -> writeFile path generatedCode
            Nothing -> putStrLn generatedCode

runDev :: DevCliOptions -> IO ()
runDev opts = do
  configExists <- doesFileExist (devCliConfigPath opts)
  if not configExists
    then do
      putStrLn $ "Error: Config file not found at " ++ devCliConfigPath opts
      exitFailure
    else do
      configResult <- Config.loadConfig (devCliConfigPath opts)
      case configResult of
        Left err -> do
          putStrLn "Error parsing config file:"
          print err
          exitFailure
        Right config ->
          let declarationsDir = Config.declarationsDir config
              devOpts = Dev.DevOptions (Config.projectPath config) declarationsDir config
           in runDevMode config