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

-- |
-- Module      : Language.Github.Actions.Job.Container
-- Description : Container configuration 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 'JobContainer' type for configuring Docker containers
-- that jobs run inside of in GitHub Actions workflows.
--
-- Job containers allow you to run job steps inside a Docker container with a specific
-- environment, dependencies, and configuration. This provides consistency across
-- different runner environments and enables the use of custom tooling.
--
-- For more information about GitHub Actions job containers, see:
-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainer>
module Language.Github.Actions.Job.Container
  ( JobContainer (..),
    gen,
  )
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

-- | Container configuration for running a job inside a Docker container.
--
-- Job containers provide an isolated, consistent environment for job execution.
-- This is useful for ensuring specific tool versions, operating system environments,
-- or complex dependency setups.
--
-- Example usage:
--
-- @
-- import Language.Github.Actions.Job.Container
-- import qualified Data.Map as Map
--
-- -- Node.js development container
-- nodeContainer :: JobContainer
-- nodeContainer = JobContainer
--  { image = Just "node:18"
--  , env = Just $ Map.fromList [("NODE_ENV", "test")]
--  , credentials = Nothing
--  , options = Nothing
--  , ports = Nothing
--  , volumes = Nothing
--  }
--
-- -- Database testing container with services
-- dbTestContainer :: JobContainer
-- dbTestContainer = JobContainer
--  { image = Just "ubuntu:22.04"
--  , env = Just $ Map.fromList [("DEBIAN_FRONTEND", "noninteractive")]
--  , credentials = Nothing
--  , options = Just "--network postgres"
--  , ports = Just ["8080:8080"]
--  , volumes = Just ["\${{ github.workspace }}:\/workspace"]
--  }
-- @
--
-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainer>
data JobContainer = JobContainer
  { -- | Registry credentials for private images
    JobContainer -> Maybe (Map Text Text)
credentials :: Maybe (Map Text Text),
    -- | Environment variables for the container
    JobContainer -> Maybe (Map Text Text)
env :: Maybe (Map Text Text),
    -- | Docker image to use for the container
    JobContainer -> Maybe Text
image :: Maybe Text,
    -- | Additional Docker run options
    JobContainer -> Maybe Text
options :: Maybe Text,
    -- | Ports to expose from the container
    JobContainer -> Maybe [Text]
ports :: Maybe [Text],
    -- | Volumes to mount in the container
    JobContainer -> Maybe [Text]
volumes :: Maybe [Text]
  }
  deriving stock (JobContainer -> JobContainer -> Bool
(JobContainer -> JobContainer -> Bool)
-> (JobContainer -> JobContainer -> Bool) -> Eq JobContainer
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: JobContainer -> JobContainer -> Bool
== :: JobContainer -> JobContainer -> Bool
$c/= :: JobContainer -> JobContainer -> Bool
/= :: JobContainer -> JobContainer -> Bool
Eq, (forall x. JobContainer -> Rep JobContainer x)
-> (forall x. Rep JobContainer x -> JobContainer)
-> Generic JobContainer
forall x. Rep JobContainer x -> JobContainer
forall x. JobContainer -> Rep JobContainer x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. JobContainer -> Rep JobContainer x
from :: forall x. JobContainer -> Rep JobContainer x
$cto :: forall x. Rep JobContainer x -> JobContainer
to :: forall x. Rep JobContainer x -> JobContainer
Generic, Eq JobContainer
Eq JobContainer =>
(JobContainer -> JobContainer -> Ordering)
-> (JobContainer -> JobContainer -> Bool)
-> (JobContainer -> JobContainer -> Bool)
-> (JobContainer -> JobContainer -> Bool)
-> (JobContainer -> JobContainer -> Bool)
-> (JobContainer -> JobContainer -> JobContainer)
-> (JobContainer -> JobContainer -> JobContainer)
-> Ord JobContainer
JobContainer -> JobContainer -> Bool
JobContainer -> JobContainer -> Ordering
JobContainer -> JobContainer -> JobContainer
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 :: JobContainer -> JobContainer -> Ordering
compare :: JobContainer -> JobContainer -> Ordering
$c< :: JobContainer -> JobContainer -> Bool
< :: JobContainer -> JobContainer -> Bool
$c<= :: JobContainer -> JobContainer -> Bool
<= :: JobContainer -> JobContainer -> Bool
$c> :: JobContainer -> JobContainer -> Bool
> :: JobContainer -> JobContainer -> Bool
$c>= :: JobContainer -> JobContainer -> Bool
>= :: JobContainer -> JobContainer -> Bool
$cmax :: JobContainer -> JobContainer -> JobContainer
max :: JobContainer -> JobContainer -> JobContainer
$cmin :: JobContainer -> JobContainer -> JobContainer
min :: JobContainer -> JobContainer -> JobContainer
Ord, Int -> JobContainer -> ShowS
[JobContainer] -> ShowS
JobContainer -> String
(Int -> JobContainer -> ShowS)
-> (JobContainer -> String)
-> ([JobContainer] -> ShowS)
-> Show JobContainer
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> JobContainer -> ShowS
showsPrec :: Int -> JobContainer -> ShowS
$cshow :: JobContainer -> String
show :: JobContainer -> String
$cshowList :: [JobContainer] -> ShowS
showList :: [JobContainer] -> ShowS
Show)

instance FromJSON JobContainer where
  parseJSON :: Value -> Parser JobContainer
parseJSON = String
-> (Object -> Parser JobContainer) -> Value -> Parser JobContainer
forall a. String -> (Object -> Parser a) -> Value -> Parser a
Aeson.withObject String
"JobContainer" ((Object -> Parser JobContainer) -> Value -> Parser JobContainer)
-> (Object -> Parser JobContainer) -> Value -> Parser JobContainer
forall a b. (a -> b) -> a -> b
$ \Object
o -> do
    Maybe (Map Text Text)
credentials <- Object
o Object -> Key -> Parser (Maybe (Map Text Text))
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"credentials"
    Maybe (Map Text Text)
env <- Object
o Object -> Key -> Parser (Maybe (Map Text Text))
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"env"
    Maybe Text
image <- Object
o Object -> Key -> Parser (Maybe Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"image"
    Maybe Text
options <- Object
o Object -> Key -> Parser (Maybe Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"options"
    Maybe [Text]
ports <- Object
o Object -> Key -> Parser (Maybe [Text])
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"ports"
    Maybe [Text]
volumes <- Object
o Object -> Key -> Parser (Maybe [Text])
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"volumes"
    JobContainer -> Parser JobContainer
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure JobContainer {Maybe [Text]
Maybe Text
Maybe (Map Text Text)
credentials :: Maybe (Map Text Text)
env :: Maybe (Map Text Text)
image :: Maybe Text
options :: Maybe Text
ports :: Maybe [Text]
volumes :: Maybe [Text]
credentials :: Maybe (Map Text Text)
env :: Maybe (Map Text Text)
image :: Maybe Text
options :: Maybe Text
ports :: Maybe [Text]
volumes :: Maybe [Text]
..}

instance ToJSON JobContainer where
  toJSON :: JobContainer -> Value
toJSON JobContainer {Maybe [Text]
Maybe Text
Maybe (Map Text Text)
credentials :: JobContainer -> Maybe (Map Text Text)
env :: JobContainer -> Maybe (Map Text Text)
image :: JobContainer -> Maybe Text
options :: JobContainer -> Maybe Text
ports :: JobContainer -> Maybe [Text]
volumes :: JobContainer -> Maybe [Text]
credentials :: Maybe (Map Text Text)
env :: Maybe (Map Text Text)
image :: Maybe Text
options :: Maybe Text
ports :: Maybe [Text]
volumes :: 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
        [ (Key
"credentials" 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
<$> Maybe (Map Text Text)
credentials,
          (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
<$> Maybe (Map Text Text)
env,
          (Key
"image" 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
image,
          (Key
"options" 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
options,
          (Key
"ports" 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]
ports,
          (Key
"volumes" 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]
volumes
        ]

gen :: (MonadGen m) => m JobContainer
gen :: forall (m :: * -> *). MonadGen m => m JobContainer
gen = do
  Maybe (Map Text Text)
credentials <- m (Map Text Text) -> m (Maybe (Map Text Text))
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m (Map Text Text)
genTextMap
  Maybe (Map Text Text)
env <- m (Map Text Text) -> m (Maybe (Map Text Text))
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m (Map Text Text)
genTextMap
  Maybe Text
image <- m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText
  Maybe Text
options <- m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText
  Maybe [Text]
ports <-
    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 [Text]) -> m Text -> m (Maybe [Text])
forall b c a. (b -> c) -> (a -> b) -> a -> c
. 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
0 Int
10) (m Text -> m (Maybe [Text])) -> m Text -> m (Maybe [Text])
forall a b. (a -> b) -> a -> b
$
      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.digit
  Maybe [Text]
volumes <- 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
0 Int
10) m Text
genText
  JobContainer -> m JobContainer
forall a. a -> m a
forall (f :: * -> *) a. Applicative f => a -> f a
pure JobContainer {Maybe [Text]
Maybe Text
Maybe (Map Text Text)
credentials :: Maybe (Map Text Text)
env :: Maybe (Map Text Text)
image :: Maybe Text
options :: Maybe Text
ports :: Maybe [Text]
volumes :: Maybe [Text]
credentials :: Maybe (Map Text Text)
env :: Maybe (Map Text Text)
image :: Maybe Text
options :: Maybe Text
ports :: Maybe [Text]
volumes :: 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
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