packages feed

aws-eventbridge-cron-0.1.0.0: src/AWS/EventBridge/Months.hs

{-# LANGUAGE OverloadedStrings #-}

module AWS.EventBridge.Months where

import qualified Data.Map.Strict as M
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
import Text.Megaparsec
import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer
import Data.Void

type Parser = Parsec Void Text

data MonthsExprT
  = AllMonths
  | AtMonth Int
  | RangeMonth Int Int
  | StepMonth Int Int
  | UnionMonth MonthsExprT MonthsExprT
  deriving (Eq, Show)

isValidMonth :: Int -> Bool
isValidMonth m = 1 <= m && m <= 12

evaluateMonthT :: MonthsExprT -> Either String [Int]
evaluateMonthT expr = fmap S.toAscList (go expr >>= ensureBounds . S.fromList)
  where
    ensureBounds xs =
      if all isValidMonth xs
        then Right xs
        else Left "month out of bounds: expected 1..12"
    go AllMonths = Right [1..12]
    go (AtMonth m)
      | isValidMonth m = Right [m]
      | otherwise      = Left ("invalid month: " ++ show m ++ " (expected 1..12)")
    go (RangeMonth a b)
      | isValidMonth a && isValidMonth b && a <= b = Right [a..b]
      | not (isValidMonth a) = Left ("invalid range start: " ++ show a ++ " (expected 1..12)")
      | not (isValidMonth b) = Left ("invalid range end: " ++ show b ++ " (expected 1..12)")
      | otherwise            = Left ("invalid range: start " ++ show a ++ " > end " ++ show b)
    go (StepMonth m step)
      | not (isValidMonth m) = Left ("invalid step start: " ++ show m ++ " (expected 1..12)")
      | step >= 1            = Right (takeWhile (<= 12) [m, m + step ..])
      | otherwise            = Left ("invalid step: " ++ show step ++ " (expected >= 1)")
    go (UnionMonth a b) = (++) <$> go a <*> go b

monthNames :: M.Map Text Int
monthNames = M.fromList
  [ ("JAN", 1), ("FEB", 2), ("MAR", 3), ("APR", 4)
  , ("MAY", 5), ("JUN", 6), ("JUL", 7), ("AUG", 8)
  , ("SEP", 9), ("OCT", 10), ("NOV", 11), ("DEC", 12)
  ]

parseMonths :: Parser MonthsExprT
parseMonths = choice
  [ try parseUnionMonth
  , try parseStepMonth
  , try parseRangeMonth
  , try parseAtMonth
  , parseAllMonth
  ]

parseAllMonth :: Parser MonthsExprT
parseAllMonth = char '*' >> pure AllMonths

parseAtMonth :: Parser MonthsExprT
parseAtMonth = AtMonth <$> monthToken

parseRangeMonth :: Parser MonthsExprT
parseRangeMonth = do
  start <- monthToken
  _ <- char '-'
  RangeMonth start <$> monthToken

parseStepMonth :: Parser MonthsExprT
parseStepMonth = do
  start <- monthToken
  _ <- char '/'
  StepMonth start <$> decimal

parseUnionMonth :: Parser MonthsExprT
parseUnionMonth = do
  s <- choice [try parseStepMonth, try parseRangeMonth, try parseAtMonth, parseAllMonth]
  _ <- char ','
  e <- choice [try parseUnionMonth, try parseStepMonth, try parseRangeMonth, try parseAtMonth, parseAllMonth]
  eof
  pure (UnionMonth s e)

monthToken :: Parser Int
monthToken = try named <|> decimal
  where
    named = do
      txt <- some letterChar
      case M.lookup (T.toUpper (T.pack txt)) monthNames of
        Just n  -> pure n
        Nothing -> fail ("unknown month name: " ++ txt)

parseMonthsText :: Text -> Either String MonthsExprT
parseMonthsText input =
  case parse parseMonths "months" (T.strip input) of
    Left err -> Left (errorBundlePretty err)
    Right expr -> Right expr