module Ideas.Text.JSON
( JSON(..), Key, Number(..)
, InJSON(..)
, lookupM
, parseJSON, compactJSON
, jsonRPC, RPCHandler, RPCResponse(..)
, propEncoding
) where
import Control.Exception
import Control.Monad
import Data.List (intersperse)
import Data.Maybe
import Ideas.Utils.Parsing hiding (string, char)
import System.IO.Error
import Test.QuickCheck
import Text.PrettyPrint.Leijen hiding ((<$>))
import qualified Ideas.Text.UTF8 as UTF8
import qualified Text.ParserCombinators.Parsec.Token as P
data JSON
= Number Number
| String String
| Boolean Bool
| Array [JSON]
| Object [(Key, JSON)]
| Null
deriving Eq
type Key = String
data Number = I Integer | D Double deriving Eq
instance Show Number where
show (I n) = show n
show (D d) = show d
instance Show JSON where
show = show . prettyJSON False
compactJSON :: JSON -> String
compactJSON = show . prettyJSON True
prettyJSON :: Bool -> JSON -> Doc
prettyJSON compact = rec
where
rec json =
case json of
Number n -> text (show n)
String s -> str (escape s)
Boolean b -> text (if b then "true" else "false")
Null -> text "null"
Array xs -> make lbracket rbracket (map rec xs)
Object xs -> make lbrace rbrace (map (uncurry (<:>)) xs)
x <:> y | compact = str x <> char ':' <> rec y
| isSimple y = str x <> string ": " <> rec y
| otherwise = align (str x <> char ':' <> line <> indent 2 (rec y))
str = dquotes . text
make open close xs
| compact || length xs < 2 =
enclose open close (hcat (intersperse comma xs))
| otherwise =
align (vsep (zipWith (<+>) (open:repeat comma) xs ++ [close]))
isSimple (Array xs) = null xs
isSimple (Object xs) = null xs
isSimple _ = True
escape :: String -> String
escape = concatMap f . fromMaybe "invalid UTF8 string" . UTF8.encodeM
where
f '\n' = "\\n"
f '\r' = ""
f '\t' = "\\t"
f '"' = "\\\""
f '\\' = "\\\\"
f c = [c]
class InJSON a where
toJSON :: a -> JSON
listToJSON :: [a] -> JSON
fromJSON :: Monad m => JSON -> m a
listFromJSON :: Monad m => JSON -> m [a]
listToJSON = Array . map toJSON
listFromJSON (Array xs) = mapM fromJSON xs
listFromJSON _ = fail "expecting an array"
instance InJSON Int where
toJSON = toJSON . toInteger
fromJSON = fmap fromInteger . fromJSON
instance InJSON Integer where
toJSON = Number . I
fromJSON (Number (I n)) = return n
fromJSON _ = fail "expecting a number"
instance InJSON Double where
toJSON = Number . D
fromJSON (Number (D n)) = return n
fromJSON _ = fail "expecting a number"
instance InJSON Char where
toJSON c = String [c]
listToJSON = String
fromJSON (String [c]) = return c
fromJSON _ = fail "expecting a string"
listFromJSON (String s) = return s
listFromJSON _ = fail "expecting a string"
instance InJSON Bool where
toJSON = Boolean
fromJSON (Boolean b) = return b
fromJSON _ = fail "expecting a boolean"
instance InJSON a => InJSON [a] where
toJSON = listToJSON
fromJSON = listFromJSON
instance (InJSON a, InJSON b) => InJSON (a, b) where
toJSON (a, b) = Array [toJSON a, toJSON b]
fromJSON (Array [a, b]) = (,) <$> fromJSON a <*> fromJSON b
fromJSON _ = fail "expecting an array with 2 elements"
instance (InJSON a, InJSON b, InJSON c) => InJSON (a, b, c) where
toJSON (a, b, c) = Array [toJSON a, toJSON b, toJSON c]
fromJSON (Array [a, b, c]) = (,,) <$> fromJSON a <*> fromJSON b <*> fromJSON c
fromJSON _ = fail "expecting an array with 3 elements"
instance (InJSON a, InJSON b, InJSON c, InJSON d) => InJSON (a, b, c, d) where
toJSON (a, b, c, d) = Array [toJSON a, toJSON b, toJSON c, toJSON d]
fromJSON (Array [a, b, c, d]) = (,,,) <$> fromJSON a <*> fromJSON b <*> fromJSON c <*> fromJSON d
fromJSON _ = fail "expecting an array with 4 elements"
parseJSON :: String -> Either String JSON
parseJSON = parseSimple json
where
json :: Parser JSON
json = choice
[ Null <$ P.reserved lexer "null"
, Boolean True <$ P.reserved lexer "true"
, Boolean False <$ P.reserved lexer "false"
, Number . either I D <$> naturalOrFloat
, String . fromMaybe [] . UTF8.decodeM <$> P.stringLiteral lexer
, Array <$> P.brackets lexer (sepBy json (P.comma lexer))
, Object <$> P.braces lexer (sepBy keyValue (P.comma lexer))
]
keyValue :: Parser (String, JSON)
keyValue = (,) <$> P.stringLiteral lexer <* P.colon lexer <*> json
lexer :: P.TokenParser a
lexer = P.makeTokenParser $ emptyDef
{ reservedNames = ["true", "false", "null"] }
data RPCRequest = Request
{ requestMethod :: String
, requestParams :: JSON
, requestId :: JSON
}
data RPCResponse = Response
{ responseResult :: JSON
, responseError :: JSON
, responseId :: JSON
}
instance Show RPCRequest where
show = show . toJSON
instance Show RPCResponse where
show = show . toJSON
instance InJSON RPCRequest where
toJSON req = Object
[ ("method", String $ requestMethod req)
, ("params", requestParams req)
, ("id" , requestId req)
]
fromJSON json =
case lookupM "method" json of
Just (String s) ->
let pj = fromMaybe Null (lookupM "params" json)
ij = fromMaybe Null (lookupM "id" json)
in return (Request s pj ij)
Just _ -> fail "expecting a string as method"
Nothing -> fail "no method specified"
instance InJSON RPCResponse where
toJSON resp = Object
[ ("result", responseResult resp)
, ("error" , responseError resp)
, ("id" , responseId resp)
]
fromJSON obj = do
rj <- lookupM "result" obj
ej <- lookupM "error" obj
ij <- lookupM "id" obj
return (Response rj ej ij)
okResponse :: JSON -> JSON -> RPCResponse
okResponse x y = Response
{ responseResult = x
, responseError = Null
, responseId = y
}
errorResponse :: JSON -> JSON -> RPCResponse
errorResponse x y = Response
{ responseResult = Null
, responseError = x
, responseId = y
}
lookupM :: Monad m => String -> JSON -> m JSON
lookupM x (Object xs) = maybe (fail $ "field " ++ x ++ " not found") return (lookup x xs)
lookupM _ _ = fail "expecting a JSON object"
type RPCHandler = String -> JSON -> IO JSON
jsonRPC :: JSON -> RPCHandler -> IO RPCResponse
jsonRPC input rpc =
case fromJSON input of
Nothing -> return (errorResponse (String "Invalid request") Null)
Just req -> do
json <- rpc (requestMethod req) (requestParams req)
return (okResponse json (requestId req))
`catch` handler req
where
handler :: RPCRequest -> SomeException -> IO RPCResponse
handler req e =
let msg = maybe (show e) ioeGetErrorString (fromException e)
in return $ errorResponse (toJSON msg) (requestId req)
instance Arbitrary JSON where
arbitrary = sized arbJSON
instance Arbitrary Number where
arbitrary = oneof [I <$> arbitrary, (D . fromInteger) <$> arbitrary]
arbJSON :: Int -> Gen JSON
arbJSON n
| n == 0 = oneof
[ Number <$> arbitrary, String <$> myStringGen
, Boolean <$> arbitrary, return Null
]
| otherwise = oneof
[ arbJSON 0
, do i <- choose (0, 6)
xs <- replicateM i rec
return (Array xs)
, do i <- choose (0, 6)
xs <- replicateM i myStringGen
ys <- replicateM i rec
return (Object (zip xs ys))
]
where
rec = arbJSON (n `div` 2)
myStringGen :: Gen String
myStringGen = do
n <- choose (1, 10)
replicateM n $ elements $
['A' .. 'Z'] ++ ['a' .. 'z'] ++ ['0' .. '9']
propEncoding :: Property
propEncoding = property $ \a ->
parseJSON (show a) == Right a