jordan-0.2.0.0: lib/Jordan/FromJSON/Attoparsec.hs
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RoleAnnotations #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- | 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.
-- This means that it is pretty fast!
-- However, you also get basically *zero* error reporting, which is generally not what you want.
module Jordan.FromJSON.Attoparsec
( attoparsecParserFor,
parseViaAttoparsecWith,
parseViaAttoparsec,
attoparsecParser,
)
where
import Control.Applicative (Alternative (..))
import Control.Monad (void, when)
import Data.Attoparsec.ByteString ((<?>))
import qualified Data.Attoparsec.ByteString as A
import qualified Data.Attoparsec.ByteString as AP
import qualified Data.Attoparsec.ByteString.Char8 as CH
import qualified Data.Attoparsec.Combinator as AC
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Unsafe as B
import Data.Char (chr, digitToInt, isControl, isHexDigit, ord)
import Data.Functor (void, ($>))
import Data.Monoid (Alt (..))
import Data.Scientific (Scientific)
import qualified Data.Scientific as Sci
import qualified Data.Scientific as Scientific
import qualified Data.Text as Text
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Debug.Trace
import Jordan.FromJSON.Class
import Jordan.FromJSON.Internal.Attoparsec
import Jordan.FromJSON.Internal.Permutation
import Numeric (showHex)
newtype ObjectParser a = ObjectParser
{runObjectParser :: Permutation AP.Parser a}
deriving (Functor, Applicative)
type role ArrayParser representational
data ArrayParser a
= ParseNoEffect a
| ParseWithEffect (AP.Parser a)
deriving (Functor)
instance Applicative ArrayParser where
pure = ParseNoEffect
f <*> a = case f of
ParseNoEffect fab -> case a of
ParseNoEffect a' -> ParseNoEffect (fab a')
ParseWithEffect pa -> ParseWithEffect (fab <$> pa)
ParseWithEffect pa -> case a of
ParseNoEffect a' -> ParseWithEffect (fmap ($ a') pa)
ParseWithEffect pa' -> ParseWithEffect $ do
f' <- pa
comma
f' <$> pa'
runArrayParser :: ArrayParser a -> AP.Parser a
runArrayParser (ParseNoEffect a) = pure a
runArrayParser (ParseWithEffect eff) = eff
instance JSONObjectParser ObjectParser where
parseFieldWith label parser =
ObjectParser $
asPermutation $
parseObjectField
label
(runAttoparsecParser parser)
{-# INLINE parseFieldWith #-}
parseFieldWithDefault f = \(AttoparsecParser parseField) def ->
ObjectParser $
asPermutationWithDefault (parseObjectField f (parseField <?> ("field " <> show f))) def
newtype AttoparsecParser a = AttoparsecParser
{runAttoparsecParser :: AP.Parser a}
deriving (Functor)
deriving (Semigroup, Monoid) via (Alt AP.Parser a)
instance JSONTupleParser ArrayParser where
consumeItemWith = \parser -> ParseWithEffect $ runAttoparsecParser parser
instance JSONParser AttoparsecParser where
parseObject = \parser -> AttoparsecParser $
label "Object" $ do
startObject
r <- wrapEffect (parseAnyField <?> "ignored field in the middle of an object") comma $ runObjectParser parser
objectEndWithJunk
pure r
{-# INLINE parseObject #-}
parseDictionary parse = AttoparsecParser $
inObjectBraces $ do
parseDictField (runAttoparsecParser parse) `AP.sepBy` comma
parseTextConstant c = AttoparsecParser (objectKey c <?> "text constant" <> Text.unpack c)
{-# INLINE parseTextConstant #-}
parseText = AttoparsecParser parseJSONText
{-# INLINE parseText #-}
parseNumber = AttoparsecParser number
{-# INLINE parseNumber #-}
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
{-# INLINE parseTuple #-}
parseArrayWith jp = AttoparsecParser $ do
startArray
r <- lexeme (runAttoparsecParser jp) `AP.sepBy` comma <?> "array items"
endArray
pure r
{-# INLINE parseArrayWith #-}
parseBool =
AttoparsecParser $
lexeme $
(AP.string "true" $> True) <|> (AP.string "false" $> False)
parseNull = AttoparsecParser $ lexeme (AP.string "null" $> ())
nameParser l = \(AttoparsecParser a) ->
AttoparsecParser $
label ("Parser '" <> Text.unpack l <> "'") a
-- | Convert an abstract JSON parser to an Attoparsec Parser.
-- This function will skip leading whitespace.
attoparsecParserFor :: (forall parser. JSONParser parser => parser a) -> AP.Parser a
attoparsecParserFor = \parser -> (skipSpace *>) $ runAttoparsecParser parser
{-# INLINE attoparsecParserFor #-}
parseViaAttoparsecWith :: (forall parser. JSONParser parser => parser a) -> ByteString -> Either String a
parseViaAttoparsecWith p = AP.parseOnly (attoparsecParserFor p)
{-# INLINE parseViaAttoparsecWith #-}
-- | Parse a ByteString via an Attoparsec Parser.
parseViaAttoparsec :: forall val. (FromJSON val) => ByteString -> Either String val
parseViaAttoparsec = parseViaAttoparsecWith (fromJSON @val)
{-# INLINE parseViaAttoparsec #-}
-- | Get an Attoparsec parser for a particular JSON-parsable value.
attoparsecParser :: (FromJSON val) => AP.Parser val
attoparsecParser = runAttoparsecParser fromJSON
{-# INLINE attoparsecParser #-}