packages feed

dojang-0.2.0: src/Dojang/Syntax/Manifest/Internal.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Dojang.Syntax.Manifest.Internal
  ( FileRoute'
  , FileRouteMap'
  , EnvironmentPredicate' (..)
  , FlatOrNonEmptyStrings (..)
  , Hook' (..)
  , Hooks' (..)
  , IgnoreMap'
  , Manifest' (..)
  , MonikerMap'
  , always
  , emptyHooks
  ) where

import Data.List.NonEmpty (NonEmpty)
import GHC.Generics (Generic)
import Prelude hiding (all, any)

import Data.CaseInsensitive (CI (original))
import Data.Map.Strict (Map)
import Data.Text (Text, pack, unpack)
import Dojang.Types.MonikerName (MonikerName, parseMonikerName)
import System.FilePattern (FilePattern)
import Toml (Value (..))
import Toml.FromValue
  ( FromKey (..)
  , FromValue (..)
  , optKey
  , parseTableFromValue
  )
import Toml.FromValue.Generic (genericParseTable)
import Toml.FromValue.Matcher (Matcher)
import Toml.ToValue
  ( ToKey (..)
  , ToTable (toTable)
  , ToValue (toValue)
  , defaultTableToValue
  , table
  )
import Toml.ToValue.Generic (genericToTable)


data FlatOrNonEmptyStrings
  = Flat String
  | NonEmpty (NonEmpty String)
  deriving (Eq, Show)


typeError :: Value -> String -> Matcher a
typeError actual wanted =
  fail $
    "type error. wanted: " ++ wanted ++ " got: " ++ case actual of
      Integer{} -> "integer"
      Float{} -> "float"
      String{} -> "string"
      Table{} -> "table"
      Array{} -> "array"
      Bool{} -> "boolean"
      TimeOfDay{} -> "local time"
      LocalTime{} -> "local date-time"
      Day{} -> "locate date"
      ZonedTime{} -> "offset date-time"


instance FromValue FlatOrNonEmptyStrings where
  fromValue v@Array{} = NonEmpty <$> fromValue v
  fromValue v@String{} = Flat <$> fromValue v
  fromValue v = typeError v "string or array of strings"


instance ToValue FlatOrNonEmptyStrings where
  toValue (Flat a) = toValue a
  toValue (NonEmpty as) = toValue as


instance FromKey MonikerName where
  fromKey key = case parseMonikerName $ pack key of
    Right monikerName -> return monikerName
    Left _ -> fail $ "invalid moniker name: " ++ show key


instance FromValue MonikerName where
  fromValue v@String{} = do
    result <- fromValue v
    case parseMonikerName result of
      Right monikerName -> return monikerName
      Left _ -> fail $ "invalid moniker name: " ++ show v
  fromValue v = typeError v "string"


instance ToKey MonikerName where
  toKey = unpack . original . (.name)


instance ToValue MonikerName where
  toValue monikerName = toValue $ original monikerName.name


data EnvironmentPredicate' = EnvironmentPredicate'
  { os :: Maybe FlatOrNonEmptyStrings
  , arch :: Maybe FlatOrNonEmptyStrings
  , kernel :: Maybe FlatOrNonEmptyStrings
  , kernelRelease :: Maybe FlatOrNonEmptyStrings
  , all :: Maybe (NonEmpty MonikerName)
  , any :: Maybe (NonEmpty MonikerName)
  , when :: Maybe Text
  }
  deriving (Eq, Show)


always :: EnvironmentPredicate'
always =
  EnvironmentPredicate'
    { os = Nothing
    , arch = Nothing
    , kernel = Nothing
    , kernelRelease = Nothing
    , all = Nothing
    , any = Nothing
    , when = Nothing
    }


instance FromValue EnvironmentPredicate' where
  fromValue =
    parseTableFromValue $
      EnvironmentPredicate'
        <$> optKey "os"
        <*> optKey "arch"
        <*> optKey "kernel"
        <*> optKey "kernel-release"
        <*> optKey "all"
        <*> optKey "any"
        <*> optKey "when"


instance ToValue EnvironmentPredicate' where
  toValue = defaultTableToValue


instance ToTable EnvironmentPredicate' where
  toTable pred' =
    table $
      fieldsToValue fields
        ++ fieldsToValue fields'
        ++ fieldsToValue
          [("when", pred'.when)]
   where
    fields :: [(String, Maybe FlatOrNonEmptyStrings)]
    fields =
      [ ("os", pred'.os)
      , ("arch", pred'.arch)
      , ("kernel", pred'.kernel)
      , ("kernel-release", pred'.kernelRelease)
      ]
    fields' :: [(String, Maybe (NonEmpty MonikerName))]
    fields' =
      [ ("all", pred'.all)
      , ("any", pred'.any)
      ]
    fieldsToValue :: (ToValue a) => [(String, Maybe a)] -> [(String, Value)]
    fieldsToValue fs = [(key, toValue value) | (key, Just value) <- fs]


type MonikerMap' = Map MonikerName EnvironmentPredicate'


type FileRoute' = Map MonikerName Text


type FileRouteMap' = Map FilePath FileRoute'


type IgnoreMap' = Map FilePath [FilePattern]


-- | A single hook configuration in detailed form.
data Hook' = Hook'
  { command :: Text
  -- ^ The executable path (required).
  , args :: Maybe [Text]
  -- ^ Command arguments (default: []).
  , moniker :: Maybe MonikerName
  -- ^ Run only when this moniker matches.
  , condition :: Maybe Text
  -- ^ Run only when this predicate matches (parsed from "when" in TOML).
  , workingDirectory :: Maybe FilePath
  -- ^ Working directory (default: repository root).
  , ignoreFailure :: Maybe Bool
  -- ^ Continue on failure (default: False).
  }
  deriving (Eq, Show)


instance FromValue Hook' where
  fromValue =
    parseTableFromValue $
      Hook'
        <$> reqKey "command"
        <*> optKey "args"
        <*> optKey "moniker"
        <*> optKey "when"
        <*> optKey "working-directory"
        <*> optKey "ignore-failure"
   where
    reqKey key =
      optKey key >>= \case
        Just v -> return v
        Nothing -> fail $ "missing required key: " ++ key


instance ToValue Hook' where
  toValue = defaultTableToValue


instance ToTable Hook' where
  toTable hook =
    table $
      [("command", toValue hook.command)]
        ++ maybeField "args" hook.args
        ++ maybeField "moniker" hook.moniker
        ++ maybeField "when" hook.condition
        ++ maybeField "working-directory" hook.workingDirectory
        ++ maybeField "ignore-failure" hook.ignoreFailure
   where
    maybeField :: (ToValue a) => String -> Maybe a -> [(String, Value)]
    maybeField key (Just v) = [(key, toValue v)]
    maybeField _ Nothing = []


-- | Hooks section of the manifest.
data Hooks' = Hooks'
  { preApply :: Maybe [Hook']
  , preFirstApply :: Maybe [Hook']
  , postFirstApply :: Maybe [Hook']
  , postApply :: Maybe [Hook']
  }
  deriving (Eq, Show)


-- | Empty hooks (no hooks defined).
emptyHooks :: Hooks'
emptyHooks =
  Hooks'
    { preApply = Nothing
    , preFirstApply = Nothing
    , postFirstApply = Nothing
    , postApply = Nothing
    }


instance FromValue Hooks' where
  fromValue =
    parseTableFromValue $
      Hooks'
        <$> optKey "pre-apply"
        <*> optKey "pre-first-apply"
        <*> optKey "post-first-apply"
        <*> optKey "post-apply"


instance ToValue Hooks' where
  toValue = defaultTableToValue


instance ToTable Hooks' where
  toTable hooks =
    table $
      maybeField "pre-apply" hooks.preApply
        ++ maybeField "pre-first-apply" hooks.preFirstApply
        ++ maybeField "post-first-apply" hooks.postFirstApply
        ++ maybeField "post-apply" hooks.postApply
   where
    maybeField :: (ToValue a) => String -> Maybe a -> [(String, Value)]
    maybeField key (Just v) = [(key, toValue v)]
    maybeField _ Nothing = []


data Manifest' = Manifest'
  { monikers :: MonikerMap'
  , dirs :: FileRouteMap'
  , files :: FileRouteMap'
  , ignores :: IgnoreMap'
  , hooks :: Maybe Hooks'
  }
  deriving (Eq, Show, Generic)


instance FromValue Manifest' where
  fromValue = parseTableFromValue genericParseTable


instance ToValue Manifest' where
  toValue = defaultTableToValue


instance ToTable Manifest' where
  toTable = genericToTable