packages feed

praxo-core-0.3.0: src/Praxo/Json.hs

module Praxo.Json (Json(..), parseJson, encodeJson, equalJson, field, asString, asArray, asObject) where

import Control.Monad (guard)
import Data.Char (chr, digitToInt, isDigit, isHexDigit, ord)
import Data.List (intercalate)
import qualified Data.Map.Strict as Map
import Data.Ratio (denominator, numerator, (%))
import Numeric (showHex)
import Text.ParserCombinators.ReadP

data Json = JObject [(String, Json)] | JArray [Json] | JString String | JNumber Rational | JBool Bool | JNull deriving (Eq, Show)

field :: String -> Json -> Json
field name (JObject fields) = maybe JNull id (lookup name (reverse fields))
field _ _ = JNull

asString :: Json -> String
asString (JString value) = value
asString _ = ""

asArray :: Json -> [Json]
asArray (JArray values) = values
asArray _ = []

asObject :: Json -> [(String, Json)]
asObject (JObject fields) = fields
asObject _ = []

equalJson :: Json -> Json -> Bool
equalJson (JObject a) (JObject b) =
    let left = Map.fromList a
        right = Map.fromList b
    in Map.keys left == Map.keys right && and (zipWith equalJson (Map.elems left) (Map.elems right))
equalJson (JArray a) (JArray b) = length a == length b && and (zipWith equalJson a b)
equalJson a b = a == b

parseJson :: String -> Either String Json
parseJson input
    | length input > 1048576 = Left "JSON_CAPACITY"
    | otherwise = case readP_to_S (space *> value 0 <* space <* eof) input of
        [(result, "")] -> Right result
        _ -> Left "JSON_INVALID"
  where
    space = skipMany (satisfy (`elem` " \t\r\n"))
    token parser = parser <* space
    value :: Int -> ReadP Json
    value depth = do
        guard (depth <= 64)
        choice
            [ JObject <$> between (token (char '{')) (char '}')
                (sepBy ((,) <$> token stringValue <* token (char ':') <*> token (value (depth + 1))) (token (char ',')))
            , JArray <$> between (token (char '[')) (char ']')
                (sepBy (token (value (depth + 1))) (token (char ',')))
            , JString <$> stringValue
            , JBool True <$ string "true"
            , JBool False <$ string "false"
            , JNull <$ string "null"
            , JNumber <$> numberValue
            ]

stringValue :: ReadP String
stringValue = between (char '"') (char '"') (many character)
  where
    character = satisfy (\c -> c /= '"' && c /= '\\' && ord c >= 32 && scalar c) <++ escaped
    escaped = do
        _ <- char '\\'
        code <- get
        case lookup code [('"', '"'), ('\\', '\\'), ('/', '/'), ('b', '\b'), ('f', '\f'), ('n', '\n'), ('r', '\r'), ('t', '\t')] of
            Just result -> pure result
            Nothing -> do
                guard (code == 'u')
                first <- hexUnit
                if first >= 0xD800 && first <= 0xDBFF then do
                    _ <- string "\\u"
                    second <- hexUnit
                    guard (second >= 0xDC00 && second <= 0xDFFF)
                    pure (chr (0x10000 + (first - 0xD800) * 1024 + second - 0xDC00))
                else do
                    guard (first < 0xD800 || first > 0xDFFF)
                    pure (chr first)
    hexUnit = foldl (\n c -> n * 16 + digitToInt c) 0 <$> count 4 (satisfy isHexDigit)

scalar :: Char -> Bool
scalar c = ord c < 0xD800 || ord c > 0xDFFF

numberValue :: ReadP Rational
numberValue = do
    sign <- option 1 (-1 <$ char '-')
    whole <- string "0" <++ ((:) <$> satisfy (`elem` ['1'..'9']) <*> munch isDigit)
    fraction <- option "" (char '.' *> munch1 isDigit)
    power <- option 0 $ do
        _ <- satisfy (`elem` "eE")
        direction <- option 1 ((-1 <$ char '-') <++ (1 <$ char '+'))
        digits <- munch1 isDigit
        guard (length digits <= 4)
        pure (direction * read digits)
    guard (length whole + length fraction <= 4096 && abs power <= 4096)
    let coefficient = sign * read (whole ++ fraction)
        scale = power - length fraction
    pure (if scale >= 0 then (coefficient * 10 ^ scale) % 1 else coefficient % (10 ^ negate scale))

encodeJson :: Json -> Either String String
encodeJson value = do
    text <- render 0 value
    if length text > 1048576 then Left "JSON_CAPACITY" else Right text
  where
    render :: Int -> Json -> Either String String
    render depth _ | depth > 64 = Left "JSON_CAPACITY"
    render depth (JObject fields) = do
        entries <- mapM (\(key, item) -> do
            name <- quoted key
            body <- render (depth + 1) item
            pure (name ++ ":" ++ body)) fields
        pure ("{" ++ intercalate "," entries ++ "}")
    render depth (JArray items) = do
        entries <- mapM (render (depth + 1)) items
        pure ("[" ++ intercalate "," entries ++ "]")
    render _ (JString text) = quoted text
    render _ (JNumber number) = decimal number
    render _ (JBool flag) = Right (if flag then "true" else "false")
    render _ JNull = Right "null"
    quoted text
        | length text > 1048576 = Left "JSON_CAPACITY"
        | not (all scalar text) = Left "JSON_INVALID"
        | otherwise = Right ('"' : concatMap escape text ++ "\"")
    escape '"' = "\\\""
    escape '\\' = "\\\\"
    escape c | ord c < 32 = let digits = showHex (ord c) "" in "\\u" ++ replicate (4 - length digits) '0' ++ digits
    escape c = [c]

decimal :: Rational -> Either String String
decimal number = do
    let n = abs (numerator number)
        d = denominator number
        remove factor rest | rest `mod` factor == 0 = remove factor (rest `div` factor)
                           | otherwise = rest
    if remove 5 (remove 2 d) /= 1 then Left "JSON_NON_DECIMAL_NUMBER" else do
        tailDigits <- fractional 0 (n `mod` d) d
        let text = (if number < 0 then "-" else "") ++ show (n `div` d) ++
                   (if null tailDigits then "" else '.' : tailDigits)
        if length text > 16384 then Left "JSON_CAPACITY" else Right text
  where
    fractional :: Int -> Integer -> Integer -> Either String String
    fractional _ 0 _ = Right ""
    fractional countDigits _ _ | countDigits >= 8192 = Left "JSON_CAPACITY"
    fractional countDigits rest divisor = do
        let (digit, remaining) = (rest * 10) `divMod` divisor
        suffix <- fractional (countDigits + 1) remaining divisor
        pure (chr (ord '0' + fromInteger digit) : suffix)