packages feed

kiroku-cli-0.2.0.11: src/Kiroku/Cli/Standalone.hs

module Kiroku.Cli.Standalone
  ( StandaloneOptions (..),
    StandaloneRuntime (..),
    standaloneOptionsParser,
    standaloneParserInfo,
    resolveStandaloneOptions,
    runStandaloneCommand,
  )
where

import Control.Applicative ((<|>))
import Data.Generics.Labels ()
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import Kiroku.Cli.Command
  ( KirokuCommand (..),
    RemoteEndpoint (..),
    StatusOptions (..),
    SubscriptionCommand (..),
  )
import Kiroku.Cli.Parser (kirokuCommandParser)
import Kiroku.Cli.Subscription.Status (renderRemoteSubscriptionStatus)
import Options.Applicative
  ( Parser,
    ParserInfo,
    fullDesc,
    header,
    helper,
    info,
    progDesc,
    (<**>),
  )

-- | Options for the standalone @kiroku@ binary. The binary is a pure remote
-- client: it carries only the parsed command (whose @--remote-url@ lives inside
-- 'StatusOptions'); it never opens a store.
newtype StandaloneOptions = StandaloneOptions
  { command :: KirokuCommand
  }
  deriving stock (Generic, Eq, Show)

-- | The resolved command, with any subscription-status endpoint filled in from
-- @--remote-url@ or @KIROKU_REMOTE_URL@.
newtype StandaloneRuntime = StandaloneRuntime
  { command :: KirokuCommand
  }
  deriving stock (Generic)

standaloneParserInfo :: ParserInfo StandaloneOptions
standaloneParserInfo =
  info
    (standaloneOptionsParser <**> helper)
    ( fullDesc
        <> progDesc
          "Inspect a running Kiroku worker over HTTP. Subscription status queries the worker's \
          \kiroku-metrics /subscriptions endpoint (--remote-url or KIROKU_REMOTE_URL); the binary \
          \opens no store of its own."
        <> header "kiroku - remote operator client for Kiroku event stores"
    )

standaloneOptionsParser :: Parser StandaloneOptions
standaloneOptionsParser =
  StandaloneOptions <$> kirokuCommandParser

-- | Resolve the parsed options into a runnable command. For a subscription-status
-- command, the endpoint is taken from @--remote-url@ or, failing that, the
-- @KIROKU_REMOTE_URL@ environment variable; if neither is present, fail with
-- guidance. Commands that need no endpoint pass through unchanged.
resolveStandaloneOptions :: [(String, String)] -> StandaloneOptions -> Either Text StandaloneRuntime
resolveStandaloneOptions env (StandaloneOptions cmd) =
  case cmd of
    KirokuNoCommand -> Right (StandaloneRuntime KirokuNoCommand)
    KirokuSubscriptions (SubscriptionStatus (StatusOptions format mEndpoint)) ->
      case mEndpoint <|> envEndpoint of
        Just ep ->
          Right
            ( StandaloneRuntime
                (KirokuSubscriptions (SubscriptionStatus (StatusOptions format (Just ep))))
            )
        Nothing -> Left noEndpointMessage
  where
    envEndpoint =
      case T.pack <$> lookup "KIROKU_REMOTE_URL" env of
        Just url | not (T.null url) -> Just (RemoteEndpoint url)
        _ -> Nothing
    noEndpointMessage =
      "kiroku: no worker endpoint; pass --remote-url or set KIROKU_REMOTE_URL. \
      \The standalone binary inspects a running worker over HTTP; it cannot see \
      \in-process subscriptions because it runs none."

runStandaloneCommand :: StandaloneRuntime -> IO Text
runStandaloneCommand (StandaloneRuntime cmd) =
  case cmd of
    KirokuNoCommand -> pure "No Kiroku operator command was selected."
    KirokuSubscriptions (SubscriptionStatus (StatusOptions format (Just ep))) ->
      renderRemoteSubscriptionStatus ep format
    KirokuSubscriptions (SubscriptionStatus (StatusOptions _ Nothing)) ->
      -- Unreachable: resolveStandaloneOptions fills the endpoint or fails.
      pure
        "kiroku: no worker endpoint; pass --remote-url or set KIROKU_REMOTE_URL."