packages feed

baikai-kit-0.2.0.0: src/Baikai/Kit/Error.hs

-- | Every way a kit operation can fail.
--
--   Library functions return these; only 'Baikai.Kit.Command.runKit'
--   turns one into a process exit. The 'Exception' instance exists for a
--   caller who prefers exceptions and can write @either throwIO pure@.
module Baikai.Kit.Error
  ( KitError (..),
    renderKitError,
  )
where

import Baikai.Prelude
import Control.Exception (Exception)
import Data.Text qualified as Text

-- | A kit operation's failure. Constructors are positional because
--   @-Wpartial-fields@ is on and a record sum would warn.
data KitError
  = -- | The kit checkout holds no @kit.json@.
    KitManifestMissing FilePath
  | -- | The manifest did not decode; the 'Text' is aeson's message.
    KitManifestInvalid FilePath Text
  | -- | The manifest declares a @version@ this installer does not support.
    KitManifestVersionUnsupported FilePath Int
  | -- | No skill or agent of that name is listed.
    KitItemNotFound Text
  | -- | The item lists no source files.
    KitItemHasNoFiles Text
  | -- | An item name failed 'Baikai.Kit.Path.safeItemName'; the second
    --   'Text' is that function's reason.
    KitUnsafeName Text Text
  | -- | A manifest path failed 'Baikai.Kit.Path.safeRelativePath'.
    KitUnsafePath Text Text
  | -- | A listed source file is not there.
    KitSourceMissing FilePath
  | -- | A component of a listed source path is a symbolic link. A kit is
    --   plain files; see 'Baikai.Kit.Path.safeSourcePath'.
    KitSourceSymlink FilePath
  | -- | The canonical source path (first field) is not below the
    --   canonical kit checkout (second field).
    KitSourceEscapes FilePath FilePath
  | -- | Inspecting or reading a source raised an 'IOException'.
    KitSourceUnreadable FilePath Text
  | -- | A first clone failed and no usable cache exists: the repository
    --   URL and git's output.
    KitCloneFailed Text Text
  | -- | @git pull@ failed during @kit update@.
    KitPullFailed Text
  | -- | An install could not be completed: the reason, the destinations
    --   restored by rollback, and the destinations left inconsistent.
    --   Both lists are empty when nothing was changed.
    KitWriteFailed Text [FilePath] [FilePath]
  deriving stock (Eq, Show)
  deriving anyclass (Exception)

-- | The message a command adapter prints. One line, except where a
--   failure needs to say what it left behind.
renderKitError :: KitError -> Text
renderKitError = \case
  KitManifestMissing path ->
    "kit.json not found in kit repository (" <> Text.pack path <> ")."
  KitManifestInvalid path reason ->
    "failed to parse " <> Text.pack path <> ": " <> reason
  -- The supported versions are spelled out rather than read from
  -- 'Baikai.Kit.Manifest.supportedManifestVersions': Manifest imports
  -- this module, so the dependency cannot run the other way.
  KitManifestVersionUnsupported path n ->
    Text.pack path
      <> " declares manifest version "
      <> Text.pack (show n)
      <> "; this installer supports versions 1 and 2."
  KitItemNotFound n -> "'" <> n <> "' not found in kit manifest."
  KitItemHasNoFiles n -> "'" <> n <> "' lists no source files."
  KitUnsafeName raw reason -> "unsafe item name '" <> raw <> "': " <> reason
  KitUnsafePath raw reason -> "unsafe manifest path '" <> raw <> "': " <> reason
  KitSourceMissing path -> "source file does not exist: " <> Text.pack path
  KitSourceSymlink path -> "refusing symbolic link in kit source: " <> Text.pack path
  KitSourceEscapes path root ->
    "kit source resolves outside the kit checkout: "
      <> Text.pack path
      <> " (checkout: "
      <> Text.pack root
      <> ")"
  KitSourceUnreadable path reason ->
    "cannot inspect kit source " <> Text.pack path <> ": " <> reason
  KitCloneFailed url output ->
    "failed to fetch kit repository " <> url <> ": " <> output
  KitPullFailed output ->
    "failed to update kit repository: "
      <> output
      <> "\nThe cached copy is unchanged; installed items were not reinstalled."
  KitWriteFailed reason restored leftInconsistent ->
    Text.intercalate "\n" ("install failed: " <> reason : aftermath)
    where
      aftermath
        | null restored && null leftInconsistent = ["No changes were made."]
        | otherwise =
            ["Restored: " <> renderPaths restored | not (null restored)]
              <> [ "Left inconsistent (repair by reinstalling): " <> renderPaths leftInconsistent
                 | not (null leftInconsistent)
                 ]
      renderPaths = Text.intercalate ", " . map Text.pack