packages feed

github-actions-0.1.0.0: src/Language/Github/Actions/Service.hs

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

-- |
-- Module      : Language.Github.Actions.Service
-- Description : GitHub Actions service containers for jobs
-- Copyright   : (c) 2025 Bellroy Pty Ltd
-- License     : BSD-3-Clause
-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>
--
-- This module provides the 'Service' type for defining service containers that run
-- alongside job steps in GitHub Actions workflows.
--
-- Service containers are Docker containers that provide services like databases,
-- message queues, or other dependencies that your job steps might need during execution.
-- They run in parallel with your job and are accessible via hostname.
--
-- For more information about GitHub Actions service containers, see:
-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idservices>
module Language.Github.Actions.Service
  ( Service (..),
    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

-- | A service container definition for GitHub Actions jobs.
--
-- Service containers run Docker images that provide services your job steps can use.
-- Common examples include databases, caches, and message queues.
--
-- Service containers are automatically started before job steps run and stopped after
-- the job completes. They can be accessed by job steps using their service ID as hostname.
--
-- Example usage:
--
-- @
-- import Language.Github.Actions.Service
-- import qualified Data.Map as Map
--
-- -- PostgreSQL database service
-- postgresService :: Service
-- postgresService = new
--  { image = Just "postgres:13"
--  , env = Just $ Map.fromList
--      [ ("POSTGRES_PASSWORD", "postgres")
--      , ("POSTGRES_DB", "testdb")
--      ]
--  , ports = Just ["5432:5432"]
--  }
--
-- -- Redis cache service
-- redisService :: Service
-- redisService = new
--  { image = Just "redis:6-alpine"
--  , ports = Just ["6379:6379"]
--  }
-- @
--
-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idservices>
data Service = Service
  { -- | Registry credentials for private images
    credentials :: Maybe (Map Text Text),
    -- | Environment variables for the service
    env :: Maybe (Map Text Text),
    -- | Docker image to use for the service
    image :: Maybe Text,
    -- | Additional Docker options
    options :: Maybe Text,
    -- | Ports to expose from the service
    ports :: Maybe [Text],
    -- | Volumes to mount in the service
    volumes :: Maybe [Text]
  }
  deriving stock (Eq, Generic, Ord, Show)

instance FromJSON Service where
  parseJSON = Aeson.withObject "Service" $ \o -> do
    credentials <- o .:? "credentials"
    env <- o .:? "env"
    image <- o .:? "image"
    options <- o .:? "options"
    ports <- o .:? "ports"
    volumes <- o .:? "volumes"
    pure Service {..}

instance ToJSON Service where
  toJSON Service {..} =
    Aeson.object $
      catMaybes
        [ ("credentials" .=) <$> credentials,
          ("env" .=) <$> env,
          ("image" .=) <$> image,
          ("options" .=) <$> options,
          ("ports" .=) <$> ports,
          ("volumes" .=) <$> volumes
        ]

gen :: (MonadGen m) => m Service
gen = do
  credentials <- Gen.maybe genTextMap
  env <- Gen.maybe genTextMap
  image <- Gen.maybe genText
  options <- Gen.maybe genText
  ports <- Gen.maybe . Gen.list (Range.linear 1 3) $ Gen.text (Range.linear 1 5) Gen.digit
  volumes <- Gen.maybe . Gen.list (Range.linear 1 3) $ genText
  pure Service {..}
  where
    genText = Gen.text (Range.linear 1 5) Gen.alphaNum
    genTextMap = Gen.map (Range.linear 1 5) $ liftA2 (,) genText genText

-- | Create a new empty 'Service' with default values.
--
-- This provides a minimal service definition that can be extended with specific
-- image, ports, environment variables, and other configuration.
--
-- Example:
--
-- @
-- databaseService = new
--   { image = Just "postgres:13"
--   , env = Just $ Map.singleton "POSTGRES_PASSWORD" "secret"
--   , ports = Just ["5432:5432"]
--   }
-- @
new :: Service
new =
  Service
    { credentials = Nothing,
      env = Nothing,
      image = Nothing,
      options = Nothing,
      ports = Nothing,
      volumes = Nothing
    }