packages feed

cloudy-0.1.0.0: src/Cloudy/InstanceSetup.hs

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE TemplateHaskell #-}

module Cloudy.InstanceSetup where

import Cloudy.InstanceSetup.Types ( InstanceSetup(..) )
import Cloudy.Path (getCloudyInstanceSetupsDir)
import Control.DeepSeq (force)
import Control.FromSum (fromEither)
import Data.Bifunctor (Bifunctor(first))
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString
import Data.FileEmbed (embedDir)
import Data.Functor ((<&>))
import Data.List (sort, find)
import Data.Text (pack, Text)
import Data.Text.Encoding (decodeUtf8')
import Data.Yaml (decodeEither', ParseException (OtherParseException))
import System.Directory (listDirectory)
import System.FilePath (takeBaseName, takeExtension, (</>))
import Control.Exception (SomeException(SomeException))

rawBuiltInInstanceSetups :: [(FilePath, ByteString)]
rawBuiltInInstanceSetups = $(embedDir "instance-setups/")

builtInInstanceSetups :: [InstanceSetup]
builtInInstanceSetups =
  -- force is used here because we want to make sure any YAML decoding errors
  -- are surfaced the very first time builtInInstanceSetups is used.  We should
  -- almost never see any errors, since we control the raw instance-setup files
  -- that are used here.
  --
  -- We also have tests that should catch problems with decoding these
  -- instance-setup files.
  force $
    sort rawBuiltInInstanceSetups <&> \(fp, rawData) ->
      fromEither
        (\err ->
            error $
              "Failed to decode instance-setup data in " <> fp <>
              " as InstanceSetupData: " <> show err
        )
        (parseInstanceSetup fp rawData)

parseInstanceSetup :: FilePath -> ByteString -> Either ParseException InstanceSetup
parseInstanceSetup fp rawData = do
  let name = pack $ takeBaseName fp
  rawDataText <- first (OtherParseException . SomeException) $ decodeUtf8' rawData
  instanceSetupData <- decodeEither' rawData
  pure $ InstanceSetup { name, instanceSetupData, rawInstanceSetupData = rawDataText }


getUserInstanceSetups :: IO [InstanceSetup]
getUserInstanceSetups = do
  instanceSetupsDir <- getCloudyInstanceSetupsDir
  files <- fmap (instanceSetupsDir </>) <$> listDirectory instanceSetupsDir
  let yamlFiles = filter isYamlExt files
  traverse yamlFileToInstanceSetup yamlFiles
  where
    isYamlExt :: FilePath -> Bool
    isYamlExt fp = takeExtension fp == ".yaml" || takeExtension fp == ".yml"

yamlFileToInstanceSetup :: FilePath -> IO InstanceSetup
yamlFileToInstanceSetup rawInstanceSetupFp = do
  rawInstanceSetupData <- ByteString.readFile rawInstanceSetupFp
  case parseInstanceSetup rawInstanceSetupFp rawInstanceSetupData of
    Left err -> error $ "Failed to decode " <> rawInstanceSetupFp <> " as instance-setup: " <> show err
    Right instanceSetup -> pure instanceSetup

-- | Find an 'InstanceSetup' from within the built-in and user-defined instance
-- setups.
findInstanceSetup ::
  -- | Name of the 'InstanceSetup' to look for.
  Text ->
  IO (Maybe InstanceSetup)
findInstanceSetup nameToFind = do
  userInstanceSetups <- getUserInstanceSetups
  let allInstanceSetups = userInstanceSetups <> builtInInstanceSetups
      maybeInstanceSetup :: Maybe InstanceSetup = find (\instSetup -> instSetup.name == nameToFind) allInstanceSetups
  pure maybeInstanceSetup