{-| Module : Data.Tyro Description : A library for automatically deriving JSON parsers from types Copyright : (c) Richard Lupton, 2017 License : BSD-3 Stability : experimental Portability : POSIX -} {-# LANGUAGE DataKinds, GADTs, KindSignatures, TypeOperators #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -fno-warn-redundant-constraints #-} module Data.Tyro ( -- * Introduction -- $introduction -- * Building types -- $typed_example Extract , type( >%> ) , List , unwrap -- * Value level API -- $value_example , Tyro , extract , (>%>) , (%%>) -- * Internal types , JSBranch , Unwrap ) where import Data.Aeson ((.:)) import qualified Data.Aeson as A import Data.Aeson.Types (Parser) import qualified Data.ByteString.Lazy as B import Data.Reflection (reifySymbol) import Data.Singletons (Sing, SingI(..)) import Data.Singletons.TypeLits ( Symbol, SSymbol, KnownSymbol , withKnownSymbol, symbolVal ) import Data.String (String) import Data.Text (pack) import Data.Vector (Vector) import qualified Data.Vector as V import Data.Tyro.Internal import Lib.Prelude -------------------------------------------------------------------------------- -- Type level API using a type family to mirror JSON structure -------------------------------------------------------------------------------- -- | @Extract a@ represents trying to parse JSON to an @a@. type Extract a = JSBranch 'JSExtract a -- | The type operator '>%> provides a way of describing how to walk -- down a JSON tree. type family (x :: Symbol) >%> (b :: *) :: * type instance (x :: Symbol) >%> JSBranch xs a = JSBranch ('JSKey x xs) a -- | The 'List' type operator constructs a parsing type for parsing -- a list of JSON objects. type family List (x :: *) :: * type instance List (JSBranch xs a) = JSBranch ('JSArray xs) a -------------------------------------------------------------------------------- -- Value level API using reification -------------------------------------------------------------------------------- -- | 'Tyro' is an abstract type representing a parser that walks down a JSON -- tree. newtype Tyro = Tyro [String] deriving (Eq, Show) -- | 'extract' is the value which represents halting the walk along the JSON -- tree, and pulling out the value there. extract :: Tyro extract = Tyro [] -- | '>%>' allows you to specify a subtree indexed by a key. It's right -- associative, so chains of keys can be specified without parenthesese. (>%>) :: String -> Tyro -> Tyro (>%>) s (Tyro t) = Tyro (s:t) infixr 9 >%> -- | Internal proxying datatype for accumulating reified values as a list data TyroProxy :: JSLens Symbol -> * where Take :: TyroProxy 'JSExtract Key :: TyroProxy s -> TyroProxy ('JSKey t s) -- | '%%>' tries to parse a ByteString along a 'Tyro' to obtain a value (%%>) :: (A.FromJSON a) => B.ByteString -> Tyro -> Maybe a (%%>) bs (Tyro xs) = go bs (reverse xs) Take where go :: (A.FromJSON a, SingI xs) => B.ByteString -> [String] -> TyroProxy xs -> Maybe a go b [] t = fmap dumbUnwrap $ parse b t go b (k:ks) t = reifySymbol k $ \p -> go b ks (extend t p) parse :: (A.FromJSON a, SingI xs) => B.ByteString -> TyroProxy xs -> Maybe (JSBranch xs a) parse b _ = A.decode b extend :: (KnownSymbol s) => TyroProxy xs -> Proxy s -> TyroProxy ('JSKey s xs) extend t _ = Key t dumbUnwrap :: JSBranch xs a -> a dumbUnwrap (JSNil x) = x dumbUnwrap (JSCons x') = dumbUnwrap x' dumbUnwrap _ = error "dumbUnwrap received unexpected input" infixl 8 %%> -------------------------------------------------------------------------------- -- Basic dependent structure -------------------------------------------------------------------------------- -- | 'JSBranch' is a dependent datatype which represents a walk down a JSON -- tree. @JSBranch ["key1", "key2"] a@ represents the walk "take the value at -- @key1@ and then the value at @key2@, and (try to) interpret that as an @a@". data JSBranch :: JSLens Symbol -> * -> * where JSNil :: a -> JSBranch 'JSExtract a JSCons :: JSBranch xs a -> JSBranch ('JSKey t xs) a JSArr :: Vector (JSBranch xs a) -> JSBranch ('JSArray xs) a -- | 'Unwrap' captures the unstructured type encapsulated by a JSBranch type family Unwrap (x :: *) :: * type instance Unwrap (JSBranch 'JSExtract a) = a type instance Unwrap (JSBranch ('JSKey k js) a) = Unwrap (JSBranch js a) type instance Unwrap (JSBranch ('JSArray js) a) = [Unwrap (JSBranch js a)] -- | 'unwrap' allows parsing types to be removed from a JSBranch unwrap :: JSBranch xs a -> Unwrap (JSBranch xs a) unwrap (JSNil x) = x unwrap (JSCons x') = unwrap x' unwrap (JSArr xs) = V.toList $ fmap unwrap xs instance (A.FromJSON a, SingI xs) => A.FromJSON (JSBranch xs a) where parseJSON :: (A.FromJSON a, SingI xs) => A.Value -> Parser (JSBranch xs a) parseJSON = parse sing where parse :: (A.FromJSON a) => Sing xs -> A.Value -> Parser (JSBranch xs a) parse s o = case s of SJSExtract -> JSNil <$> A.parseJSON o SJSKey x xs -> case o of A.Object v -> let key = pack (reflectSymbol x) in JSCons <$> (v .: key >>= parse xs) _ -> empty SJSArray xs -> case o of A.Array vs -> JSArr <$> ( sequence $ fmap (parse xs) vs ) _ -> empty -- | 'reflectSym' reflects a type level symbol into a value level string reflectSymbol :: SSymbol s -> String reflectSymbol s = withKnownSymbol s $ proxySym s Proxy where proxySym :: (KnownSymbol n) => SSymbol n -> Proxy n -> String proxySym _ = symbolVal -------------------------------------------------------------------------------- -- Documentation -------------------------------------------------------------------------------- -- $introduction -- 'Tyro' provides a type driven way of obtaining simple JSON parsers, and -- a simple value driven interface to obtain values deep inside a JSON object. -- $typed_example -- A small (artificial) example demonstrating how to use the typed interface. -- -- > {-# LANGUAGE OverloadedStrings #-} -- > {-# LANGUAGE DataKinds #-} -- > {-# LANGUAGE TypeOperators #-} -- > import Data.Tyro -- > import Data.Aeson (decode) -- > import qualified Data.ByteString.Lazy as B -- > -- > json = "{\"key1\":[{\"key2\":41},{\"key2\":42}]}" :: B.ByteString -- > -- > -- Extract [41, 42] inside the Tyro types -- > parsed = decode json :: Maybe ("key1" >%> List ("key2" >%> Extract Integer)) -- > -- > -- We can dispose of the types using unwrap: 'values' will have the value -- > -- Just [41, 42] -- > values :: Maybe [Integer] -- > values = fmap unwrap parsed -- $value_example -- (Experimental!) -- The value level interface allows a piece of the JSON object to be extracted -- in a similar way to most dynamically typed languages. -- -- > {-# LANGUAGE OverloadedStrings #-} -- > import Data.Tyro -- > -- > json = "{\"key1\": {\"key2\" : [41, 42]}}" :: B.ByteString -- > -- > -- Extract [41, 42] inside the JSON -- > parsed = json %%> "key1" >%> "key2" >%> extract :: Maybe [Integer] -- -- Not the overloaded strings extension in the above is only used to define -- the 'json' 'ByteString'..