aeson-quick-0.1.1: Data/Aeson/Quick.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
module Data.Aeson.Quick
(
-- $use
-- $examples
module Ae
, (.?)
, (.!)
, extract
, (.%)
, build
, Structure(..)
, parseStructure
) where
import Control.Applicative
import Control.Monad
import Control.DeepSeq
import Data.Aeson as Ae
import qualified Data.Aeson.Types as AT
import Data.Attoparsec.Text hiding (parse)
import Data.Char
import qualified Data.HashMap.Strict as H
import Data.Monoid
import Data.String
import qualified Data.Text as T
import qualified Data.Vector as V
import GHC.Generics (Generic)
data Structure = Obj [(T.Text, Bool, Structure)]
| Arr Structure
| Val
deriving (Eq, Ord, Generic)
instance NFData Structure
instance IsString Structure where
fromString s =
let e = error $ "Invalid structure: " ++ s
in either (\_ -> e) id $ parseStructure $ T.pack s
instance Show Structure where
show (Val) = "Val"
show (Arr s) = "[" ++ show s ++ "]"
show (Obj xs) = "{" ++ drop 1 (concatMap go xs) ++ "}"
where go (k,o,s) = "," ++ T.unpack k ++ (if o then "?" else "")
++ (if s == Val then "" else ":" ++ show s)
-- | Parse a structure, can fail
parseStructure :: T.Text -> Either String Structure
parseStructure = parseOnly structure
where
structure = object' <|> array
object' = Obj <$> ("{" *> sepBy1 lookups (char ',') <* "}")
array = Arr <$> ("[" *> structure <* "]")
lookups = (,,) <$> (takeWhile1 isKeyChar)
<*> ("?" *> pure True <|> pure False)
<*> (":" *> structure <|> pure Val)
isKeyChar = isAlphaNum -- TODO
{- |
Extracts instances of 'FromJSON' from a 'Value'
This is a wrapper around 'extract' which does the actual work.
Examples assume 'FromJSON' Foo and 'FromJSON' Bar.
Extract key from object:
>>> "{key}" .? value :: Maybe Foo
Extract list of objects:
>>> "[{key}]" .? value :: Maybe [Foo]
Extract with optional key:
>>> "{key,opt?}" .? value :: Maybe (Foo, Maybe Bar)
-}
(.?) :: FromJSON a => Value -> Structure -> Maybe a
(.?) = AT.parseMaybe . flip extract
{-# INLINE (.?) #-}
-- TODO: Appropriate infixes?
{- |
The (very!) unsafe version of '.?'. This can fail very easily, do not depend on this in your program. Will probably be removed.
-}
(.!) :: FromJSON a => Value -> Structure -> a
(.!) v s = either err id $ AT.parseEither (extract s) v
where err msg = error $ show s ++ ": " ++ msg ++ " in " ++ show v
{-# INLINE (.!) #-}
{- |
The 'Parser' that executes a 'Structure' against a 'Value' to return an instance of 'FromJSON'.
-}
extract :: FromJSON a => Structure -> Value -> AT.Parser a
extract structure = go structure >=> parseJSON
where
go (Obj [s]) = withObject "" (flip look s)
go (Obj sx) = withObject "" (forM sx . look) >=> pure . toJSON
go (Arr s) = withArray "" (V.mapM $ go s) >=> pure . Array
go Val = pure
look v (k,False,Val) = v .: k
look v (k,False,s) = v .: k >>= go s
look v (k,True,s) = v .:? k >>= maybe (pure Null) (go s)
{- |
Turns data into JSON objects.
This is a wrapper around 'build' which does the actual work.
Build a simple Value:
>>> encode $ "{a}" .% True
{\"a\": True}
Build a complex Value:
>>> encode $ "[{a}]" '.%' [True, False]
"[{\"a\":true},{\"a\":false}]"
-}
(.%) :: ToJSON a => Structure -> a -> Value
(.%) s = build s Null
{-# INLINE (.%) #-}
{- |
Executes a 'Structure' against provided data to update a 'Value'.
Note: /Still has undefined behaviours/, not at all stable.
-}
build :: ToJSON a => Structure -> Value -> a -> Value
build structure val = go structure val . toJSON
where
go (Val) _ r = r
go (Arr s) (Array v) (Array r) = Array $ V.zipWith (go s) v r
go (Arr s) Null (Array r) = Array $ V.map (go s Null) r
go (Arr s) Null r = toJSON [go s Null r]
go (Obj [ks]) (Object v) r = Object $ update v ks r
go (Obj keys) Null r = go (Obj keys) (Object mempty) r
go (Obj ks) (Object v) (Array r) = Object $
let maps = zip ks (V.toList r)
in foldl (\v' (s,r') -> update v' s r') v maps
go (Obj keys) (Object v) r = r
go a b c = error $ show (a,b,c) -- TODO
update v (k,_,s) r =
let startVal = go s (H.lookupDefault Null k v) r
in H.insert k startVal v
-- $use
--
-- aeson-quick is a library for terse marshalling of data to and from aeson's
-- 'Value'.
--
-- It works on the observation that by turning objects into tuples inside
-- the 'Value', the type system can be employed to do more of the work.
--
-- For example, given the JSON:
--
-- > { "name": "bob"
-- > , "age": 29
-- > , "hobbies": [{"name": "tennis"}, {"name": "cooking"}]
-- > }
--
-- You can write:
--
-- @
-- extractHobbyist :: 'Value' -> 'Maybe' ('String', 'Int', ['String'])
-- extractHobbyist value = value '.?' "{name,age,hobbies:[{name}]}"
-- @
--