{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- |
-- Module      : Language.Github.Actions.Job.Strategy
-- Description : Matrix strategies for GitHub Actions jobs
-- Copyright   : (c) 2025 Bellroy Pty Ltd
-- License     : BSD-3-Clause
-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>
--
-- This module provides the 'JobStrategy' type for configuring matrix strategies
-- that allow jobs to run multiple times with different configurations.
--
-- Matrix strategies are useful for testing across multiple:
-- * Operating systems (Ubuntu, Windows, macOS)
-- * Language versions (Node 16, 18, 20)
-- * Database versions (PostgreSQL 12, 13, 14)
-- * Or any other configurable parameters
--
-- For more information about GitHub Actions matrix strategies, see:
-- <https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/running-variations-of-jobs-in-a-workflow>
module Language.Github.Actions.Job.Strategy
  ( JobStrategy (..),
    gen,
  )
where

import Data.Aeson (FromJSON, ToJSON, (.!=), (.:?), (.=))
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (catMaybes)
import Data.String (fromString)
import Data.Text (Text)
import qualified Data.Text as Text
import GHC.Generics (Generic)
import Hedgehog (MonadGen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range

-- | Matrix strategy configuration for running job variations.
--
-- Matrix strategies define how to run a job multiple times with different
-- configurations. The matrix creates a cross-product of all variable combinations,
-- with options to include additional combinations or exclude specific ones.
--
-- Example usage:
--
-- @
-- import Language.Github.Actions.Job.Strategy
-- import qualified Data.Map as Map
-- import qualified Data.Aeson as Aeson
--
-- -- Simple OS and Node version matrix
-- nodeMatrix :: JobStrategy
-- nodeMatrix = JobStrategy
--  { exclude = Nothing
--  , failFast = Just False
--  , include = Nothing
--  , maxParallel = Just 3
--  , otherVariables = Just $ Map.fromList
--      [ ("os", Aeson.toJSON ["ubuntu-latest", "windows-latest", "macos-latest"])
--      , ("node-version", Aeson.toJSON ["16", "18", "20"])
--      ]
--  }
--
-- -- Matrix with exclusions and includes
-- complexMatrix :: JobStrategy
-- complexMatrix = JobStrategy
--  { exclude = Just ["{ \"os\": \"windows-latest\", \"node-version\": \"16\" }"]
--  , failFast = Just True
--  , include = Just ["{ \"os\": \"ubuntu-latest\", \"node-version\": \"21\", \"experimental\": true }"]
--  , maxParallel = Nothing
--  , otherVariables = Just $ Map.fromList
--      [ ("os", Aeson.toJSON ["ubuntu-latest", "windows-latest"])
--      , ("node-version", Aeson.toJSON ["18", "20"])
--      ]
--  }
-- @
--
-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/running-variations-of-jobs-in-a-workflow>
data JobStrategy = JobStrategy
  { -- | Matrix combinations to exclude
    JobStrategy -> Maybe [Text]
exclude :: Maybe [Text],
    -- | Whether to cancel all jobs when one fails
    JobStrategy -> Maybe Bool
failFast :: Maybe Bool,
    -- | Additional matrix combinations to include
    JobStrategy -> Maybe [Text]
include :: Maybe [Text],
    -- | Maximum number of parallel jobs
    JobStrategy -> Maybe Int
maxParallel :: Maybe Int,
    -- | Matrix variables (os, version, etc.)
    JobStrategy -> Maybe (Map Text Value)
otherVariables :: Maybe (Map Text Aeson.Value)
  }
  deriving stock (JobStrategy -> JobStrategy -> Bool
(JobStrategy -> JobStrategy -> Bool)
-> (JobStrategy -> JobStrategy -> Bool) -> Eq JobStrategy
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: JobStrategy -> JobStrategy -> Bool
== :: JobStrategy -> JobStrategy -> Bool
$c/= :: JobStrategy -> JobStrategy -> Bool
/= :: JobStrategy -> JobStrategy -> Bool
Eq, (forall x. JobStrategy -> Rep JobStrategy x)
-> (forall x. Rep JobStrategy x -> JobStrategy)
-> Generic JobStrategy
forall x. Rep JobStrategy x -> JobStrategy
forall x. JobStrategy -> Rep JobStrategy x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. JobStrategy -> Rep JobStrategy x
from :: forall x. JobStrategy -> Rep JobStrategy x
$cto :: forall x. Rep JobStrategy x -> JobStrategy
to :: forall x. Rep JobStrategy x -> JobStrategy
Generic, Eq JobStrategy
Eq JobStrategy =>
(JobStrategy -> JobStrategy -> Ordering)
-> (JobStrategy -> JobStrategy -> Bool)
-> (JobStrategy -> JobStrategy -> Bool)
-> (JobStrategy -> JobStrategy -> Bool)
-> (JobStrategy -> JobStrategy -> Bool)
-> (JobStrategy -> JobStrategy -> JobStrategy)
-> (JobStrategy -> JobStrategy -> JobStrategy)
-> Ord JobStrategy
JobStrategy -> JobStrategy -> Bool
JobStrategy -> JobStrategy -> Ordering
JobStrategy -> JobStrategy -> JobStrategy
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: JobStrategy -> JobStrategy -> Ordering
compare :: JobStrategy -> JobStrategy -> Ordering
$c< :: JobStrategy -> JobStrategy -> Bool
< :: JobStrategy -> JobStrategy -> Bool
$c<= :: JobStrategy -> JobStrategy -> Bool
<= :: JobStrategy -> JobStrategy -> Bool
$c> :: JobStrategy -> JobStrategy -> Bool
> :: JobStrategy -> JobStrategy -> Bool
$c>= :: JobStrategy -> JobStrategy -> Bool
>= :: JobStrategy -> JobStrategy -> Bool
$cmax :: JobStrategy -> JobStrategy -> JobStrategy
max :: JobStrategy -> JobStrategy -> JobStrategy
$cmin :: JobStrategy -> JobStrategy -> JobStrategy
min :: JobStrategy -> JobStrategy -> JobStrategy
Ord, Int -> JobStrategy -> ShowS
[JobStrategy] -> ShowS
JobStrategy -> String
(Int -> JobStrategy -> ShowS)
-> (JobStrategy -> String)
-> ([JobStrategy] -> ShowS)
-> Show JobStrategy
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> JobStrategy -> ShowS
showsPrec :: Int -> JobStrategy -> ShowS
$cshow :: JobStrategy -> String
show :: JobStrategy -> String
$cshowList :: [JobStrategy] -> ShowS
showList :: [JobStrategy] -> ShowS
Show)

instance FromJSON JobStrategy where
  parseJSON :: Value -> Parser JobStrategy
parseJSON = String
-> (Object -> Parser JobStrategy) -> Value -> Parser JobStrategy
forall a. String -> (Object -> Parser a) -> Value -> Parser a
Aeson.withObject String
"JobStrategy" ((Object -> Parser JobStrategy) -> Value -> Parser JobStrategy)
-> (Object -> Parser JobStrategy) -> Value -> Parser JobStrategy
forall a b. (a -> b) -> a -> b
$ \Object
o -> do
    Object
matrix <- Object
o Object -> Key -> Parser (Maybe Object)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"matrix" Parser (Maybe Object) -> Object -> Parser Object
forall a. Parser (Maybe a) -> a -> Parser a
.!= Object
forall a. Monoid a => a
mempty
    Maybe [Text]
exclude <- Object
matrix Object -> Key -> Parser (Maybe [Text])
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"exclude"
    Maybe Bool
failFast <- Object
o Object -> Key -> Parser (Maybe Bool)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"fail-fast"
    Maybe [Text]
include <- Object
matrix Object -> Key -> Parser (Maybe [Text])
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"include"
    Maybe Int
maxParallel <- Object
o Object -> Key -> Parser (Maybe Int)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"max-parallel"
    Map Text Value
rawMatrix :: Map Text Aeson.Value <- Object
o Object -> Key -> Parser (Maybe (Map Text Value))
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"matrix" Parser (Maybe (Map Text Value))
-> Map Text Value -> Parser (Map Text Value)
forall a. Parser (Maybe a) -> a -> Parser a
.!= Map Text Value
forall a. Monoid a => a
mempty
    let Text
excludeKey :: Text = Text
"exclude"
        Text
includeKey :: Text = Text
"include"
        filteredRawMatrix :: Map Text Value
filteredRawMatrix =
          (Text -> Value -> Bool) -> Map Text Value -> Map Text Value
forall k a. (k -> a -> Bool) -> Map k a -> Map k a
Map.filterWithKey
            (\Text
k Value
_ -> Text
k Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
/= Text
excludeKey Bool -> Bool -> Bool
&& Text
k Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
/= Text
includeKey)
            Map Text Value
rawMatrix
        otherVariables :: Maybe (Map Text Value)
otherVariables =
          if Map Text Value -> Bool
forall a. Map Text a -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null Map Text Value
filteredRawMatrix
            then Maybe (Map Text Value)
forall a. Maybe a
Nothing
            else Map Text Value -> Maybe (Map Text Value)
forall a. a -> Maybe a
Just Map Text Value
filteredRawMatrix
    JobStrategy -> Parser JobStrategy
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure JobStrategy {Maybe Bool
Maybe Int
Maybe [Text]
Maybe (Map Text Value)
exclude :: Maybe [Text]
failFast :: Maybe Bool
include :: Maybe [Text]
maxParallel :: Maybe Int
otherVariables :: Maybe (Map Text Value)
exclude :: Maybe [Text]
failFast :: Maybe Bool
include :: Maybe [Text]
maxParallel :: Maybe Int
otherVariables :: Maybe (Map Text Value)
..}

instance ToJSON JobStrategy where
  toJSON :: JobStrategy -> Value
toJSON JobStrategy {Maybe Bool
Maybe Int
Maybe [Text]
Maybe (Map Text Value)
exclude :: JobStrategy -> Maybe [Text]
failFast :: JobStrategy -> Maybe Bool
include :: JobStrategy -> Maybe [Text]
maxParallel :: JobStrategy -> Maybe Int
otherVariables :: JobStrategy -> Maybe (Map Text Value)
exclude :: Maybe [Text]
failFast :: Maybe Bool
include :: Maybe [Text]
maxParallel :: Maybe Int
otherVariables :: Maybe (Map Text Value)
..} =
    [Pair] -> Value
Aeson.object ([Pair] -> Value) -> [Pair] -> Value
forall a b. (a -> b) -> a -> b
$
      [Maybe Pair] -> [Pair]
forall a. [Maybe a] -> [a]
catMaybes
        [ (Key
"fail-fast" Key -> Bool -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (Bool -> Pair) -> Maybe Bool -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Bool
failFast,
          (Key
"matrix" Key -> Value -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (Value -> Pair) -> Maybe Value -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Value
maybeMatrixObject,
          (Key
"max-parallel" Key -> Int -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (Int -> Pair) -> Maybe Int -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Int
maxParallel
        ]
    where
      maybeMatrixObject :: Maybe Aeson.Value
      maybeMatrixObject :: Maybe Value
maybeMatrixObject =
        let pairs :: [Pair]
pairs =
              [Pair]
-> (Map Text Value -> [Pair]) -> Maybe (Map Text Value) -> [Pair]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe [] Map Text Value -> [Pair]
otherVariableMapToAesonPair Maybe (Map Text Value)
otherVariables
                [Pair] -> [Pair] -> [Pair]
forall a. [a] -> [a] -> [a]
++ [Maybe Pair] -> [Pair]
forall a. [Maybe a] -> [a]
catMaybes
                  [ (Key
"exclude" Key -> [Text] -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) ([Text] -> Pair) -> Maybe [Text] -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe [Text]
exclude,
                    (Key
"include" Key -> [Text] -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) ([Text] -> Pair) -> Maybe [Text] -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe [Text]
include
                  ]
         in if [Pair] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Pair]
pairs
              then Maybe Value
forall a. Maybe a
Nothing
              else Value -> Maybe Value
forall a. a -> Maybe a
Just (Value -> Maybe Value) -> Value -> Maybe Value
forall a b. (a -> b) -> a -> b
$ [Pair] -> Value
Aeson.object [Pair]
pairs

      otherVariableMapToAesonPair :: Map Text Aeson.Value -> [Aeson.Pair]
      otherVariableMapToAesonPair :: Map Text Value -> [Pair]
otherVariableMapToAesonPair =
        (Text -> Value -> [Pair]) -> Map Text Value -> [Pair]
forall m k a. Monoid m => (k -> a -> m) -> Map k a -> m
Map.foldMapWithKey
          ( \Text
k Value
v ->
              [String -> Key
forall a. IsString a => String -> a
fromString (Text -> String
Text.unpack Text
k) Key -> Value -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Value
v]
          )

gen :: (MonadGen m) => m JobStrategy
gen :: forall (m :: * -> *). MonadGen m => m JobStrategy
gen = do
  Maybe [Text]
exclude <- m [Text] -> m (Maybe [Text])
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe (m [Text] -> m (Maybe [Text])) -> m [Text] -> m (Maybe [Text])
forall a b. (a -> b) -> a -> b
$ Range Int -> m Text -> m [Text]
forall (m :: * -> *) a. MonadGen m => Range Int -> m a -> m [a]
Gen.list (Int -> Int -> Range Int
forall a. Integral a => a -> a -> Range a
Range.linear Int
1 Int
3) m Text
genText
  Maybe Bool
failFast <- m Bool -> m (Maybe Bool)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Bool
forall (m :: * -> *). MonadGen m => m Bool
Gen.bool
  Maybe [Text]
include <- m [Text] -> m (Maybe [Text])
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe (m [Text] -> m (Maybe [Text])) -> m [Text] -> m (Maybe [Text])
forall a b. (a -> b) -> a -> b
$ Range Int -> m Text -> m [Text]
forall (m :: * -> *) a. MonadGen m => Range Int -> m a -> m [a]
Gen.list (Int -> Int -> Range Int
forall a. Integral a => a -> a -> Range a
Range.linear Int
1 Int
3) m Text
genText
  Maybe Int
maxParallel <- m Int -> m (Maybe Int)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe (m Int -> m (Maybe Int)) -> m Int -> m (Maybe Int)
forall a b. (a -> b) -> a -> b
$ Range Int -> m Int
forall (m :: * -> *). MonadGen m => Range Int -> m Int
Gen.int (Int -> Int -> Range Int
forall a. Integral a => a -> a -> Range a
Range.linear Int
1 Int
10)
  Maybe (Map Text Value)
otherVariables <- m (Map Text Value) -> m (Maybe (Map Text Value))
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe (m (Map Text Value) -> m (Maybe (Map Text Value)))
-> m (Map Text Value) -> m (Maybe (Map Text Value))
forall a b. (a -> b) -> a -> b
$ ((Map Text Text -> Map Text Value)
-> m (Map Text Text) -> m (Map Text Value)
forall a b. (a -> b) -> m a -> m b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ((Map Text Text -> Map Text Value)
 -> m (Map Text Text) -> m (Map Text Value))
-> ((Text -> Value) -> Map Text Text -> Map Text Value)
-> (Text -> Value)
-> m (Map Text Text)
-> m (Map Text Value)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text -> Value) -> Map Text Text -> Map Text Value
forall a b. (a -> b) -> Map Text a -> Map Text b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap) Text -> Value
Aeson.String m (Map Text Text)
genTextMap
  JobStrategy -> m JobStrategy
forall a. a -> m a
forall (f :: * -> *) a. Applicative f => a -> f a
pure JobStrategy {Maybe Bool
Maybe Int
Maybe [Text]
Maybe (Map Text Value)
exclude :: Maybe [Text]
failFast :: Maybe Bool
include :: Maybe [Text]
maxParallel :: Maybe Int
otherVariables :: Maybe (Map Text Value)
exclude :: Maybe [Text]
failFast :: Maybe Bool
include :: Maybe [Text]
maxParallel :: Maybe Int
otherVariables :: Maybe (Map Text Value)
..}
  where
    genText :: m Text
genText = Range Int -> m Char -> m Text
forall (m :: * -> *). MonadGen m => Range Int -> m Char -> m Text
Gen.text (Int -> Int -> Range Int
forall a. Integral a => a -> a -> Range a
Range.linear Int
1 Int
5) m Char
forall (m :: * -> *). MonadGen m => m Char
Gen.alphaNum
    genTextMap :: m (Map Text Text)
genTextMap = Range Int -> m (Text, Text) -> m (Map Text Text)
forall (m :: * -> *) k v.
(MonadGen m, Ord k) =>
Range Int -> m (k, v) -> m (Map k v)
Gen.map (Int -> Int -> Range Int
forall a. Integral a => a -> a -> Range a
Range.linear Int
1 Int
5) (m (Text, Text) -> m (Map Text Text))
-> m (Text, Text) -> m (Map Text Text)
forall a b. (a -> b) -> a -> b
$ (Text -> Text -> (Text, Text))
-> m Text -> m Text -> m (Text, Text)
forall a b c. (a -> b -> c) -> m a -> m b -> m c
forall (f :: * -> *) a b c.
Applicative f =>
(a -> b -> c) -> f a -> f b -> f c
liftA2 (,) m Text
genText m Text
genText