{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Medea.Parser.Primitive
( Identifier (..),
MedeaString (..),
Natural,
PrimTypeIdentifier (..),
ReservedIdentifier (..),
identFromReserved,
isReserved,
isStartIdent,
parseIdentifier,
parseKeyVal,
parseLine,
parseNatural,
parseReserved,
parseString,
tryPrimType,
)
where
import Control.Monad (replicateM_, when)
import qualified Data.ByteString as BS
import Data.Char (isControl, isDigit, isSeparator)
import Data.Hashable (Hashable (..))
import Data.Maybe (isJust)
import Data.Medea.JSONType (JSONType (..))
import Data.Medea.Parser.Types (MedeaParser, ParseError (..))
import Data.Text (Text, head, pack, unpack)
import Data.Text.Encoding (encodeUtf8)
import Text.Megaparsec
( customFailure,
manyTill,
takeWhile1P,
)
import Text.Megaparsec.Char (char, eol)
import Text.Megaparsec.Char.Lexer (charLiteral)
import Prelude hiding (head)
newtype Identifier = Identifier {toText :: Text}
deriving newtype (Eq, Ord, Show)
parseIdentifier :: MedeaParser Identifier
parseIdentifier = do
ident <- takeWhile1P (Just "Non-separator") (not . isSeparatorOrControl)
checkedConstruct Identifier ident
data ReservedIdentifier
= RSchema
| RStart
| RType
| RStringValues
| RProperties
| RPropertyName
| RPropertySchema
| RAdditionalPropertiesAllowed
| RAdditionalPropertySchema
| ROptionalProperty
| RMinLength
| RMaxLength
| RElementType
| RTuple
| RArray
| RBoolean
| RNull
| RNumber
| RObject
| RString
deriving stock (Eq, Show)
fromReserved :: ReservedIdentifier -> Text
fromReserved RSchema = "$schema"
fromReserved RStart = "$start"
fromReserved RType = "$type"
fromReserved RStringValues = "$string-values"
fromReserved RProperties = "$properties"
fromReserved RPropertyName = "$property-name"
fromReserved RPropertySchema = "$property-schema"
fromReserved RAdditionalPropertiesAllowed = "$additional-properties-allowed"
fromReserved RAdditionalPropertySchema = "$additional-property-schema"
fromReserved ROptionalProperty = "$optional-property"
fromReserved RMinLength = "$min-length"
fromReserved RMaxLength = "$max-length"
fromReserved RElementType = "$element-type"
fromReserved RTuple = "$tuple"
fromReserved RArray = "$array"
fromReserved RBoolean = "$boolean"
fromReserved RNull = "$null"
fromReserved RNumber = "$number"
fromReserved RObject = "$object"
fromReserved RString = "$string"
identFromReserved :: ReservedIdentifier -> Identifier
identFromReserved = Identifier . fromReserved
tryReserved :: Text -> Maybe ReservedIdentifier
tryReserved "$schema" = Just RSchema
tryReserved "$start" = Just RStart
tryReserved "$type" = Just RType
tryReserved "$string-values" = Just RStringValues
tryReserved "$properties" = Just RProperties
tryReserved "$property-name" = Just RPropertyName
tryReserved "$property-schema" = Just RPropertySchema
tryReserved "$additional-properties-allowed" = Just RAdditionalPropertiesAllowed
tryReserved "$additional-property-schema" = Just RAdditionalPropertySchema
tryReserved "$optional-property" = Just ROptionalProperty
tryReserved "$min-length" = Just RMinLength
tryReserved "$max-length" = Just RMaxLength
tryReserved "$element-type" = Just RElementType
tryReserved "$tuple" = Just RTuple
tryReserved "$array" = Just RArray
tryReserved "$boolean" = Just RBoolean
tryReserved "$null" = Just RNull
tryReserved "$number" = Just RNumber
tryReserved "$object" = Just RObject
tryReserved "$string" = Just RString
tryReserved _ = Nothing
parseReserved :: ReservedIdentifier -> MedeaParser Identifier
parseReserved reserved = do
ident <- takeWhile1P Nothing (not . isSeparatorOrControl)
let reservedText = fromReserved reserved
when (ident /= reservedText) $ customFailure . ExpectedReservedIdentifier $ reservedText
checkedConstruct Identifier ident
newtype PrimTypeIdentifier = PrimTypeIdentifier {typeOf :: JSONType}
deriving newtype (Eq)
tryPrimType :: Identifier -> Maybe PrimTypeIdentifier
tryPrimType (Identifier ident) = tryReserved ident >>= reservedToPrim
reservedToPrim :: ReservedIdentifier -> Maybe PrimTypeIdentifier
reservedToPrim RNull = Just . PrimTypeIdentifier $ JSONNull
reservedToPrim RBoolean = Just . PrimTypeIdentifier $ JSONBoolean
reservedToPrim RObject = Just . PrimTypeIdentifier $ JSONObject
reservedToPrim RArray = Just . PrimTypeIdentifier $ JSONArray
reservedToPrim RNumber = Just . PrimTypeIdentifier $ JSONNumber
reservedToPrim RString = Just . PrimTypeIdentifier $ JSONString
reservedToPrim _ = Nothing
isReserved :: Identifier -> Bool
isReserved = isJust . tryReserved . toText
isStartIdent :: Identifier -> Bool
isStartIdent = (== Just RStart) . tryReserved . toText
type Natural = Word
parseNatural :: MedeaParser Natural
parseNatural = do
digits <- takeWhile1P (Just "digits") isDigit
when (head digits == '0')
$ customFailure . LeadingZero
$ digits
pure . read . unpack $ digits
newtype MedeaString = MedeaString {unwrap :: Text}
deriving newtype (Eq, Ord, Show, Hashable)
parseString :: MedeaParser MedeaString
parseString = do
string <- char '"' *> manyTill charLiteral (char '"')
pure . MedeaString . pack $ string
{-# INLINE parseLine #-}
parseLine :: Int -> MedeaParser a -> MedeaParser a
parseLine spaces p = replicateM_ spaces (char ' ') *> p <* eol
parseKeyVal :: ReservedIdentifier -> MedeaParser a -> MedeaParser a
parseKeyVal key = (parseReserved key *> char ' ' *>)
checkedConstruct ::
(Text -> a) -> Text -> MedeaParser a
checkedConstruct f t =
if (> 32) . BS.length . encodeUtf8 $ t
then customFailure . IdentifierTooLong $ t
else pure . f $ t
isSeparatorOrControl :: Char -> Bool
isSeparatorOrControl c = isSeparator c || isControl c