packages feed

poppy-codegen-1.0.0: src/Poppy/Codegen/CLI.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Codegen CLI: write files, @--check@ freshness, @--check-schema@ drift, @--list@ paths.
module Poppy.Codegen.CLI
  ( generate,
    mainWith,
    simpleTarget,
    CodegenTarget,
  )
where

import Data.Text (Text, pack)
import qualified Data.Text.IO as TIO
import Poppy.Codegen.Drift (checkSchema, formatDriftError)
import Poppy.Codegen.IR (Schema)
import Poppy.Codegen.Introspect (introspectCatalog)
import Poppy.Codegen.Run (GenOutput (..), allOutputs, checkOutputs, schemasForTargets, writeOutputs)
import Poppy.Codegen.Target
  ( CodegenTarget,
    simpleTarget,
    targetSchemas,
  )
import Poppy.Codegen.Validate (ValidationError (..), validateSchema)
import Poppy.Internal.Db (closePool, connect, withConn)
import System.Directory (getCurrentDirectory)
import System.Environment (getArgs, lookupEnv)
import System.Exit (exitFailure, exitSuccess)

-- | Write table types and a Client under @dir@ (@src/Schema@ → module prefix @Schema@).
--
-- Flags on the process argv: none (write), @--check@, @--check-schema@, @--list@.
-- @--check-schema@ reads @DATABASE_URL@, or the URL passed as @--database-url@.
generate :: FilePath -> Schema -> IO ()
generate dir schema = mainWith [simpleTarget dir schema]

-- | Flags: none (write files), @--check@, @--check-schema@, @--list@.
--
-- @--check-schema@ may be followed by @--database-url URL@, which overrides
-- @DATABASE_URL@. The two arguments can appear in either order.
mainWith :: [CodegenTarget] -> IO ()
mainWith targets = do
  root <- getCurrentDirectory
  args <- getArgs
  case args of
    ("--check" : _) -> runCheck root targets
    ("--list" : _) -> runList targets
    _ | "--check-schema" `elem` args -> do
      urlFlag <- checkSchemaDatabaseUrl args
      runCheckSchema urlFlag targets
    _ -> runWrite root targets

runWrite :: FilePath -> [CodegenTarget] -> IO ()
runWrite root targets = do
  validateOrExit (schemasForTargets targets)
  writeOutputs root targets
  TIO.putStrLn $ "Wrote " <> pack (show (length (allOutputs targets))) <> " generated files."

runCheck :: FilePath -> [CodegenTarget] -> IO ()
runCheck root targets = do
  validateOrExit (schemasForTargets targets)
  stale <- checkOutputs root targets
  case stale of
    [] -> exitSuccess
    paths -> do
      TIO.putStrLn "Generated files are out of date. Re-run codegen without --check."
      mapM_ (TIO.putStrLn . ("  " <>)) (pack <$> paths)
      exitFailure

runCheckSchema :: Maybe String -> [CodegenTarget] -> IO ()
runCheckSchema urlFlag targets = do
  let schemas = targetSchemas targets
  validateOrExit schemas
  url <- schemaDatabaseUrl urlFlag
  pool <- connect url
  catalog <- withConn pool introspectCatalog
  closePool pool
  case concatMap (`checkSchema` catalog) schemas of
    [] -> exitSuccess
    errs -> do
      TIO.putStrLn "IR does not match the database. Haskell IR is the source of truth."
      mapM_ (TIO.putStrLn . ("  " <>) . formatDriftError) errs
      exitFailure

-- | @--database-url@ overrides @DATABASE_URL@. Other arguments besides
-- @--check-schema@ are rejected.
checkSchemaDatabaseUrl :: [String] -> IO (Maybe String)
checkSchemaDatabaseUrl = go Nothing
  where
    go found [] = pure found
    go found ("--check-schema" : rest) = go found rest
    go found ("--database-url" : url : rest)
      | isOption url = missingUrl
      | otherwise =
          case found of
            Just _ -> do
              TIO.putStrLn "--database-url was given more than once."
              exitFailure
            Nothing -> go (Just url) rest
    go _ ("--database-url" : _) = missingUrl
    go _ (other : _) = do
      TIO.putStrLn $ "Unknown argument: " <> pack other <> "."
      TIO.putStrLn "Usage: --check-schema [--database-url URL]"
      exitFailure
    missingUrl = do
      TIO.putStrLn "--database-url requires a URL."
      exitFailure
    isOption ('-' : '-' : _) = True
    isOption _ = False

schemaDatabaseUrl :: Maybe String -> IO String
schemaDatabaseUrl (Just url) = pure url
schemaDatabaseUrl Nothing = do
  mDb <- lookupEnv "DATABASE_URL"
  case mDb of
    Just url -> pure url
    Nothing -> do
      TIO.putStrLn "Set DATABASE_URL, or pass --database-url URL, to check the Schema against Postgres."
      exitFailure

runList :: [CodegenTarget] -> IO ()
runList targets = mapM_ (TIO.putStrLn . pack . outputPath) (allOutputs targets)

validateOrExit :: [Schema] -> IO ()
validateOrExit schemas =
  case concatMap validateSchema schemas of
    [] -> pure ()
    errs -> do
      TIO.putStrLn "Schema validation failed:"
      mapM_ (TIO.putStrLn . ("  " <>) . formatValidationError) errs
      exitFailure

formatValidationError :: ValidationError -> Text
formatValidationError err = pack (show err)