packages feed

panfiguration-0.0: src/Panfiguration/Core.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TupleSections #-}
module Panfiguration.Core (
     Panfiguration(..)
    , Result(..)
    , Source(..)
    , declCase
    , asCase
    , withNames
    , envs
    , opts
    , defaults
    , fullDefaults
    , logger
    , Panfigurable
    , exec
    , run
    , runMaybe
    ) where

import Barbies
import Barbies.Bare
import Barbies.Constraints (Dict(..))
import Barbies.TH
import Control.Applicative
import Control.Monad (forM)
import Data.Bifunctor
import Data.Functor.Compose
import Data.Functor.Identity
import Data.List (intercalate)
import qualified Data.List.NonEmpty as NE
import Data.Maybe (fromMaybe)
import Data.Monoid (First(..))
import qualified Options.Applicative as O
import System.Environment (getEnvironment)

import Panfiguration.FromParam
import Panfiguration.Case

data Result a = Result
    { resultSources :: [String]
    , resultUsed :: [String]
    , resultContent :: Maybe a
    }
    deriving (Show, Eq, Ord)

mkResult :: String -> Maybe a -> Result a
mkResult key = Result [key] [key]

instance FromParam a => Semigroup (Result a) where
    Result s0 _ Nothing <> Result s1 _ Nothing = Result (s0 <> s1) mempty Nothing
    Result s0 _ Nothing <> Result s1 u (Just a) = Result (s0 <> s1) u (Just a)
    Result s0 u (Just a) <> Result s1 _ Nothing = Result (s0 <> s1) u (Just a)
    Result s0 u0 (Just a) <> Result s1 u1 (Just b)
        =   let (side, c) = mergeParams a b
                used = case side of
                    LT -> u0
                    EQ -> u0 <> u1
                    GT -> u1
            in Result (s0 <> s1) used $ Just c

instance FromParam a => Monoid (Result a) where
    mempty = Result [] [] Nothing

data Source h = Source
    { sourceCase :: Case
    , sourceRun :: h (Const String) -> IO (h Result)
    }

mapConsts :: FunctorB h => (a -> b) -> h (Const a) -> h (Const b)
mapConsts f = bmap (first f)

data Panfiguration h = Panfiguration
    { fieldNameCase :: First Case
    , loggerFunction :: First (String -> IO ())
    , sources :: [Source h]
    }

instance Semigroup (Panfiguration h) where
    Panfiguration a b c <> Panfiguration x y z = Panfiguration (a <> x) (b <> y) (c <> z)

instance Monoid (Panfiguration h) where
    mempty = Panfiguration mempty mempty mempty

mkSource :: Case -> (h (Const String) -> IO (h Result)) -> Panfiguration h
mkSource c f = mempty { sources = [Source c f] }

-- | Set the letter case of the data declaration
declCase :: Case -> Panfiguration h
declCase c = mempty { fieldNameCase = pure c }

-- | Set the letter case of the sources
asCase :: Panfiguration h -> Case -> Panfiguration h
asCase pfg c = pfg { sources = [ s { sourceCase = c } | s <- sources pfg] }

-- | Update names being used for the backends
withNames :: Panfiguration h -> (h (Const String) -> h (Const String)) -> Panfiguration h
withNames pfg f = pfg { sources = [ s { sourceRun = sourceRun s . f } | s <- sources pfg] }

envs :: (TraversableB h, ConstraintsB h, AllB FromParam h) => Panfiguration h
envs = mkSource SNAKE $ \envNames -> do
    vars <- getEnvironment
    either fail pure $ btraverseC @FromParam
        (\(Const k) -> tag k <$> traverse fromParam (lookup k vars))
        envNames
  where
    tag k = mkResult $ "env:" <> k

opts :: (TraversableB h, ConstraintsB h, AllB FromParam h) => Panfiguration h
opts = mkSource kebab $ \optNames -> do
    let parsers = btraverseC @FromParam
            (\(Const k) -> fmap (tag k) $ optional $ mkOption k)
            optNames
    O.execParser $ O.info (parsers <**> O.helper) mempty
  where
    tag k = mkResult $ "--" <> k
    mkOption k = O.option (O.eitherReader fromParam) $ O.long k

defaults :: FunctorB h => h Maybe -> Panfiguration h
defaults def = mkSource AsIs $ const $ pure $ bmap (mkResult "the default") def

-- | Provide all the default values by a plain record
fullDefaults :: (BareB b, FunctorB (b Covered)) => b Bare Identity  -> Panfiguration (b Covered)
fullDefaults = defaults . bmap (Just . runIdentity) . bcover

logger :: (String -> IO ()) -> Panfiguration h
logger f = mempty { loggerFunction = pure f }

resolve :: (String -> IO ()) -> Dict Show a -> Const (NE.NonEmpty String) a -> Result a -> Compose IO Maybe a
resolve logFunc Dict (Const key) (Result srcs used r) = Compose $ r <$ case r of
    Nothing -> logFunc $ unwords [displayKey key <> ":", "None of", commas srcs, "provides a value"]
    Just v -> logFunc $ unwords [displayKey key <> ":", "using", show v, "from", commas used]
    where
        displayKey = intercalate "." . NE.toList
        commas [] = ""
        commas [a] = a
        commas [a, b] = unwords [a, "and", b]
        commas (x : xs) = x <> ", " <> commas xs


type Panfigurable h = (FieldNamesB h
    , TraversableB h
    , ApplicativeB h
    , ConstraintsB h
    , AllB Show h
    , AllB FromParam h)

-- | Parse all the relevant environment variables and command line options, then merges them.
exec :: (Panfigurable h)
    => Panfiguration h
    -> IO (h Result)
exec Panfiguration{..} = do
    let names = mapConsts
            (fmap $ split $ fromMaybe camel $ getFirst fieldNameCase)
            bnestedFieldNames

    results <- forM sources $ \Source{..} -> sourceRun
        $ mapConsts (join sourceCase . concat) names
 
    pure $ foldr (bzipWithC @FromParam (<>)) (bpureC @FromParam mempty) results

runMaybe :: (Panfigurable h)
    => Panfiguration h
    -> IO (h Maybe)
runMaybe panfig = do
    result <- exec panfig
    let logFunc = fromMaybe mempty $ getFirst $ loggerFunction panfig
    bsequence $ bzipWith3 (resolve logFunc) bdicts bnestedFieldNames result

run :: (BareB b, Panfigurable (b Covered))
    => Panfiguration (b Covered)
    -> IO (b Bare Identity)
run panfig = do
    maybes <- runMaybe panfig
    fmap bstrip <$> maybe (error "Failed to run panfiguration") pure $ bsequence' maybes