----------------------------------------------------------------------------- -- | -- Module : Text.JSON.Permissive -- Copyright : (c) Jonathan Kochems 2015 -- License : BSD-3 -- Maintainer : jonathan.kochems@gmail.com -- Stability : experimental -- Portability : portable -- -- This module extends Text.JSON to enable the decoding of strings containing literal JS objects. -- In particular, it relaxes the restriction that fields in JSON objects must be strings. -- -- For example: -- -- > JSON conformant: literal JS object: -- > { "foo" : "bar" } { foo : "bar" } -- ----------------------------------------------------------------------------- module Text.JSON.Permissive(decodePermissive, get_fields) where import Text.JSON (Result(..)) import Text.JSON.Parsec ( runParser, try, CharParser(..), spaces, space, char, sepBy, manyTill, anyChar, string, p_number, p_string, p_boolean, p_null, many, choice, noneOf, option, lookAhead, ParseError, optionMaybe ) import Text.JSON.Types (JSValue(..), JSObject(..), toJSObject, toJSString, fromJSObject) import Control.Monad(mzero) import Data.Maybe (fromMaybe, isJust) import Control.Applicative ((<$>)) {-------------------------------------------------------------------- Decoding --------------------------------------------------------------------} -- | decodes a string encoding a JSON object in a relaxed fashion -- -- > decode "{ foo : \"bar\" }" -- > Error "Malformed JSON: expecting string: foo : \"b" -- > -- > decodePermissive "{ foo : \"bar\" }" == Ok $ toJSObject [("foo", JSString $ toJSString "bar")] -- > decodePermissive "{ \"foo\" : \"bar\" }" == Ok $ toJSObject [("foo", JSString $ toJSString "bar")] decodePermissive :: String -> Result (JSObject JSValue) decodePermissive s = either (Error . show) (Ok . toJSObject) $ runParser p_object () "stdin" s {-------------------------------------------------------------------- JSON Object Interaction --------------------------------------------------------------------} -- | returns the list of fields of a JSON Object -- -- > do obj <- decodePermissive "{ foo : \"bar\", fooz : \"baz\" }" -- > return $ get_fields obj == Ok ["foo", "fooz"] -- -- > do obj <- decodePermissive "{ foo : \"bar\", fooz : \"baz\" }" -- > return $ get_field obj $ head $ get_fields obj == Ok (Just $ JSString $ toJSString "bar" ) get_fields :: JSObject a -> [String] get_fields = map fst . fromJSObject {-------------------------------------------------------------------- Helper Parsers --------------------------------------------------------------------} -- The main change is in p_object to relax the restrictions on -- fields. p_object :: CharParser () [(String, JSValue)] p_object = do _ <- spaces _ <- char '{' _ <- spaces rs <- option [] $ try $ entry `sepBy` string "," _ <- spaces _ <- char '}' return rs where entry = do _ <- spaces x <- choice [try_wrapper p_string' "\"", many $ noneOf ":, }"] _ <- spaces _ <- char ':' _ <- spaces val <- p_jvalue _ <- spaces return (x,val) -- p_jvalue just hooks in the p_object parser (and also the modified p_array parser) p_jvalue :: CharParser () JSValue p_jvalue = choice [ (JSObject . toJSObject) <$> try_wrapper p_object "{", JSArray <$> try_wrapper p_array "[", (JSString . toJSString) <$> try_wrapper p_string' "\"", JSRational False <$> try p_number, JSBool <$> try p_boolean, (\() -> JSNull) <$> try p_null ] -- p_array just hooks into the p_jvalue parser p_array :: CharParser () [JSValue] p_array = do _ <- char '[' rs <- entry `sepBy` string "," _ <- char ']' return rs where entry = do _ <- spaces res <- p_jvalue _ <- spaces return res -- p_string' is a modified version of p_string that preserves leading whitespace. From the documention -- of Text.JSON it would appear this is undesired behaviour. However, the quick check tests show that -- Text.JSON.decode in fact does preserve leading whitespace in strings. p_string' = do ws <- leadingWhiteSpace s <- p_string return $ ws ++ s leadingWhiteSpace = lookAhead $ try $ do _ <- string "\"" many space -- Helper function to use in non-deterministic choice with lookAhead try_wrapper :: CharParser () a -> String -> CharParser () a try_wrapper p prefix = do try $ lookAhead (string prefix) try p