packages feed

cabal-matrix-1.0.0.0: src/Cabal/Matrix/Files.hs

module Cabal.Matrix.Files
  ( writeExprFile
  , readExprFile
  , writeMatrixFile
  , readMatrixFile
  , writeRecordFile
  , readRecordFile
  ) where

import Cabal.Matrix.CabalArgs
import Cabal.Matrix.Matrix
import Cabal.Matrix.Record
import Cabal.Matrix.Rectangle qualified as Rectangle
import Data.Aeson
import Data.Aeson qualified as Aeson
import Data.Aeson.Encoding qualified as Encoding
import Data.Aeson.Types
import Data.Foldable
import Data.Functor
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as Text
import Distribution.Parsec
import Distribution.Pretty
import Distribution.Types.PackageName
import Distribution.Types.Version
import Distribution.Types.VersionRange
import System.Exit


newtype PreferJSON = PreferJSON Prefer

instance ToJSON PreferJSON where
  toJSON = \case
    PreferJSON PreferOldest -> "old"
    PreferJSON PreferNewest -> "new"

instance FromJSON PreferJSON where
  parseJSON = withText "Prefer" \case
    "old" -> pure $ PreferJSON PreferOldest
    "new" -> pure $ PreferJSON PreferNewest
    _ -> fail "Expected \"old\" or \"new\""

newtype VersionJSON = VersionJSON (Either Version VersionRange)

instance ToJSON VersionJSON where
  toJSON = \case
    VersionJSON (Left version) -> toJSON $ prettyShow version
    VersionJSON (Right range) -> toJSON $ prettyShow range

instance FromJSON VersionJSON where
  parseJSON = withText "Version" \(Text.unpack -> s)
    -> VersionJSON <$> case eitherParsec @Version s of
      Right ver -> pure $ Left ver
      Left err1 -> case eitherParsec @VersionRange s of
        Right range -> pure $ Right range
        Left err2 -> fail $ unlines
          ["Expected a version or a version range: ", err1, err2]

newtype MatrixExprJSON = MatrixExprJSON MatrixExpr

instance ToJSON MatrixExprJSON where
  toJSON (MatrixExprJSON e) = go e
    where
      go = \case
        TimesExpr x y -> object
          [ "times" .= toJSON (MatrixExprJSON x, MatrixExprJSON y)
          ]
        AddExpr x y -> object
          [ "add" .= toJSON (MatrixExprJSON x, MatrixExprJSON y)
          ]
        SubtractExpr x y -> object
          [ "subtract" .= toJSON (MatrixExprJSON x, MatrixExprJSON y)
          ]
        SeqExpr x y -> object
          [ "seq" .= toJSON (MatrixExprJSON x, MatrixExprJSON y)
          ]
        UnitExpr -> "unit"
        CompilersExpr compilers -> object
          [ "compilers" .= toJSON
            (compilers <&> \(Compiler compiler) -> compiler)
          ]
        PreferExpr values -> object
          [ "prefer" .= toJSON (PreferJSON <$> values)
          ]
        PackageVersionExpr packageName versions -> object
          [ "package" .= unPackageName packageName
          , "versions" .= toJSON (VersionJSON <$> versions)
          ]
        CustomUnorderedExpr key options -> object
          [ "custom_unordered" .= key
          , "options" .= toJSON options
          ]
        CustomOrderedExpr key options -> object
          [ "custom_ordered" .= key
          , "options" .= toJSON options
          ]

instance FromJSON MatrixExprJSON where
  parseJSON (String "unit") = pure $ MatrixExprJSON UnitExpr
  parseJSON (String _) = fail "Expected string to be \"unit\""
  parseJSON (Object o) = do
    parses <- sequenceA
      [ (o .:? "times") <&> fmap @Maybe
        \(MatrixExprJSON x, MatrixExprJSON y) -> TimesExpr x y
      , (o .:? "add") <&> fmap @Maybe
        \(MatrixExprJSON x, MatrixExprJSON y) -> AddExpr x y
      , (o .:? "subtract") <&> fmap @Maybe
        \(MatrixExprJSON x, MatrixExprJSON y) -> SubtractExpr x y
      , (o .:? "seq") <&> fmap @Maybe
        \(MatrixExprJSON x, MatrixExprJSON y) -> SeqExpr x y
      , (o .:? "compilers") <&> fmap @Maybe (CompilersExpr . map Compiler)
      , (o .:? "prefer") <&> fmap @Maybe
        (PreferExpr . map \(PreferJSON value) -> value)
      , do
        mPackage <- o .:? "package"
        mVersions <- o .:? "versions"
        case (mPackage, mVersions) of
          (Nothing, Nothing) -> pure Nothing
          (Just package, Just versions) -> pure $ Just $ PackageVersionExpr
            (mkPackageName package)
            (versions <&> \(VersionJSON version) -> version)
          (Nothing, Just _) -> fail
            "Expected \"versions\" to be accompanied by \"package\""
          (Just _, Nothing) -> fail
            "Expected \"package\" to be accompanied by \"versions\""
      , o .:? "custom_unordered" >>= \case
        Nothing -> pure Nothing
        Just key -> o .:? "options" >>= \case
          Just options -> pure $ Just $ CustomUnorderedExpr key options
          Nothing -> fail
            "Expected \"custom_unordered\" to be accompanied by \"options\""
      , o .:? "custom_ordered" >>= \case
        Nothing -> pure Nothing
        Just key -> o .:? "options" >>= \case
          Just options -> pure $ Just $ CustomOrderedExpr key options
          Nothing -> fail
            "Expected \"custom_ordered\" to be accompanied by \"options\""
      ]
    case catMaybes parses of
      [x] -> pure $ MatrixExprJSON x
      _ -> fail "Expected exactly one of: \"times\", \"add\", \"subtract\", \
        \\"seq\", \"compilers\", \"prefer\", \"package\", \
        \\"custom_unordered\", \"custom_ordered\""
  parseJSON v = typeMismatch "Object or String" v

writeExprFile :: FilePath -> MatrixExpr -> IO ()
writeExprFile file = Aeson.encodeFile @MatrixExprJSON file . MatrixExprJSON

readExprFile :: FilePath -> IO MatrixExpr
readExprFile file = Aeson.eitherDecodeFileStrict @MatrixExprJSON file >>= \case
  Left err -> fail err
  Right (MatrixExprJSON expr) -> pure expr

newtype MatrixRowJSON = MatrixRowJSON (Flavor, Map Text Text)

instance ToJSON MatrixRowJSON where
  toJSON (MatrixRowJSON (flavor, row)) = object
    [ "ordered_options" .= flavor.orderedOptions
    , "unordered_options" .= flavor.unorderedOptions
    , "flavor" .= row
    ]

instance FromJSON MatrixRowJSON where
  parseJSON = withObject "MatrixRow" \o -> do
    orderedOptions <- o .: "ordered_options"
    unorderedOptions <- o .: "unordered_options"
    flavor <- o .: "flavor"
    pure $ MatrixRowJSON (Flavor{..}, flavor)

newtype MatrixJSON = MatrixJSON Matrix

instance ToJSON MatrixJSON where
  toJSON (MatrixJSON matrix) = toJSON
    [ MatrixRowJSON (flavor, Map.fromList $ mapMaybe sequenceA row)
    | (flavor, row) <- Rectangle.toRowMajor matrix
    ]

instance FromJSON MatrixJSON where
  parseJSON v = MatrixJSON <$> do
    !rows <- parseJSON v
    let
      !cols = Set.toList $ Set.unions
        [ Map.keysSet row
        | MatrixRowJSON (_, row) <- rows
        ]
      !matrix = Rectangle.fromRowMajor cols
        [ ( flavor
          , [Map.lookup col row | col <- cols]
          )
        | MatrixRowJSON (flavor, row) <- rows
        ]
    pure matrix

writeMatrixFile :: FilePath -> Matrix -> IO ()
writeMatrixFile file = Aeson.encodeFile @MatrixJSON file . MatrixJSON

readMatrixFile :: FilePath -> IO Matrix
readMatrixFile file = Aeson.eitherDecodeFileStrict @MatrixJSON file >>= \case
  Left err -> fail err
  Right (MatrixJSON matrix) -> pure matrix

newtype StepResultJSON = StepResultJSON StepResult

instance ToJSON StepResultJSON where
  toJSON (StepResultJSON result) = object
    [ "cmdline" .= result.cmdline
    , "output" .= result.output
    , "exit_code" .= case result.exitCode of
      ExitSuccess -> 0
      ExitFailure code -> code
    ]
  -- Have to manually define toEncoding to force field ordering.
  toEncoding (StepResultJSON result) = Encoding.pairs $ mconcat
    [ Encoding.pair "cmdline" $ toEncoding result.cmdline
    , Encoding.pair "output" $ toEncoding result.output
    , Encoding.pair "exit_code" $ toEncoding case result.exitCode of
      ExitSuccess -> 0
      ExitFailure code -> code
    ]

instance FromJSON StepResultJSON where
  parseJSON = Aeson.withObject "StepResult" \o -> do
    cmdline <- o Aeson..: "cmdline"
    output <- o Aeson..: "output"
    exitCode <- o Aeson..: "exit_code" <&> \case
      0 -> ExitSuccess
      code -> ExitFailure code
    pure $ StepResultJSON StepResult{..}

newtype FlavorResultJSON = FlavorResultJSON FlavorResult

instance ToJSON FlavorResultJSON where
  toJSON (FlavorResultJSON result) = object $ catMaybes
    [ ("flavor" .=) <$> Just result.flavor
    , ("dry_run" .=) . StepResultJSON
      <$> result.steps.dryRun
    , ("only_download" .=) . StepResultJSON
      <$> result.steps.onlyDownload
    , ("only_dependencies" .=) . StepResultJSON
      <$> result.steps.onlyDependencies
    , ("full_build" .=) . StepResultJSON
      <$> result.steps.fullBuild
    ]
  -- Have to manually define toEncoding to force field ordering.
  toEncoding (FlavorResultJSON result) = Encoding.pairs $ fold
    [ Encoding.pair "flavor" $ toEncoding result.flavor
    , foldMap (Encoding.pair "dry_run" . toEncoding)
      $ StepResultJSON <$> result.steps.dryRun
    , foldMap (Encoding.pair "only_download" . toEncoding)
      $ StepResultJSON <$> result.steps.onlyDownload
    , foldMap (Encoding.pair "only_dependencies" . toEncoding)
      $ StepResultJSON <$> result.steps.onlyDependencies
    , foldMap (Encoding.pair "full_build" . toEncoding)
      $ StepResultJSON <$> result.steps.fullBuild
    ]

instance FromJSON FlavorResultJSON where
  parseJSON = withObject "FlavorResult" \o -> do
    flavor <- o .: "flavor"
    dryRun <- o .:? "dry_run"
    onlyDownload <- o .:? "only_download"
    onlyDependencies <- o .:? "only_dependencies"
    fullBuild <- o .:? "full_build"
    pure $ FlavorResultJSON FlavorResult
      { flavor
      , steps = PerCabalStep{..}
        <&> fmap @Maybe \(StepResultJSON result) -> result
      }

writeRecordFile :: FilePath -> [FlavorResult] -> IO ()
writeRecordFile file = Aeson.encodeFile @[FlavorResultJSON] file
  . map FlavorResultJSON

readRecordFile :: FilePath -> IO [FlavorResult]
readRecordFile file = Aeson.eitherDecodeFileStrict @[FlavorResultJSON] file
  >>= \case
    Left err -> fail err
    Right results -> pure $ results <&> \(FlavorResultJSON result) -> result