packages feed

sajson-0.1.0.0: src/Data/Sajson.hs

{-# LANGUAGE ForeignFunctionInterface #-}

-- |
-- Module:      Data.Sajson
-- Copyright:   (c) 2017 Zhouyu Qian
--              (c) 2012-2017 Chad Austin
-- License:     MIT
-- Maintainer:  Zhouyu Qian
--
-- A more efficient C++-based JSON parser.

module Data.Sajson
  ( -- * Introduction
    -- $introduction

    -- * Native APIs
    -- $native
    sajsonParse
  , unsafeMutableByteStringSajsonParse
  , SajsonParseError(..)

    -- * Drop-in Replacements
    -- $replacements
  , eitherDecodeStrict
  , decodeStrict
  , eitherDecode
  , decode
  ) where

import Control.Exception
import Control.Monad
import Data.Aeson.Types (FromJSON (..), Value (..), parseEither, parseMaybe)
import Data.Bits
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Unsafe as BU
import qualified Data.HashMap.Strict as HM
import Data.Scientific
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Vector as V
import Data.Word
import Foreign.C.String
import Foreign.C.Types
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Foreign.Storable
import System.IO.Unsafe

-- $introduction
-- This library is a high-performance parser for JSON. It is an alternative
-- to the native parsers in 'Data.Aeson'.
--
-- Before using this library, please carefully consider whether you actually
-- should use this library. This library is specifically designed for
-- Haskell users who find the performance of the pure-Haskell 'Data.Aeson'
-- parser inadequate. This library focuses on performance, not portability
-- or absolute correctness. You should /only/ use this library if all of the
-- following is true:
--
--   *  You are using a system that is 64-bit.
--   *  You are using a system where the alignment of @double@ is no stricter than
--      that of @size_t@.
--   *  You do not expect to parse floating point numbers beyond the range of
--      `double`.
--   *  You do not expect to parse integers that are longer than 53 bits.
--   *  You do not expect parsing a JSON number will produce a Haskell
--      'Data.Scientific.Scientific' value that exactly represents the
--      mathematical value of the JSON number, or even the closest
--      approximation in IEEE-754 `double`.
--   *  You wish to sacrifice memory consumption for speed.
--   *  You are parsing JSON that is known to be valid.
--
-- When all of the above are true, this library could work for you. Again,
-- please benchmark and show that JSON parsing is indeed a bottleneck before
-- using this library.

-- $native
-- A single function 'sajsonParse' is provided to access the parsing
-- functionality directly. However, you might instead want to use one of the
-- drop-in replacements below.

-- | Phantom type for a pointer to sajson_document.
data SajsonDocument

-- | A structure containing information about parse failures. The error line,
-- column, and messages are from the @sajson@ C++ library.
data SajsonParseError
  = SajsonParseError
  { sajsonParseErrorLine :: Int -- ^ The line where the error occurred.
  , sajsonParseErrorColumn :: Int -- ^ The column where the error occurred.
  , sajsonParseErrorMessage :: String -- ^ The error message. This is a canned message; it is not generated from the input.
  } deriving (Show, Eq)

type SajsonValueType = Word8
type SajsonValuePayload = Ptr CSize
type SajsonValueInputMutableView = Ptr CUChar

foreign import ccall unsafe "sajson_wrapper.h sajson_document_sizeof"
  c_sajson_document_sizeof :: CSize

foreign import ccall unsafe "sajson_wrapper.h sajson_parse_single_allocation"
  c_sajson_parse_single_allocation :: Ptr CChar -> CSize -> Ptr CSize -> Ptr CChar -> IO (Ptr SajsonDocument)

foreign import ccall unsafe "sajson_wrapper.h sajson_has_error"
  c_sajson_has_error :: Ptr SajsonDocument -> IO CInt

foreign import ccall unsafe "sajson_wrapper.h sajson_get_error_line"
  c_sajson_get_error_line :: Ptr SajsonDocument -> IO CSize

foreign import ccall unsafe "sajson_wrapper.h sajson_get_error_column"
  c_sajson_get_error_column :: Ptr SajsonDocument -> IO CSize

foreign import ccall unsafe "sajson_wrapper.h sajson_get_error_message"
  c_sajson_get_error_message :: Ptr SajsonDocument -> IO CString

foreign import ccall unsafe "sajson_wrapper.h sajson_get_root_type"
  c_sajson_get_root_type :: Ptr SajsonDocument -> IO SajsonValueType

foreign import ccall unsafe "sajson_wrapper.h sajson_get_root"
  c_sajson_get_root :: Ptr SajsonDocument -> IO SajsonValuePayload

foreign import ccall unsafe "sajson_wrapper.h sajson_get_input"
  c_sajson_get_input :: Ptr SajsonDocument -> IO SajsonValueInputMutableView

-- | Parse a 'CStringLen' into a 'Value' with the possibility of failing with
-- 'SajsonParseError'.
doParse :: CStringLen -> IO (Either SajsonParseError Value)
doParse (ptr, size) =
  allocaBytes (fromIntegral c_sajson_document_sizeof) $ \rvbuf ->
  allocaBytes (8 * size) $ \buf -> do
  doc <- c_sajson_parse_single_allocation ptr (fromIntegral size) buf rvbuf
  hasError <- c_sajson_has_error doc
  case hasError of
    0 -> Right <$> join (constructHaskellValue
                         <$> c_sajson_get_root_type doc
                         <*> c_sajson_get_root doc
                         <*> c_sajson_get_input doc)
    _ -> Left <$> (SajsonParseError
                   <$> (fromIntegral <$> c_sajson_get_error_line doc)
                   <*> (fromIntegral <$> c_sajson_get_error_column doc)
                   <*> (c_sajson_get_error_message doc >>= peekCString))
{-# INLINE doParse #-}

-- | Parse a 'B.ByteString' into a 'Value' with the possibility of failing with
-- 'SajsonParseError'.
sajsonParse :: B.ByteString -> IO (Either SajsonParseError Value)
sajsonParse bs = B.useAsCStringLen bs doParse

unsafeMutableByteStringSajsonParse :: B.ByteString -> IO (Either SajsonParseError Value)
unsafeMutableByteStringSajsonParse bs = BU.unsafeUseAsCStringLen bs doParse

makeString :: SajsonValuePayload -> Int -> SajsonValueInputMutableView -> IO T.Text
makeString payload index buf = do
  start <- peekElemOff payload index
  end <- peekElemOff payload (1 + index)
  bs <- B.packCStringLen (buf `plusPtr` fromIntegral start, fromIntegral (end - start))
  pure (TE.decodeUtf8 bs)
{-# INLINE makeString #-}

makeValue :: SajsonValuePayload -> Int -> SajsonValueInputMutableView -> IO Value
makeValue payload index buf = do
    element <- peekElemOff payload index
    let vt = fromIntegral (element .&. 7)
        vp = payload `plusPtr` fromIntegral (8 * (element `shiftR` 3)) -- where 8 == sizeof(size_t)
    constructHaskellValue vt vp buf

constructHaskellValue :: SajsonValueType -> SajsonValuePayload -> SajsonValueInputMutableView -> IO Value
constructHaskellValue 0 payload _ = do -- TYPE_INTEGER
  i <- peekElemOff (castPtr payload) 0
  pure (Number (fromIntegral (i :: CInt)))
constructHaskellValue 1 payload _ = do -- TYPE_DOUBLE
  d <- peekElemOff (castPtr payload) 0
  pure (Number (fromFloatDigits (d :: CDouble)))
constructHaskellValue 2 _ _ = -- TYPE_NULL
  pure Null
constructHaskellValue 3 _ _ = -- TYPE_FALSE
  pure (Bool False)
constructHaskellValue 4 _ _ = -- TYPE_TRUE
  pure (Bool True)
constructHaskellValue 5 payload buf = -- TYPE_STRING
  String <$> makeString payload 0 buf
constructHaskellValue 6 payload buf = do -- TYPE_ARRAY
  len <- peekElemOff payload 0
  (Array <$>) . V.generateM (fromIntegral len) $ \i -> makeValue payload (1 + i) buf
constructHaskellValue 7 payload buf = do -- TYPE_OBJECT
  len <- peekElemOff payload 0
  Object <$> V.foldM' go HM.empty (V.enumFromN 0 (fromIntegral len))
  where go hm i = do
          key <- makeString payload (1 + i * 3) buf
          val <- makeValue payload (3 + i * 3) buf
          pure $ HM.insert key val hm
constructHaskellValue vt _ _ = throwIO (ErrorCall $ "Data.Sajson internal error: unexpected sajson type tag: " ++ show vt)

-- $replacements
-- These functions are (almost) drop-in replacements for the equivalently named
-- functions in 'Data.Aeson', with the caveat that the outermost structure must
-- be either an 'Object' or an 'Array'.

toDataEither :: FromJSON a => Value -> Either String a
toDataEither = parseEither parseJSON

toData :: FromJSON a => Value -> Maybe a
toData = parseMaybe parseJSON

describeSajsonParseError :: SajsonParseError -> String
describeSajsonParseError (SajsonParseError line col err) = "Error in sajson parser: line " ++ show line ++ " column " ++ show col ++ ": " ++ err

-- | Efficiently deserialize a JSON value from a strict 'B.ByteString'. If this
-- fails, 'Left' is returned with a 'String' describing the issue.
eitherDecodeStrict :: FromJSON a => B.ByteString -> Either String a
eitherDecodeStrict bs = unsafePerformIO $ either (Left . describeSajsonParseError) toDataEither <$> sajsonParse bs

-- | Efficiently deserialize a JSON value from a strict 'B.ByteString'. If this
-- fails, 'Nothing' is returned.
decodeStrict :: FromJSON a => B.ByteString -> Maybe a
decodeStrict bs = unsafePerformIO $ either (pure Nothing) toData <$> sajsonParse bs

-- | Deserialize a JSON value from a lazy 'BL.ByteString'. If this fails, 'Left'
-- is returned with a 'String' describing the issue.
--
-- It should be noted that this function first converts the lazy 'BL.ByteString'
-- into a strict 'B.ByteString'. It does not allow streaming. Therefore it
-- requires at least twice the total size of the 'BL.ByteString' to be in memory.
eitherDecode :: FromJSON a => BL.ByteString -> Either String a
eitherDecode = eitherDecodeStrict . BL.toStrict

-- | Deserialize a JSON value from a lazy 'BL.ByteString'. If this
-- fails, 'Nothing' is returned.
--
-- It should be noted that this function first converts the lazy 'BL.ByteString'
-- into a strict 'B.ByteString'. It does not allow streaming. Therefore it
-- requires at least twice the total size of the 'BL.ByteString' to be in memory.
decode :: FromJSON a => BL.ByteString -> Maybe a
decode = decodeStrict . BL.toStrict