aboutsummaryrefslogtreecommitdiff
path: root/src/Cron/Parser.hs
blob: 304a1e3c141b21542b28b48bb9d6d5659721e567 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
module Cron.Parser (parse) where

import Common (fromDigits)
import Control.Applicative (Alternative, empty, liftA2, many, some, (*>), (<$), (<|>))
import Control.Arrow (first, (>>>))
import qualified Cron.Expr as E
import Data.List.NonEmpty (NonEmpty ((:|)))
import Intro
import Text.Read (readMaybe)

type Token = Char

newtype Parser a = Parser {runParser :: [Token] -> Maybe (a, [Token])}

instance Functor Parser where
  fmap f (Parser p) = Parser (p >>> fmap (first f))

instance Applicative Parser where
  pure a = Parser (\input -> Just (a, input))
  Parser pF <*> Parser pA = Parser $ \input -> do
    (f, rest) <- pF input
    (a, s) <- pA rest
    pure (f a, s)

instance Alternative Parser where
  empty = Parser (const Nothing)
  Parser pA <|> Parser pB = Parser $ \input -> case (pA input, pB input) of
    (Nothing, expr) -> expr
    (expr, _) -> expr

match :: (Token -> Bool) -> Parser Token
match p = Parser $ \case
  t : ts | p t -> Just (t, ts)
  _ -> Nothing

token :: Token -> Parser Token
token = (==) >>> match

read :: (Token -> Maybe a) -> Parser a
read f = Parser $ \case
  t : ts -> case f t of
    Just x -> Just (x, ts)
    _ -> Nothing
  _ -> Nothing

sepBy1 :: Parser a -> Parser b -> Parser (NonEmpty a)
sepBy1 p s = liftA2 (:|) p (many (s *> p))

pair :: Parser a -> Parser b -> Parser c -> Parser (a, b)
pair p1 p2 s = liftA2 (,) p1 (s *> p2)

digit :: Parser Int
digit = read $ \case
  x | x `elem` ['0' .. '9'] -> readMaybe [x] :: Maybe Int
  _ -> Nothing

number :: Parser Int
number = fromDigits <$> some digit

--
-- Cron Exprs
--

cRange :: Parser E.Expr
cRange = uncurry E.Range <$> pair number number (token '-')

cStepL :: Parser E.StepLExpr
cStepL =
  (E.StepLEvery <$ token '*')
    <|> (uncurry E.StepLRange <$> pair number number (token '-'))

cStep :: Parser E.Expr
cStep = uncurry E.Step <$> pair cStepL number (token '/')

cMulti :: Parser E.Expr
cMulti = E.Multi <$> sepBy1 number (token ',')

cEvery :: Parser E.Expr
cEvery = E.Every <$ token '*'

cExpr :: Parser E.Expr
cExpr = cStep <|> cEvery <|> cRange <|> cMulti

parse :: [Token] -> Either String E.Expr
parse xs = case runParser cExpr xs of
  Nothing -> Left "Invalid cron expression"
  Just (expr, []) -> Right expr
  Just (_, rest) -> Left ("Invalid cron expression. Couldn't parse `" ++ rest ++ "`")