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

-- |
-- Module      : Language.Github.Actions.Step
-- Description : GitHub Actions step definition and serialization
-- Copyright   : (c) 2025 Bellroy Pty Ltd
-- License     : BSD-3-Clause
-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>
--
-- This module provides the 'Step' type for representing individual steps within GitHub Actions jobs.
-- Steps are the individual tasks that make up a job, such as checking out code, running commands,
-- or using pre-built actions.
--
-- A step can either run a command/script or use a pre-built action from the GitHub marketplace
-- or a custom action. Steps run sequentially within a job.
--
-- For more information about GitHub Actions step syntax, see:
-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idsteps>
module Language.Github.Actions.Step
  ( Step (..),
    gen,
    new,
  )
where

import Data.Aeson (FromJSON, ToJSON (..), (.!=), (.:?), (.=))
import qualified Data.Aeson as Aeson
import Data.Map (Map)
import Data.Maybe (catMaybes)
import Data.Text (Text)
import GHC.Generics (Generic)
import Hedgehog (MonadGen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Language.Github.Actions.RunIf (RunIf)
import qualified Language.Github.Actions.RunIf as RunIf
import Language.Github.Actions.Shell (Shell)
import qualified Language.Github.Actions.Shell as Shell
import Language.Github.Actions.Step.Id (StepId)
import qualified Language.Github.Actions.Step.Id as StepId
import Language.Github.Actions.Step.With (StepWith)
import qualified Language.Github.Actions.Step.With as StepWith

-- | A step within a GitHub Actions job.
--
-- A step is an individual task within a job. Steps can run commands, scripts, or actions.
-- Each step runs in the runner environment and can use outputs from previous steps.
--
-- There are two main types of steps:
-- * Command steps that use 'run' to execute shell commands
-- * Action steps that use 'uses' to run a pre-built action
--
-- Example usage:
--
-- @
-- import Language.Github.Actions.Step
--
-- -- A command step
-- commandStep :: Step
-- commandStep = new
--  { name = Just "Run tests"
--  , run = Just "npm test"
--  }
--
-- -- An action step
-- actionStep :: Step
-- actionStep = new
--  { name = Just "Checkout code"
--  , uses = Just "actions/checkout\@v4"
--  }
-- @
--
-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idsteps>
data Step = Step
  { -- | Whether to continue job execution if this step fails
    Step -> Bool
continueOnError :: Bool,
    -- | Environment variables for this step
    Step -> Map Text Text
env :: Map Text Text,
    -- | Display name for the step
    Step -> Maybe Text
name :: Maybe Text,
    -- | Command or script to run
    Step -> Maybe Text
run :: Maybe Text,
    -- | Condition for running this step
    Step -> Maybe RunIf
runIf :: Maybe RunIf,
    -- | Shell to use for running commands
    Step -> Maybe Shell
shell :: Maybe Shell,
    -- | Unique identifier for this step
    Step -> Maybe StepId
stepId :: Maybe StepId,
    -- | Timeout for the step in minutes
    Step -> Maybe Int
timeoutMinutes :: Maybe Int,
    -- | Action to run (e.g., "actions/checkout@v4")
    Step -> Maybe Text
uses :: Maybe Text,
    -- | Inputs for the action
    Step -> Maybe StepWith
with :: Maybe StepWith,
    -- | Working directory for the step
    Step -> Maybe Text
workingDirectory :: Maybe Text
  }
  deriving stock (Step -> Step -> Bool
(Step -> Step -> Bool) -> (Step -> Step -> Bool) -> Eq Step
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Step -> Step -> Bool
== :: Step -> Step -> Bool
$c/= :: Step -> Step -> Bool
/= :: Step -> Step -> Bool
Eq, (forall x. Step -> Rep Step x)
-> (forall x. Rep Step x -> Step) -> Generic Step
forall x. Rep Step x -> Step
forall x. Step -> Rep Step x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. Step -> Rep Step x
from :: forall x. Step -> Rep Step x
$cto :: forall x. Rep Step x -> Step
to :: forall x. Rep Step x -> Step
Generic, Eq Step
Eq Step =>
(Step -> Step -> Ordering)
-> (Step -> Step -> Bool)
-> (Step -> Step -> Bool)
-> (Step -> Step -> Bool)
-> (Step -> Step -> Bool)
-> (Step -> Step -> Step)
-> (Step -> Step -> Step)
-> Ord Step
Step -> Step -> Bool
Step -> Step -> Ordering
Step -> Step -> Step
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 :: Step -> Step -> Ordering
compare :: Step -> Step -> Ordering
$c< :: Step -> Step -> Bool
< :: Step -> Step -> Bool
$c<= :: Step -> Step -> Bool
<= :: Step -> Step -> Bool
$c> :: Step -> Step -> Bool
> :: Step -> Step -> Bool
$c>= :: Step -> Step -> Bool
>= :: Step -> Step -> Bool
$cmax :: Step -> Step -> Step
max :: Step -> Step -> Step
$cmin :: Step -> Step -> Step
min :: Step -> Step -> Step
Ord, Int -> Step -> ShowS
[Step] -> ShowS
Step -> String
(Int -> Step -> ShowS)
-> (Step -> String) -> ([Step] -> ShowS) -> Show Step
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Step -> ShowS
showsPrec :: Int -> Step -> ShowS
$cshow :: Step -> String
show :: Step -> String
$cshowList :: [Step] -> ShowS
showList :: [Step] -> ShowS
Show)

instance FromJSON Step where
  parseJSON :: Value -> Parser Step
parseJSON = String -> (Object -> Parser Step) -> Value -> Parser Step
forall a. String -> (Object -> Parser a) -> Value -> Parser a
Aeson.withObject String
"Step" ((Object -> Parser Step) -> Value -> Parser Step)
-> (Object -> Parser Step) -> Value -> Parser Step
forall a b. (a -> b) -> a -> b
$ \Object
o -> do
    Bool
continueOnError <- Object
o Object -> Key -> Parser (Maybe Bool)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"continue-on-error" Parser (Maybe Bool) -> Bool -> Parser Bool
forall a. Parser (Maybe a) -> a -> Parser a
.!= Bool
False
    Map Text Text
env <- Object
o Object -> Key -> Parser (Maybe (Map Text Text))
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"env" Parser (Maybe (Map Text Text))
-> Map Text Text -> Parser (Map Text Text)
forall a. Parser (Maybe a) -> a -> Parser a
.!= Map Text Text
forall a. Monoid a => a
mempty
    Maybe Text
name <- Object
o Object -> Key -> Parser (Maybe Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"name"
    Maybe Text
run <- Object
o Object -> Key -> Parser (Maybe Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"run"
    Maybe RunIf
runIf <- Object
o Object -> Key -> Parser (Maybe RunIf)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"if"
    Maybe Text
uses <- Object
o Object -> Key -> Parser (Maybe Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"uses"
    Maybe Shell
shell <- Object
o Object -> Key -> Parser (Maybe Shell)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"shell"
    Maybe StepId
stepId <- Object
o Object -> Key -> Parser (Maybe StepId)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"id"
    Maybe Int
timeoutMinutes <- Object
o Object -> Key -> Parser (Maybe Int)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"timeout-minutes"
    Maybe StepWith
with <- Object
o Object -> Key -> Parser (Maybe StepWith)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"with"
    Maybe Text
workingDirectory <- Object
o Object -> Key -> Parser (Maybe Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"working-directory"
    Step -> Parser Step
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Step {Bool
Maybe Int
Maybe Text
Maybe RunIf
Maybe Shell
Maybe StepId
Maybe StepWith
Map Text Text
run :: Maybe Text
uses :: Maybe Text
continueOnError :: Bool
env :: Map Text Text
name :: Maybe Text
runIf :: Maybe RunIf
shell :: Maybe Shell
stepId :: Maybe StepId
timeoutMinutes :: Maybe Int
with :: Maybe StepWith
workingDirectory :: Maybe Text
continueOnError :: Bool
env :: Map Text Text
name :: Maybe Text
run :: Maybe Text
runIf :: Maybe RunIf
uses :: Maybe Text
shell :: Maybe Shell
stepId :: Maybe StepId
timeoutMinutes :: Maybe Int
with :: Maybe StepWith
workingDirectory :: Maybe Text
..}

instance ToJSON Step where
  toJSON :: Step -> Value
toJSON Step {Bool
Maybe Int
Maybe Text
Maybe RunIf
Maybe Shell
Maybe StepId
Maybe StepWith
Map Text Text
run :: Step -> Maybe Text
uses :: Step -> Maybe Text
continueOnError :: Step -> Bool
env :: Step -> Map Text Text
name :: Step -> Maybe Text
runIf :: Step -> Maybe RunIf
shell :: Step -> Maybe Shell
stepId :: Step -> Maybe StepId
timeoutMinutes :: Step -> Maybe Int
with :: Step -> Maybe StepWith
workingDirectory :: Step -> Maybe Text
continueOnError :: Bool
env :: Map Text Text
name :: Maybe Text
run :: Maybe Text
runIf :: Maybe RunIf
shell :: Maybe Shell
stepId :: Maybe StepId
timeoutMinutes :: Maybe Int
uses :: Maybe Text
with :: Maybe StepWith
workingDirectory :: Maybe Text
..} =
    [Pair] -> Value
Aeson.object ([Pair] -> Value) -> [Pair] -> Value
forall a b. (a -> b) -> a -> b
$
      [Maybe Pair] -> [Pair]
forall a. [Maybe a] -> [a]
catMaybes
        [ if Bool
continueOnError then Pair -> Maybe Pair
forall a. a -> Maybe a
Just (Key
"continue-on-error" Key -> Bool -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Bool
True) else Maybe Pair
forall a. Maybe a
Nothing,
          (Key
"env" Key -> Map Text Text -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (Map Text Text -> Pair) -> Maybe (Map Text Text) -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Map Text Text -> Maybe (Map Text Text)
forall a. (Eq a, Monoid a) => a -> Maybe a
monoidToMaybe Map Text Text
env,
          (Key
"id" Key -> StepId -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (StepId -> Pair) -> Maybe StepId -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe StepId
stepId,
          (Key
"if" Key -> RunIf -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (RunIf -> Pair) -> Maybe RunIf -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe RunIf
runIf,
          (Key
"name" 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
name,
          (Key
"run" 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
run,
          (Key
"shell" Key -> Shell -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (Shell -> Pair) -> Maybe Shell -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Shell
shell,
          (Key
"timeout-minutes" 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
timeoutMinutes,
          (Key
"uses" 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
uses,
          (Key
"with" Key -> StepWith -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (StepWith -> Pair) -> Maybe StepWith -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe StepWith
with,
          (Key
"working-directory" 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
workingDirectory
        ]
    where
      monoidToMaybe :: (Eq a, Monoid a) => a -> Maybe a
      monoidToMaybe :: forall a. (Eq a, Monoid a) => a -> Maybe a
monoidToMaybe a
a = if a
a a -> a -> Bool
forall a. Eq a => a -> a -> Bool
== a
forall a. Monoid a => a
mempty then Maybe a
forall a. Maybe a
Nothing else a -> Maybe a
forall a. a -> Maybe a
Just a
a

gen :: (MonadGen m) => m Step
gen :: forall (m :: * -> *). MonadGen m => m Step
gen = do
  Bool
continueOnError <- m Bool
forall (m :: * -> *). MonadGen m => m Bool
Gen.bool
  Map Text Text
env <- m (Map Text Text)
genTextMap
  Maybe Text
name <- m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText
  Maybe Text
run <- m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText
  Maybe RunIf
runIf <- m RunIf -> m (Maybe RunIf)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m RunIf
forall (m :: * -> *). MonadGen m => m RunIf
RunIf.gen
  Maybe Shell
shell <- m Shell -> m (Maybe Shell)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Shell
forall (m :: * -> *). MonadGen m => m Shell
Shell.gen
  Maybe StepId
stepId <- m StepId -> m (Maybe StepId)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m StepId
forall (m :: * -> *). MonadGen m => m StepId
StepId.gen
  Maybe Int
timeoutMinutes <- 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
120)
  Maybe Text
uses <- m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText
  Maybe StepWith
with <- m StepWith -> m (Maybe StepWith)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m StepWith
forall (m :: * -> *). MonadGen m => m StepWith
StepWith.gen
  Maybe Text
workingDirectory <- m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText
  Step -> m Step
forall a. a -> m a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Step {Bool
Maybe Int
Maybe Text
Maybe RunIf
Maybe Shell
Maybe StepId
Maybe StepWith
Map Text Text
run :: Maybe Text
uses :: Maybe Text
continueOnError :: Bool
env :: Map Text Text
name :: Maybe Text
runIf :: Maybe RunIf
shell :: Maybe Shell
stepId :: Maybe StepId
timeoutMinutes :: Maybe Int
with :: Maybe StepWith
workingDirectory :: Maybe Text
continueOnError :: Bool
env :: Map Text Text
name :: Maybe Text
run :: Maybe Text
runIf :: Maybe RunIf
shell :: Maybe Shell
stepId :: Maybe StepId
timeoutMinutes :: Maybe Int
uses :: Maybe Text
with :: Maybe StepWith
workingDirectory :: Maybe Text
..}
  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
3 Int
20) (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

-- | Create a new empty 'Step' with default values.
--
-- This provides a minimal step that can be extended with specific commands,
-- actions, or other configuration.
--
-- Example:
--
-- @
-- checkoutStep = new
--   { name = Just "Checkout repository"
--   , uses = Just "actions/checkout\@v4"
--   }
--
-- commandStep = new
--   { name = Just "Build project"
--   , run = Just "make build"
--   }
-- @
new :: Step
new :: Step
new =
  Step
    { continueOnError :: Bool
continueOnError = Bool
False,
      env :: Map Text Text
env = Map Text Text
forall a. Monoid a => a
mempty,
      name :: Maybe Text
name = Maybe Text
forall a. Maybe a
Nothing,
      run :: Maybe Text
run = Maybe Text
forall a. Maybe a
Nothing,
      runIf :: Maybe RunIf
runIf = Maybe RunIf
forall a. Maybe a
Nothing,
      shell :: Maybe Shell
shell = Maybe Shell
forall a. Maybe a
Nothing,
      stepId :: Maybe StepId
stepId = Maybe StepId
forall a. Maybe a
Nothing,
      timeoutMinutes :: Maybe Int
timeoutMinutes = Maybe Int
forall a. Maybe a
Nothing,
      uses :: Maybe Text
uses = Maybe Text
forall a. Maybe a
Nothing,
      with :: Maybe StepWith
with = Maybe StepWith
forall a. Maybe a
Nothing,
      workingDirectory :: Maybe Text
workingDirectory = Maybe Text
forall a. Maybe a
Nothing
    }