microaeson-0.1.0.0: src/Data/Aeson/Micro.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Minimal JavaScript Object Notation (JSON) support as per <https://tools.ietf.org/html/rfc8259 RFC 8259>.
--
-- This API provides a subset (with a couple of divergences; see below) of
-- [aeson API](https://hackage.haskell.org/package/aeson/docs/Data-Aeson.html)
-- but puts the emphasis on simplicity rather than performance and features.
--
-- The 'ToJSON' and 'FromJSON' instances are intended to have an encoding
-- compatible with @aeson@'s encoding.
--
-- == Limitations and divergences from @aeson@'s API
--
-- In order to reduce the dependency footprint and keep the code
-- simpler, the following divergences from the @aeson@ API have to be
-- made:
--
-- * There are no `FromJSON`/`ToJSON` instances for `Char` & `String`.
-- * The type synonym (& the constructor of the same name) 'Object' uses @containers@'s 'Map.Map' rather than a 'HashMap' @unordered-containers@.
-- * 'Array' is represented by an ordinary list rather than a 'Vector' from the @vector@ package.
-- * 'Number' uses 'Double' instead of 'Scientific'
--
module Data.Aeson.Micro
( -- * Core JSON types
Value(..)
, Object
, Pair
-- ** Constructors
, (.=)
, object
, emptyArray
, emptyObject
-- ** Accessors
, (.:)
, (.:?)
, (.:!)
, (.!=)
-- * Encoding and decoding
, encode
, encodeStrict
, encodeToBuilder
, decodeStrict
, decode
, decodeStrictN
-- * Prism-style parsers
, withObject
, withText
, withArray
, withNumber
, withBool
-- * Type conversion
, FromJSON(parseJSON)
, Parser, parseMaybe
, ToJSON(toJSON)
) where
import Control.Monad
import Data.Char
import Data.Data (Data)
import Data.Int
import Data.List
import Data.Monoid
import Data.String
import Data.Typeable (Typeable)
import Data.Word
import GHC.Generics (Generic)
import Control.DeepSeq
import qualified Data.ByteString as BS
import Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BS.Lazy
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import Data.Aeson.Micro.Parser
import Data.Aeson.Micro.Scanner (Lexeme (..), scanLexemes)
-- TODO: We may want to replace 'String' with 'Text' or 'ByteString'
-- | A JSON value represented as a Haskell value.
data Value = Object !Object
| Array [Value]
| String !Text
| Number !Double
| Bool !Bool
| Null
deriving (Eq, Read, Show, Generic, Data, Typeable)
instance NFData Value
instance IsString Value where
fromString = String . fromString
-- | A key\/value pair for an 'Object'
type Pair = (Text, Value)
-- | A JSON \"object\" (key/value map).
type Object = Map.Map Text Value
infixr 8 .=
-- | A key-value pair for encoding a JSON object.
(.=) :: ToJSON v => Text -> v -> Pair
k .= v = (k, toJSON v)
-- | Create a 'Value' from a list of name\/value 'Pair's.
object :: [Pair] -> Value
object = Object . Map.fromList
-- | The empty JSON 'Object' (i.e. @{}@).
emptyObject :: Value
emptyObject = Object mempty
-- | The empty JSON 'Array' (i.e. @[]@).
emptyArray :: Value
emptyArray = Array mempty
----------------------------------------------------------------------------
(.:) :: FromJSON a => Object -> Text -> Parser a
m .: k = maybe (pfail "key not found") parseJSON (Map.lookup k m)
(.:?) :: FromJSON a => Object -> Text -> Parser (Maybe a)
m .:? k = maybe (pure Nothing) parseJSON (Map.lookup k m)
(.:!) :: FromJSON a => Object -> Text -> Parser (Maybe a)
m .:! k = maybe (pure Nothing) (fmap Just . parseJSON) (Map.lookup k m)
(.!=) :: Parser (Maybe a) -> a -> Parser a
mv .!= def = fmap (maybe def id) mv
----------------------------------------------------------------------------
-- | A type that can be converted to JSON.
class ToJSON a where
-- | Convert a Haskell value to a JSON-friendly intermediate type.
toJSON :: a -> Value
instance ToJSON () where
toJSON () = Array []
instance ToJSON Value where
toJSON = id
instance ToJSON Bool where
toJSON = Bool
instance ToJSON a => ToJSON [a] where
toJSON = Array . map toJSON
instance ToJSON v => ToJSON (Map.Map Text v) where
toJSON = Object . Map.map toJSON
instance ToJSON a => ToJSON (Maybe a) where
toJSON Nothing = Null
toJSON (Just a) = toJSON a
instance (ToJSON a,ToJSON b) => ToJSON (a,b) where
toJSON (a,b) = Array [toJSON a, toJSON b]
instance (ToJSON a,ToJSON b,ToJSON c) => ToJSON (a,b,c) where
toJSON (a,b,c) = Array [toJSON a, toJSON b, toJSON c]
instance (ToJSON a,ToJSON b,ToJSON c, ToJSON d) => ToJSON (a,b,c,d) where
toJSON (a,b,c,d) = Array [toJSON a, toJSON b, toJSON c, toJSON d]
instance ToJSON Text where
toJSON = String
instance ToJSON TL.Text where
toJSON = toJSON . TL.toStrict
instance ToJSON Float where
toJSON = Number . realToFrac
instance ToJSON Double where
toJSON = Number
instance ToJSON Int where toJSON = Number . realToFrac
instance ToJSON Int8 where toJSON = Number . realToFrac
instance ToJSON Int16 where toJSON = Number . realToFrac
instance ToJSON Int32 where toJSON = Number . realToFrac
instance ToJSON Word where toJSON = Number . realToFrac
instance ToJSON Word8 where toJSON = Number . realToFrac
instance ToJSON Word16 where toJSON = Number . realToFrac
instance ToJSON Word32 where toJSON = Number . realToFrac
-- | Possibly lossy due to conversion to 'Double'
instance ToJSON Int64 where toJSON = Number . realToFrac
-- | Possibly lossy due to conversion to 'Double'
instance ToJSON Word64 where toJSON = Number . realToFrac
-- | Possibly lossy due to conversion to 'Double'
instance ToJSON Integer where toJSON = Number . fromInteger
------------------------------------------------------------------------------
-- 'BB.Builder'-based encoding
-- | Serialise value as JSON/UTF-8-encoded strict 'BS.ByteString'
encodeStrict :: ToJSON a => a -> BS.ByteString
encodeStrict = BS.Lazy.toStrict . encode
-- | Serialise value as JSON/UTF-8-encoded lazy 'BS.Lazy.ByteString'
encode :: ToJSON a => a -> BS.Lazy.ByteString
encode = BB.toLazyByteString . encodeToBuilder
-- | Serialise value as JSON/UTF8-encoded 'Builder'
encodeToBuilder :: ToJSON a => a -> Builder
encodeToBuilder = encodeValueBB . toJSON
encodeValueBB :: Value -> Builder
encodeValueBB jv = case jv of
Bool True -> "true"
Bool False -> "false"
Null -> "null"
Number n
| isNaN n || isInfinite n -> encodeValueBB Null
| Just i <- doubleToInt64 n -> BB.int64Dec i
| otherwise -> BB.doubleDec n
Array a -> encodeArrayBB a
String s -> encodeStringBB s
Object o -> encodeObjectBB o
encodeArrayBB :: [Value] -> Builder
encodeArrayBB [] = "[]"
encodeArrayBB jvs = BB.char8 '[' <> go jvs <> BB.char8 ']'
where
go = Data.Monoid.mconcat . intersperse (BB.char8 ',') . map encodeValueBB
encodeObjectBB :: Object -> Builder
encodeObjectBB m
| Map.null m = "{}"
| otherwise = BB.char8 '{' <> go jvs <> BB.char8 '}'
where
jvs = Map.toList m
go = Data.Monoid.mconcat . intersperse (BB.char8 ',') . map encPair
encPair (l,x) = encodeStringBB l <> BB.char8 ':' <> encodeValueBB x
encodeStringBB :: Text -> Builder
encodeStringBB str = BB.char8 '"' <> go str <> BB.char8 '"'
where
go = T.encodeUtf8Builder . escapeText
------------------------------------------------------------------------------
-- helpers
-- | Try to convert 'Double' into 'Int64', return 'Nothing' if not
-- representable loss-free as integral 'Int64' value.
doubleToInt64 :: Double -> Maybe Int64
doubleToInt64 x
| fromInteger x' == x
, x' <= toInteger (maxBound :: Int64)
, x' >= toInteger (minBound :: Int64)
= Just (fromIntegral x')
| otherwise = Nothing
where
x' = round x
-- | Minimally escape a 'String' in accordance with [RFC 8259, "7. Strings"](https://tools.ietf.org/html/rfc8259#section-7)
escapeText :: Text -> Text
escapeText s
| not (T.any needsEscape s) = s
| otherwise = (T.pack . escape . T.unpack) s
where
escape [] = []
escape (x:xs) = case x of
'\\' -> '\\':'\\':escape xs
'"' -> '\\':'"':escape xs
'\b' -> '\\':'b':escape xs
'\f' -> '\\':'f':escape xs
'\n' -> '\\':'n':escape xs
'\r' -> '\\':'r':escape xs
'\t' -> '\\':'t':escape xs
c | ord c < 0x10 -> '\\':'u':'0':'0':'0':intToDigit (ord c):escape xs
| ord c < 0x20 -> '\\':'u':'0':'0':'1':intToDigit (ord c - 0x10):escape xs
| otherwise -> c : escape xs
-- unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
needsEscape c = ord c < 0x20 || c `elem` ['\\','"']
----------------------------------------------------------------------------
----------------------------------------------------------------------------
-- | JSON Parser 'Monad' used by 'FromJSON'
newtype Parser a = P { unP :: Maybe a }
deriving (Functor,Applicative,Monad)
-- | Run 'Parser'.
--
-- A common use-case is @'parseMaybe' 'parseJSON'@.
parseMaybe :: (a -> Parser b) -> a -> Maybe b
parseMaybe m v = unP (m v)
pfail :: String -> Parser a
pfail _ = P Nothing
-- | A type that JSON can be deserialised into
class FromJSON a where
-- | Decode a JSON value into a native Haskell type
parseJSON :: Value -> Parser a
instance FromJSON Value where
parseJSON = pure
instance FromJSON Bool where
parseJSON = withBool "Bool" pure
instance FromJSON Text where
parseJSON = withText "Text" pure
instance FromJSON TL.Text where
parseJSON = withText "Text" (pure . TL.fromStrict)
instance FromJSON a => FromJSON [a] where
parseJSON = withArray "[a]" (mapM parseJSON)
instance FromJSON Double where
parseJSON Null = pure (0/0)
parseJSON j = withNumber "Double" pure j
instance FromJSON Float where
parseJSON Null = pure (0/0)
parseJSON j = withNumber "Float" (pure . realToFrac) j
-- FIXME: lossy conversions
instance FromJSON Integer where
parseJSON = withNumber "Int" (pure . round)
instance FromJSON Int where
parseJSON = withNumber "Int" (pure . fromInteger . round)
instance FromJSON Int8 where
parseJSON = withNumber "Int8" (pure . fromInteger . round)
instance FromJSON Int16 where
parseJSON = withNumber "Int16" (pure . fromInteger . round)
instance FromJSON Int32 where
parseJSON = withNumber "Int32" (pure . fromInteger . round)
instance FromJSON Int64 where
parseJSON = withNumber "Int64" (pure . fromInteger . round)
instance FromJSON Word where
parseJSON = withNumber "Word" (pure . fromInteger . round)
instance FromJSON Word8 where
parseJSON = withNumber "Word8" (pure . fromInteger . round)
instance FromJSON Word16 where
parseJSON = withNumber "Word16" (pure . fromInteger . round)
instance FromJSON Word32 where
parseJSON = withNumber "Word32" (pure . fromInteger . round)
instance FromJSON Word64 where
parseJSON = withNumber "Word64" (pure . fromInteger . round)
instance FromJSON () where
parseJSON = withArray "()" $ \lst ->
case lst of
[] -> pure ()
_ -> pfail "expected ()"
instance (FromJSON a, FromJSON b) => FromJSON (a,b) where
parseJSON = withArray "(a,b)" $ \lst ->
case lst of
[a,b] -> liftM2 (,) (parseJSON a) (parseJSON b)
_ -> pfail "expected (a,b)"
instance (FromJSON a, FromJSON b, FromJSON c) => FromJSON (a,b,c) where
parseJSON = withArray "(a,b,c)" $ \lst ->
case lst of
[a,b,c] -> liftM3 (,,) (parseJSON a) (parseJSON b) (parseJSON c)
_ -> pfail "expected (a,b,c)"
instance (FromJSON a, FromJSON b, FromJSON c, FromJSON d) => FromJSON (a,b,c,d) where
parseJSON = withArray "(a,b,c,d)" $ \lst ->
case lst of
[a,b,c,d] -> liftM4 (,,,) (parseJSON a) (parseJSON b) (parseJSON c) (parseJSON d)
_ -> pfail "expected (a,b,c,d)"
instance FromJSON a => FromJSON (Maybe a) where
parseJSON Null = pure Nothing
parseJSON j = Just <$> parseJSON j
instance FromJSON Ordering where
parseJSON = withText "{'LT','EQ','GT'}" $ \s ->
case s of
"LT" -> pure LT
"EQ" -> pure EQ
"GT" -> pure GT
_ -> pfail "expected {'LT','EQ','GT'}"
instance FromJSON v => FromJSON (Map.Map Text v) where
parseJSON = withObject "Map Text v" $ mapM parseJSON
-- "prisms"
withBool :: String -> (Bool -> Parser a) -> Value -> Parser a
withBool _ f (Bool arr) = f arr
withBool expected _ v = typeMismatch expected v
withText :: String -> (Text -> Parser a) -> Value -> Parser a
withText _ f (String txt) = f txt
withText expected _ v = typeMismatch expected v
withArray :: String -> ([Value] -> Parser a) -> Value -> Parser a
withArray _ f (Array lst) = f lst
withArray expected _ v = typeMismatch expected v
withObject :: String -> (Object -> Parser a) -> Value -> Parser a
withObject _ f (Object obj) = f obj
withObject expected _ v = typeMismatch expected v
withNumber :: String -> (Double -> Parser a) -> Value -> Parser a
withNumber _ f (Number n) = f n
withNumber expected _ v = typeMismatch expected v
typeMismatch :: String -> Value -> Parser a
typeMismatch expected _ = pfail ("expected " ++ expected)
----------------------------------------------------------------------------
-- | Decode a single JSON document
decode :: FromJSON a => BS.Lazy.ByteString -> Maybe a
decode = decodeStrict . BS.Lazy.toStrict
-- | Decode a single JSON document
decodeStrict :: FromJSON a => BS.ByteString -> Maybe a
decodeStrict bs = do
v <- decodeValue bs
unP (parseJSON v)
-- | Decode multiple concatenated JSON documents
decodeStrictN :: FromJSON a => BS.ByteString -> Maybe [a]
decodeStrictN = go [] . scanLexemes
where
go acc [] = Just $! reverse acc
go acc ls = do
(ls', v) <- parseValue ls
a <- unP (parseJSON v)
go (a:acc) ls'
----
type LexStream = [(Lexeme,BS.ByteString)]
decodeValue :: BS.ByteString -> Maybe Value
decodeValue bs = case parseValue (scanLexemes bs) of
Just ([], v) -> Just v
_ -> Nothing
parseValue :: LexStream -> Maybe (LexStream, Value)
parseValue = goValue
where
goValue :: LexStream -> Maybe (LexStream, Value)
goValue ((L_True,_):xs) = Just (xs,Bool True)
goValue ((L_False,_):xs) = Just (xs,Bool False)
goValue ((L_Null,_):xs) = Just (xs,Null)
goValue ((L_Number,bs):xs) = (\n->(xs,Number n)) <$> decodeNumber bs
goValue ((L_StrStart,_):xs) = goString xs
goValue ((L_ArrStart,_):xs) = goArray xs
goValue ((L_ObjStart,_):xs) = goObject xs
goValue _ = Nothing
goArray :: LexStream -> Maybe (LexStream, Value)
goArray xs0 = (Array <$>) <$> go0 xs0
where
go0 ((L_ArrEnd,_):xs) = pure (xs, [])
go0 xs = do
(xs', v) <- goValue xs
go1 [v] xs'
go1 acc ((L_ArrEnd,_):xs) = pure (xs, reverse acc)
go1 acc ((L_Comma, _):xs) = do
(xs', v) <- goValue xs
go1 (v:acc) xs'
go1 _ _ = Nothing
goObject :: LexStream -> Maybe (LexStream, Value)
goObject xs0 = ((Object . Map.fromList) <$>) <$> go0 xs0
where
go0 ((L_ObjEnd,_):xs) = pure (xs, [])
go0 xs = do
((L_Colon,_):xs', String k) <- goValue xs
(xs'',v) <- goValue xs'
go1 [(k,v)] xs''
go1 acc ((L_ObjEnd,_):xs) = pure (xs, reverse acc)
go1 acc ((L_Comma, _):xs) = do
((L_Colon,_):xs', String k) <- goValue xs
(xs'',v) <- goValue xs'
go1 ((k,v):acc) xs''
go1 _ _ = Nothing
goString :: LexStream -> Maybe (LexStream, Value)
goString xs0 = ((String . T.pack) <$>) <$> go [] xs0
where
go _ [] = Nothing
go acc ((lx,chunk):xs) = case lx of
L_StrEnd -> pure (xs, concat (reverse acc))
L_StrUnescaped -> do
s <- decodeUnescaped chunk
go (s:acc) xs
L_StrEscaped -> do
c <- decodeEscaped chunk
go ([c]:acc) xs
L_StrEscapedHex -> do
c <- decodeEscapedHex chunk
go ([c]:acc) xs
L_StrEscapedHexSurr -> do
c <- decodeEscapedHexSurr chunk
go ([c]:acc) xs
_ -> Nothing