packages feed

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 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