packages feed

json-assertions (empty) → 1.0.0

raw patch · 4 files changed

+233/−0 lines, 4 filesdep +aesondep +basedep +indexedsetup-changed

Dependencies added: aeson, base, indexed, indexed-free, lens, lens-aeson, text

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013, Oliver Charles++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Oliver Charles nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ json-assertions.cabal view
@@ -0,0 +1,26 @@+name: json-assertions+version: 1.0.0+synopsis: Test that your (Aeson) JSON encoding matches your expectations+homepage: http://github.com/ocharles/json-assertions.git+license: BSD3+license-file: LICENSE+author: Oliver Charles+maintainer: ollie@ocharles.org.uk+copyright: Oliver Charles (c) 2013+category: Testing+build-type: Simple+cabal-version: >=1.10++library+  exposed-modules:     Test.JSON.Assertions+  build-depends:+    aeson >= 0.6,+    base >= 4.5 && < 5,+    indexed >= 0.1,+    indexed-free >= 0.3,+    lens >= 3.10,+    lens-aeson >= 0.1.2,+    text >= 0.11.3.1+  hs-source-dirs:      src+  default-language:    Haskell2010+  ghc-options: -O2 -Wall
+ src/Test/JSON/Assertions.hs view
@@ -0,0 +1,175 @@+{-# LANGUAGE GADTs #-}++{-|+@json-assertions@ is a library for validating that your JSON encoding matches +what you are actually expecting. It does this by providing you with a DSL to +traverse a JSON document at the same time as you traverse the value that was +encoded. As you traverse the JSON document, you are building up assertions (by +asserting that you expect certain keys and array indices to exist), and you can +also add your own assertions to check the contents of object properties.++'JSONTest' is an indexed monad, so you will need to enable @RebindableSyntax@ +and bring indexed monadic bind into scope:++> {-# LANGUAGE RebindableSyntax #-}+> import Prelude hiding (Monad(..))+> import Control.Monad.Indexed ((>>>=), ireturn)+> import Test.JSON.Assertions+> import Data.Aeson+>+> return :: a -> JSONTest i i a+> return = ireturn+>+> (>>=) :: m i j a -> (a -> m j k b) -> m i k b+> (>>=) = (>>>=)++You can now write tests as an action in the 'JSONTest' monad. The first index +is the type of the object you wish to encode, and the second parameter is the +type that the test ends in. For example, consider the following:++> data Person = Person { personName :: String }+> instance ToJSON Person where+>   toJSON p = object [ "name" .= personName p ]++We can write a test to check that the JSON encoding of a @Person@'s name is+correct:++> personTest :: JSONTest Person String String+> personTest = do+>   expectedName <- key "name"+>   assertEq expectedName++For more information, you may wish to read <http://ocharles.org.uk/blog/posts/2013-11-24-using-indexed-free-monads-to-quickcheck-json.html>.++-}++module Test.JSON.Assertions+    ( -- * Tests and Traversals+      key+    , nth+    , assertEq+    , stop+    , jsonTest++      -- * Test Interpreters+    , testJSON++    , JSONTest+    ) where++import Control.Monad.Indexed (IxFunctor(..), (>>>=))+import Control.MonadPlus.Indexed.Free (IxFree(..))+import Data.Monoid (First)++import qualified Control.Lens as Lens+import qualified Control.Lens.Aeson as Aeson+import qualified Data.Aeson as Aeson+import qualified Data.Text as Text++--------------------------------------------------------------------------------+data JSONF i j a where+  Key :: String -> (i -> j) -> (j -> a) -> JSONF i j a+  Index :: Int -> (i -> j) -> (j -> a) -> JSONF i j a+  Assert :: (Aeson.Value -> Either String ()) -> a -> JSONF i i a+  Stop :: JSONF i () a++instance IxFunctor JSONF where+  imap g (Key keyS f k) = Key keyS f (g . k)+  imap g (Index n f k) = Index n f (g . k)+  imap f (Assert p k) = Assert p (f k)+  imap _ Stop = Stop+++--------------------------------------------------------------------------------+type JSONTest = IxFree JSONF+++--------------------------------------------------------------------------------+-- | Traverse into the value underneath a specific key in the JSON structure. +-- The return value is the value inside the Haskell value - that is, the result +-- applying the associated morphism.+key :: String   -- ^ JSON Key+    -> (i -> j) -- ^ An associated morphism into a substructure of the test environment+    -> JSONTest i j j+key k f = Free (Key k f Pure)+++--------------------------------------------------------------------------------+-- | Traverse the specific index of a JSON array.+-- The return value is the value inside the Haskell value - that is, the result +-- applying the associated morphism.+nth :: Int      -- ^ JSON array index+    -> (i -> j) -- ^ An associated morphism into a substructure of the test environment+    -> JSONTest i j j+nth i f = Free (Index i f Pure)+++--------------------------------------------------------------------------------+-- | Assert that the current JSON value is exactly equal to the result of+-- calling 'Aeson.toJSON' on a value.+assertEq :: Aeson.ToJSON a => a -> JSONTest i i ()+assertEq expected =+  let expectedJSON = Aeson.toJSON expected+      p actual | actual == expectedJSON = Right ()+               | otherwise = Left $ unlines+                               [ "Expected: " ++ show expectedJSON+                               , "     Got: " ++ show actual+                               ]+  in Free (Assert p (Pure ()))+++--------------------------------------------------------------------------------+-- | Using 'stop' discards the indices in the monad, which can help when you+-- need to 'isum' multiple tests that end in different states.+stop :: JSONTest a () r+stop = Free Stop+++--------------------------------------------------------------------------------+-- | Finalize a 'JSONTest' by calling 'stop' at the end.+jsonTest :: JSONTest i j a -> JSONTest i () a+jsonTest = (>>>= const stop)++--------------------------------------------------------------------------------+-- | Run a 'JSONTest' against a Haskell value that can be encoded to JSON. +-- Returns a list of strings describing the failed assertions, or the empty list+-- if all assertions were satisfied.+testJSON :: Aeson.ToJSON i => JSONTest i j a -> i -> [String]+testJSON tests env = go tests (Aeson.toJSON env) env "subject"+ + where++  go :: JSONTest i j a -> Aeson.Value -> i -> String -> [String]+ +  go (Pure _) _ _ _ = []+ +  go (Free (Key keyS f k)) actual expected descr =+    tryLens (Aeson.key (Text.pack keyS)) f actual expected k $+      descr ++ "[\"" ++ keyS ++ "\"]"++  go (Free (Index n f k)) actual expected descr =+    tryLens (Aeson.nth n) f actual expected k $+      descr ++ " failed to match any targets"++  go (Free (Assert p k)) actual expected descr =+    either+      (return . ((descr ++ " failed assertion\n") ++))+      (const $ go k actual expected descr)+      (p actual)++  go (Free Stop) _ _ _ = []++  go (Plus steps) actual expected descr =+    concatMap (\s -> go s actual expected descr) steps++  tryLens :: Lens.Getting (First Aeson.Value) Aeson.Value Aeson.Value+          -> (i -> j) -> Aeson.Value+          -> i -> (j -> JSONTest j k a)+          -> String+          -> [String]++  tryLens l f actual expected k path =+    case Lens.preview l actual of+      Nothing -> [path ++ " failed to match any targets"]+      Just matched ->+        go (k (f expected)) matched (f expected) path