tyro 0.1.1.1 → 0.2.0.0
raw patch · 4 files changed
+185/−47 lines, 4 filesdep +bytestringdep +reflectionPVP ok
version bump matches the API change (PVP)
Dependencies added: bytestring, reflection
API changes (from Hackage documentation)
- Data.Tyro: type Parse a = JSBranch '[] a
+ Data.Tyro: (%%>) :: (FromJSON a) => ByteString -> Tyro -> Maybe a
+ Data.Tyro: (>%>) :: String -> Tyro -> Tyro
+ Data.Tyro: data Tyro
+ Data.Tyro: extract :: Tyro
+ Data.Tyro: infixl 8 %%>
+ Data.Tyro: infixr 9 >%>
+ Data.Tyro: instance GHC.Classes.Eq Data.Tyro.Tyro
+ Data.Tyro: instance GHC.Show.Show Data.Tyro.Tyro
+ Data.Tyro: type Extract a = JSBranch '[] a
Files
- README.md +18/−4
- src/Data/Tyro.hs +117/−37
- test/Spec.hs +47/−5
- tyro.cabal +3/−1
README.md view
@@ -2,8 +2,10 @@ `tyro` is a dependently type JSON parsing library, that provides a quick way to create JSON parsers by deriving them from a type level description of the position of the value to be obtained. -## Example+## Examples +### Type driven interface+ ```Haskell {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DataKinds #-}@@ -11,14 +13,26 @@ import Data.Tyro import Data.Aeson (decode)-import Data.Text (Text)+import qualified Data.ByteString.Lazy as B -json = "{\"key1\":[{\"key2\":41},{\"key2\":42}]}" :: Text+json = "{\"key1\":[{\"key2\":41},{\"key2\":42}]}" :: B.ByteString -- Extract [41, 42] inside the Tyro types-parsed = decode json :: Maybe ("key1" |>| List ("key2" |>| Parse Integer))+parsed = decode json :: Maybe ("key1" >%> List ("key2" >%> Extract Integer)) -- We can dispose of the types using unwrap values :: Maybe [Integer] values = fmap unwrap parsed+```++### Value driven interface++```Haskell+{-# 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] ```
src/Data/Tyro.hs view
@@ -12,14 +12,22 @@ {-# OPTIONS_GHC -fno-warn-redundant-constraints #-} module Data.Tyro ( -- * Introduction- -- $example+ -- $introduction -- * Building types- Parse-, type( |>| )+ -- $typed_example+ Extract+, type( >%> ) , List , unwrap +-- * Value level API+-- $value_example+, Tyro+, extract+, (>%>)+, (%%>)+ -- * Internal types , JSBranch ) where @@ -27,6 +35,8 @@ 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.Prelude.List (Sing(SNil, SCons)) import Data.Singletons.TypeLits ( Symbol, SSymbol, KnownSymbol@@ -37,47 +47,73 @@ import Lib.Prelude --- $example--- A small (artificial) example demonstrating how to use the types defined here.------ > {-# LANGUAGE OverloadedStrings #-}--- > {-# LANGUAGE DataKinds #-}--- > {-# LANGUAGE TypeOperators #-}--- > import Data.Tyro--- > import Data.Aeson (decode)--- > import Data.Text (Text)--- >--- > json = "{\"key1\":[{\"key2\":41},{\"key2\":42}]}" :: Text--- >--- > -- Extract [41, 42] inside the Tyro types--- > parsed = decode json :: Maybe ("key1" |>| List ("key2" |>| Parse Integer))--- >--- > -- We can dispose of the types using unwrap: 'values' will have the value--- > -- Just [41, 42]--- > values :: Maybe [Integer]--- > values = fmap unwrap parsed -- -------------------------------------------------------------------------------- -- Type level API using a type family to mirror JSON structure -------------------------------------------------------------------------------- --- | @Parse a@ represents trying to parse JSON to an @a@.-type Parse a = JSBranch '[] a+-- | @Extract a@ represents trying to parse JSON to an @a@.+type Extract a = JSBranch '[] a --- | The type operator '|>|' provides a way of describing how to walk+-- | 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 (x ': xs) a+type family (x :: Symbol) >%> (b :: *) :: *+type instance (x :: Symbol) >%> JSBranch xs a = JSBranch (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) = Parse [JSBranch xs a]+type instance List (JSBranch xs a) = Extract [JSBranch 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 :: [Symbol] -> * where+ Take :: TyroProxy '[]+ Key :: TyroProxy s -> TyroProxy (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 unwrap $ 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 (s ': xs)+ extend t _ = Key t+infixl 8 %%>++++-------------------------------------------------------------------------------- -- Basic dependent structure -------------------------------------------------------------------------------- @@ -98,22 +134,66 @@ 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 = parseSing sing+ parseJSON = parse sing where- parseSing :: (A.FromJSON a) => Sing xs -> A.Value -> Parser (JSBranch xs a)- parseSing s o = case s of+ parse :: (A.FromJSON a) => Sing xs -> A.Value -> Parser (JSBranch xs a)+ parse s o = case s of SNil -> JSNil <$> A.parseJSON o x `SCons` xs -> case o of- A.Object v -> let key = pack (reflectSym x) in- JSCons <$> (v .: key >>= parseSing xs)+ A.Object v -> let key = pack (reflectSymbol x) in+ JSCons <$> (v .: key >>= parse xs) _ -> empty -- | 'reflectSym' reflects a type level symbol into a value level string-reflectSym :: SSymbol s -> String-reflectSym s = withKnownSymbol s $ proxySym s Proxy+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+-- 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'..
test/Spec.hs view
@@ -16,19 +16,25 @@ main = defaultMain tests tests :: [ TF.Test ]-tests = [ genericJSONParserTests ]+tests = [ typeLevelJSONParserTests+ , valueLevelJSONParserTests ] -genericJSONParserTests :: TF.Test-genericJSONParserTests = testGroup "\nGeneric JSON Parser tests" . hUnitTestToTests $+typeLevelJSONParserTests :: TF.Test+typeLevelJSONParserTests = testGroup "\nType level JSON Parser tests" . hUnitTestToTests $ HU.TestList [ canExtractTextInJSBranch , canExtractIntegerInJSBranch , canExtractListInJSBranch , canExtractListInJSBranchWithTypeFamily ] +valueLevelJSONParserTests :: TF.Test+valueLevelJSONParserTests = testGroup "Value leve JSON Parser tests" . hUnitTestToTests $+ HU.TestList [ canUseValueLevelKeysToParseInteger+ , canUseValueLevelKeysToExtractList+ , canUseOperatorsWithoutBrackets ] ----------------------------------------------------------------------------------- Generic JSON Parser tests+-- Type level JSON Parser tests -------------------------------------------------------------------------------- canExtractTextInJSBranch :: HU.Test@@ -60,6 +66,42 @@ canExtractListInJSBranchWithTypeFamily = "Can extract JSBranch on List with type family" ~: let json = "{\"key1\":[{\"key2\":41},{\"key2\":42}]}" expected = Just [41,42]- decoded = A.decode json :: Maybe ("key1" |>| List ("key2" |>| Parse Integer))+ decoded = A.decode json :: Maybe ("key1" >%> List ("key2" >%> Extract Integer)) in expected @=? (fmap (fmap unwrap) . (fmap unwrap) $ decoded)+++--------------------------------------------------------------------------------+-- Value level parsing tests+--------------------------------------------------------------------------------++canUseValueLevelKeysToParseInteger :: HU.Test+canUseValueLevelKeysToParseInteger =+ "Can extract integer using value level API" ~:+ let json = "{\"key1\":{\"key2\":{\"key3\":42}}}"+ parser = "key1" >%> "key2" >%> "key3" >%> extract+ expected = Just 42+ decoded = json %%> parser :: Maybe Integer+ in+ expected @=? decoded+++canUseValueLevelKeysToExtractList :: HU.Test+canUseValueLevelKeysToExtractList =+ "Can extract list of integers using value level API" ~:+ let json = "{\"key1\": {\"key2\" : [41, 42]}}"+ parser = "key1" >%> "key2" >%> extract+ expected = Just [41,42]+ decoded = json %%> parser :: Maybe [Integer]+ in+ expected @=? decoded+++canUseOperatorsWithoutBrackets :: HU.Test+canUseOperatorsWithoutBrackets =+ "Can use operators without brackets in mainline case" ~:+ let json = "{\"key1\": {\"key2\" : [41, 42]}}"+ expected = Just [41,42]+ decoded = json %%> "key1" >%> "key2" >%> extract :: Maybe [Integer]+ in+ expected @=? decoded
tyro.cabal view
@@ -1,5 +1,5 @@ name: tyro-version: 0.1.1.1+version: 0.2.0.0 synopsis: Type derived JSON parsing using Aeson description: A library for deriving JSON parsers (using Aeson) by indicating@@ -24,7 +24,9 @@ , protolude >= 0.1.6 && < 0.2 , aeson , text+ , bytestring , singletons+ , reflection default-language: Haskell2010 default-extensions: OverloadedStrings, NoImplicitPrelude