highjson 0.3.0.0 → 0.4.0.0
raw patch · 5 files changed
+282/−79 lines, 5 filesdep ~hvectPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: hvect
API changes (from Hackage documentation)
- Data.HighJson: BodySpecRecord :: !(RecordSpec a as) -> BodySpec a as
- Data.HighJson: BodySpecSum :: !(SumSpec a as) -> BodySpec a as
+ Data.HighJson: (:&) :: a -> b -> (:&) a b
+ Data.HighJson: (@->) :: Text -> Prism' t () -> EnumOption t
+ Data.HighJson: EnumOption :: !Text -> !(Prism' t ()) -> EnumOption t
+ Data.HighJson: EnumSpec :: [EnumOption a] -> EnumSpec a
+ Data.HighJson: PhantomEnumContainer :: [EnumOption t] -> PhantomEnumContainer t
+ Data.HighJson: SpecEnum :: SpecType
+ Data.HighJson: SpecRecord :: SpecType
+ Data.HighJson: SpecSum :: SpecType
+ Data.HighJson: [BodySpecEnum] :: !(EnumSpec a) -> BodySpec SpecEnum a as
+ Data.HighJson: [BodySpecRecord] :: !(RecordSpec a as) -> BodySpec SpecRecord a as
+ Data.HighJson: [BodySpecSum] :: !(SumSpec a as) -> BodySpec SpecSum a as
+ Data.HighJson: [eo_jsonKey] :: EnumOption t -> !Text
+ Data.HighJson: [eo_prism] :: EnumOption t -> !(Prism' t ())
+ Data.HighJson: [es_options] :: EnumSpec a -> [EnumOption a]
+ Data.HighJson: [unPhantomEnumContainer] :: PhantomEnumContainer t -> [EnumOption t]
+ Data.HighJson: class CombinableContainer t
+ Data.HighJson: class IsDataSpec t where type DFields t :: [*] type DType t type DContainer t :: * -> [*] -> * where {
+ Data.HighJson: combineContainer :: CombinableContainer t => t a (as :: [*]) -> t a (bs :: [*]) -> t a (Append as bs)
+ Data.HighJson: compileRec :: IsDataSpec t => t -> (DContainer t) (DType t) (DFields t)
+ Data.HighJson: data (:&) a b
+ Data.HighJson: data EnumOption t
+ Data.HighJson: data EnumSpec a
+ Data.HighJson: data SpecType
+ Data.HighJson: enumOpt :: Text -> Prism' t () -> EnumOption t
+ Data.HighJson: enumSpec :: (IsDataSpec q, DContainer q ~ PhantomEnumContainer) => Text -> Maybe Text -> q -> EnumTypeSpec (DType q) (DFields q)
+ Data.HighJson: instance (Data.HighJson.IsDataSpec x, Data.HighJson.IsDataSpec y, Data.HighJson.DType x ~ Data.HighJson.DType y, Data.HighJson.DContainer x ~ Data.HighJson.DContainer y, Data.HighJson.CombinableContainer (Data.HighJson.DContainer x)) => Data.HighJson.IsDataSpec (x Data.HighJson.:& y)
+ Data.HighJson: instance (GHC.Base.Monoid a, GHC.Base.Monoid b) => GHC.Base.Monoid (a Data.HighJson.:& b)
+ Data.HighJson: instance (GHC.Classes.Eq b, GHC.Classes.Eq a) => GHC.Classes.Eq (a Data.HighJson.:& b)
+ Data.HighJson: instance (GHC.Enum.Bounded b, GHC.Enum.Bounded a) => GHC.Enum.Bounded (a Data.HighJson.:& b)
+ Data.HighJson: instance (GHC.Show.Show b, GHC.Show.Show a) => GHC.Show.Show (a Data.HighJson.:& b)
+ Data.HighJson: instance Data.Foldable.Foldable ((Data.HighJson.:&) a)
+ Data.HighJson: instance Data.HighJson.CombinableContainer Data.HighJson.PhantomEnumContainer
+ Data.HighJson: instance Data.HighJson.CombinableContainer Data.HighJson.Types.RecordFields
+ Data.HighJson: instance Data.HighJson.CombinableContainer Data.HighJson.Types.SumOptions
+ Data.HighJson: instance Data.HighJson.IsDataSpec (Data.HighJson.Types.EnumOption t)
+ Data.HighJson: instance Data.HighJson.IsDataSpec (Data.HighJson.Types.RecordField t f)
+ Data.HighJson: instance Data.HighJson.IsDataSpec (Data.HighJson.Types.SumOption t f)
+ Data.HighJson: instance Data.Traversable.Traversable ((Data.HighJson.:&) a)
+ Data.HighJson: instance GHC.Base.Functor ((Data.HighJson.:&) a)
+ Data.HighJson: newtype PhantomEnumContainer t (ts :: [*])
+ Data.HighJson: type EnumTypeSpec t flds = HighSpec t SpecEnum flds
+ Data.HighJson: type RecordTypeSpec t flds = HighSpec t SpecRecord flds
+ Data.HighJson: type SumTypeSpec t flds = HighSpec t SpecSum flds
+ Data.HighJson: type family DContainer t :: * -> [*] -> *;
+ Data.HighJson: }
- Data.HighJson: HighSpec :: !Text -> !(Maybe Text) -> !(BodySpec a as) -> HighSpec a as
+ Data.HighJson: HighSpec :: !Text -> !(Maybe Text) -> !(BodySpec ty a as) -> HighSpec a as
- Data.HighJson: [hs_bodySpec] :: HighSpec a as -> !(BodySpec a as)
+ Data.HighJson: [hs_bodySpec] :: HighSpec a as -> !(BodySpec ty a as)
- Data.HighJson: data BodySpec a as
+ Data.HighJson: data BodySpec ty a as
- Data.HighJson: data HighSpec a as
+ Data.HighJson: data HighSpec a (ty :: SpecType) as
- Data.HighJson: jsonEncoder :: AllHave ToJSON as => HighSpec a as -> a -> Encoding
+ Data.HighJson: jsonEncoder :: AllHave ToJSON as => HighSpec a ty as -> a -> Encoding
- Data.HighJson: jsonParser :: AllHave FromJSON as => HighSpec a as -> Value -> Parser a
+ Data.HighJson: jsonParser :: AllHave FromJSON as => HighSpec a ty as -> Value -> Parser a
- Data.HighJson: jsonSerializer :: AllHave ToJSON as => HighSpec a as -> a -> Value
+ Data.HighJson: jsonSerializer :: AllHave ToJSON as => HighSpec a ty as -> a -> Value
- Data.HighJson: recSpec :: Text -> Maybe Text -> HVectElim flds t -> RecordFields t flds -> HighSpec t flds
+ Data.HighJson: recSpec :: (IsDataSpec q, DContainer q ~ RecordFields) => Text -> Maybe Text -> HVectElim (DFields q) (DType q) -> q -> RecordTypeSpec (DType q) (DFields q)
- Data.HighJson: sumSpec :: Text -> Maybe Text -> SumOptions t flds -> HighSpec t flds
+ Data.HighJson: sumSpec :: (IsDataSpec q, DContainer q ~ SumOptions) => Text -> Maybe Text -> q -> SumTypeSpec (DType q) (DFields q)
Files
- bench/Twitter.hs +28/−32
- highjson.cabal +2/−2
- src/Data/HighJson.hs +141/−10
- src/Data/HighJson/Types.hs +61/−18
- test/Data/JsonSpec.hs +50/−17
bench/Twitter.hs view
@@ -23,11 +23,10 @@ result_type :: Text } deriving (Eq, Show, Typeable, Data, Generic) -metadataSpec :: HighSpec Metadata '[Text]+metadataSpec :: RecordTypeSpec Metadata '[Text] metadataSpec = recSpec "MetaData" Nothing Metadata $ "result_type" .= result_type- :+: RFEmpty instance FromJSON Metadata where parseJSON = jsonParser metadataSpec@@ -49,12 +48,11 @@ , coordinates :: (Double, Double) } deriving (Eq, Show, Typeable, Data, Generic) -geoSpec :: HighSpec Geo '[Text, (Double, Double)]+geoSpec :: RecordTypeSpec Geo '[Text, (Double, Double)] geoSpec = recSpec "Geo" Nothing Geo $ "type_" .= type_- :+: "coordinates" .= coordinates- :+: RFEmpty+ :& "coordinates" .= coordinates instance FromJSON Geo where parseJSON = jsonParser geoSpec@@ -89,25 +87,24 @@ , source :: Text } deriving (Show, Typeable, Data, Generic) -storySpec :: HighSpec Story '[Text, Text, Text, Text, Text, Metadata, Maybe Int64, Text, Int64,+storySpec :: RecordTypeSpec Story '[Text, Text, Text, Text, Text, Metadata, Maybe Int64, Text, Int64, Int64, Maybe Geo, Text, Maybe Text, Text] storySpec = recSpec "Story" Nothing Story $ "from_user_id_str" .= from_user_id_str- :+: "profile_image_url" .= profile_image_url- :+: "created_at" .= created_at- :+: "from_user" .= from_user- :+: "id_str" .= id_str- :+: "metadata" .= metadata- :+: "to_user_id" .= to_user_id- :+: "text" .= text- :+: "id" .= id- :+: "from_user_id" .= from_user_id- :+: "geo" .= geo- :+: "iso_language_code" .= iso_language_code- :+: "to_user_id_str" .= to_user_id_str- :+: "source" .= source- :+: RFEmpty+ :& "profile_image_url" .= profile_image_url+ :& "created_at" .= created_at+ :& "from_user" .= from_user+ :& "id_str" .= id_str+ :& "metadata" .= metadata+ :& "to_user_id" .= to_user_id+ :& "text" .= text+ :& "id" .= id+ :& "from_user_id" .= from_user_id+ :& "geo" .= geo+ :& "iso_language_code" .= iso_language_code+ :& "to_user_id_str" .= to_user_id_str+ :& "source" .= source instance FromJSON Story where parseJSON = jsonParser storySpec@@ -150,22 +147,21 @@ , query :: Text } deriving (Show, Typeable, Data, Generic) -resultSpec :: HighSpec Result '[[Story], Int64, Int64, Text, Text, Int, Int, Double, Text, Text,+resultSpec :: RecordTypeSpec Result '[[Story], Int64, Int64, Text, Text, Int, Int, Double, Text, Text, Text] resultSpec = recSpec "Result" Nothing Result $ "results" .= results- :+: "max_id" .= max_id- :+: "since_id" .= since_id- :+: "refresh_url" .= refresh_url- :+: "next_page" .= next_page- :+: "results_per_page" .= results_per_page- :+: "page" .= page- :+: "completed_in" .= completed_in- :+: "since_id_str" .= since_id_str- :+: "max_id_str" .= max_id_str- :+: "query" .= query- :+: RFEmpty+ :& "max_id" .= max_id+ :& "since_id" .= since_id+ :& "refresh_url" .= refresh_url+ :& "next_page" .= next_page+ :& "results_per_page" .= results_per_page+ :& "page" .= page+ :& "completed_in" .= completed_in+ :& "since_id_str" .= since_id_str+ :& "max_id_str" .= max_id_str+ :& "query" .= query instance FromJSON Result where parseJSON = jsonParser resultSpec
highjson.cabal view
@@ -1,5 +1,5 @@ name: highjson-version: 0.3.0.0+version: 0.4.0.0 synopsis: Spec based JSON parsing/serialisation description: Low boilerplate, easy to use and very fast JSON serialisation and parsing without generics or TemplateHaskell homepage: https://github.com/agrafix/highjson@@ -26,7 +26,7 @@ build-depends: aeson >= 1.0, base >=4.8 && <5,- hvect >=0.2,+ hvect >=0.4, text >=1.1.1, lens hs-source-dirs: src
src/Data/HighJson.hs view
@@ -1,20 +1,33 @@+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE DataKinds #-} module Data.HighJson ( -- * A json specification for any type- HighSpec(..)+ HighSpec(..), SpecType(..) -- * Construct specifications for records- , recSpec, RecordFields(..), reqField, (.=), optField, (.=?)+ , recSpec, RecordTypeSpec, reqField, (.=), optField, (.=?) -- * Construct specifications for sum types- , sumSpec, SumOptions(..), sumOpt, (.->)+ , sumSpec, SumTypeSpec, sumOpt, (.->)+ -- * Construct specifications for enum types+ , enumSpec, EnumTypeSpec, enumOpt, (@->)+ -- * Shared between specifications for simplicity+ , IsDataSpec(..), (:&)(..) -- * Generate json serializers/encoders and parsers from specs , jsonSerializer, jsonEncoder, jsonParser -- * Specification structures , BodySpec(..)- , RecordField(..), RecordSpec(..)- , SumOption(..), SumSpec(..)+ , RecordField(..), RecordSpec(..), RecordFields(..)+ , SumOption(..), SumSpec(..), SumOptions(..)+ , EnumOption(..), EnumSpec(..) -- * Aeson reexports , ToJSON(..), FromJSON(..)+ -- * Implementation detail structures+ , PhantomEnumContainer(..), CombinableContainer(..) ) where @@ -22,9 +35,79 @@ import Control.Lens hiding ((.=)) import Data.Aeson ((.:), (.:?), FromJSON(..), ToJSON(..))+import Data.Typeable import qualified Data.HVect as HV import qualified Data.Text as T +-- | Combination of two local specifications. For records, these are fields, for sum types and enums+-- these are the options.+data a :& b+ = a :& b+ deriving (Typeable, Eq, Show, Functor, Traversable, Foldable, Bounded)+infixr 8 :&++instance (Monoid a, Monoid b) => Monoid (a :& b) where+ mempty = mempty :& mempty+ (a :& b) `mappend` (a' :& b') = (a `mappend` a') :& (b `mappend` b')++-- | A monoidal type class that respects type level lists associated to the bodies+class CombinableContainer t where+ combineContainer :: t a (as :: [*]) -> t a (bs :: [*]) -> t a (HV.Append as bs)++instance CombinableContainer RecordFields where+ combineContainer = recAppend++instance CombinableContainer SumOptions where+ combineContainer = sumAppend++instance CombinableContainer PhantomEnumContainer where+ combineContainer (PhantomEnumContainer x) (PhantomEnumContainer y) =+ PhantomEnumContainer $ x ++ y++-- | A type class that allows a unified notation for records and sum types. Build specifications+-- using '(:&)' and '(.=)', '(.=?)', '(.->)' or '(@->)'+class IsDataSpec t where+ type DFields t :: [*]+ type DType t+ type DContainer t :: * -> [*] -> *+ compileRec :: t -> (DContainer t) (DType t) (DFields t)++instance IsDataSpec (RecordField t f) where+ type DFields (RecordField t f) = (f ': '[])+ type DType (RecordField t f) = t+ type DContainer (RecordField t f) = RecordFields+ compileRec x = x :+: RFEmpty++instance IsDataSpec (SumOption t f) where+ type DFields (SumOption t f) = (f ': '[])+ type DType (SumOption t f) = t+ type DContainer (SumOption t f) = SumOptions+ compileRec x = x :|: SOEmpty++newtype PhantomEnumContainer t (ts :: [*])+ = PhantomEnumContainer { unPhantomEnumContainer :: [EnumOption t] }++instance IsDataSpec (EnumOption t) where+ type DFields (EnumOption t) = (() ': '[])+ type DType (EnumOption t) = t+ type DContainer (EnumOption t) = PhantomEnumContainer+ compileRec x = PhantomEnumContainer [x]++instance (IsDataSpec x, IsDataSpec y, DType x ~ DType y, DContainer x ~ DContainer y, CombinableContainer (DContainer x)) => IsDataSpec (x :& y) where+ type DFields (x :& y) = HV.Append (DFields x) (DFields y)+ type DType (x :& y) = DType x+ type DContainer (x :& y) = DContainer x+ compileRec (x :& y) = combineContainer (compileRec x) (compileRec y)++recAppend :: RecordFields t as -> RecordFields t bs -> RecordFields t (HV.Append as bs)+recAppend RFEmpty bs = bs+recAppend (a :+: as) bs = a :+: (as `recAppend` bs)++sumAppend :: SumOptions t as -> SumOptions t bs -> SumOptions t (HV.Append as bs)+sumAppend SOEmpty bs = bs+sumAppend (a :|: as) bs = a :|: (as `sumAppend` bs)++-- | A required json field. The key must be present in the json. reqField :: FromJSON f => T.Text -> (t -> f) -> RecordField t f reqField jsonKey g = RecordField@@ -34,9 +117,11 @@ , rf_get = g } +-- | Alias for 'reqField' (.=) :: FromJSON f => T.Text -> (t -> f) -> RecordField t f jsonKey .= reader = reqField jsonKey reader +-- | An optional json field. optField :: FromJSON f => T.Text -> (t -> Maybe f) -> RecordField t (Maybe f) optField jsonKey g = RecordField@@ -46,9 +131,11 @@ , rf_get = g } +-- | Alias for 'optField' (.=?) :: FromJSON f => T.Text -> (t -> Maybe f) -> RecordField t (Maybe f) name .=? reader = optField name reader +-- | An option of a sum type sumOpt :: T.Text -> Prism' t o -> SumOption t o sumOpt jsonKey p = SumOption@@ -56,22 +143,66 @@ , so_prism = p } +-- | Alias for 'sumOpt' (.->) :: T.Text -> Prism' t o -> SumOption t o jsonKey .-> p = sumOpt jsonKey p -recSpec :: T.Text -> Maybe T.Text -> HV.HVectElim flds t -> RecordFields t flds -> HighSpec t flds+-- | An option of a classic enum+enumOpt :: T.Text -> Prism' t () -> EnumOption t+enumOpt jsonKey p =+ EnumOption+ { eo_jsonKey = jsonKey+ , eo_prism = p+ }++-- | Alias for 'enumOpt'+(@->) :: T.Text -> Prism' t () -> EnumOption t+jsonKey @-> p = enumOpt jsonKey p++-- | A specification for a record+type RecordTypeSpec t flds = HighSpec t 'SpecRecord flds++-- | The specification for a record. Contains a name, an optional description,+-- the constructor and a description how to parse and serialize fields respecting+-- a given json key.+recSpec ::+ (IsDataSpec q, DContainer q ~ RecordFields)+ => T.Text -> Maybe T.Text -> HV.HVectElim (DFields q) (DType q)+ -> q+ -> RecordTypeSpec (DType q) (DFields q) recSpec name mDesc mk fields = HighSpec { hs_name = name , hs_description = mDesc- , hs_bodySpec =- BodySpecRecord $ RecordSpec (HV.uncurry mk) fields+ , hs_bodySpec = BodySpecRecord $ RecordSpec (HV.uncurry mk) (compileRec fields) } -sumSpec :: T.Text -> Maybe T.Text -> SumOptions t flds -> HighSpec t flds+-- | A specification for an arbitrary sum type+type SumTypeSpec t flds = HighSpec t 'SpecSum flds++-- | The specification for a sum type. Contains a name, an optional description+-- and a mapping from all constructor (prims) to their respective json fields+sumSpec ::+ (IsDataSpec q, DContainer q ~ SumOptions)+ => T.Text -> Maybe T.Text -> q -> SumTypeSpec (DType q) (DFields q) sumSpec name mDesc opts = HighSpec { hs_name = name , hs_description = mDesc- , hs_bodySpec = BodySpecSum $ SumSpec opts+ , hs_bodySpec = BodySpecSum $ SumSpec (compileRec opts)+ }++-- | A specification for a classic enum+type EnumTypeSpec t flds = HighSpec t 'SpecEnum flds++-- | The specification for a classic enum type. Contains a name, an optional description+-- and a mapping from all constructors to ther counterpart json string names.+enumSpec ::+ (IsDataSpec q, DContainer q ~ PhantomEnumContainer)+ => T.Text -> Maybe T.Text -> q -> EnumTypeSpec (DType q) (DFields q)+enumSpec name mDesc opts =+ HighSpec+ { hs_name = name+ , hs_description = mDesc+ , hs_bodySpec = BodySpecEnum $ EnumSpec (unPhantomEnumContainer $ compileRec opts) }
src/Data/HighJson/Types.hs view
@@ -1,13 +1,15 @@+{-# LANGUAGE KindSignatures #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} module Data.HighJson.Types- ( HighSpec(..)+ ( HighSpec(..), SpecType(..) , BodySpec(..) , RecordFields(..), RecordField(..), RecordSpec(..) , SumOptions(..), SumOption(..), SumSpec(..)+ , EnumOption(..), EnumSpec(..) , jsonSerializer, jsonEncoder, jsonParser ) where@@ -21,16 +23,22 @@ import Data.Monoid import qualified Data.Text as T -data HighSpec a as+data SpecType+ = SpecRecord+ | SpecSum+ | SpecEnum++data HighSpec a (ty :: SpecType) as = HighSpec { hs_name :: !T.Text , hs_description :: !(Maybe T.Text)- , hs_bodySpec :: !(BodySpec a as)+ , hs_bodySpec :: !(BodySpec ty a as) } -data BodySpec a as- = BodySpecRecord !(RecordSpec a as)- | BodySpecSum !(SumSpec a as)+data BodySpec ty a as where+ BodySpecRecord :: !(RecordSpec a as) -> BodySpec 'SpecRecord a as+ BodySpecSum :: !(SumSpec a as) -> BodySpec 'SpecSum a as+ BodySpecEnum :: !(EnumSpec a) -> BodySpec 'SpecEnum a as data RecordFields t fs where RFEmpty :: RecordFields t '[]@@ -69,20 +77,41 @@ { ss_options :: SumOptions a os } -jsonSerializer :: AllHave ToJSON as => HighSpec a as -> a -> Value+data EnumOption t+ = EnumOption+ { eo_jsonKey :: !T.Text+ , eo_prism :: !(Prism' t ())+ }++data EnumSpec a+ = EnumSpec+ { es_options :: [EnumOption a]+ }++jsonSerializer :: AllHave ToJSON as => HighSpec a ty as -> a -> Value jsonSerializer hs val =- object $ fst $ case hs_bodySpec hs of- BodySpecSum s -> jsonSerSum s val- BodySpecRecord r -> jsonSerRec r val+ BodySpecSum s -> object $ fst $ jsonSerSum s val+ BodySpecRecord r -> object $ fst $ jsonSerRec r val+ BodySpecEnum e -> toJSON $ jsonSerEnum e val -jsonEncoder :: AllHave ToJSON as => HighSpec a as -> a -> Encoding+jsonEncoder :: AllHave ToJSON as => HighSpec a ty as -> a -> Encoding jsonEncoder hs val =- pairs $ snd $ case hs_bodySpec hs of- BodySpecSum s -> jsonSerSum s val- BodySpecRecord r -> jsonSerRec r val+ BodySpecSum s -> pairs $ snd $ jsonSerSum s val+ BodySpecRecord r -> pairs $ snd $ jsonSerRec r val+ BodySpecEnum e -> toEncoding $ jsonSerEnum e val +jsonSerEnum :: EnumSpec a -> a -> T.Text+jsonSerEnum (EnumSpec opts) val =+ loop opts+ where+ loop [] = error "Empty enum spec"+ loop (x : xs) =+ case val ^? eo_prism x of+ Just () -> eo_jsonKey x+ Nothing -> loop xs+ jsonSerSum :: forall a as. AllHave ToJSON as => SumSpec a as -> a -> ([Pair], Series) jsonSerSum (SumSpec sopts) val = loop sopts@@ -115,12 +144,15 @@ encoder = rf_jsonKey f .= rf_get f val in loop fs ((pair : ps), encoder <> encoding) -jsonParser :: AllHave FromJSON as => HighSpec a as -> Value -> Parser a+jsonParser :: AllHave FromJSON as => HighSpec a ty as -> Value -> Parser a jsonParser hs =- withObject (T.unpack $ hs_name hs) $ \obj -> case hs_bodySpec hs of- BodySpecRecord r -> jsonParserRecord r obj- BodySpecSum s -> jsonParserSum (hs_name hs) s obj+ BodySpecRecord r ->+ withObject (T.unpack $ hs_name hs) (jsonParserRecord r)+ BodySpecSum s ->+ withObject (T.unpack $ hs_name hs) (jsonParserSum (hs_name hs) s)+ BodySpecEnum e ->+ withText (T.unpack $ hs_name hs) (jsonParserEnum (hs_name hs) e) jsonParserRecord :: forall a as. AllHave FromJSON as => RecordSpec a as -> Object -> Parser a jsonParserRecord (RecordSpec mk rflds) obj =@@ -151,3 +183,14 @@ let parse = liftM (so_prism o #) $ obj .: so_jsonKey o in parse <|> loop os+++jsonParserEnum :: Monad m => T.Text -> EnumSpec a -> T.Text -> m a+jsonParserEnum name (EnumSpec sopts) t =+ loop sopts+ where+ loop [] = fail $ "Failed to parse as " ++ T.unpack name+ loop (x : xs) =+ if t == eo_jsonKey x+ then pure (eo_prism x # ())+ else loop xs
test/Data/JsonSpec.hs view
@@ -22,15 +22,14 @@ , sd_maybe :: Maybe Int } deriving (Show, Eq) -someDummySpec :: HighSpec SomeDummy '[Int, Bool, T.Text, Either Bool T.Text, Maybe Int]+someDummySpec :: RecordTypeSpec SomeDummy '[Int, Bool, T.Text, Either Bool T.Text, Maybe Int] someDummySpec = recSpec "Some Dummy" Nothing SomeDummy $ "int" .= sd_int- :+: "bool" .= sd_bool- :+: "text" .= sd_text- :+: "either" .= sd_either- :+: "maybe" .= sd_maybe- :+: RFEmpty+ :& "bool" .= sd_bool+ :& "text" .= sd_text+ :& "either" .= sd_either+ :& "maybe" .= sd_maybe instance ToJSON SomeDummy where toJSON = jsonSerializer someDummySpec@@ -65,12 +64,11 @@ , sn_obj :: Maybe SomeNested } deriving (Show, Eq, Typeable) -someNestedSpec :: HighSpec SomeNested '[[SomeNested], Maybe SomeNested]+someNestedSpec :: RecordTypeSpec SomeNested '[[SomeNested], Maybe SomeNested] someNestedSpec = recSpec "Nested" Nothing SomeNested $ "list" .= sn_list- :+: "obj" .=? sn_obj- :+: RFEmpty+ :& "obj" .=? sn_obj instance ToJSON SomeNested where toJSON = jsonSerializer someNestedSpec@@ -88,14 +86,13 @@ makePrisms ''SomeSumType -someSumSpec :: HighSpec SomeSumType '[SomeDummy, Int, Bool, ()]+someSumSpec :: SumTypeSpec SomeSumType '[SomeDummy, Int, Bool, ()] someSumSpec = sumSpec "SomeSum" Nothing $ "some_dummy" .-> _SomeDummyT- :|: "some_int" .-> _SomeInt- :|: "some_bool" .-> _SomeBool- :|: "some_enum" .-> _SomeEnum- :|: SOEmpty+ :& "some_int" .-> _SomeInt+ :& "some_bool" .-> _SomeBool+ :& "some_enum" .-> _SomeEnum instance ToJSON SomeSumType where toJSON = jsonSerializer someSumSpec@@ -118,12 +115,11 @@ , pt_val :: T.Text } deriving (Show, Eq, Typeable) -paramTypeSpec :: FromJSON a => HighSpec (ParamType a) '[a, T.Text]+paramTypeSpec :: FromJSON a => RecordTypeSpec (ParamType a) '[a, T.Text] paramTypeSpec = recSpec "Some Param" Nothing ParamType $ "key" .= pt_key- :+: "val" .= pt_val- :+: RFEmpty+ :& "val" .= pt_val instance (ToJSON a, FromJSON a) => ToJSON (ParamType a) where toJSON = jsonSerializer paramTypeSpec@@ -136,6 +132,39 @@ arbitrary = ParamType <$> arbitrary <*> (unSomeText <$> arbitrary) +data SomeEnumType+ = SomeEnumA+ | SomeEnumB+ | SomeEnumC+ | SomeEnumD+ deriving (Show, Eq, Typeable)++makePrisms ''SomeEnumType++someEnumSpec :: EnumTypeSpec SomeEnumType '[(), (), (), ()]+someEnumSpec =+ enumSpec "SomeSum" Nothing $+ "a" @-> _SomeEnumA+ :& "b" @-> _SomeEnumB+ :& "c" @-> _SomeEnumC+ :& "d" @-> _SomeEnumD++instance ToJSON SomeEnumType where+ toJSON = jsonSerializer someEnumSpec+ toEncoding = jsonEncoder someEnumSpec++instance FromJSON SomeEnumType where+ parseJSON = jsonParser someEnumSpec++instance Arbitrary SomeEnumType where+ arbitrary =+ oneof+ [ pure SomeEnumA+ , pure SomeEnumB+ , pure SomeEnumC+ , pure SomeEnumD+ ]+ spec :: Spec spec = describe "Parser and Serialiser" $@@ -148,8 +177,12 @@ (eitherDecode . encode) t == Right (t :: SomeDummy) it "Handles arbitrary custom sum types correctly" $ property $ \t -> (eitherDecode . encode) t == Right (t :: SomeSumType)+ it "Handles arbitrary custom enum types correctly" $ property $ \t ->+ (eitherDecode . encode) t == Right (t :: SomeEnumType) it "Handles arbitrary custom parametrized types correctly" $ property $ \t -> (eitherDecode . encode) t == Right (t :: ParamType SomeSumType)+ it "Handles arbitrary custom parametrized types correctly 2" $ property $ \t ->+ (eitherDecode . encode) t == Right (t :: ParamType SomeEnumType) it "Handles nested types correctly" $ do let nested = SomeNested [SomeNested [] Nothing] (Just $ SomeNested [] Nothing) eitherDecode (encode nested) `shouldBe` Right nested