packages feed

ghcup-0.2.6.0: lib/GHCup/Types/JSON.hs

{-# OPTIONS_GHC -Wno-orphans #-}

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

{-|
Module      : GHCup.Types.JSON
Description : GHCup JSON types/instances
Copyright   : (c) Julian Ospald, 2020
License     : LGPL-3.0
Maintainer  : hasufell@hasufell.de
Stability   : experimental
Portability : portable
-}
module GHCup.Types.JSON where

import GHCup.Input.Parsers.URI
import GHCup.Prelude.JSON
import GHCup.Prelude.MegaParsec
import GHCup.Types
import GHCup.Types.JSON.MapIgnoreUnknownKeys
    ()
import GHCup.Types.JSON.Utils
import GHCup.Types.JSON.Versions
    ()
import GHCup.Types.Stack                     ( SetupInfo )

import Control.Applicative ( (<|>) )
import Control.Exception (displayException)
import Control.Monad
import Data.Aeson          hiding ( Key )
import Data.Aeson.TH
import Data.Aeson.Types    hiding ( Key )
import Data.ByteString     ( ByteString )
import Data.Foldable
import Data.Maybe
import Data.Scientific (toBoundedInteger)
import Data.Text.Encoding  as E
import Data.Versions
import System.FilePath     ( hasDrive, isAbsolute, splitPath )
import Text.Casing
import URI.ByteString      hiding ( parseURI )
import Data.Functor.Contravariant (Contravariant(contramap))

import qualified Data.Yaml.Aeson  as Y
import qualified Data.Map.Strict          as M
import qualified Data.List.NonEmpty       as NE
import qualified Data.Text                as T
import qualified Data.Text.Encoding.Error as E
import qualified Text.Megaparsec          as MP
import qualified Text.Megaparsec.Char.Lexer as L

safePath :: FilePath -> Bool
safePath fp
  | "." `notElem` fp'
  , ".." `notElem` fp'
  , not (hasDrive fp)
  , not (isAbsolute fp)
  = True
  | otherwise = False
 where
  fp' = splitPath fp

checkSafePath :: MonadFail m => FilePath -> m ()
checkSafePath fp = unless (safePath fp) $ fail "'..' or '.' are not allowed"

safeFilename :: FilePath -> Bool
safeFilename fp
  | length fp' == 1
  , "." `notElem` fp'
  , ".." `notElem` fp'
  , not (hasDrive fp)
  , not (isAbsolute fp)
  = True
  | otherwise = False
 where
  fp' = splitPath fp

checkSafeFilename :: MonadFail m => FilePath -> m ()
checkSafeFilename fp = unless (safeFilename fp) $ fail "'..' or '.' are not allowed and filepath must have no path separators"

safeVersion :: TargetVersion -> Bool
safeVersion TargetVersion{..}
  | pver `notElem` ["db", "set", "ghc", "cabal", "stack", "hls", "ghcup"]
  , pver' `notElem` cabalBadNames
  , safeFilename pver'
  , maybe True (`notElem` ["db", "set", "ghc", "cabal", "stack", "hls", "ghcup"]) _tvTarget
  , maybe True (safeFilename . T.unpack) _tvTarget
  , Left _ <- hasRev pver
  = True
  | otherwise = False
 where
  pver = prettyVer _tvVersion
  pver' = T.unpack $ prettyVer _tvVersion
  hasRev = MP.parse hasRevP "hasRev"
  hasRevP = do
    _ <- parseUntilEmpty "-r"
    _ <- MP.chunk "-r" *> (L.decimal @_ @_ @_ @Int)
    pure ()


-- This is sad, but our version parsers are too lax,
-- so we need to make sure that e.g. 'cabal-audit'
-- is not parser as cabal with version 'audit'.
-- Backwards compatibility is a b*tch.
cabalBadNames :: [String]
cabalBadNames =
  [ "plan"
  , "add"
  , "audit"
  , "bounds"
  , "bundler"
  , "cache"
  , "clean"
  , "core-inspection"
  , "deps"
  , "diff"
  , "docspec"
  , "doctest"
  , "edit"
  , "env"
  , "fmt"
  , "haddock-server"
  , "hasklint"
  , "helper"
  , "hie"
  , "hoogle"
  , "ifacy-query"
  , "progdeps"
  , "sort"
  , "store-check"
  , "store-gc"
  ]


checkSafeVersion :: MonadFail m => TargetVersion -> m ()
checkSafeVersion v' = unless (safeVersion v') $ fail "Unsafe version, try something more vanilla like '1.2.3'"

safeToolname :: Tool -> Bool
safeToolname (Tool t)
  | t `notElem` ["db", "set", "bin", "cache", "env", "config.yaml", "tmp", "trash", "logs"]
  , safeFilename t
  = True
  | otherwise = False

checkSafeToolname :: MonadFail m => Tool -> m ()
checkSafeToolname t = unless (safeToolname t) $ fail "Unsafe tool name, try something more vanilla"

instance ToJSON LinuxDistro where
  toJSON = String . T.pack . show

instance FromJSON LinuxDistro where
  parseJSON = withText "LinuxDistro" $ \t -> case T.unpack (T.toLower t) of
    "debian"       -> pure Debian
    "ubuntu"       -> pure Ubuntu
    "mint"         -> pure Mint
    "fedora"       -> pure Fedora
    "centos"       -> pure CentOS
    "redhat"       -> pure RedHat
    "alpine"       -> pure Alpine
    "amazonlinux"  -> pure AmazonLinux
    "rocky"        -> pure Rocky
    "void"         -> pure Void
    "gentoo"       -> pure Gentoo
    "exherbo"      -> pure Exherbo
    "opensuse"     -> pure OpenSUSE
    "unknownlinux" -> pure UnknownLinux
    _              -> fail "Unknown Linux distro"

deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''ProcessSpec
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''MetaMode
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''Architecture
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''VSep
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''MChunk
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''Platform
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''Mess
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''Chunk
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''Release
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''SemVer
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''KeepDirs
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''Downloader
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''GPGSetting
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''EnvUnion
deriveJSON defaultOptions { fieldLabelModifier = \str' -> maybe str' T.unpack . T.stripPrefix (T.pack "r-") . T.pack . kebab . tail $ str' } ''PlatformRequest

instance ToJSON Tag where
  toJSON Latest             = String "Latest"
  toJSON Recommended        = String "Recommended"
  toJSON Prerelease         = String "Prerelease"
  toJSON Nightly            = String "Nightly"
  toJSON Old                = String "Old"
  toJSON (Base       pvp'') = String ("base-" <> prettyPVP pvp'')
  toJSON (GHCCompat  ghcs)  = object ["GHCCompat" .= ghcs]
  toJSON LatestPrerelease   = String "LatestPrerelease"
  toJSON LatestNightly      = String "LatestNightly"
  toJSON Experimental       = String "Experimental"
  toJSON (UnknownTag x    ) = String (T.pack x)

instance FromJSON Tag where
  parseJSON v = textParser v <|> objectParser v <|> generic v <|> pure (UnknownTag "??")
   where
    textParser = withText "Tag" $ \t -> case T.unpack t of
      "Latest"                             -> pure Latest
      "Recommended"                        -> pure Recommended
      "Prerelease"                         -> pure Prerelease
      "Nightly"                            -> pure Nightly
      "LatestPrerelease"                   -> pure LatestPrerelease
      "LatestNightly"                      -> pure LatestNightly
      "Experimental"                       -> pure Experimental
      "old"                                -> pure Old
      "Old"                                -> pure Old
      ('b' : 'a' : 's' : 'e' : '-' : ver') -> case pvp (T.pack ver') of
        Right x -> pure $ Base x
        Left  e -> fail . show $ e
      x -> do
        case Y.decodeEither' @Value (E.encodeUtf8 $ T.pack x) of
          Right val -> objectParser val <|> pure (UnknownTag x)
          Left _ -> pure (UnknownTag x)
    objectParser = withObject "Tag" $ \o ->
      (do
         ghcs <- o .:: ["GHCCompat", "ghcCompat"]
         pure $ GHCCompat ghcs
      )
      <|>
      (do
         base <- o .:: ["Base", "base"]
         case pvp base of
           Right v' -> pure $ Base v'
           Left e -> fail (displayException e)
      )
      <|>
      (do
         tag <- o .:: ["UnknownTag", "unknownTag"]
         pure $ UnknownTag tag
      )
    generic = genericParseJSON defaultOptions { unwrapUnaryRecords = True }

instance ToJSON Tool where
  toJSON (Tool t) = String (T.toLower . T.pack $ t)

instance FromJSON Tool where
  parseJSON = withText "Tool" $ \t -> do
    let tool = Tool . T.unpack . T.toLower $ t
    checkSafeToolname tool
    pure tool

instance ToJSON URI where
  toJSON = toJSON . E.decodeUtf8With E.lenientDecode . serializeURIRef'


instance FromJSON URI where
  parseJSON = withText "URL" $ \t ->
    case parseURI (encodeUtf8 t) of
      Right x -> pure x
      Left  e -> fail . show $ e

instance ToJSON TargetVersion where
  toJSON = toJSON . tVerToText

instance FromJSON TargetVersion where
  parseJSON = withText "TargetVersion" $ \t -> case MP.parse ghcTargetVerP "" t of
    Right x -> do
      checkSafeVersion x
      pure x
    Left  e -> fail $ "Failure in TargetVersion (FromJSON)" <> show e

instance ToJSONKey TargetVersion where
  toJSONKey = toJSONKeyText $ \x -> tVerToText x

deriveJSON defaultOptions { unwrapUnaryRecords = True } ''Rev

instance FromJSONKey Rev where
  fromJSONKey = Rev <$> fromJSONKey @Int

instance ToJSONKey Rev where
  toJSONKey = contramap unRev toJSONKey

instance FromJSONKey TargetVersion where
  fromJSONKey = FromJSONKeyTextParser $ \t -> case MP.parse ghcTargetVerP "" t of
    Right x -> do
      checkSafeVersion x
      pure x
    Left  e -> fail $ "Failure in TargetVersion (FromJSONKey)" <> show e


instance ToJSONKey Platform where
  toJSONKey = toJSONKeyText $ \case
    Darwin  -> T.pack "Darwin"
    FreeBSD -> T.pack "FreeBSD"
    Linux d -> T.pack ("Linux_" <> show d)
    OpenBSD -> T.pack "OpenBSD"
    Windows -> T.pack "Windows"

instance FromJSONKey Platform where
  fromJSONKey = FromJSONKeyTextParser $ \t -> if
    | T.pack "Darwin" == t -> pure Darwin
    | T.pack "FreeBSD" == t -> pure FreeBSD
    | T.pack "OpenBSD" == t -> pure OpenBSD
    | T.pack "Windows" == t -> pure Windows
    | T.pack "Linux_" `T.isPrefixOf` t -> case
        T.stripPrefix (T.pack "Linux_") t
      of
        Just dstr ->
          case
              (decodeStrict (E.encodeUtf8 (T.pack "\"" <> dstr <> T.pack "\"")) :: Maybe
                  LinuxDistro
              )
            of
              Just d -> pure $ Linux d
              Nothing ->
                fail
                  $  "Unexpected failure in decoding LinuxDistro: "
                  <> show dstr
        Nothing -> fail "Unexpected failure in Platform stripPrefix"
    | otherwise -> fail "Failure in Platform (FromJSONKey)"

instance ToJSONKey Architecture where
  toJSONKey = genericToJSONKey defaultJSONKeyOptions

instance FromJSONKey Architecture where
  fromJSONKey = genericFromJSONKey defaultJSONKeyOptions

instance ToJSONKey Tool where
  toJSONKey = toJSONKeyText $ \(Tool t) -> T.toLower . T.pack $ t

instance FromJSONKey Tool where
  fromJSONKey = FromJSONKeyTextParser $ \(T.unpack . T.toLower -> t) -> pure (Tool t)

instance ToJSON TarDir where
  toJSON (RealDir  p) = object ["RealDir" .= p]
  toJSON (RegexDir r) = object ["RegexDir" .= r]

instance FromJSON TarDir where
  parseJSON v = realDir v <|> regexDir v <|> realDirStrict v
   where
    realDir = withText "TarDir" $ \t -> do
      fp <- parseJSON (String t)
      pure (RealDir fp)
    realDirStrict = withObject "TarDir" $ \o -> do
      r <- o .:: ["RealDir", "realDir"]
      pure $ RealDir r
    regexDir = withObject "TarDir" $ \o -> do
      r <- o .:: ["RegexDir", "regexDir"]
      pure $ RegexDir r


instance ToJSON VersionCmp where
  toJSON = String . versionCmpToText

instance FromJSON VersionCmp where
  parseJSON = withText "VersionCmp" $ \t -> do
    case MP.parse versionCmpP "" t of
      Right r -> pure r
      Left  e -> fail (MP.errorBundlePretty e)

instance ToJSON ByteString where
  toJSON = toJSON . E.decodeUtf8With E.lenientDecode

instance FromJSON ByteString where
  parseJSON = withText "ByteString" $ \t -> pure $ E.encodeUtf8 t

versionCmpToText :: VersionCmp -> T.Text
versionCmpToText (VR_gt   ver') = "> " <> prettyV ver'
versionCmpToText (VR_gteq ver') = ">= " <> prettyV ver'
versionCmpToText (VR_lt   ver') = "< " <> prettyV ver'
versionCmpToText (VR_lteq ver') = "<= " <> prettyV ver'
versionCmpToText (VR_eq   ver') = "== " <> prettyV ver'

instance ToJSON VersionRange where
  toJSON = String . verRangeToText

verRangeToText :: VersionRange -> T.Text
verRangeToText  (SimpleRange cmps) =
  let inner = foldr1 (\x y -> x <> " && " <> y)
                     (versionCmpToText <$> NE.toList cmps)
  in  "( " <> inner <> " )"
verRangeToText (OrRange cmps range) =
  let left  = verRangeToText (SimpleRange cmps)
      right = verRangeToText range
  in  left <> " || " <> right

instance FromJSON VersionRange where
  parseJSON = withText "VersionRange" $ \t -> do
    case MP.parse versionRangeP "" t of
      Right r -> pure r
      Left  e -> fail (MP.errorBundlePretty e)


instance ToJSONKey (Maybe VersionRange) where
  toJSONKey = toJSONKeyText $ \case
    Just x -> verRangeToText x
    Nothing -> "unknown_versioning"

instance FromJSONKey (Maybe VersionRange)  where
  fromJSONKey = FromJSONKeyTextParser $ \t ->
    if t == T.pack "unknown_versioning" then pure Nothing else just t
   where
    just t = case MP.parse versionRangeP "" t of
      Right x -> pure $ Just x
      Left  e -> fail $ "Failure in (Maybe VersionRange) (FromJSONKey)" <> MP.errorBundlePretty e

deriveToJSON defaultOptions { fieldLabelModifier = mapHead lower . drop 3 } ''SymlinkSpec
deriveToJSON defaultOptions { fieldLabelModifier = mapHead lower . drop 3
                            , sumEncoding = UntaggedValue
                            } ''SymlinkInputSpec
deriveToJSON defaultOptions { fieldLabelModifier = mapHead lower . drop 4
                            , sumEncoding = UntaggedValue
                            } ''InstallFileRule
deriveJSON defaultOptions { fieldLabelModifier = mapHead lower . drop 3 } ''EnvSpec
deriveToJSON defaultOptions { fieldLabelModifier = mapHead lower . drop 3 } ''ConfigSpec
deriveToJSON defaultOptions { fieldLabelModifier = mapHead lower . drop 3 } ''MakeSpec

instance FromJSON SymlinkFileSpec where
  parseJSON = withObject "SymlinkSpec" $ \o -> do
    _slTarget <-   o .:  "target"
    _slLinkName <- o .: "linkName"
    _slPVPMajorLinks <- fromMaybe False <$> o .:? "pVPMajorLinks"
    _slSetName <- o .:? "setName"
    pure SymlinkSpec{..}

instance FromJSON SymlinkInputSpec where
  parseJSON = withObject "SymlinkInputSpec" $ \o -> do
    mTarget <- o .:?  "target"
    case mTarget of
      Just _slTarget -> do
        _slLinkName <- o .: "linkName"
        _slPVPMajorLinks <- o .:? "pVPMajorLinks" .!= False
        _slSetName <- o .:? "setName"
        pure SymlinkInputSpec{..}
      Nothing -> do
        _slTargetPattern <- o .: "targetPattern"
        _slTargetPatternIgnore <- o .:? "targetPatternIgnore" .!= []
        _slLinkName <- o .: "linkName"
        _slPVPMajorLinks <- o .:? "pVPMajorLinks" .!= False
        _slSetName <- o .:? "setName"
        pure SymlinkPatternSpec{..}

instance FromJSON InstallFileRule where
  parseJSON = withObject "InstallFileRule" $ \o -> do
    installPattern   <- o .:? "installPattern"
    case installPattern of
      Just iPs -> do
        forM_ iPs checkSafePath
        pure $ InstallFilePatternRule iPs
      Nothing -> do
        installSource <- o .: "installSource"
        checkSafePath installSource
        installDest   <- o .:? "installDest"
        checkSafePath (fromMaybe "" installDest)
        pure $ InstallFileRule installSource installDest


instance FromJSON ConfigSpec where
  parseJSON = withObject "ConfigSpec" $ \o -> do
    _csConfigArgs     <- o .:  "configArgs"
    _csConfigEnv      <- o .:? "configEnv"
    _csConfigFile     <- o .:? "configFile"

    pure $ ConfigSpec {..}

instance FromJSON MakeSpec where
  parseJSON = withObject "MakeSpec" $ \o -> do
    _msMakeArgs       <- o .:  "makeArgs"
    _msMakeEnv        <- o .:? "makeEnv"

    pure $ MakeSpec {..}

instance FromJSON a => FromJSON (InstallationSpecGen a) where
  parseJSON = withObject "InstallationSpec" $ \o -> do
    _isExeRules       <- o .:? "exeRules" .!= []
    _isDataRules      <- o .:? "dataRules" .!= []
    _isExeSymLinked   <- o .:? "exeSymLinked" .!= []
    _isConfigure      <- o .:? "configure"
    _isMake           <- o .:? "make"
    _isPreserveMtimes <- o .:? "preserveMtimes" .!= False

    pure $ InstallationSpec {..}

instance ToJSON a => ToJSON (InstallationSpecGen a) where
  toJSON = genericToJSON defaultOptions { fieldLabelModifier = mapHead lower . drop 3 }


deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''Requirements
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''DownloadInfo
deriveJSON defaultOptions { unwrapUnaryRecords = True } ''PlatformVersionSpec
deriveJSON defaultOptions { unwrapUnaryRecords = True } ''PlatformSpec
deriveJSON defaultOptions { unwrapUnaryRecords = True } ''ArchitectureSpec
deriveToJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''VersionInfo
deriveJSON defaultOptions { fieldLabelModifier = mapHead lower . drop 1 } ''ToolDescription

instance FromJSON InstallMetadata where
  parseJSON v = newParse v <|> legacyParse v
   where
    legacyParse o = do
      v'@DownloadInfo{..} <- parseJSON o
      case _dlInstallSpec of
        Nothing -> fail "No install metadata in legacy parser"
        Just InstallationSpec{..} -> do
          isExeSymLinked' <- forM _isExeSymLinked toSymlSpec
          pure $ InstallMetadata v' InstallationSpec{ _isExeSymLinked = isExeSymLinked', ..} Nothing 0

    toSymlSpec SymlinkInputSpec{..} = pure SymlinkSpec{..}
    toSymlSpec _ = fail "Can't handle SymlinkPatternSpec in legacy parser"

    newParse = do
      withObject "InstallMetadata" $ \o -> do
        _imDownloadInfo <- o .: "downloadInfo"
        _imResolvedInstallSpec <- o .: "resolvedInstallSpec"
        _imToolDescription <- o .:? "toolDescription"
        _imRevision <- o .:? "revision" .!= 0
        pure InstallMetadata{..}

instance FromJSON VersionInfo where
  parseJSON = withObject "VersionInfo" $ \o -> do
    _viSourceDL <- o .::? ["viSourceDL", "sourceDL"]
    _viTestDL <- o .::? ["viTestDL", "testDL"]
    _viArch <- o .:: ["viArch", "arch"]
    pure VersionInfo{..}

deriveJSON defaultOptions ''DhallRevision
deriveJSON defaultOptions { unwrapUnaryRecords = True } ''RevisionSpecDhall

instance FromJSON RevisionSpec where
  parseJSON v = newParse v <|> legacyParse v <|> dhallParse
   where
    legacyParse o = do
      vi <- parseJSON o
      pure $ RevisionSpec $ M.singleton (Rev 0) vi
    newParse o = do
      m <- parseJSON o
      pure $ RevisionSpec m
    dhallParse =
      (\(unRevisionSpecDhall -> dhallSpec) ->
        RevisionSpec $ M.fromList $ fmap (\DhallRevision{..} -> (Rev mapKey, mapValue)) dhallSpec
      )
      <$> parseJSON @RevisionSpecDhall v


instance FromJSON VersionMetadata where
  parseJSON v = newParse v <|> legacyParse v
   where
    legacyParse =
      withObject "VersionMetadata" $ \o -> do
        _vmTags <- o .: "viTags"
        _vmReleaseDay <- o .:? "viReleaseDay"
        _vmChangeLog <- o .:? "viChangeLog"
        _viSourceDL <- o .:? "viSourceDL"
        _viTestDL <- o .:? "viTestDL"
        _viArch <- ArchitectureSpec <$> o .: "viArch"
        let vi = VersionInfo {..}
        let _vmRevisionSpec = RevisionSpec $ M.singleton (Rev 0) vi
        _vmPreInstall <- o .:? "viPreInstall"
        _vmPostInstall <- o .:? "viPostInstall"
        _vmPostRemove <- o .:? "viPostRemove"
        _vmPreCompile <- o .:? "viPreCompile"
        pure VersionMetadata{..}
    newParse = do
      withObject "VersionMetadata" $ \o -> do
        _vmTags         <- o .:? "tags" .!= []
        _vmReleaseDay   <- o .:? "releaseDay"
        _vmChangeLog    <- o .::? ["changelog", "changeLog"]
        _vmPreInstall   <- o .:? "preInstall"
        _vmPostInstall  <- o .:? "postInstall"
        _vmPostRemove   <- o .:? "postRemove"
        _vmPreCompile   <- o .:? "preCompile"
        _vmRevisionSpec <- o .:  "revisionSpec"
        pure VersionMetadata{..}

deriveToJSON defaultOptions { unwrapUnaryRecords = True } ''RevisionSpec

instance ToJSON VersionMetadata where
  toJSON VersionMetadata{..} = object
    [ "tags"         .= _vmTags
    , "releaseDay"   .= _vmReleaseDay
    , "changeLog"    .= _vmChangeLog
    , "preInstall"   .= _vmPreInstall
    , "postInstall"  .= _vmPostInstall
    , "postRemove"   .= _vmPostRemove
    , "preCompile"   .= _vmPreCompile
    , "revisionSpec" .= _vmRevisionSpec
    ]

deriveJSON defaultOptions { unwrapUnaryRecords = True } ''ToolVersionSpec
deriveToJSON defaultOptions { fieldLabelModifier = mapHead lower . drop 3 } ''InstallMetadata
deriveToJSON defaultOptions { fieldLabelModifier = mapHead lower . drop 1 } ''ToolInfo

instance FromJSON ToolInfo where
  parseJSON v = newParse v <|> legacyParse v
   where
    legacyParse o = do
      v' <- parseJSON o
      pure $ ToolInfo v' Nothing
    newParse = do
      withObject "ToolInfo" $ \o -> do
        _toolVersions <- o .: "toolVersions"
        _toolDetails <- o .: "toolDetails"
        pure ToolInfo{..}

deriveJSON defaultOptions { unwrapUnaryRecords = True } ''GHCupDownloads

instance FromJSON GHCupInfo where
  parseJSON = withObject "GHCupInfo" $ \o -> do
    toolRequirements' <- o .:? "toolRequirements" .!= mempty
    metadataUpdate    <- o .:? "metadataUpdate"
    ghcupDownloads'   <- o .:  "ghcupDownloads"
    pure (GHCupInfo toolRequirements' ghcupDownloads' metadataUpdate)

deriveToJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''GHCupInfo

instance ToJSON NewURLSource where
  toJSON NewGHCupURL         = String "GHCupURL"
  toJSON NewStackSetupURL    = String "StackSetupURL"
  toJSON (NewGHCupInfo gi)   = object [ "ghcup-info" .= gi ]
  toJSON (NewSetupInfo si)   = object [ "setup-info" .= si ]
  toJSON (NewURI uri)        = toJSON uri
  toJSON (NewChannelAlias c) = toJSON c

instance ToJSON URLSource where
  toJSON = toJSON . fromURLSource

instance ToJSON ChannelAlias where
  toJSON = String . channelAliasText

instance FromJSON ChannelAlias where
  parseJSON = withText "ChannelAlias" $ \t ->
    let aliases = map (\c -> (channelAliasText c, c)) [minBound..maxBound]
    in case lookup t aliases of
      Just c  -> pure c
      Nothing -> fail $ "Unexpected ChannelAlias: " <> T.unpack t

deriveJSON defaultOptions { sumEncoding = ObjectWithSingleField } ''Key
deriveJSON defaultOptions { sumEncoding = ObjectWithSingleField } ''Modifier
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel, unwrapUnaryRecords = True } ''Port
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel, unwrapUnaryRecords = True } ''Host
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''UserInfo
deriveJSON defaultOptions { fieldLabelModifier = \str' -> maybe str' (T.unpack . T.toLower) . T.stripPrefix (T.pack "authority") . T.pack $ str' } ''Authority
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''DownloadMirror
deriveJSON defaultOptions { fieldLabelModifier = removeLensFieldLabel } ''DownloadMirrors


instance FromJSON URLSource where
  parseJSON v =
        parseGHCupURL v
    <|> parseStackURL v
    <|> parseOwnSourceLegacy v
    <|> parseOwnSourceNew1 v
    <|> parseOwnSourceNew2 v
    <|> parseOwnSpec v
    <|> legacyParseAddSource v
    <|> newParseAddSource v
    -- new since Stack SetupInfo
    <|> parseOwnSpecNew v
    <|> parseOwnSourceNew3 v
    <|> newParseAddSource2 v
    -- more lenient versions
    <|> parseOwnSpecLenient v
    <|> parseOwnSourceLenient v
    <|> parseAddSourceLenient v
    -- simplified list
    <|> parseNewUrlSource v
    <|> parseNewUrlSource' v
   where
    convert'' :: Either GHCupInfo URI -> Either (Either GHCupInfo SetupInfo) URI
    convert'' (Left gi)   = Left (Left gi)
    convert'' (Right uri) = Right uri

    parseOwnSourceLegacy = withObject "URLSource" $ \o -> do
      r :: URI <- o .: "OwnSource"
      pure (OwnSource [Right r])
    parseOwnSourceNew1 = withObject "URLSource" $ \o -> do
      r :: [URI] <- o .: "OwnSource"
      pure (OwnSource (fmap Right r))
    parseOwnSourceNew2 = withObject "URLSource" $ \o -> do
      r :: [Either GHCupInfo URI] <- o .: "OwnSource"
      pure (OwnSource (convert'' <$> r))
    parseOwnSpec = withObject "URLSource" $ \o -> do
      r :: GHCupInfo <- o .: "OwnSpec"
      pure (OwnSpec $ Left r)
    parseGHCupURL = withObject "URLSource" $ \o -> do
      _ :: [Value] <- o .: "GHCupURL"
      pure GHCupURL
    parseStackURL = withObject "URLSource" $ \o -> do
      _ :: [Value] <- o .: "StackSetupURL"
      pure StackSetupURL
    legacyParseAddSource = withObject "URLSource" $ \o -> do
      r :: Either GHCupInfo URI <- o .: "AddSource"
      pure (AddSource [convert'' r])
    newParseAddSource = withObject "URLSource" $ \o -> do
      r :: [Either GHCupInfo URI] <- o .: "AddSource"
      pure (AddSource (convert'' <$> r))

    -- new since Stack SetupInfo
    parseOwnSpecNew = withObject "URLSource" $ \o -> do
      r :: Either GHCupInfo SetupInfo <- o .: "OwnSpec"
      pure (OwnSpec r)
    parseOwnSourceNew3 = withObject "URLSource" $ \o -> do
      r :: [Either (Either GHCupInfo SetupInfo) URI] <- o .: "OwnSource"
      pure (OwnSource r)
    newParseAddSource2 = withObject "URLSource" $ \o -> do
      r :: [Either (Either GHCupInfo SetupInfo) URI] <- o .: "AddSource"
      pure (AddSource r)

    -- more lenient versions
    parseOwnSpecLenient = withObject "URLSource" $ \o -> do
      spec :: Object <- o .: "OwnSpec"
      OwnSpec <$> lenientInfoParser spec
    parseOwnSourceLenient = withObject "URLSource" $ \o -> do
      mown :: Array <- o .: "OwnSource"
      OwnSource . toList <$> mapM lenientInfoUriParser mown
    parseAddSourceLenient = withObject "URLSource" $ \o -> do
      madd :: Array <- o .: "AddSource"
      AddSource . toList <$> mapM lenientInfoUriParser madd

    -- simplified
    parseNewUrlSource = withArray "URLSource" $ \a -> do
      SimpleList . toList <$> mapM parseJSON a
    parseNewUrlSource' v' = SimpleList .(:[]) <$> parseJSON v'


lenientInfoUriParser :: Value -> Parser (Either (Either GHCupInfo SetupInfo) URI)
lenientInfoUriParser (Object o) = Left <$> lenientInfoParser o
lenientInfoUriParser v@(String _) = Right <$> parseJSON v
lenientInfoUriParser _ = fail "Unexpected json in lenientInfoUriParser"


lenientInfoParser :: Object -> Parser (Either GHCupInfo SetupInfo)
lenientInfoParser o = do
  setup_info :: Maybe Object <- o .:? "setup-info"
  case setup_info of
    Nothing -> do
      r <- parseJSON (Object o)
      pure $ Left r
    Just setup_info' -> do
      r <- parseJSON (Object setup_info')
      pure $ Right r

instance FromJSON NewURLSource where
  parseJSON v = uri v <|> url v <|> alias v <|> gi v <|> si v
   where
    alias = withText "NewURLSource" $ \t -> NewChannelAlias <$> parseJSON (String t)
    uri = withText "NewURLSource" $ \t -> NewURI <$> parseJSON (String t)
    url = withText "NewURLSource" $ \t -> case T.unpack t of
                                            "GHCupURL" -> pure NewGHCupURL
                                            "StackSetupURL" -> pure NewStackSetupURL
                                            t' -> fail $ "Unexpected text value in NewURLSource: " <> t'
    gi = withObject "NewURLSource" $ \o -> do
       ginfo :: GHCupInfo <- o .: "ghcup-info"
       pure $ NewGHCupInfo ginfo

    si = withObject "NewURLSource" $ \o -> do
       sinfo :: SetupInfo <- o .: "setup-info"
       pure $ NewSetupInfo sinfo


instance FromJSON KeyCombination where
  parseJSON v = proper v <|> simple v
   where
    simple = withObject "KeyCombination" $ \o -> do
      k <- parseJSON (Object o)
      pure (KeyCombination k [])
    proper = withObject "KeyCombination" $ \o -> do
      k <- o .: "Key"
      m <- o .: "Mods"
      pure $ KeyCombination k m

instance ToJSON KeyCombination where
  toJSON (KeyCombination k m) = object ["Key" .= k, "Mods" .= m]

instance FromJSON PagerConfig where
  parseJSON v = p1 v <|> p2 v <|> p3 v
   where
    p2 = withBool "PagerConfig" $ \b -> pure $ PagerConfig b Nothing
    p3 = withText "PagerConfig" $ \t -> pure $ allPagerConfig (T.unpack t)
    p1 = withObject "PagerConfig" $ \o -> do
       list <- o .:  "list"
       cmd  <- o .:? "cmd"
       pure $ PagerConfig list cmd

instance FromJSON Verbosity where
  parseJSON v = new v <|> legacy v
   where
    legacy = withBool "Verbosity" $ \b -> do
      pure $ Verbosity (if b then 1 else 0)
    new = withScientific "Verbosity" $ \s -> do
      int <- maybe (fail "Verbosity integer out of bounds") pure $ toBoundedInteger s
      pure $ Verbosity int

deriveToJSON defaultOptions { unwrapUnaryRecords = True } ''Verbosity
deriveToJSON defaultOptions { fieldLabelModifier = \str' -> maybe str' T.unpack . T.stripPrefix (T.pack "pager-") . T.pack . kebab $ str' } ''PagerConfig
deriveToJSON defaultOptions { fieldLabelModifier = drop 2 . kebab } ''KeyBindings -- move under key-bindings key
deriveJSON defaultOptions { fieldLabelModifier = \str' -> maybe str' T.unpack . T.stripPrefix (T.pack "k-") . T.pack . kebab $ str' } ''UserKeyBindings
deriveJSON defaultOptions { fieldLabelModifier = \str' -> maybe str' T.unpack . T.stripPrefix (T.pack "u-") . T.pack . kebab $ str' } ''UserSettings
deriveToJSON defaultOptions { fieldLabelModifier = kebab } ''Settings