packages feed

hix-0.9.0: lib/Hix/OutputWriter.hs

module Hix.OutputWriter where

import qualified Data.ByteString as ByteString
import qualified Data.Text.IO as Text
import Exon (exon)
import Path (Abs, File, Path, toFilePath, parseAbsFile)

import qualified Hix.Console as Console
import Hix.Data.Monad (M)
import Hix.Data.OutputTarget (OutputTarget (..))
import Hix.Error (pathText, ErrorMessage (..))
import Hix.Monad (fatalError, tryIOMWithM, tryIOMAs, note, throwM)
import System.Environment (getEnv)
import Data.Aeson (ToJSON)
import qualified Data.Aeson as Aeson
import Hix.Data.OutputFormat (OutputFormat (..))
import qualified Data.Text as Text
import qualified Data.Map.Strict as Map

data WriteError =
  WriteError {
    msg :: Text,
    file :: Path Abs File
  }
  deriving stock (Eq, Show, Generic)

data OutputWriter =
  OutputWriter {
    bytes :: ByteString -> M (),
    bytesAppend :: ByteString -> M (),
    text :: Text -> M (),
    textAppend :: Text -> M ()
  }

fileWriter :: (WriteError -> M ()) -> Path Abs File -> OutputWriter
fileWriter errorHandler file =
  OutputWriter {
    bytes = handleError . ByteString.writeFile fp,
    bytesAppend = handleError . ByteString.appendFile fp,
    text = handleError . Text.writeFile fp,
    textAppend = handleError . Text.appendFile fp
  }
  where
    handleError = tryIOMWithM \ err -> errorHandler WriteError {msg = show err, file}
    fp = toFilePath file

defaultWriteError :: WriteError -> M ()
defaultWriteError err =
  fatalError [exon|Couldn't write to #{pathText err.file}: #{err.msg}|]

stdoutWriter :: OutputWriter
stdoutWriter =
  OutputWriter {
    bytes = Console.bytesOut,
    bytesAppend = Console.bytesOut,
    text = Console.out,
    textAppend = Console.out
  }

githubActionsWriter :: M OutputWriter
githubActionsWriter = do
  var <- tryIOMAs envVarError (getEnv "GITHUB_OUTPUT")
  outFile <- note envVarError (parseAbsFile var)
  pure (fileWriter writeError outFile)
  where
    envVarError = Client "The variable $GITHUB_OUTPUT does not contain a file path"
    writeError err = throwM (Fatal [exon|Couldn't write to $GITHUB_OUTPUT: #{err.msg}|])

outputWriter :: M OutputWriter -> OutputTarget -> M OutputWriter
outputWriter defaultWriter = \case
  OutputFile file -> pure (fileWriter defaultWriteError file)
  OutputStdout -> pure stdoutWriter
  OutputDefault -> defaultWriter
  OutputGithub -> githubActionsWriter

outputWriterGlobal :: OutputTarget -> M OutputWriter
outputWriterGlobal = outputWriter (pure stdoutWriter)

-- TODO The Maybes have been ported from the previous implementation.
-- I think they ware supposed to encode that no changes were to be committed, but this should probably be abstracted
-- over by this new concept.
-- Also since maint does have a useful commit message output, there needs to be an error variant (although that could
-- just be an exception in the callback).
type OutputSpecial :: Type -> Type
data OutputSpecial a where
  OutputSpecialCommitMsg :: OutputSpecial (Maybe (Text, [Text]))
  OutputSpecialGaPr :: OutputSpecial (Maybe (Map Text [Text]))
  OutputSpecialGithubOutputKey :: OutputSpecial Text

newtype OutputAdapter a =
  OutputAdapter { write :: OutputWriter -> a -> M () }

instance Semigroup (OutputAdapter a) where
  OutputAdapter l <> OutputAdapter r = OutputAdapter \ writer a -> l writer a *> r writer a

adapterSpecial ::
  (∀ b . a -> OutputSpecial b -> M b) ->
  OutputSpecial c ->
  OutputAdapter c ->
  OutputAdapter a
adapterSpecial f special (OutputAdapter use) =
  OutputAdapter \ writer a -> do
    c <- f a special
    use writer c

adapterCommit :: OutputAdapter (Maybe (Text, [Text]))
adapterCommit =
  OutputAdapter \ writer -> traverse_ \ (msg, body) -> writer.text (Text.intercalate "\n" ([msg, ""] ++ body))

formatGaPrOutput :: Text -> [Text] -> [Text]
formatGaPrOutput key = \case
  [value] -> [[exon|#{key}=#{value}|]]
  value -> [exon|#{key}<<EOF|] : value ++ ["EOF"]

adapterGaPr :: OutputAdapter (Maybe (Map Text [Text]))
adapterGaPr =
  OutputAdapter \ writer -> traverse_ \ values -> writer.textAppend (Text.unlines (format values))
  where
    format = concatMap (uncurry formatGaPrOutput) . Map.toList

adapterGaKey :: OutputAdapter Text
adapterGaKey =
  OutputAdapter \ writer key -> do
    writer.textAppend key
    writer.textAppend "="

adapterJson :: ToJSON a => Bool -> OutputAdapter a
adapterJson append =
  OutputAdapter \ writer a -> write writer (toStrict (Aeson.encode a))
  where
    write = if append then (.bytesAppend) else (.bytes)

defaultAdapter ::
  ToJSON a =>
  (∀ b . a -> OutputSpecial b -> M b) ->
  OutputFormat ->
  OutputAdapter a
defaultAdapter special = \case
  OutputNone -> OutputAdapter \ _ _ -> unit
  OutputJson -> adapterJson False
  OutputCommitMsg -> adapterSpecial special OutputSpecialCommitMsg adapterCommit
  OutputGaPr -> adapterSpecial special OutputSpecialGaPr adapterGaPr

outputAdapter ::
  ToJSON a =>
  (∀ b . a -> OutputSpecial b -> M b) ->
  OutputTarget ->
  OutputFormat ->
  OutputAdapter a
outputAdapter special target format
  | OutputGithub <- target
  , OutputJson <- format
  = adapterSpecial special OutputSpecialGithubOutputKey adapterGaKey <> adapterJson True
  | otherwise
  = defaultAdapter special format

defaultWriterFor :: OutputFormat -> M OutputWriter
defaultWriterFor = \case
  OutputGaPr -> githubActionsWriter
  _ -> pure stdoutWriter

writeOutput ::
  ToJSON a =>
  (∀ b . a -> OutputSpecial b -> M b) ->
  OutputTarget ->
  OutputFormat ->
  a ->
  M ()
writeOutput special target format value = do
  writer <- outputWriter (defaultWriterFor format) target
  adapter.write writer value
  where
    adapter = outputAdapter special target format