packages feed

jordan-0.1.0.0: lib/Jordan/FromJSON/Attoparsec.hs

{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
-- | Implementation of FromJSON parsers via Attoparsec.
--
-- This module does not construct intermediate data structures like maps or key-value lists,
-- and instead uses permutation parsers in order to parse your data structure directly.
module Jordan.FromJSON.Attoparsec
    ( convertParserToAttoparsecParser
    , runParserViaAttoparsec
    , parseViaAttoparsec
    , attoparsecParser
    ) where

import Control.Applicative (Alternative(..))
import Data.Attoparsec.ByteString ((<?>))
import qualified Data.Attoparsec.ByteString as AP
import qualified Data.Attoparsec.ByteString.Char8 as CH
import Data.ByteString (ByteString)
import Data.Char (chr, digitToInt, isControl, isHexDigit, ord)
import Data.Functor (void, ($>))
import Data.Monoid (Alt(..))
import Data.Scientific (Scientific)
import qualified Data.Text as Text
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Jordan.FromJSON.Class
import Jordan.FromJSON.ParseInternal
import Numeric (showHex)
import qualified Text.Megaparsec as Text

newtype ObjectParser a
  = ObjectParser
  { runObjectParser :: Permutation AP.Parser a }
  deriving (Functor, Applicative)

newtype ArrayParser a
  = ArrayParser
  { runArrayParser :: AP.Parser a }
  deriving (Functor)

instance Applicative ArrayParser where
  pure = ArrayParser . pure
  f <*> a = ArrayParser $ do
    f' <- runArrayParser f
    comma
    a' <- runArrayParser a
    pure $ f' a'

skipSpace :: AP.Parser ()
skipSpace = AP.skipWhile isSpace
  where
    isSpace = \case
      32 -> True
      10 -> True
      13 -> True
      9 -> True
      _ -> False

lexeme :: AP.Parser a -> AP.Parser a
lexeme a = a <* skipSpace

label :: String -> AP.Parser a -> AP.Parser a
label l p = p <?> l

parseAnyField :: AP.Parser ()
parseAnyField = label "junk field" $ void $  do
  lexeme parseJSONText
  labelSep
  lexeme anyDatum

junkFieldAtEnd :: AP.Parser ()
junkFieldAtEnd = void $ do
  comma
  parseAnyField `AP.sepBy` comma

comma :: AP.Parser ()
comma = label "comma character" $ void $ lexeme (AP.string ",")

quotation :: AP.Parser ()
quotation = label "quotation mark" $ void $ AP.word8 34

parseJSONText :: AP.Parser Text.Text
parseJSONText = label "JSON text" $ do
  quotation
  innerText

innerText :: AP.Parser Text.Text
innerText = do
  chunk <- AP.takeWhile $ \char -> char /= 92 && char /= 34
  l <- AP.peekWord8
  case l of
    Nothing -> fail "string without end"
    Just 34 -> do
      AP.anyWord8
      pure $ decodeUtf8 chunk
    Just 92 -> do
      AP.anyWord8
      r <- label "escape value" parseEscape
      rest <- innerText
      pure $ decodeUtf8 chunk <> r <> rest
    Just _ -> fail "IMPOSSIBLE"

parseEscape :: AP.Parser Text.Text
parseEscape
  = quote
  <|> backslash
  <|> solidus
  <|> backspace
  <|> formfeed
  <|> linefeed
  <|> carriage
  <|> tab
  <|> escaped
  where
    backslash = AP.string "\\" $> "\\"
    quote = AP.string "\"" $> "\""
    solidus = AP.string "/" $> "/"
    backspace = AP.string "b" $> "\b"
    formfeed = AP.string "f" $> "\f"
    linefeed = AP.string "n" $> "\n"
    carriage = AP.string "r" $> "\r"
    tab = AP.string "t" $> "\t"
    escaped = do
      AP.string "u"
      a <- parseHexDigit
      b <- parseHexDigit
      c <- parseHexDigit
      d <- parseHexDigit
      let s = (((a * 16) + b) * 16 + c) * 16 + d
      pure $ Text.pack [chr s]

parseHexDigit :: AP.Parser Int
parseHexDigit = label "hex digit" (digitToInt <$> CH.satisfy isHexDigit)

parseCharInText :: Char -> AP.Parser ()
parseCharInText a = parseLit a <|> escaped a
  where
    parseLit :: Char -> AP.Parser ()
    parseLit = \case
      '\\' -> void $ AP.string "\\\\"
      '"' -> void $ AP.string "\\\""
      '/' -> void $ AP.string "/" <|> AP.string "\\/"
      '\b' -> void $ AP.string "\\b"
      '\f' -> void $ AP.string "\\f"
      '\n' -> void $ AP.string "\\n"
      '\r' -> void $ AP.string "\\r"
      '\t' -> void $ AP.string "\\t"
      a -> if isControl a then empty else void $ AP.string $ encodeUtf8 $ Text.singleton a
    escaped :: Char -> AP.Parser ()
    escaped a = void $ AP.string $ encodeUtf8 $ Text.pack $ withEscaped $ (showHex $ ord a) []
    withEscaped :: String -> String
    withEscaped a@[_] = "\\u000" <> a
    withEscaped a@[_,_] = "\\u00" <> a
    withEscaped a@[_,_,_] = "\\u0" <> a
    withEscaped r = "\\u" <> r

objectKey :: Text.Text -> AP.Parser ()
objectKey k = lexeme $ do
  quotation
  Text.foldr (\c a -> parseCharInText c *> a) (pure ()) k
  quotation
  pure ()

startObject :: AP.Parser ()
startObject
  = label "object starting brace ('{')"
  $ lexeme
  $ void
  $ AP.word8 123

endObject :: AP.Parser ()
endObject
  = label "object ending brace ('}')"
  $ lexeme
  $ void
  $ AP.word8 125

inObjectBraces :: AP.Parser a -> AP.Parser a
inObjectBraces interior = startObject *> interior <* endObject

startArray :: AP.Parser ()
startArray
  = label "array starting brace ('[')"
  $ lexeme
  $ void
  $ AP.word8 91

endArray :: AP.Parser ()
endArray
  = label "array ending brace (']')"
  $ lexeme
  $ void
  $ AP.word8 93

labelSep :: AP.Parser ()
labelSep = label "key-value separator (':')" $ void $ lexeme $ AP.string ":"

anyDatum :: AP.Parser ()
anyDatum = lexeme inner
  where
    inner
      = runAttoparsecParser parseNull
      <|> void (runAttoparsecParser parseBool)
      <|> void (runAttoparsecParser parseText)
      <|> void (runAttoparsecParser parseNumber)
      <|> void (runAttoparsecParser parseBool)
      <|> anyObject
      <|> anyArray

anyArray :: AP.Parser ()
anyArray = label "ignored array" $ void $ do
  startArray
  anyDatum `AP.sepBy` comma
  endArray

number :: AP.Parser Scientific
number = CH.scientific

anyObject :: AP.Parser ()
anyObject = label "ignored object" $ void $ do
  startObject
  flip AP.sepBy comma $ do
    parseJSONText
    labelSep
    anyDatum
  endObject

parseObjectField
  :: Text.Text
  -> AP.Parser a
  -> AP.Parser a
parseObjectField t f = do
  objectKey t
  labelSep
  lexeme f

parseDictField
  :: AP.Parser a
  -> AP.Parser (Text.Text, a)
parseDictField p = do
  key <- parseJSONText
  labelSep
  val <- p
  pure (key, val)

instance JSONObjectParser ObjectParser where
  parseFieldWith label
    = ObjectParser
    . asPermutation
    . parseObjectField label
    . runAttoparsecParser

newtype AttoparsecParser a
  = AttoparsecParser
  { runAttoparsecParser :: AP.Parser a }
  deriving (Functor)
  deriving (Semigroup, Monoid) via (Alt AP.Parser a)

instance JSONTupleParser ArrayParser where
  consumeItemWith = ArrayParser . runAttoparsecParser

instance JSONParser AttoparsecParser where
  parseObject _ p = AttoparsecParser $ inObjectBraces $ do
    r <- wrapEffect parseAnyField comma $ runObjectParser p
    label "junk object fields at the end of a parsed object" $ many junkFieldAtEnd
    pure r
  parseDictionary parse = AttoparsecParser $ inObjectBraces $ do
    parseDictField (runAttoparsecParser parse) `AP.sepBy` comma
  parseTextConstant c = AttoparsecParser (objectKey c <?> "text constant" <> Text.unpack c)
  parseText = AttoparsecParser parseJSONText
  parseNumber = AttoparsecParser number
  validateJSON v = AttoparsecParser $ do
    r <- runAttoparsecParser v
    case r of
      Left err -> fail (Text.unpack err)
      Right e -> pure e
  parseTuple ap = AttoparsecParser $ do
    lexeme $ AP.word8 91
    r <- runArrayParser ap
    lexeme $ AP.word8 93
    pure r
  parseArrayWith jp = AttoparsecParser $ do
    startArray
    r <- lexeme (runAttoparsecParser jp) `AP.sepBy` comma <?> "array items"
    endArray
    pure r
  parseBool = AttoparsecParser $ lexeme $
    (AP.string "true" $> True) <|> (AP.string "false" $> False)
  parseNull = AttoparsecParser $ lexeme (AP.string "null" $> ())

-- | Convert an abstract JSON parser to an Attoparsec Parser.
-- This function will skip leading whitespace.
convertParserToAttoparsecParser :: (forall parser. JSONParser parser => parser a) -> AP.Parser a
convertParserToAttoparsecParser = (skipSpace *>) .  runAttoparsecParser

runParserViaAttoparsec :: (forall parser. JSONParser parser => parser a) -> ByteString -> Either String a
runParserViaAttoparsec p = AP.parseOnly (convertParserToAttoparsecParser p)

-- | Parse a ByteString via an Attoparsec Parser.
parseViaAttoparsec :: (FromJSON val) => ByteString -> Either String val
parseViaAttoparsec = AP.parseOnly (skipSpace *> runAttoparsecParser fromJSON)

-- | Get an Attoparsec parser for a particular JSON-parsable value.
attoparsecParser :: (FromJSON val) => AP.Parser val
attoparsecParser = runAttoparsecParser fromJSON