avro 0.2.1.1 → 0.3.0.0
raw patch · 17 files changed
+898/−224 lines, 17 filesdep +directorydep ~base
Dependencies added: directory
Dependency ranges changed: base
Files
- avro.cabal +142/−115
- src/Data/Avro.hs +28/−28
- src/Data/Avro/Deriving.hs +115/−27
- src/Data/Avro/FromAvro.hs +6/−4
- src/Data/Avro/JSON.hs +118/−0
- src/Data/Avro/Schema.hs +92/−34
- test/Avro/DefaultsSpec.hs +47/−0
- test/Avro/JSONSpec.hs +95/−0
- test/Avro/THSimpleSpec.hs +12/−10
- test/Avro/THUnionSpec.hs +32/−0
- test/DecodeContainer.hs +25/−0
- test/Example1.hs +84/−0
- test/data/enums-object.json +1/−0
- test/data/maybe.avsc +18/−6
- test/data/unions-object-a.json +16/−0
- test/data/unions-object-b.json +21/−0
- test/data/unions.avsc +46/−0
avro.cabal view
@@ -1,133 +1,160 @@--- Initial avro.cabal generated by cabal init. For further documentation,--- see http://haskell.org/cabal/users-guide/+-- This file has been generated from package.yaml by hpack version 0.20.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: b4a4d7f0413845a9e4530632fc5abaf13e59faf4ba490991cd165e3ad78684be -name: avro-version: 0.2.1.1-synopsis: Avro serialization support for Haskell-description: Avro serialization and deserialization support for Haskell-homepage: https://github.com/GaloisInc/avro.git-license: BSD3-license-file: LICENSE-author: Thomas M. DuBuisson-maintainer: Alexey Raga <alexey.raga@gmail.com>--- copyright:-category: Data-build-type: Simple-extra-source-files: ChangeLog.md, test/data/reused.avsc, test/data/small.avsc, test/data/enums.avsc, test/data/maybe.avsc-cabal-version: >=1.10+name: avro+version: 0.3.0.0+synopsis: Avro serialization support for Haskell+description: Avro serialization and deserialization support for Haskell+category: Data+homepage: https://github.com/GaloisInc/avro.git#readme+bug-reports: https://github.com/GaloisInc/avro.git/issues+author: Thomas M. DuBuisson+maintainer: Alexey Raga <alexey.raga@gmail.com>+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10 +extra-source-files:+ ChangeLog.md+ test/data/enums-object.json+ test/data/enums.avsc+ test/data/maybe.avsc+ test/data/reused.avsc+ test/data/small.avsc+ test/data/unions-object-a.json+ test/data/unions-object-b.json+ test/data/unions.avsc+ source-repository head- type: git+ type: git location: https://github.com/GaloisInc/avro.git flag dev- default: False- manual: True description: Use development GHC flags+ manual: True+ default: False -flag templateHaskell- Description: Build Avro.Deriving, which uses Template Haskell.- Default: True+flag templatehaskell+ description: Build Avro.Deriving, which uses Template Haskell.+ manual: False+ default: True library- exposed-modules: Data.Avro,- Data.Avro.Decode,- Data.Avro.DecodeRaw,- Data.Avro.Deconflict,- Data.Avro.Encode,- Data.Avro.EncodeRaw,- Data.Avro.Schema,- Data.Avro.Types,- Data.Avro.Zag,- Data.Avro.Zig,- Data.Avro.HasAvroSchema,- Data.Avro.FromAvro,- Data.Avro.ToAvro- other-extensions: OverloadedStrings- build-depends: base >=4.8 && <5.0,- aeson,- array,- base16-bytestring,- binary,- bytestring,- containers,- data-binary-ieee754,- entropy,- fail,- hashable,- mtl,- pure-zlib,- scientific,- semigroups,- tagged,- text,- unordered-containers,- vector-- if flag(templateHaskell)- build-depends: template-haskell >= 2.4+ hs-source-dirs:+ src+ other-extensions: OverloadedStrings+ ghc-options: -O2+ build-depends:+ aeson+ , array+ , base >=4.8 && <5.0+ , base16-bytestring+ , binary+ , bytestring+ , containers+ , data-binary-ieee754+ , entropy+ , fail+ , hashable+ , mtl+ , pure-zlib+ , scientific+ , semigroups+ , tagged+ , text+ , unordered-containers+ , vector+ if flag(templatehaskell) other-extensions: TemplateHaskell- exposed-modules: Data.Avro.Deriving- other-modules: Data.Avro.Deriving.NormSchema-- hs-source-dirs: src- default-language: Haskell2010- ghc-options: -O2+ build-depends:+ template-haskell >=2.4 if flag(dev) ghc-options: -Wall -Werror+ exposed-modules:+ Data.Avro+ Data.Avro.Decode+ Data.Avro.DecodeRaw+ Data.Avro.Deconflict+ Data.Avro.Encode+ Data.Avro.Deriving+ Data.Avro.EncodeRaw+ Data.Avro.JSON+ Data.Avro.Schema+ Data.Avro.Types+ Data.Avro.Zag+ Data.Avro.Zig+ Data.Avro.HasAvroSchema+ Data.Avro.FromAvro+ Data.Avro.ToAvro+ other-modules:+ Data.Avro.Deriving.NormSchema+ Paths_avro+ default-language: Haskell2010 test-suite test- type: exitcode-stdio-1.0- default-language: Haskell2010- hs-source-dirs: test- other-modules: Avro.Codec.ArraySpec- , Avro.Codec.BoolSpec- , Avro.Codec.CodecRawSpec- , Avro.Codec.DoubleSpec- , Avro.Codec.FloatSpec- , Avro.Codec.Int64Spec- , Avro.Codec.MaybeSpec- , Avro.Codec.NestedSpec- , Avro.Codec.TextSpec- , Avro.Codec.ZigZagSpec- , Avro.EncodeRawSpec- , Avro.ToAvroSpec-- build-depends: base >=4.6 && < 5- , aeson- , lens-aeson- , array- , avro- , base16-bytestring- , binary- , bytestring- , containers- , entropy- , extra- , fail- , hashable- , hspec- , lens- , mtl- , pure-zlib- , QuickCheck- , scientific- , semigroups- , tagged- , text- , transformers- , unordered-containers- , vector-- if flag(templateHaskell)- build-depends: template-haskell+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs:+ test+ ghc-options: -threaded+ build-depends:+ QuickCheck+ , aeson+ , array+ , avro+ , base >=4.6 && <5+ , base16-bytestring+ , binary+ , bytestring+ , containers+ , directory+ , entropy+ , extra+ , fail+ , hashable+ , hspec+ , lens+ , lens-aeson+ , mtl+ , pure-zlib+ , scientific+ , semigroups+ , tagged+ , text+ , transformers+ , unordered-containers+ , vector+ if flag(templatehaskell) other-extensions: TemplateHaskell- other-modules: Avro.THEnumSpec- , Avro.THReusedSpec- , Avro.THSimpleSpec- , Avro.THEncodeContainerSpec-- main-is: Spec.hs- ghc-options: -threaded+ build-depends:+ template-haskell if flag(dev) ghc-options: -Wall -Werror+ other-modules:+ Avro.Codec.ArraySpec+ Avro.Codec.BoolSpec+ Avro.Codec.CodecRawSpec+ Avro.Codec.DoubleSpec+ Avro.Codec.FloatSpec+ Avro.Codec.Int64Spec+ Avro.Codec.MaybeSpec+ Avro.Codec.NestedSpec+ Avro.Codec.TextSpec+ Avro.Codec.ZigZagSpec+ Avro.DefaultsSpec+ Avro.EncodeRawSpec+ Avro.JSONSpec+ Avro.THEncodeContainerSpec+ Avro.THEnumSpec+ Avro.THReusedSpec+ Avro.THSimpleSpec+ Avro.THUnionSpec+ Avro.ToAvroSpec+ DecodeContainer+ Example1+ Paths_avro+ default-language: Haskell2010
src/Data/Avro.hs view
@@ -1,7 +1,7 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE MultiWayIf #-} -- | Avro encoding and decoding routines. -- -- This library provides a high level interface for encoding (and decoding)@@ -86,34 +86,34 @@ , schemaOf ) where -import Prelude as P-import Control.Arrow (first)-import qualified Data.Avro.Decode as D-import Data.Avro.Deconflict as C-import qualified Data.Avro.Encode as E-import Data.Avro.Schema as S-import Data.Avro.Types as T-import qualified Data.Binary.Get as G-import qualified Data.Binary.Put as P-import qualified Data.ByteString as B-import Data.ByteString.Lazy (ByteString)-import qualified Data.ByteString.Lazy as BL-import Data.Foldable (toList)-import qualified Data.HashMap.Strict as HashMap+import Control.Arrow (first)+import qualified Data.Avro.Decode as D+import Data.Avro.Deconflict as C+import qualified Data.Avro.Encode as E+import Data.Avro.Schema as S+import Data.Avro.Types as T+import qualified Data.Binary.Get as G+import qualified Data.Binary.Put as P+import qualified Data.ByteString as B+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as BL+import Data.Foldable (toList)+import qualified Data.HashMap.Strict as HashMap import Data.Int-import Data.List.NonEmpty (NonEmpty(..))-import qualified Data.Map as Map-import Data.Monoid ((<>))-import Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Lazy as TL+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.Map as Map+import Data.Monoid ((<>)) import Data.Tagged-import qualified Data.Vector as V+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Lazy as TL+import qualified Data.Vector as V import Data.Word+import Prelude as P -import Data.Avro.FromAvro-import Data.Avro.ToAvro-import Data.Avro.HasAvroSchema+import Data.Avro.FromAvro+import Data.Avro.HasAvroSchema+import Data.Avro.ToAvro type Avro a = (FromAvro a, ToAvro a) @@ -167,7 +167,7 @@ decodeContainerBytes bs = case D.decodeContainerWith schemaBytes bs of Right (writerSchema, val) -> val- Left e -> error $ "Could not decode container: " <> e+ Left e -> error $ "Could not decode container: " <> e where schemaBytes sch = do start <- G.bytesRead
src/Data/Avro/Deriving.hs view
@@ -1,6 +1,9 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ViewPatterns #-} module Data.Avro.Deriving ( deriveAvro@@ -9,29 +12,33 @@ ) where -import Control.Monad (join)-import Data.Aeson (eitherDecode)-import Data.Char (isAlphaNum)-import qualified Data.Aeson as J-import Data.Avro hiding (decode, encode)-import Data.Avro.Schema as S-import qualified Data.Avro.Types as AT-import Data.ByteString (ByteString)+import Control.Monad (join)+import Data.Aeson (eitherDecode)+import qualified Data.Aeson as J+import Data.Avro hiding (decode, encode)+import Data.Avro.Schema as S+import qualified Data.Avro.Types as AT+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import Data.Char (isAlphaNum) import Data.Int-import Data.List.NonEmpty (NonEmpty( (:|) ))-import Data.Map (Map)-import Data.Maybe (fromMaybe)-import Data.Semigroup ((<>))-import Language.Haskell.TH as TH+import Data.List.NonEmpty (NonEmpty ((:|)))+import qualified Data.List.NonEmpty as NE+import Data.Map (Map)+import Data.Maybe (fromMaybe)+import Data.Semigroup ((<>))+import Language.Haskell.TH as TH import Language.Haskell.TH.Syntax -import Data.Avro.Deriving.NormSchema+import Data.Avro.Deriving.NormSchema -import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as LBS-import qualified Data.ByteString.Lazy.Char8 as LBSC8-import Data.Text (Text)-import qualified Data.Text as T+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString.Lazy.Char8 as LBSC8+import qualified Data.HashMap.Strict as HM+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Vector as V -- | Derives Avro from a given schema file. -- Generates data types, FromAvro and ToAvro instances.@@ -62,8 +69,8 @@ qAddDependentFile p mbSchema <- runIO $ decodeSchema p case mbSchema of- Left err -> fail $ "Unable to generate AVRO for " <> p <> ": " <> err- Right sch -> pure sch+ Left err -> fail $ "Unable to generate AVRO for " <> p <> ": " <> err+ Right sch -> pure sch genFromAvro :: Schema -> Q [Dec] genFromAvro (S.Enum n _ _ _ _ _) =@@ -145,18 +152,99 @@ schemaDef sname sch = setName sname $ [d| x :: Schema- x = fromMaybe undefined (J.decode (LBSC8.pack $(mkLit (LBSC8.unpack $ J.encode sch))))+ x = $(mkSchema sch) |]+ where mkSchema = \case+ Null -> [e| Null |]+ Boolean -> [e| Boolean |]+ Int -> [e| Int |]+ Long -> [e| Long |]+ Float -> [e| Float |]+ Double -> [e| Double |]+ Bytes -> [e| Bytes |]+ String -> [e| String |]+ Array item -> [e| Array $(mkSchema item) |]+ Map values -> [e| Map $(mkSchema values) |]+ NamedType name -> [e| NamedType $(mkName name) |]+ Record {..} -> [e| Record { name = $(mkName name)+ , namespace = $(mkMaybeText namespace)+ , aliases = $(ListE <$> mapM mkName aliases)+ , doc = $(mkMaybeText doc)+ , order = $(mkOrder order)+ , fields = $(ListE <$> mapM mkField fields)+ }+ |]+ Enum {..} -> [e| mkEnum $(mkName name)+ $(ListE <$> mapM mkName aliases)+ $(mkMaybeText namespace)+ $(mkMaybeText doc)+ $(ListE <$> mapM mkText symbols)+ |]+ Union {..} -> [e| mkUnion $(mkNE options) |]+ Fixed {..} -> [e| Fixed { name = $(mkName name)+ , namespace = $(mkMaybeText namespace)+ , aliases = $(ListE <$> mapM mkName aliases)+ , size = $(litE $ IntegerL $ fromIntegral size)+ }+ |] + mkText text = [e| T.pack $(mkTextLit text) |]++ mkName (TN name) = [e| TN $(mkText name) |]++ mkMaybeText (Just text) = [e| Just $(mkText text) |]+ mkMaybeText Nothing = [e| Nothing |]++ mkOrder (Just Ascending) = [e| Just Ascending |]+ mkOrder (Just Descending) = [e| Just Descending |]+ mkOrder (Just Ignore) = [e| Just Ignore |]+ mkOrder Nothing = [e| Nothing |]++ mkField Field {..} =+ [e| Field { fldName = $(mkText fldName)+ , fldAliases = $(ListE <$> mapM mkText fldAliases)+ , fldDoc = $(mkMaybeText fldDoc)+ , fldOrder = $(mkOrder fldOrder)+ , fldType = $(mkSchema fldType)+ , fldDefault = $(fromMaybe [e|Nothing|] $ mkJust . mkDefaultValue <$> fldDefault)+ }+ |]++ mkJust exp = [e|Just $(exp)|]++ mkDefaultValue = \case+ AT.Null -> [e| AT.Null |]+ AT.Boolean b -> [e| AT.Boolean $(if b then [e|True|] else [e|False|]) |]+ AT.Int n -> [e| AT.Int $(litE $ IntegerL $ fromIntegral n) |]+ AT.Long n -> [e| AT.Long $(litE $ IntegerL $ fromIntegral n) |]+ AT.Float f -> [e| AT.Long $(litE $ FloatPrimL $ realToFrac f) |]+ AT.Double f -> [e| AT.Long $(litE $ FloatPrimL $ realToFrac f) |]+ AT.Bytes bs -> [e| AT.Bytes $(mkByteString bs) |]+ AT.String s -> [e| AT.String $(mkText s) |]+ AT.Array vec -> [e| AT.Array $ V.fromList $(ListE <$> mapM mkDefaultValue (V.toList vec)) |]+ AT.Map m -> [e| AT.Map $ $(mkMap m) |]+ AT.Record s m -> [e| AT.Record $(mkSchema s) $(mkMap m) |]+ AT.Union _ _ v -> mkDefaultValue v+ AT.Fixed s bs -> [e| AT.Fixed $(mkSchema s) $(mkByteString bs) |]+ AT.Enum s n sym -> [e| AT.Enum $(mkSchema s) $(litE $ IntegerL $ fromIntegral n) $(mkText sym) |]++ mkByteString bs = [e| B.pack $(ListE <$> mapM numericLit (B.unpack bs)) |]+ where numericLit = litE . IntegerL . fromIntegral++ mkMap (HM.toList -> xs) = [e| HM.fromList $(ListE <$> mapM mkKVPair xs) |]+ mkKVPair (k, v) = [e| ($(mkText k), $(mkDefaultValue v)) e|]++ mkNE (NE.toList -> xs) = [e| NE.fromList $(ListE <$> mapM mkSchema xs) |]+ -- | A hack around TemplateHaskell limitation: -- It is currently not possible to splice variable name in QQ. -- This function allows to replace hardcoded name into the specified one. setName :: Name -> Q [Dec] -> Q [Dec] setName = fmap . map . sn where- sn n (SigD _ t) = SigD n t+ sn n (SigD _ t) = SigD n t sn n (ValD (VarP _) x y) = ValD (VarP n) x y- sn _ d = d+ sn _ d = d genType :: Schema -> Q [Dec] genType (S.Record n _ _ _ _ fs) = do
src/Data/Avro/FromAvro.hs view
@@ -40,10 +40,12 @@ Just v -> fromAvro v instance (FromAvro a, FromAvro b) => FromAvro (Either a b) where- fromAvro e@(T.Union _ v x) =- if | v == untag (schema :: Tagged a Type) -> Left <$> fromAvro x- | v == untag (schema :: Tagged b Type) -> Right <$> fromAvro x- | otherwise -> badValue e "either"+ fromAvro e@(T.Union _ branch x)+ | S.matches branch schemaA = Left <$> fromAvro x+ | S.matches branch schemaB = Right <$> fromAvro x+ | otherwise = badValue e "either"+ where Tagged schemaA = schema :: Tagged a Type+ Tagged schemaB = schema :: Tagged b Type fromAvro x = badValue x "either" instance FromAvro Bool where fromAvro (T.Boolean b) = pure b
+ src/Data/Avro/JSON.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- | Avro supports a JSON representation of Avro objects alongside the+-- Avro binary format. An Avro schema can be used to generate and+-- validate JSON representations of Avro objects.+--+-- The JSON format is the same format as used for default values in+-- schemas except unions are encoded differently. Non-union values are+-- encoded as follows:+--+-- +--------------+----------+----------++-- |Avro Type |JSON Type |Example |+-- +==============+==========+==========++-- |null |null |null |+-- +--------------+----------+----------++-- |boolean |boolean |true |+-- +--------------+----------+----------++-- |int, long |integer |1 |+-- +--------------+----------+----------++-- |float, double |number |1.1 |+-- +--------------+----------+----------++-- |bytes |string |"\u00FF" |+-- +--------------+----------+----------++-- |string |string |"foo" |+-- +--------------+----------+----------++-- |record |object |{"a":1} |+-- +--------------+----------+----------++-- |enum |string |"FOO" |+-- +--------------+----------+----------++-- |array |array |[1] |+-- +--------------+----------+----------++-- |map |object |{"a":1} |+-- +--------------+----------+----------++-- |fixed |string |"\u00FF" |+-- +--------------+----------+----------++--+-- (Table from the Avro 1.8.2 specification:+-- <https://avro.apache.org/docs/1.8.2/spec.html#schema_record>)+--+-- Bytes and fixed are encoded as JSON strings where each byte is+-- translated into the corresponding Unicode codepoint between 0–255,+-- which includes non-printable characters. Note that this encoding+-- happens at the Unicode code-point level, meaning it is independent+-- of text encoding. (JSON is, by definition, encoded in UTF8.)+--+-- Unions are encoded as an object with a single field that specifies+-- the "branch" of the union. If the branch is a primitive type like+-- @"string"@, the name of the primitive type is used:+--+-- @+-- { "string" : "foo" }+-- @+--+-- For named types (record, enum and fixed), the name of the type is+-- used:+--+-- @+-- { "MyRecord" : { ... } }+-- @+module Data.Avro.JSON where++import Data.Semigroup ((<>))++import qualified Data.Aeson as Aeson+import Data.ByteString.Lazy (ByteString)+import Data.HashMap.Strict ((!))+import qualified Data.HashMap.Strict as HashMap+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE+import Data.Tagged+import qualified Data.Text as Text++import Data.Avro (FromAvro (..), Result (..), ToAvro (..))+import qualified Data.Avro as Avro+import Data.Avro.Schema (Schema, parseAvroJSON)+import qualified Data.Avro.Schema as Schema+import qualified Data.Avro.Types as Avro++-- | Convert a 'Aeson.Value' into a type that has an Avro schema. The+-- schema is used to validate the JSON and will return an 'Error' if+-- the JSON object is not encoded correctly or does not match the schema.+fromJSON :: forall a. (FromAvro a) => Aeson.Value -> Result a+fromJSON json = parseAvroJSON union env schema json >>= fromAvro+ where schema = untag (Avro.schema :: Tagged a Schema)+ env = Schema.buildTypeEnvironment missing schema . Schema.TN+ missing name = fail $ "Type " <> show name <> " not in schema."++ union (Schema.Union schemas _) Aeson.Null+ | Schema.Null `elem` schemas = pure $ Avro.Union schemas Schema.Null Avro.Null+ | otherwise = fail "Null not in union."+ union (Schema.Union schemas _) (Aeson.Object obj)+ | length obj < 1 = fail "Invalid encoding of union: empty object ({})."+ | length obj > 1 = fail "Invalid encoding of union: object with too many fields."+ | otherwise =+ let branch = head $ HashMap.keys obj+ names = HashMap.fromList [(Schema.typeName t, t) | t <- NE.toList schemas]+ in case HashMap.lookup branch names of+ Just t -> do+ nested <- parseAvroJSON union env t $ obj ! branch+ return $ Avro.Union schemas t nested+ Nothing -> fail $ "Type '" <> Text.unpack branch <> "' not in union: " <> show schemas+ union Schema.Union{} _ =+ Avro.Error "Invalid JSON representation for union: has to be a JSON object with exactly one field."+ union _ _ = error "Impossible: function given non-union schema."++-- | Parse a 'ByteString' as JSON and convert it to a type with an+-- Avro schema. Will return 'Error' if the input is not valid JSON or+-- the JSON does not convert with the specified schema.+parseJSON :: forall a. (FromAvro a) => ByteString -> Result a+parseJSON input = case Aeson.eitherDecode input of+ Left msg -> Error msg+ Right value -> fromJSON value++-- | Convert an object with an Avro schema to JSON using that schema.+--+-- We always need the schema to /encode/ to JSON because representing+-- unions requires using the names of named types.+toJSON :: forall a. (ToAvro a) => a -> Aeson.Value+toJSON = Aeson.toJSON . toAvro
src/Data/Avro/Schema.hs view
@@ -1,9 +1,12 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE ViewPatterns #-}+ -- | Avro 'Schema's, represented here as values of type 'Schema', -- describe the serialization and de-serialization of values. --@@ -22,6 +25,13 @@ , typeName , buildTypeEnvironment , Result(..)++ , matches++ , parseBytes+ , serializeBytes++ , parseAvroJSON ) where import Control.Applicative@@ -33,7 +43,10 @@ import qualified Data.Aeson as A import Data.Aeson.Types (Parser, typeMismatch) import qualified Data.Avro.Types as Ty+import qualified Data.ByteString as B import qualified Data.ByteString.Base16 as Base16+import qualified Data.Char as Char+import Data.Function (on) import Data.Hashable import qualified Data.HashMap.Strict as HashMap import Data.Int@@ -50,8 +63,9 @@ import Data.Text.Encoding as T import qualified Data.Vector as V import Prelude as P-import Text.Show.Functions +import Text.Show.Functions ()+ -- |An Avro schema is either -- * A "JSON object in the form `{"type":"typeName" ...` -- * A "JSON string, naming a defined type" (basic type w/o free variables/names)@@ -112,7 +126,8 @@ Array ty == Array ty2 = ty == ty2 Map ty == Map ty2 = ty == ty2 NamedType t == NamedType t2 = t == t2- Record _ _ _ _ _ fs == Record _ _ _ _ _ fs2 = fs == fs2+ Record name1 ns1 _ _ _ fs1 == Record name2 ns2 _ _ _ fs2 =+ and [name1 == name2, ns1 == ns2, fs1 == fs2] Enum _ _ _ _ s _ == Enum _ _ _ _ s2 _ = s == s2 Union a _ == Union b _ = a == b Fixed _ _ _ s == Fixed _ _ _ s2 = s == s2@@ -282,7 +297,7 @@ ty <- o .: "type" let err = fail "Haskell Avro bindings does not support default for aliased or recursive types at this time." defM <- o .:! "default"- def <- case parseAvroJSON err ty <$> defM of+ def <- case parseFieldDefault err ty <$> defM of Just (Success x) -> return (Just x) Just (Error e) -> fail e Nothing -> return Nothing@@ -314,14 +329,14 @@ Ty.Long i -> A.Number (fromIntegral i) Ty.Float f -> A.Number (realToFrac f) Ty.Double d -> A.Number (realToFrac d)- Ty.Bytes bs -> A.String ("\\u" <> T.decodeUtf8 (Base16.encode bs))+ Ty.Bytes bs -> A.String (serializeBytes bs) Ty.String t -> A.String t Ty.Array vec -> A.Array (V.map toJSON vec) Ty.Map mp -> A.Object (HashMap.map toJSON mp) Ty.Record _ flds -> A.Object (HashMap.map toJSON flds) Ty.Union _ _ Ty.Null -> A.Null Ty.Union _ ty val -> object [ typeName ty .= val ]- Ty.Fixed _ bs -> A.String ("\\u" <> T.decodeUtf8 (Base16.encode bs)) -- XXX the example wasn't literal - this should be an actual bytestring... somehow.+ Ty.Fixed _ bs -> A.String (serializeBytes bs) Ty.Enum _ _ txt -> A.String txt data Result a = Success a | Error String@@ -363,25 +378,49 @@ traverse _ (Error err) = pure (Error err) traverse f (Success v) = Success <$> f v --- |Parse JSON-encoded avro data.-parseAvroJSON :: (Text -> Maybe Type) -> Type -> A.Value -> Result (Ty.Value Type)-parseAvroJSON env (NamedType (TN tn)) av =+-- | Field defaults are in the normal Avro JSON format except for+-- unions. Default values for unions are specified as JSON encodings+-- of the first type in the union.+parseFieldDefault :: (Text -> Maybe Type) -> Type -> A.Value -> Result (Ty.Value Type)+parseFieldDefault env schema value = parseAvroJSON defaultUnion env schema value+ where defaultUnion (Union (t :| _) _) val = parseFieldDefault env t val+ defaultUnion _ _ = error "Impossible: not Union."++-- | Parse JSON-encoded avro data.+parseAvroJSON :: (Type -> A.Value -> Result (Ty.Value Type))+ -- ^ How to handle unions. The way unions are+ -- formatted in JSON depends on whether we're parsing+ -- a normal Avro object or we're parsing a default+ -- declaration in a schema.+ --+ -- This function will only ever be passed 'Union'+ -- schemas. It /should/ error out if this is not the+ -- case—it represents a bug in this code.+ -> (Text -> Maybe Type)+ -> Type+ -> A.Value+ -> Result (Ty.Value Type)+parseAvroJSON union env (NamedType (TN tn)) av = case env tn of Nothing -> fail $ "Could not resolve type name for " <> show tn- Just t -> parseAvroJSON env t av-parseAvroJSON env ty av =+ Just t -> parseAvroJSON union env t av+parseAvroJSON union _ u@Union{} av = union u av+parseAvroJSON union env ty av = case av of- A.String s ->+ A.String s -> case ty of- String -> return $ Ty.String s- Enum {..} ->+ String -> return $ Ty.String s+ Enum {..} -> if s `elem` symbols then return $ Ty.Enum ty (maybe (error "IMPOSSIBLE BUG") id $ lookup s (zip symbols [0..])) s else fail $ "JSON string is not one of the expected symbols for enum '" <> show name <> "': " <> T.unpack s- Union tys _ -> do- f <- tryAllTypes env tys av- maybe (fail $ "No match for String in union '" <> show (typeName ty) <> "'.") pure f- _ -> avroTypeMismatch ty "string"+ Bytes -> Ty.Bytes <$> parseBytes s+ Fixed {..} -> do+ bytes <- parseBytes s+ let len = B.length bytes+ when (len /= size) $+ fail $ "Fixed string wrong size. Expected " <> show size <> " but got " <> show len+ return $ Ty.Fixed ty bytes A.Bool b -> case ty of Boolean -> return $ Ty.Boolean b _ -> avroTypeMismatch ty "boolean"@@ -391,42 +430,42 @@ Long -> return $ Ty.Long (floor i) Float -> return $ Ty.Float (realToFrac i) Double -> return $ Ty.Double (realToFrac i)- Union tys _ -> do- f <- tryAllTypes env tys av- maybe (fail $ "No match for Number in union '" <> show (typeName ty) <> "'.") pure f _ -> avroTypeMismatch ty "number" A.Array vec -> case ty of- Array t -> Ty.Array <$> V.mapM (parseAvroJSON env t) vec- Union tys _ -> do- f <- tryAllTypes env tys av- maybe (fail $ "No match for Array in union '" <> show (typeName ty) <> "'.") pure f+ Array t -> Ty.Array <$> V.mapM (parseAvroJSON union env t) vec _ -> avroTypeMismatch ty "array" A.Object obj -> case ty of- Map mTy -> Ty.Map <$> mapM (parseAvroJSON env mTy) obj+ Map mTy -> Ty.Map <$> mapM (parseAvroJSON union env mTy) obj Record {..} -> do let lkAndParse f = case HashMap.lookup (fldName f) obj of Nothing -> case fldDefault f of Just v -> return v Nothing -> fail $ "Decode failure: No record field '" <> T.unpack (fldName f) <> "' and no default in schema."- Just v -> parseAvroJSON env (fldType f) v+ Just v -> parseAvroJSON union env (fldType f) v Ty.Record ty . HashMap.fromList <$> mapM (\f -> (fldName f,) <$> lkAndParse f) fields- Union tys _ -> do- f <- tryAllTypes env tys av- maybe (fail $ "No match for given record in union '" <> show (typeName ty) <> "'.") pure f _ -> avroTypeMismatch ty "object" A.Null -> case ty of Null -> return Ty.Null- Union us _ | Null `elem` NE.toList us -> return $ Ty.Union us Null Ty.Null _ -> avroTypeMismatch ty "null" -tryAllTypes :: (Text -> Maybe Type) -> NonEmpty Type -> A.Value -> Result (Maybe (Ty.Value Type))-tryAllTypes env tys av =- getFirst <$> foldMap (\t -> First . Just <$> parseAvroJSON env t av) (NE.toList tys)- `catchError` (\_ -> return mempty)+-- | Parses a string literal into a bytestring in the format expected+-- for bytes and fixed values. Will fail if every character does not+-- have a codepoint between 0 and 255.+parseBytes :: Text -> Result B.ByteString+parseBytes bytes = case T.find (not . inRange) bytes of+ Just badChar -> fail $ "Invalid character in bytes or fixed string representation: " <> show badChar+ Nothing -> return $ B.pack $ fromIntegral . Char.ord <$> T.unpack bytes+ where inRange (Char.ord -> c) = c >= 0x00 && c <= 0xFF +-- | Turn a 'ByteString' into a 'Text' that matches the format Avro+-- expects from bytes and fixed literals in JSON. Each byte is mapped+-- to a single Unicode codepoint between 0 and 255.+serializeBytes :: B.ByteString -> Text+serializeBytes = T.pack . map (Char.chr . fromIntegral) . B.unpack+ avroTypeMismatch :: Type -> Text -> Result a avroTypeMismatch expected actual = fail $ "Could not resolve type '" <> T.unpack actual <> "' with expected type: " <> show expected@@ -487,3 +526,22 @@ Fixed {..} -> mk name aliases namespace Array {..} -> go item _ -> []++-- | Checks that two schemas match. This is like equality of schemas,+-- except 'NamedTypes' match against other types /with the same name/.+--+-- This extends recursively: two records match if they have the same+-- name, the same number of fields and the fields all match.+matches :: Type -> Type -> Bool+matches (NamedType (TN n)) t = n == typeName t+matches t (NamedType (TN n)) = typeName t == n+matches (Array itemA) (Array itemB) = matches itemA itemB+matches a@Record{} b@Record{} =+ and [ name a == name b+ , namespace a == namespace b+ , length (fields a) == length (fields b)+ , and $ zipWith fieldMatches (fields a) (fields b)+ ]+ where fieldMatches = matches `on` fldType+matches a@Union{} b@Union{} = and $ NE.zipWith matches (options a) (options b)+matches t1 t2 = t1 == t2
+ test/Avro/DefaultsSpec.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+module Avro.DefaultsSpec+where++import qualified Data.Aeson as J+import Data.Avro+import Data.Avro.Deriving+import Data.Avro.Schema+import qualified Data.Avro.Types as Ty+import qualified Data.HashMap.Strict as M+import qualified Data.Vector as V++import Test.Hspec++{-# ANN module ("HLint: ignore Redundant do" :: String) #-}++deriveAvro "test/data/maybe.avsc"++spec :: Spec+spec = describe "Avro.DefaultsSpec: Schema with named types" $ do+ it "should decode value" $+ let msg = MaybeTest (Just "value") (FixedTag "\0\42\255") "\0\37\255"+ in fromAvro (toAvro msg) `shouldBe` pure msg++ it "should decode no value" $+ let msg = MaybeTest Nothing (FixedTag "\0\42\255") "\0\37\255"+ in fromAvro (toAvro msg) `shouldBe` pure msg++ it "should read default from Schema" $+ let+ msgSchema = schemaOf (undefined :: MaybeTest)+ fixedSchema = schemaOf (undefined :: FixedTag)+ defaults = fldDefault <$> fields msgSchema+ in defaults `shouldBe` [ Just Ty.Null+ , Just $ Ty.Fixed fixedSchema "\0\42\255"+ , Just $ Ty.Bytes "\0\37\255"+ ]++ it "should encode schema with default" $+ let+ msgSchema = schemaOf (undefined :: MaybeTest)+ (J.Object jSchema) = J.toJSON msgSchema+ (Just (J.Array flds)) = M.lookup "fields" jSchema+ (J.Object jFld) = V.head flds+ in M.lookup "default" jFld `shouldBe` Just J.Null
+ test/Avro/JSONSpec.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+module Avro.JSONSpec where++import Control.Monad (forM_)++import qualified Data.Aeson as Aeson+import qualified Data.ByteString.Lazy as LBS++import Data.Avro.Deriving+import Data.Avro.JSON++import Test.Hspec++import Paths_avro++import System.Directory (doesFileExist, getCurrentDirectory)+import System.Environment (setEnv)++deriveAvro "test/data/enums.avsc"+deriveAvro "test/data/reused.avsc"+deriveAvro "test/data/small.avsc"+deriveAvro "test/data/unions.avsc"++spec :: Spec+spec = describe "Avro.JSONSpec: JSON serialization/parsing" $ do+ it "should do roundtrip (enums)" $ do+ let msg = EnumWrapper+ { enumWrapperId = 42+ , enumWrapperName = "Text"+ , enumWrapperReason = EnumReasonBecause+ }+ parseJSON (Aeson.encode (toJSON msg)) `shouldBe` pure msg+ it "should do roundtrip (reused)" $ do+ let msg = ReusedWrapper+ { reusedWrapperFull = ReusedChild 42+ , reusedWrapperInner = ContainerChild+ { containerChildFstIncluded = ReusedChild 37+ , containerChildSndIncluded = ReusedChild 64+ }+ }+ parseJSON (Aeson.encode (toJSON msg)) `shouldBe` pure msg+ it "should do roundtrip (small)" $ do+ let msgs =+ [ Endpoint+ { endpointIps = ["192.168.1.1", "127.0.0.1"]+ , endpointPorts = [PortRange 1 10, PortRange 11 20]+ , endpointOpaque = Opaque "16-b-long-string"+ , endpointCorrelation = Opaque "opaq-correlation"+ , endpointTag = Left 14+ }+ , Endpoint+ { endpointIps = []+ , endpointPorts = [PortRange 1 10, PortRange 11 20]+ , endpointOpaque = Opaque "opaque-long-text"+ , endpointCorrelation = Opaque "correlation-data"+ , endpointTag = Right "first-tag"+ }+ ]+ forM_ msgs $ \ msg ->+ parseJSON (Aeson.encode (toJSON msg)) `shouldBe` pure msg++ enumsExampleJSON <- runIO $ getFileName "test/data/enums-object.json" >>= LBS.readFile+ it "should parse (enums)" $ do+ let expected = EnumWrapper 37 "blarg" EnumReasonInstead+ parseJSON enumsExampleJSON `shouldBe` pure expected+ let unionsExampleA = Unions+ { unionsScalars = Left "blarg"+ , unionsNullable = Nothing+ , unionsRecords = Left $ Foo { fooStuff = "stuff" }+ , unionsSameFields = Left $ Foo { fooStuff = "foo stuff" }+ }+ unionsExampleB = Unions+ { unionsScalars = Right 37+ , unionsNullable = Just 42+ , unionsRecords = Right $ Bar { barStuff = "stuff"+ , barThings = Foo "things"+ }+ , unionsSameFields = Right $ NotFoo { notFooStuff = "not foo stuff" }+ }+ it "should roundtrip (unions)" $ do+ forM_ [unionsExampleA, unionsExampleB] $ \ msg ->+ parseJSON (Aeson.encode (toJSON msg)) `shouldBe` pure msg+ unionsJsonA <- runIO $ getFileName "test/data/unions-object-a.json" >>= LBS.readFile+ unionsJsonB <- runIO $ getFileName "test/data/unions-object-b.json" >>= LBS.readFile+ it "should parse (unions)" $ do+ parseJSON unionsJsonA `shouldBe` pure unionsExampleA+ parseJSON unionsJsonB `shouldBe` pure unionsExampleB++getFileName :: FilePath -> IO FilePath+getFileName p = do+ path <- getDataFileName p+ isOk <- doesFileExist path+ pure $ if isOk then path else p
test/Avro/THSimpleSpec.hs view
@@ -4,21 +4,23 @@ module Avro.THSimpleSpec where -import Control.Monad import Control.Lens+import Control.Monad++import qualified Data.Aeson as J import Data.Aeson.Lens+import qualified Data.ByteString as B+import qualified Data.Char as Char+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T++import Test.Hspec+ import Data.Avro import Data.Avro.Deriving import Data.Avro.Schema-import Data.Monoid ((<>))-import qualified Data.Aeson as J-import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import qualified Data.ByteString.Base16 as Base16 -import Test.Hspec- {-# ANN module ("HLint: ignore Redundant do" :: String) #-} deriveAvro "test/data/small.avsc"@@ -63,5 +65,5 @@ json ^? key "tag" . _Value . key "string" . _String `shouldBe` endpointTag msg ^? _Right where encodeOpaque :: Opaque -> Text- encodeOpaque v = "\\u" <> T.decodeUtf8 (Base16.encode $ unOpaque v)+ encodeOpaque (Opaque bs) = T.pack $ Char.chr . fromIntegral <$> B.unpack bs
+ test/Avro/THUnionSpec.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+module Avro.THUnionSpec+where++import Data.Avro+import Data.Avro.Deriving++import Test.Hspec++deriveAvro "test/data/unions.avsc"++spec :: Spec+spec = describe "Avro.THUnionSpec: Schema with unions." $ do+ let objA = Unions+ { unionsScalars = Left "foo"+ , unionsNullable = Nothing+ , unionsRecords = Left $ Foo { fooStuff = "stuff" }+ , unionsSameFields = Left $ Foo { fooStuff = "more stuff" }+ }+ objB = Unions+ { unionsScalars = Right 42+ , unionsNullable = Just 37+ , unionsRecords = Right $ Bar { barStuff = "stuff"+ , barThings = Foo { fooStuff = "things" }+ }+ , unionsSameFields = Right $ NotFoo { notFooStuff = "different from Foo" }+ }+ it "records with unions should roundtrip" $ do+ fromAvro (toAvro objA) `shouldBe` pure objA+ fromAvro (toAvro objB) `shouldBe` pure objB
+ test/DecodeContainer.hs view
@@ -0,0 +1,25 @@+module DecodeContainer+where++import qualified Data.Aeson as A+import Data.Avro.Decode+import Data.Avro.Deconflict as D+import qualified Data.ByteString.Lazy as BL+import System.Environment++main :: IO ()+main =+ do (file:rest) <- getArgs+ cont <- BL.readFile file+ case decodeContainer cont of+ Left e -> print e+ Right (s,v) ->+ do putStrLn $ "Schema: " ++ show s+ putStrLn "--------------------------------------------------"+ print v+ case rest of+ [schFile] -> do+ putStrLn "---- DECONFLICTED ------"+ Just readerSchema <- A.decode <$> BL.readFile schFile+ print (map (map (D.deconflict s readerSchema)) v)+ _ -> return ()
+ test/Example1.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE OverloadedStrings #-}+module Example1+where+import Data.Avro+import Data.Avro.Schema+import qualified Data.Avro.Types as Ty+import qualified Data.ByteString.Lazy as BL+import Data.List.NonEmpty (NonEmpty (..))+import Data.Map as M+import Data.Text (Text)+import qualified Data.Text as Text++data MyEnum = A | B | C | D deriving (Eq,Ord,Show,Enum)+data MyStruct = MyStruct (Either MyEnum Text) Int deriving (Eq,Ord,Show)++-- Explicit 'Schema' types that specify the Avro encoding of a structure.+-- Schema's often come from an external JSON definition (.avsc files) or+-- embedded in object files.+meSchema :: Schema+meSchema = mkEnum "MyEnum" [] Nothing Nothing ["A","B","C","D"]++msSchema :: Schema+msSchema =+ Record "MyStruct" Nothing [] Nothing Nothing+ [ fld "enumOrString" eOrS (Just $ Ty.String "The Default")+ , fld "intvalue" Long Nothing+ ]+ where+ fld nm ty def = Field nm [] Nothing Nothing ty def+ eOrS = mkUnion (meSchema :| [String])++-- Encoding data, via the ToAvro class, requires both the routine that encodes+-- data as well as the schema under which it is encoded. The encoding and+-- schema must match, though there is no type or programmatic routine that enforces+-- this law.+instance HasAvroSchema MyEnum where+ schema = pure meSchema++instance ToAvro MyEnum where+ toAvro x = Ty.Enum meSchema (fromEnum x) (Text.pack $ show x)+ -- schema = pure meSchema++instance HasAvroSchema MyStruct where+ schema = pure msSchema++instance ToAvro MyStruct where+ toAvro (MyStruct ab i) =+ record msSchema+ [ "enumOrString" .= ab+ , "intvalue" .= i+ ]+ -- schema = pure msSchema++-- Much like Aeson, decoding data is involves pattern matching the value+-- constructor then building the ADT.+instance FromAvro MyStruct where+ fromAvro (Ty.Record _fs r) =+ MyStruct <$> r .: "enumOrString"+ <*> r .: "intvalue"+ fromAvro v = badValue v "MyStruct"++instance FromAvro MyEnum where+ fromAvro (Ty.Enum _ i _) = pure (toEnum i)+ fromAvro v = badValue v "MyEnum"++main = do+ let valR = MyStruct (Right "Hello") 1+ encR = toAvro valR+ valL = MyStruct (Left C) (negate 1944)+ encL = toAvro valL+ putStrLn "----------- MS Right value -------------"+ print (fromAvro encR `asTypeOf` Success valR)+ print (fromAvro encR == Success valR)+ putStrLn "----------- MS Left value --------------"+ print (fromAvro encL `asTypeOf` Success valL)+ print (fromAvro encL == Success valL)+ putStrLn "----------- MS Right full bytestring enc/dec--------------"+ print (BL.unpack $ encode valR)+ print (decode (encode valR) `asTypeOf` Success valR)+ print (decode (encode valR) == Success valR)+ putStrLn "----------- MS Left full bytestring enc/dec--------------"+ print (BL.unpack $ encode valL)+ print (decode (encode valL) `asTypeOf` Success valL)+ print (decode (encode valL) == Success valL)
+ test/data/enums-object.json view
@@ -0,0 +1,1 @@+{"id":37,"name":"blarg","reason":"Instead"}
test/data/maybe.avsc view
@@ -1,7 +1,19 @@ {- "type": "record",- "name": "MaybeTest",- "fields": [- { "name": "tag", "type": ["null", "string"], "default": null }- ]- }+ "type": "record",+ "name": "MaybeTest",+ "fields": [+ { "name": "tag", "type": ["null", "string"], "default": null },+ { "name": "fixedTag",+ "type": {+ "type": "fixed",+ "name": "FixedTag",+ "size": 3+ },+ "default": "\u0000\u002a\u00ff"+ },+ { "name": "bytesTag",+ "type": "bytes",+ "default": "\u0000\u0025\u00ff"+ }+ ]+}
+ test/data/unions-object-a.json view
@@ -0,0 +1,16 @@+{+ "scalars": {+ "string": "blarg"+ },+ "nullable": null,+ "records": {+ "Foo": {+ "stuff": "stuff"+ }+ },+ "sameFields": {+ "Foo": {+ "stuff": "foo stuff"+ }+ }+}
+ test/data/unions-object-b.json view
@@ -0,0 +1,21 @@+{+ "scalars": {+ "long": 37+ },+ "nullable": {+ "int": 42+ },+ "records": {+ "Bar": {+ "stuff": "stuff",+ "things": {+ "stuff": "things"+ }+ }+ },+ "sameFields": {+ "NotFoo": {+ "stuff": "not foo stuff"+ }+ }+}
+ test/data/unions.avsc view
@@ -0,0 +1,46 @@+{ "type" : "record",+ "name" : "Unions",+ "namespace" : "haskell.avro.example",+ "fields" : [+ { "name" : "scalars",+ "type" : ["string", "long"]+ },+ { "name" : "nullable",+ "type" : ["null", "int"]+ },+ { "name" : "records",+ "type" : [+ { "type" : "record",+ "name" : "Foo",+ "fields" : [+ { "name" : "stuff",+ "type" : "string"+ }+ ]+ },+ { "type" : "record",+ "name" : "Bar",+ "fields" : [+ { "name" : "stuff",+ "type" : "string"+ },+ { "name" : "things",+ "type" : "Foo"+ }+ ]+ }+ ]+ },+ { "name" : "sameFields",+ "type" : [+ "Foo",+ { "type" : "record",+ "name" : "NotFoo",+ "fields" : [+ { "name" : "stuff", "type" : "string" }+ ]+ }+ ]+ }+ ]+}