comparse-0.2.0.0: test/Json.hs
{-# LANGUAGE TypeFamilies #-}
module Json (jsonTests) where
import Control.Monad (void)
import Control.Monad.Parser
import Data.Char (isAlpha, isDigit)
import Data.Text (Text)
import qualified Data.Text as T
import Test.Tasty
import Test.Tasty.HUnit
data JValue
= JString String
| JNumber Int
| JBool Bool
| JNull
| JObject [(String, JValue)]
| JArray [JValue]
deriving (Eq, Show)
jsonTests :: [TestTree]
jsonTests =
testsWith parseString
++ testsWith (parseText . T.pack)
testsWith :: (String -> Maybe JValue) -> [TestTree]
testsWith p =
[ testCase "string" $ p "\"foo\"" @?= Just (JString "foo"),
testCase "empty string" $ p "\"\"" @?= Just (JString ""),
testCase "number" $ p "123" @?= Just (JNumber 123),
testCase "negative number" $ p "-123" @?= Just (JNumber (-123)),
testCase "positive number" $ p "+123" @?= Just (JNumber 123),
testCase "bool true" $ p "true" @?= Just (JBool True),
testCase "bool false" $ p "false" @?= Just (JBool False),
testCase "null" $ p "null" @?= Just JNull,
testCase "object" $
p "{\"foo\": \"bar\"}"
@?= Just (JObject [("foo", JString "bar")]),
testCase "empty object" $ p "{}" @?= Just (JObject []),
testCase "array" $
p "[1, 2, 3]"
@?= Just (JArray [JNumber 1, JNumber 2, JNumber 3]),
testCase "empty array" $ p "[]" @?= Just (JArray []),
testCase "nested array" $
p "[1, [2, 3], 4]"
@?= Just (JArray [JNumber 1, JArray [JNumber 2, JNumber 3], JNumber 4]),
testCase "nested object" $
p "{\"foo\": {\"bar\": \"baz\"}}"
@?= Just (JObject [("foo", JObject [("bar", JString "baz")])]),
testCase "nested object with array" $
p "{\"foo\": {\"bar\": [1, 2, 3]}}"
@?= Just
( JObject
[ ( "foo",
JObject
[ ("bar", JArray [JNumber 1, JNumber 2, JNumber 3])
]
)
]
),
testCase "invalid input" $ p "foo" @?= Nothing,
testCase "weird spacing" $
p " {\t \n\"foo\"\r \t: \"bar\"\n } \t"
@?= Just (JObject [("foo", JString "bar")]),
testCase "number followed by letter" $ p "123a" @?= Nothing
]
parseString :: String -> Maybe JValue
parseString s =
case runStringParser (json <* eof) s of
Parsed v _ _ -> Just v
_ -> Nothing
parseText :: Text -> Maybe JValue
parseText t =
case runTextParser (json <* eof) t of
Parsed v _ _ -> Just v
_ -> Nothing
lexeme :: CharParser p => p a -> p a
lexeme p = spaces *> p <* spaces
where
spaces = void $ many $ oneOf " \n\r\t"
symbol :: CharParser p => String -> p String
symbol = lexeme . string
json :: CharParser p => p JValue
json =
JString <$> stringLiteral
<|> JNumber <$> number
<|> JBool <$> bool
<|> JNull <$ symbol "null"
<|> JObject <$> object
<|> JArray <$> array
stringLiteral :: CharParser p => p String
stringLiteral = lexeme $ like '"' *> many (unlike '\"') <* like '"'
number :: CharParser p => p Int
number =
lexeme
( read <$> ((:) <$> like '-' <*> many1 digit)
<|> read <$> (optional (like '+') *> many1 digit)
)
<* notFollowedBy (match isAlpha)
where
digit = match isDigit
bool :: CharParser p => p Bool
bool = True <$ symbol "true" <|> False <$ symbol "false"
object :: CharParser p => p [(String, JValue)]
object =
symbol "{"
*> sepBy ((,) <$> (stringLiteral <* symbol ":") <*> json) (symbol ",")
<* symbol "}"
array :: CharParser p => p [JValue]
array = symbol "[" *> sepBy json (symbol ",") <* symbol "]"