packages feed

json-spec-2.0.0.0: src/Data/JsonSpec/Language/Parser.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}

{-|
  Description : Parser for the JsonSpec textual language

  Megaparsec parser for the JsonSpec textual language.

  See @docs\/language-spec.md@. Trailing commas are allowed in
  objects (JSON-familiar). Identifiers accept letters matching
  'isAlpha'. Keyword names may be written as backtick-escaped
  identifiers (e.g. @`string`@).
-}
module Data.JsonSpec.Language.Parser (
  -- * AST
  Program(..),
  Binding(..),
  Spec(..),
  Field(..),

  -- * Parsing
  parseProgram,
  parseSpec,
  program,
  spec,
) where

import Control.Applicative
  ( Alternative((<|>), many), Applicative((<*), pure), (<$>), optional
  )
import Control.Monad (void)
import Data.Char (isAlpha, isAlphaNum)
import Data.Text (Text)
import Data.Void (Void)
import Prelude
  ( Bool(False, True), Either(Left, Right), Enum(fromEnum, toEnum)
  , Eq((/=), (==)), Functor(fmap), Maybe(Just, Nothing), Monad((>>))
  , MonadFail(fail), Num((*), (+), (-)), Ord((<=), (>=)), Semigroup((<>)), ($)
  , (&&), (.), (||), Char, Int, Show, String, otherwise
  )
import Text.Megaparsec
  ( MonadParsec(eof, notFollowedBy, takeWhile1P, try), Parsec, between, choice
  , errorBundlePretty, manyTill, parse, satisfy, sepEndBy
  )
import Text.Megaparsec.Char (char, space1, string)
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Text.Megaparsec.Char.Lexer as L

{-| A full program: one top-level closed @module@ binding. -}
data Program = Program
  { programName :: Text
  , programSpec :: Spec
  }
  deriving stock (Eq, Show)


{-| A @type@ or @module@ binding inside a @let@. -}
data Binding
  = TypeBinding Text Spec
  | ModuleBinding Text Spec
  deriving stock (Eq, Show)


{-| A specification expression. -}
data Spec
  = LetSpec [Binding] Spec
  | EitherSpec [Spec]
  | DictSpec Spec
  | NullSpec Spec
  | StringSpec
  | NumberSpec
  | IntSpec
  | BoolSpec
  | DateTimeSpec
  | RawSpec
  | TagSpec Text
  | RefSpec Text
  | ObjectSpec [Field]
  | ArraySpec Spec
  deriving stock (Eq, Show)


{-| An object field. -}
data Field = Field
  { fieldName     :: Text
  , fieldOptional :: Bool
  , fieldSpec     :: Spec
  }
  deriving stock (Eq, Show)


type Parser = Parsec Void Text


{-| Parse a full program (@module Name = …@). -}
parseProgram
  :: String
  -> Text
  -> Either String Program
parseProgram name input =
  case parse (sc >> program <* eof) name input of
    Left err ->
      Left (errorBundlePretty err)
    Right p ->
      Right p


{-| Parse a bare specification expression (not a full program). -}
parseSpec
  :: String
  -> Text
  -> Either String Spec
parseSpec name input =
  case parse (sc >> spec <* eof) name input of
    Left err ->
      Left (errorBundlePretty err)
    Right s ->
      Right s


sc :: Parser ()
sc =
  L.space
    space1
    (L.skipLineComment "--")
    (L.skipBlockCommentNested "{-" "-}")


lexeme :: Parser a -> Parser a
lexeme =
  L.lexeme sc


symbol :: Text -> Parser Text
symbol =
  L.symbol sc


{-| Top-level @module Name = spec@. -}
program :: Parser Program
program = do
  void (keyword "module")
  name <- ident
  void (symbol "=")
  body <- spec
  pure (Program name body)


{-| Parse a specification. -}
spec :: Parser Spec
spec =
  choice
    [ letSpec
    , eitherSpec
    , dictSpec
    , nullSpec
    , primary
    ]


letSpec :: Parser Spec
letSpec = do
  void (keyword "let")
  void (symbol "{")
  binds <- many binding
  void (keyword "in")
  body <- spec
  void (symbol "}")
  checkDuplicateBinds binds
  pure (LetSpec binds body)


binding :: Parser Binding
binding =
  typeBind <|> moduleBind


typeBind :: Parser Binding
typeBind = do
  void (keyword "type")
  name <- ident
  void (symbol "=")
  TypeBinding name <$> spec


moduleBind :: Parser Binding
moduleBind = do
  void (keyword "module")
  name <- ident
  void (symbol "=")
  ModuleBinding name <$> spec


eitherSpec :: Parser Spec
eitherSpec = do
  void (keyword "either")
  firstBranch <- eitherBranch
  rest <- many (try (symbol "|" >> primary))
  pure (EitherSpec (firstBranch : rest))


eitherBranch :: Parser Spec
eitherBranch = do
  _ <- optional (symbol "|")
  primary


dictSpec :: Parser Spec
dictSpec = do
  void (keyword "dict")
  DictSpec <$> primary


nullSpec :: Parser Spec
nullSpec = do
  void (keyword "null")
  NullSpec <$> primary


primary :: Parser Spec
primary =
  choice
    [ try (keyword "string")   >> pure StringSpec
    , try (keyword "number")   >> pure NumberSpec
    , try (keyword "int")      >> pure IntSpec
    , try (keyword "bool")     >> pure BoolSpec
    , try (keyword "datetime") >> pure DateTimeSpec
    , try (keyword "raw")      >> pure RawSpec
    , TagSpec <$> stringLit
    , RefSpec <$> ident
    , objectSpec
    , arraySpec
    , between (symbol "(") (symbol ")") spec
    ]


objectSpec :: Parser Spec
objectSpec = do
  void (symbol "{")
  fields <- field `sepEndBy` symbol ","
  void (symbol "}")
  checkDuplicateFields fields
  pure (ObjectSpec fields)


field :: Parser Field
field = do
  name <- stringLit
  opt <- optional (symbol "?")
  void (symbol ":")
  s <- spec
  pure Field
    { fieldName = name
    , fieldOptional = case opt of
        Just _ ->
          True
        Nothing ->
          False
    , fieldSpec = s
    }


arraySpec :: Parser Spec
arraySpec =
  ArraySpec <$> between (symbol "[") (symbol "]") spec


keywords :: Set.Set Text
keywords =
  Set.fromList
    [ "module", "type", "let", "in", "either", "dict", "null"
    , "string", "number", "int", "bool", "datetime", "raw"
    ]


keyword :: Text -> Parser ()
keyword w = lexeme . try $ do
  void (string w)
  notFollowedBy (satisfy identChar)


{-| Binding name or reference: bare non-keyword, or backtick-escaped. -}
ident :: Parser Text
ident =
  escapedIdent <|> bareIdent


{-| @`name`@ — may be a keyword (e.g. @`string`@, @`type`@). -}
escapedIdent :: Parser Text
escapedIdent = lexeme . try $ do
  void (char '`')
  name <- identBody
  void (char '`')
  pure name


{-| Bare identifier; keywords are rejected. -}
bareIdent :: Parser Text
bareIdent = lexeme . try $ do
  full <- identBody
  if Set.member full keywords then
    fail ("unexpected keyword " <> T.unpack full)
  else
    pure full


identBody :: Parser Text
identBody = do
  first <- takeWhile1P (Just "identifier") identCharStart
  rest <- fmap T.pack (many (satisfy identChar))
  pure (first <> rest)


identCharStart :: Char -> Bool
identCharStart c =
  isAlpha c || c == '_'


identChar :: Char -> Bool
identChar c =
  isAlphaNum c || c == '_'


stringLit :: Parser Text
stringLit = lexeme $ do
  void (char '"')
  chars <- manyTill stringChar (char '"')
  pure (T.pack chars)


stringChar :: Parser Char
stringChar =
  satisfy (\c -> c /= '"' && c /= '\\')
  <|> (char '\\' >> escape)


escape :: Parser Char
escape =
  choice
    [ char '"'  >> pure '"'
    , char '\\' >> pure '\\'
    , char '/'  >> pure '/'
    , char 'b'  >> pure '\b'
    , char 'f'  >> pure '\f'
    , char 'n'  >> pure '\n'
    , char 'r'  >> pure '\r'
    , char 't'  >> pure '\t'
    , char 'u'  >> unicodeEscape
    ]


unicodeEscape :: Parser Char
unicodeEscape = do
  d1 <- hexDigit
  d2 <- hexDigit
  d3 <- hexDigit
  d4 <- hexDigit
  pure (toEnum (d1 * 4096 + d2 * 256 + d3 * 16 + d4))


hexDigit :: Parser Int
hexDigit = do
  c <- satisfy isHex
  pure (hexVal c)


isHex :: Char -> Bool
isHex c =
  (c >= '0' && c <= '9')
  || (c >= 'a' && c <= 'f')
  || (c >= 'A' && c <= 'F')


hexVal :: Char -> Int
hexVal c
  | c >= '0' && c <= '9' =
      fromEnum c - fromEnum '0'
  | c >= 'a' && c <= 'f' =
      fromEnum c - fromEnum 'a' + 10
  | otherwise =
      fromEnum c - fromEnum 'A' + 10


checkDuplicateBinds :: [Binding] -> Parser ()
checkDuplicateBinds binds =
  checkDups "duplicate binding" (fmap bindName binds)


checkDuplicateFields :: [Field] -> Parser ()
checkDuplicateFields fields =
  checkDups "duplicate field" (fmap fieldName fields)


bindName :: Binding -> Text
bindName (TypeBinding n _) =
  n
bindName (ModuleBinding n _) =
  n


checkDups :: String -> [Text] -> Parser ()
checkDups msg names =
  go Set.empty names
  where
    go :: Set.Set Text -> [Text] -> Parser ()
    go _seen [] =
      pure ()
    go seen (n:ns)
      | Set.member n seen =
          fail (msg <> ": " <> T.unpack n)
      | otherwise =
          go (Set.insert n seen) ns