{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}

-- |
-- Module      : Language.Github.Actions.Job.Needs
-- Description : Job dependency specification for GitHub Actions
-- Copyright   : (c) 2025 Bellroy Pty Ltd
-- License     : BSD-3-Clause
-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>
--
-- This module provides the 'JobNeeds' type for representing job dependencies
-- in GitHub Actions workflows. GitHub Actions allows both strings and
-- lists of strings for the 'needs' field.
--
-- Examples of valid 'needs' specifications:
-- * @needs: build@ - Single job specified as a string
-- * @needs: [build]@ - Single job specified as a list of strings
-- * @needs: [build, test]@ - Multiple job dependencies specified as a list of strings
--
-- For more information about GitHub Actions job dependencies, see:
-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds>
module Language.Github.Actions.Job.Needs
  ( JobNeeds (..),
    gen,
  )
where

import Data.Aeson (FromJSON, ToJSON (..), Value (..))
import qualified Data.Aeson as Aeson
import Data.List.NonEmpty (NonEmpty)
import GHC.Generics (Generic)
import Hedgehog (MonadGen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Language.Github.Actions.Job.Id (JobId)
import qualified Language.Github.Actions.Job.Id as JobId

-- | Job dependency specification that preserves YAML representation.
--
-- GitHub Actions supports flexible job dependency specification:
--
-- * 'JobNeedsString' - Single job dependency as string like @needs: build@
-- * 'JobNeedsArray' - Multiple job dependencies as array like @needs: [build, test]@
--
-- Examples:
--
-- @
-- -- Single job dependency (string form)
-- stringDep :: JobNeeds
-- stringDep = JobNeedsString (JobId "build")
--
-- -- Multiple job dependencies (array form)
-- arrayDeps :: JobNeeds
-- arrayDeps = JobNeedsArray (JobId "build" :| [JobId "test", JobId "lint"])
-- @
--
-- The type preserves the original YAML format during round-trip serialization.
-- A string input will serialize back to a string, and an array input will
-- serialize back to an array, preventing information loss.
data JobNeeds
  = JobNeedsString JobId
  | JobNeedsArray (NonEmpty JobId)
  deriving stock (JobNeeds -> JobNeeds -> Bool
(JobNeeds -> JobNeeds -> Bool)
-> (JobNeeds -> JobNeeds -> Bool) -> Eq JobNeeds
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: JobNeeds -> JobNeeds -> Bool
== :: JobNeeds -> JobNeeds -> Bool
$c/= :: JobNeeds -> JobNeeds -> Bool
/= :: JobNeeds -> JobNeeds -> Bool
Eq, (forall x. JobNeeds -> Rep JobNeeds x)
-> (forall x. Rep JobNeeds x -> JobNeeds) -> Generic JobNeeds
forall x. Rep JobNeeds x -> JobNeeds
forall x. JobNeeds -> Rep JobNeeds x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. JobNeeds -> Rep JobNeeds x
from :: forall x. JobNeeds -> Rep JobNeeds x
$cto :: forall x. Rep JobNeeds x -> JobNeeds
to :: forall x. Rep JobNeeds x -> JobNeeds
Generic, Eq JobNeeds
Eq JobNeeds =>
(JobNeeds -> JobNeeds -> Ordering)
-> (JobNeeds -> JobNeeds -> Bool)
-> (JobNeeds -> JobNeeds -> Bool)
-> (JobNeeds -> JobNeeds -> Bool)
-> (JobNeeds -> JobNeeds -> Bool)
-> (JobNeeds -> JobNeeds -> JobNeeds)
-> (JobNeeds -> JobNeeds -> JobNeeds)
-> Ord JobNeeds
JobNeeds -> JobNeeds -> Bool
JobNeeds -> JobNeeds -> Ordering
JobNeeds -> JobNeeds -> JobNeeds
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 :: JobNeeds -> JobNeeds -> Ordering
compare :: JobNeeds -> JobNeeds -> Ordering
$c< :: JobNeeds -> JobNeeds -> Bool
< :: JobNeeds -> JobNeeds -> Bool
$c<= :: JobNeeds -> JobNeeds -> Bool
<= :: JobNeeds -> JobNeeds -> Bool
$c> :: JobNeeds -> JobNeeds -> Bool
> :: JobNeeds -> JobNeeds -> Bool
$c>= :: JobNeeds -> JobNeeds -> Bool
>= :: JobNeeds -> JobNeeds -> Bool
$cmax :: JobNeeds -> JobNeeds -> JobNeeds
max :: JobNeeds -> JobNeeds -> JobNeeds
$cmin :: JobNeeds -> JobNeeds -> JobNeeds
min :: JobNeeds -> JobNeeds -> JobNeeds
Ord, Int -> JobNeeds -> ShowS
[JobNeeds] -> ShowS
JobNeeds -> String
(Int -> JobNeeds -> ShowS)
-> (JobNeeds -> String) -> ([JobNeeds] -> ShowS) -> Show JobNeeds
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> JobNeeds -> ShowS
showsPrec :: Int -> JobNeeds -> ShowS
$cshow :: JobNeeds -> String
show :: JobNeeds -> String
$cshowList :: [JobNeeds] -> ShowS
showList :: [JobNeeds] -> ShowS
Show)

instance FromJSON JobNeeds where
  parseJSON :: Value -> Parser JobNeeds
parseJSON v :: Value
v@(Array Array
_) = NonEmpty JobId -> JobNeeds
JobNeedsArray (NonEmpty JobId -> JobNeeds)
-> Parser (NonEmpty JobId) -> Parser JobNeeds
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Value -> Parser (NonEmpty JobId)
forall a. FromJSON a => Value -> Parser a
Aeson.parseJSON Value
v
  parseJSON Value
v = JobId -> JobNeeds
JobNeedsString (JobId -> JobNeeds) -> Parser JobId -> Parser JobNeeds
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Value -> Parser JobId
forall a. FromJSON a => Value -> Parser a
Aeson.parseJSON Value
v

instance ToJSON JobNeeds where
  toJSON :: JobNeeds -> Value
toJSON (JobNeedsString JobId
jobId) = JobId -> Value
forall a. ToJSON a => a -> Value
toJSON JobId
jobId
  toJSON (JobNeedsArray NonEmpty JobId
jobIds) = NonEmpty JobId -> Value
forall a. ToJSON a => a -> Value
toJSON NonEmpty JobId
jobIds

gen :: (MonadGen m) => m JobNeeds
gen :: forall (m :: * -> *). MonadGen m => m JobNeeds
gen =
  [m JobNeeds] -> m JobNeeds
forall (m :: * -> *) a. MonadGen m => [m a] -> m a
Gen.choice
    [ JobId -> JobNeeds
JobNeedsString (JobId -> JobNeeds) -> m JobId -> m JobNeeds
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> m JobId
forall (m :: * -> *). MonadGen m => m JobId
JobId.gen,
      NonEmpty JobId -> JobNeeds
JobNeedsArray (NonEmpty JobId -> JobNeeds) -> m (NonEmpty JobId) -> m JobNeeds
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Range Int -> m JobId -> m (NonEmpty JobId)
forall (m :: * -> *) a.
MonadGen m =>
Range Int -> m a -> m (NonEmpty a)
Gen.nonEmpty (Int -> Int -> Range Int
forall a. Integral a => a -> a -> Range a
Range.linear Int
1 Int
5) m JobId
forall (m :: * -> *). MonadGen m => m JobId
JobId.gen
    ]