packages feed

aeson-injector (empty) → 1.0.0.0

raw patch · 5 files changed

+665/−0 lines, 5 filesdep +HUnitdep +aesondep +aeson-injectorsetup-changed

Dependencies added: HUnit, aeson, aeson-injector, base, deepseq, lens, swagger2, text, unordered-containers

Files

+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2016 Anton Gushcha++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ aeson-injector.cabal view
@@ -0,0 +1,49 @@+name:                aeson-injector+version:             1.0.0.0+synopsis:            Injecting fields into aeson values+description:         See Readme.md+license:             MIT+license-file:        LICENSE+author:              Anton Gushcha +maintainer:          ncrashed@gmail.com+copyright:           2016 Anton Gushcha+category:            Data, JSON, Web+build-type:          Simple+cabal-version:       >=1.18+tested-with: +    GHC == 7.10.3+  , GHC == 8.0.1+  , GHC == 8.0.2++source-repository head+  type: git+  location: https://github.com/NCrashed/aeson-injector++library+  default-language: Haskell2010+  hs-source-dirs: src+  exposed-modules:+      Data.Aeson.WithField+    +  build-depends: +      base                 >= 4.7   && < 4.10+    , aeson                >= 0.11  && < 0.13+    , deepseq              >= 1.4   && < 2+    , lens                 >= 4.13  && < 5+    , swagger2             >= 2.0   && < 3.0+    , text                 >= 1.2   && < 2.0+    , unordered-containers >= 0.2.7 && < 0.3++test-suite test-aeson-injector+  default-language: Haskell2010+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  main-is: Main.hs+  build-depends: +      base+    , aeson+    , aeson-injector+    , HUnit >= 1.3+    , lens+    , swagger2 +    , text
+ src/Data/Aeson/WithField.hs view
@@ -0,0 +1,338 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-|+Module      : Data.Aeson.WithField+Description : Provides utility to inject fields into aeson values.+Copyright   : (c) Anton Gushcha, 2016+License     : MIT+Maintainer  : ncrashed@gmail.com+Stability   : experimental+Portability : Portable++When builds a RESTful API one often faces the problem that some methods+need inbound data without unique identifier (for instance, a creation of+new resource) and some methods need the same outbound data with additional+fields attached to the response.++The module provides you with 'WithField' and 'WithFields' data types that+help you to solve the issue without code duplication. ++It is small utility library that is intented to be used in RESTful APIs, +especially with <http://haskell-servant.readthedocs.io/en/stable/ servant> +and <http://swagger.io/ Swagger>. Its main purpose is simple injection of +fields into JSONs produced by <https://hackage.haskell.org/package/aeson aeson> +library.++Consider the following common data type in web service developing:++@+data News = News {+  title :: Text+, body :: Text+, author :: Text+, timestamp :: UTCTime  +}++-- Consider we have simple 'ToJSON' and 'FromJSON' instances+$(deriveJSON defaultOptions ''News) +@++'ToJSON' instance produces JSON's like:++@+{+  "title": "Awesome piece of news!"+, "body": "Big chunk of text"+, "author": "Just Me"+, "timestamp": "2016-07-26T18:54:42.678999Z"+}+@++Now one can create a simple web server with servant DSL:++> type NewsId = Word +> +> type NewsAPI = +>        ReqBody '[JSON] News :> Post '[JSON] NewsId+>   :<|> Capture "news-id" NewsId :> Get '[JSON] News+>   :<|> "list" :> Get '[JSON] [News]+++All seems legit, but, wait a second, an API user definitely would +like to know id of news in the "list" method. One way to do this is declare +new data type @NewsInfo@ with additional field, but it is bad solution as requires +to code duplication for each resource. ++So, here @aeson-injector@ steps in, now you can write:++> type NewsAPI = +>        ReqBody '[JSON] News :> Post '[JSON] NewsId+>   :<|> Capture "news-id" NewsId :> Get '[JSON] News+>   :<|> "list" :> Get '[JSON] [WithField "id" NewsId News]++@'WithField' "id" NewsId News@ or simply @'WithId' NewsId News@ wraps you data type +and injects "id" field in produced JSON values:++>>> encode (WithField 42 myNews :: WithField "id" NewsId News)++> {+>   "id": 42+> , "title": "Awesome piece of news!"+> , "body": "Big chunk of text"+> , "author": "Just Me"+> , "timestamp": "2016-07-26T18:54:42.678999Z"+> }++'WithField' data type has `FromJSON` instance for seamless parsing of data with +injected fields and 'ToSchema' instance for <https://hackage.haskell.org/package/servant-swagger servant-swagger> support.++= Injecting multiple values++The library also has more general data type 'WithFields a b' that injects fields of 'toJSON a' into 'toJSON b'. ++@ haskell+data NewsPatch = NewsPatch {+  taggs :: [Text]+, rating :: Double+}+$(deriveJSON defaultOptions ''NewsPatch) +@++@ haskell+let myNewsPatch = NewsPatch ["tag1", "tag2"] 42 +in encode $ WithFields myNewsPatch myNews+@++> {+>   "title": "Awesome piece of news!"+> , "body": "Big chunk of text"+> , "author": "Just Me"+> , "timestamp": "2016-07-26T18:54:42.678999Z"+> , "tags": ["tag1", "tag2"]+> , "rating": 42.0+> }++= Corner cases++Unfortunately, we cannot inject in non object values of produced JSON, +so the library creates a wrapper object around non-object value:++@+encode (WithId 0 "non-object" :: WithId Int String)+@++@+{+  "id": 0 +, "value": "non-object"+}+@++The same story is about 'WithFields' data type:++@+encode (WithFields 0 "non-object" :: WithFields Int String)+@++@+{+  "injected": 0 +, "value": "non-object"+}+@++-}+module Data.Aeson.WithField(+  -- * Single field injector+    WithField(..)+  , WithId+  -- * Multiple fields injector+  , WithFields(..)+  ) where ++import Control.Applicative+import Control.DeepSeq+import Control.Lens hiding ((.=))+import Control.Monad +import Data.Aeson +import Data.Monoid +import Data.Proxy +import Data.Swagger +import GHC.Generics +import GHC.TypeLits ++import qualified Data.HashMap.Strict as H +import qualified Data.Text as T ++-- | Injects field 'a' into 'b' with tag 's'. It has+-- special instances for 'ToJSON' and 'FromJSON' for +-- such injection and corresponding Swagger 'ToSchema'+-- instance.+--+-- For instance:+--+-- >>> encode (WithField "val" (Left 42) :: WithField "injected" String (Either Int Int))+-- "{\"Left\":42,\"id\":\"val\"}"+--+-- If the instance cannot inject field (in case of single values and arrays),+-- it wraps the result in the following way:+--+-- >>> encode (WithField "val" 42 :: WithField "injected" String Int)+-- "{\"value\":42,\"injected\":\"val\"}"+data WithField (s :: Symbol) a b = WithField !a !b +  deriving (Generic, Eq, Show, Read)++instance (NFData a, NFData b) => NFData (WithField s a b)++-- | Workaround for a problem that is discribed as:+-- sometimes I need a id with the data, sometimes not.+--+-- The important note that 'ToJSON' and 'FromJSON' instances+-- behaves as it is 'a' but with additional 'id' field.+type WithId i a = WithField "id" i a ++-- | Note: the instance injects field only in 'Object' case.+-- In other cases it forms a wrapper around the 'Value' produced +-- by 'toJSON' of inner 'b' body.+--+-- Example of wrapper:+--+-- > { "id": 0, "value": [1, 2, 3] }+instance (KnownSymbol s, ToJSON a, ToJSON b) => ToJSON (WithField s a b) where +  toJSON (WithField a b) = let+    jsonb = toJSON b +    field = T.pack $ symbolVal (Proxy :: Proxy s)+    in case toJSON b of +      Object vs -> Object $ H.insert field (toJSON a) vs +      _ -> object [+          field .= a +        , "value" .= jsonb+        ]++-- | Note: the instance tries to parse the json as object with +-- additional field value, if it fails it assumes that it is a+-- wrapper produced by corresponding 'ToJSON' instance.+instance (KnownSymbol s, FromJSON a, FromJSON b) => FromJSON (WithField s a b) where +  parseJSON val@(Object o) = injected <|> wrapper +    where +    field = T.pack $ symbolVal (Proxy :: Proxy s)+    injected = WithField +      <$> o .: field+      <*> parseJSON val+    wrapper = WithField +      <$> o .: field+      <*> o .: "value"+  parseJSON _ = mzero++-- | Note: the instance tries to generate schema of the json as object with +-- additional field value, if it fails it assumes that it is a+-- wrapper produced by corresponding 'ToJSON' instance.+instance (KnownSymbol s, ToSchema a, ToSchema b) => ToSchema (WithField s a b) where +  declareNamedSchema _ = do +    NamedSchema n s <- declareNamedSchema (Proxy :: Proxy b)+    if s ^. type_ == SwaggerObject then inline n s +      else wrapper n s+    where +    field = T.pack $ symbolVal (Proxy :: Proxy s)+    namePrefix = "WithField '" <> field <> "' "+    wrapper n s = do +      indexSchema <- declareSchema (Proxy :: Proxy a)+      return $ NamedSchema (fmap (namePrefix <>) n) $ mempty+        & type_ .~ SwaggerObject+        & properties .~+            [ (field, Inline indexSchema)+            , ("value", Inline s)+            ]+        & required .~ [ field, "value" ]+    inline n s = do +      indexSchema <- declareSchema (Proxy :: Proxy a)+      return $ NamedSchema (fmap (namePrefix <>) n) $ s+        & properties %~ ([(field, Inline indexSchema)] <>)+        & required %~ ([field] <>)++-- | Merge fields of 'a' into 'b', more general version of 'WithField'.+--+-- The usual mode of the data type assumes that 'ToJSON' instances of 'a' and 'b' +-- produce 'Object' subtype of aeson 'Value'. If it is not true, a wrapper +-- layer is introduced.+--+-- If 'a' is not a 'Object', the wrapper contains 'injected' field with body of 'a'.+-- If 'b' is not a 'Object', the wrapper contains 'value' field with body of 'b'.+-- If both are not 'Object', the wrapper contains 'injected' and 'value' keys with+-- 'a' and 'b' respectively.+data WithFields a b = WithFields !a !b +  deriving (Generic, Eq, Show, Read)++instance (NFData a, NFData b) => NFData (WithFields a b)++-- | Note: the instance injects field only in 'Object' case.+-- In other cases it forms a wrapper around the 'Value' produced +-- by 'toJSON' of inner 'b' body.+--+-- Example of wrapper when 'b' is not a 'Object', 'b' goes into "value" field:+-- +-- > { "field1": 0, "field2": "val", "value": [1, 2, 3] }+--+-- Example of wrapper when 'a' is not a 'Object', but 'b' is. 'a' goes into +-- "injected" field:+-- +-- > { "field1": 0, "field2": "val", "injected": [1, 2, 3] }+--+-- Example of wrapper when as 'a' is not a 'Object', as 'b' is not. 'a' goes into +-- "injected" field, 'b' goes into "value" field:+--+-- > { "value": 42, "injected": [1, 2, 3] }+instance (ToJSON a, ToJSON b) => ToJSON (WithFields a b) where +  toJSON (WithFields a b) = let+    jsonb = toJSON b +    jsona = toJSON a+    in case jsonb of +      Object bvs -> case jsona of +        Object avs -> Object $ H.union avs bvs+        _ -> Object $ H.insert "injected" jsona bvs +      _ -> case jsona of +        Object avs -> Object $ H.insert "value" jsonb avs+        _ -> object [+            "injected" .= jsona+          , "value" .= jsonb+          ]++-- | Note: the instance tries to parse the json as object with +-- additional field value, if it fails it assumes that it is a+-- wrapper produced by corresponding 'ToJSON' instance.+instance (FromJSON a, FromJSON b) => FromJSON (WithFields a b) where +  parseJSON val@(Object o) = WithFields+    <$> (parseJSON val <|> o .: "injected")+    <*> (parseJSON val <|> o .: "value")+  parseJSON _ = mzero++-- | Note: the instance tries to generate schema of the json as object with +-- additional field value, if it fails it assumes that it is a+-- wrapper produced by corresponding 'ToJSON' instance.+instance (ToSchema a, ToSchema b) => ToSchema (WithFields a b) where +  declareNamedSchema _ = do +    NamedSchema nb sb <- declareNamedSchema (Proxy :: Proxy b)+    NamedSchema na sa <- declareNamedSchema (Proxy :: Proxy a)+    let newName = combinedName <$> na <*> nb+    return . NamedSchema newName $ case (sa ^. type_ , sb ^. type_) of +      (SwaggerObject, SwaggerObject) -> sa <> sb +      (SwaggerObject, _) -> sa <> bwrapper sb+      (_, SwaggerObject) -> awrapper sa <> sb+      _ -> awrapper sa <> bwrapper sb+    where+    combinedName a b = "WithFields_" <> a <> "_" <> b+    awrapper nas = mempty+      & type_ .~ SwaggerObject+      & properties .~ [ ("injected", Inline nas) ]+      & required .~ [ "injected" ]+    bwrapper nbs = mempty+      & type_ .~ SwaggerObject+      & properties .~ [ ("value", Inline nbs) ]+      & required .~ [ "value" ]+  
+ test/Main.hs view
@@ -0,0 +1,256 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Main where++import Control.Lens hiding ((.=))+import Control.Monad +import Data.Aeson+import Data.Aeson.WithField+import Data.Swagger+import Data.Swagger.Internal.Schema+import Data.Text +import Data.Proxy+import Test.HUnit ++main :: IO ()+main = runTestTT tests >> return ()+  where+  tests = TestList [+      (TestLabel "WithField tests" withFieldTests)+    , (TestLabel "WithFields tests" withFieldsTests)+    ]++data TestObj = TestObj !Text +  deriving (Eq, Show)++instance ToJSON TestObj where +  toJSON (TestObj t) = object ["field" .= t]+instance FromJSON TestObj where +  parseJSON (Object o) = TestObj <$> o .: "field"+  parseJSON _ = mzero+instance ToSchema TestObj where +  declareNamedSchema prx = do +    t <- declareSchema (Proxy :: Proxy Text)+    return $ NamedSchema (Just "TestObj") $ mempty +      & type_ .~ SwaggerObject+      & properties .~ [("field", Inline t)]+      & required .~ ["field"]++withFieldTests :: Test+withFieldTests = TestList [+    TestLabel "toJSON" testsToJSON+  , TestLabel "fromJSON" testsFromJSON+  , TestLabel "toSchema" testsToSchema+  ]+  where+  testsToJSON = TestList [+      TestLabel "Inline mode: atomic field" $ TestCase $ do +        let expected = object ["a" .= (0 :: Int), "field" .= ("val" :: Text)]+        let actual = toJSON (WithField 0 (TestObj "val") :: WithField "a" Int TestObj)+        expected @=? actual+    , TestLabel "Inline mode: complex field" $ TestCase $ do +        let expected = object [+                "a" .= object ["field" .= ("key" :: Text)]+              , "field" .= ("val" :: Text)]+        let actual = toJSON (WithField (TestObj "key") (TestObj "val") +              :: WithField "a" TestObj TestObj)+        expected @=? actual+    , TestLabel "Wrapper mode: atomic" $ TestCase $ do +        let expected = object ["a" .= (0 :: Int), "value" .= ("val" :: Text)]+        let actual = toJSON (WithField 0 "val" :: WithField "a" Int String)+        expected @=? actual+    , TestLabel "Wrapper mode: array" $ TestCase $ do +        let expected = object ["a" .= (0 :: Int), "value" .= (["val1", "val2"] :: [Text])]+        let actual = toJSON (WithField 0 ["val1", "val2"] :: WithField "a" Int [String])+        expected @=? actual+    , TestLabel "Wrapper mode: complex field" $ TestCase $ do +        let expected = object [+                "a" .= object ["field" .= ("key" :: Text)]+              , "value" .= ("val":: Text)]+        let actual = toJSON (WithField (TestObj "key") "val" :: WithField "a" TestObj String)+        expected @=? actual+    ]+  testsFromJSON = TestList [+      TestLabel "Inline mode: atomic field" $ TestCase $ do +        let Success (expected :: WithField "a" Int TestObj) = fromJSON $ object [+                "a" .= (0 :: Int)+              , "field" .= ("val" :: Text)]+        let actual = WithField 0 (TestObj "val") :: WithField "a" Int TestObj+        expected @=? actual+    , TestLabel "Inline mode: complex field" $ TestCase $ do +        let Success (expected :: WithField "a" TestObj TestObj) = fromJSON $ object [+                "a" .= object ["field" .= ("key" :: Text)]+              , "field" .= ("val" :: Text)]+        let actual = WithField (TestObj "key") (TestObj "val") :: WithField "a" TestObj TestObj+        expected @=? actual+    , TestLabel "Wrapper mode: atomic" $ TestCase $ do +        let Success (expected :: WithField "a" Int String) = fromJSON $ object [+                "a" .= (0 :: Int)+              , "value" .= ("val" :: Text)]+        let actual = WithField 0 "val" :: WithField "a" Int String+        expected @=? actual+    , TestLabel "Wrapper mode: array" $ TestCase $ do +        let Success (expected :: WithField "a" Int [String]) = fromJSON $ object [+                "a" .= (0 :: Int)+              , "value" .= (["val1", "val2"] :: [Text]) ]+        let actual = WithField 0 ["val1", "val2"] :: WithField "a" Int [String]+        expected @=? actual+    , TestLabel "Wrapper mode: complex field" $ TestCase $ do +        let Success (expected :: WithField "a" TestObj String) = fromJSON $ object [+                "a" .= object ["field" .= ("key" :: Text)]+              , "value" .= ("val":: Text)]+        let actual = WithField (TestObj "key") "val" :: WithField "a" TestObj String+        expected @=? actual+    ]+  testsToSchema = TestList [+      TestLabel "Inline mode: atomic field" $ TestCase $ do +        let expected = [+                ("a", Inline $ toSchema (Proxy :: Proxy Int))+              , ("field", Inline $ toSchema (Proxy :: Proxy String))]+        let actual = toSchema (Proxy :: Proxy (WithField "a" Int TestObj))+        expected @=? (actual ^. properties)+    , TestLabel "Inline mode: complex field" $ TestCase $ do +        let expected = [+                ("a", Inline $ toSchema (Proxy :: Proxy TestObj))+              , ("field", Inline $ toSchema (Proxy :: Proxy String))]+        let actual = toSchema (Proxy :: Proxy (WithField "a" TestObj TestObj))+        expected @=? (actual ^. properties)+    , TestLabel "Wrapper mode: atomic" $ TestCase $ do +        let expected = [+                ("a", Inline $ toSchema (Proxy :: Proxy Int))+              , ("value", Inline $ toSchema (Proxy :: Proxy String))]+        let actual = toSchema (Proxy :: Proxy (WithField "a" Int String))+        expected @=? (actual ^. properties)+    , TestLabel "Wrapper mode: array" $ TestCase $ do +        let expected = [+                ("a", Inline $ toSchema (Proxy :: Proxy Int))+              , ("value", Inline $ toSchema (Proxy :: Proxy [String]))]+        let actual = toSchema (Proxy :: Proxy (WithField "a" Int [String]))+        expected @=? (actual ^. properties)+    , TestLabel "Wrapper mode: complex field" $ TestCase $ do +        let expected = [+                ("a", Inline $ toSchema (Proxy :: Proxy TestObj))+              , ("value", Inline $ toSchema (Proxy :: Proxy String))]+        let actual = toSchema (Proxy :: Proxy (WithField "a" TestObj String))+        expected @=? (actual ^. properties)+    ]++data TestObj1 = TestObj1 !Text +  deriving (Eq, Show)++instance ToJSON TestObj1 where +  toJSON (TestObj1 t) = object ["field1" .= t]+instance FromJSON TestObj1 where +  parseJSON (Object o) = TestObj1 <$> o .: "field1"+  parseJSON _ = mzero+instance ToSchema TestObj1 where +  declareNamedSchema prx = do +    t <- declareSchema (Proxy :: Proxy Text)+    return $ NamedSchema (Just "TestObj1") $ mempty +      & type_ .~ SwaggerObject+      & properties .~ [("field1", Inline t)]+      & required .~ ["field1"]++data TestObj2 = TestObj2 !Text +  deriving (Eq, Show)++instance ToJSON TestObj2 where +  toJSON (TestObj2 t) = object ["field2" .= t]+instance FromJSON TestObj2 where +  parseJSON (Object o) = TestObj2 <$> o .: "field2"+  parseJSON _ = mzero+instance ToSchema TestObj2 where +  declareNamedSchema prx = do +    t <- declareSchema (Proxy :: Proxy Text)+    return $ NamedSchema (Just "TestObj2") $ mempty +      & type_ .~ SwaggerObject+      & properties .~ [("field2", Inline t)]+      & required .~ ["field2"]++withFieldsTests :: Test+withFieldsTests = TestList [+    TestLabel "toJSON" testsToJSON+  , TestLabel "fromJSON" testsFromJSON+  , TestLabel "toSchema" testsToSchema+  ]+  where+  testsToJSON = TestList [+      TestLabel "Inline mode" $ TestCase $ do +        let expected = object [+                "field1" .= ("val1" :: Text)+              , "field2" .= ("val2" :: Text) ]+        let actual = toJSON (WithFields (TestObj1 "val1") (TestObj2 "val2"))+        expected @=? actual+    , TestLabel "Wrapper mode: first" $ TestCase $ do +        let expected = object [+                "injected" .= ("val1" :: Text)+              , "field2" .= ("val2" :: Text) ]+        let actual = toJSON (WithFields ("val1" :: Text) (TestObj2 "val2"))+        expected @=? actual+    , TestLabel "Wrapper mode: second" $ TestCase $ do +        let expected = object [+                "field1" .= ("val1" :: Text)+              , "value" .= ("val2" :: Text) ]+        let actual = toJSON (WithFields (TestObj1 "val1") ("val2" :: Text))+        expected @=? actual+    , TestLabel "Wrapper mode: both" $ TestCase $ do +        let expected = object [+                "injected" .= ("val1" :: Text)+              , "value" .= ("val2" :: Text) ]+        let actual = toJSON (WithFields ("val1" :: Text) ("val2" :: Text))+        expected @=? actual+    ]+  testsFromJSON = TestList [+      TestLabel "Inline mode" $ TestCase $ do +        let Success (expected :: WithFields TestObj1 TestObj2) = fromJSON $ object [+                "field1" .= ("val1" :: Text)+              , "field2" .= ("val2" :: Text)]+        let actual = WithFields (TestObj1 "val1") (TestObj2 "val2")+        expected @=? actual+    , TestLabel "Wrapper mode: first" $ TestCase $ do +        let Success (expected :: WithFields Text TestObj2) = fromJSON $ object [+                "injected" .= ("val1" :: Text)+              , "field2" .= ("val2" :: Text)]+        let actual = WithFields ("val1" :: Text) (TestObj2 "val2")+        expected @=? actual+    , TestLabel "Wrapper mode: second" $ TestCase $ do +        let Success (expected :: WithFields TestObj1 Text) = fromJSON $ object [+                "field1" .= ("val1" :: Text)+              , "value" .= ("val2" :: Text)]+        let actual = WithFields (TestObj1 "val1") ("val2" :: Text)+        expected @=? actual+    , TestLabel "Wrapper mode: both" $ TestCase $ do +        let Success (expected :: WithFields Text Text) = fromJSON $ object [+                "injected" .= ("val1" :: Text)+              , "value" .= ("val2" :: Text)]+        let actual = WithFields ("val1" :: Text) ("val2" :: Text)+        expected @=? actual+    ]+  testsToSchema = TestList [+      TestLabel "Inline mode" $ TestCase $ do +        let expected = [+                ("field1", Inline $ toSchema (Proxy :: Proxy Text))+              , ("field2", Inline $ toSchema (Proxy :: Proxy Text))]+        let actual = toSchema (Proxy :: Proxy (WithFields TestObj1 TestObj2))+        expected @=? (actual ^. properties)+    , TestLabel "Wrapper mode: first" $ TestCase $ do +        let expected = [+                ("injected", Inline $ toSchema (Proxy :: Proxy Text))+              , ("field2", Inline $ toSchema (Proxy :: Proxy Text))]+        let actual = toSchema (Proxy :: Proxy (WithFields Text TestObj2))+        expected @=? (actual ^. properties)+    , TestLabel "Wrapper mode: second" $ TestCase $ do +        let expected = [+                ("field1", Inline $ toSchema (Proxy :: Proxy Text))+              , ("value", Inline $ toSchema (Proxy :: Proxy Text))]+        let actual = toSchema (Proxy :: Proxy (WithFields TestObj1 Text))+        expected @=? (actual ^. properties)+    , TestLabel "Wrapper mode: both" $ TestCase $ do +        let expected = [+                ("injected", Inline $ toSchema (Proxy :: Proxy Text))+              , ("value", Inline $ toSchema (Proxy :: Proxy Text))]+        let actual = toSchema (Proxy :: Proxy (WithFields Text Text))+        expected @=? (actual ^. properties)+    ]