packages feed

dojang-0.2.0: src/Dojang/Syntax/EnvironmentPredicate/Parser.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}

module Dojang.Syntax.EnvironmentPredicate.Parser
  ( Field (..)
  , FieldOp (..)
  , ParseErrorBundle
  , andExpression
  , bareStringLiteral
  , charactersInStringLiteral
  , doubleQuoteStringLiteral
  , equalOp
  , errorBundlePretty
  , expression
  , field
  , fieldOp
  , inOp
  , notEqualOp
  , notInOp
  , parse
  , parseEnvironmentPredicate
  , prefixOp
  , simpleExpression
  , singleQuoteStringLiteral
  , stringLiteral
  , strings
  , suffixOp
  ) where

import Control.Applicative (optional, (<|>))
import Control.Applicative.Combinators (choice, sepBy1, sepEndBy)
import Control.Monad (void)
import Dojang.Types.EnvironmentPredicate (EnvironmentPredicate (..))
import Dojang.Types.MonikerName (parseMonikerName)

import Data.CaseInsensitive (mk)
import Data.Char (isAlpha, isAlphaNum, isDigit)
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.String (IsString (fromString))
import Data.Text (Text, concat, cons, pack, singleton, unpack)
import Data.Void (Void)
import Text.Megaparsec
  ( MonadParsec (eof, takeWhile1P, takeWhileP, try)
  , Parsec
  , label
  , many
  , parse
  , satisfy
  )
import Text.Megaparsec.Char (char, hexDigitChar, space, space1, string)
import Text.Megaparsec.Error (ParseErrorBundle)
import Text.Megaparsec.Error qualified (errorBundlePretty)
import Prelude hiding (concat)


type Parser = Parsec Void Text


-- | Parse a 'EnvironmentPredicate' from a 'Text'.
parseEnvironmentPredicate
  :: FilePath
  -- ^ The name of source file.
  -> Text
  -- ^ The input text.
  -> Either (ParseErrorBundle Text Void) EnvironmentPredicate
  -- ^ The result of parsing.
parseEnvironmentPredicate = parse $ expression <* eof


-- | A parser for 'EnvironmentPredicate'.
expression :: Parser EnvironmentPredicate
expression = label "expression" $ do
  space
  subExprs <- andExpression `sepBy1` (space >> string "||" >> space)
  space
  return $ case subExprs of
    [x] -> x
    x : xs -> Or $ x :| xs
    [] -> error "sepBy1 should never return an empty list"


andExpression :: Parser EnvironmentPredicate
andExpression = do
  space
  subExprs <- simpleExpression `sepBy1` (space >> string "&&" >> space)
  space
  return $ case subExprs of
    [x] -> x
    x : xs -> And $ x :| xs
    [] -> error "sepBy1 should never return an empty list"


simpleExpression :: Parser EnvironmentPredicate
simpleExpression = do
  space
  expr <-
    choice
      [ char '(' >> space >> expression <* space <* char ')'
      , string "always" >> return Always
      , string "never" >> return (Not Always)
      , do
          void $ char '!'
          space
          void $ char '('
          space
          expr' <- expression
          space
          void $ char ')'
          return $ Not expr'
      , fieldOp
      ]
  space
  return expr


data FieldOp
  = EqualOp Text
  | NotEqualOp Text
  | PrefixOp Text
  | SuffixOp Text
  | InOp [Text]
  | NotInOp [Text]


fieldOp :: Parser EnvironmentPredicate
fieldOp = do
  space
  field' <- field
  op <-
    choice
      [ try equalOp
      , try notEqualOp
      , try inOp
      , try notInOp
      , if field' == KernelRelease' then try prefixOp else fail ""
      , if field' == KernelRelease' then try suffixOp else fail ""
      ]
  space
  return $ case op of
    EqualOp value -> makePredicate field' value
    NotEqualOp value -> Not $ makePredicate field' value
    PrefixOp prefix -> KernelReleasePrefix $ mk prefix
    SuffixOp suffix -> KernelReleaseSuffix $ mk suffix
    InOp [] -> Not Always
    InOp (x : xs) -> Or $ makePredicate field' <$> (x :| xs)
    NotInOp [] -> Always
    NotInOp (x : xs) -> And $ Not . makePredicate field' <$> (x :| xs)
 where
  makePredicate :: Field -> Text -> EnvironmentPredicate
  makePredicate OS = OperatingSystem . fromString . unpack
  makePredicate Arch = Architecture . fromString . unpack
  makePredicate Kernel = KernelName . fromString . unpack
  makePredicate KernelRelease' = KernelRelease . fromString . unpack
  makePredicate Moniker' =
    either (const $ Not Always) Moniker . parseMonikerName


equalOp :: Parser FieldOp
equalOp = do
  space
  void $ char '='
  void $ optional (char '=')
  space
  EqualOp <$> stringLiteral


notEqualOp :: Parser FieldOp
notEqualOp = do
  space
  void $ string "!="
  space
  NotEqualOp <$> stringLiteral


prefixOp :: Parser FieldOp
prefixOp = do
  space
  void $ string "^="
  space
  PrefixOp <$> stringLiteral


suffixOp :: Parser FieldOp
suffixOp = do
  space
  void $ string "$="
  space
  SuffixOp <$> stringLiteral


inOp :: Parser FieldOp
inOp = do
  space1
  void $ string "in"
  space
  InOp <$> strings


notInOp :: Parser FieldOp
notInOp = do
  space1
  void $ string "not"
  space1
  void $ string "in"
  space
  NotInOp <$> strings


strings :: Parser [Text]
strings = do
  space
  void $ char '('
  space
  members <- stringLiteral `sepEndBy` (space >> char ',' >> space)
  space
  void $ char ')'
  space
  return members


data Field = OS | Arch | Kernel | KernelRelease' | Moniker' deriving (Eq)


field :: Parser Field
field =
  choice
    [ OS <$ string "os"
    , Arch <$ string "arch"
    , KernelRelease' <$ string "kernel-release"
    , Kernel <$ string "kernel"
    , Moniker' <$ string "moniker"
    ]


stringLiteral :: Parser Text
stringLiteral = label "string literal" $ do
  space
  string' <-
    choice
      [ doubleQuoteStringLiteral
      , singleQuoteStringLiteral
      , bareStringLiteral
      ]
  space
  return string'


bareStringLiteral :: Parser Text
bareStringLiteral = label "bare string literal" $ do
  first <- satisfy isAlpha
  str <- takeWhileP Nothing isAlphaNum
  return $ first `cons` str


doubleQuoteStringLiteral :: Parser Text
doubleQuoteStringLiteral = label "double quote string literal" $ do
  void $ char '"'
  str <- charactersInStringLiteral '"'
  void $ char '"'
  return str


singleQuoteStringLiteral :: Parser Text
singleQuoteStringLiteral = label "single quote string literal" $ do
  void $ char '\''
  str <- charactersInStringLiteral '\''
  void $ char '\''
  return str


charactersInStringLiteral :: Char -> Parser Text
charactersInStringLiteral terminal = do
  chunks <-
    many $
      takeWhile1P (Just "string character") (\c -> c /= '\\' && c /= terminal)
        <|> singleton
          <$> charEscape
  return $ concat chunks
 where
  charEscape :: Parser Char
  charEscape = do
    void $ char '\\'
    choice
      [ char '\\' >> return '\\'
      , char '\'' >> return '\''
      , char '"' >> return '"'
      , char 'b' >> return '\b'
      , char 'f' >> return '\f'
      , char 'n' >> return '\n'
      , char 'r' >> return '\r'
      , char 't' >> return '\t'
      , char 'v' >> return '\v'
      , char '0' >> return '\0'
      , do
          void $ char 'x'
          d1 <- hex
          d2 <- hex
          return $ toEnum $ d1 * 0x10 + d2
      , do
          void $ char 'u'
          d1 <- hex
          d2 <- hex
          d3 <- hex
          d4 <- hex
          return $ toEnum $ d1 * 0x1000 + d2 * 0x100 + d3 * 0x10 + d4
      , do
          void $ char 'U'
          d1 <- hex
          d2 <- hex
          d3 <- hex
          d4 <- hex
          d5 <- hex
          d6 <- hex
          d7 <- hex
          d8 <- hex
          return $
            toEnum $
              d1
                * 0x10000000
                + d2
                  * 0x1000000
                + d3
                  * 0x100000
                + d4
                  * 0x10000
                + d5
                  * 0x1000
                + d6
                  * 0x100
                + d7
                  * 0x10
                + d8
      ]
  hex :: Parser Int
  hex = do
    c <- hexDigitChar
    return $
      if isDigit c
        then fromEnum c - 0x30
        else
          if 'A' <= c && c <= 'F'
            then fromEnum c - 0x37
            else fromEnum c - 0x57


-- | Basically the same as 'Text.Megaparsec.Error.errorBundlePretty' but
-- returns a 'Text' instead of a 'String'.
errorBundlePretty :: ParseErrorBundle Text Void -> Text
errorBundlePretty = pack . Text.Megaparsec.Error.errorBundlePretty