Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements null value suppression and adds unit tests for it #17

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions Data/Aeson/Encode/Pretty.hs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ module Data.Aeson.Encode.Pretty (
-- |Serves as an order-preserving (non-)sort function. Re-exported from
-- "Data.Monoid".
compare,
-- |Sort keys in their natural order, i.e. by comparing character codes.
-- |Sort keys in standard "ASCIIbetical" order, i.e. by comparing character codes.
-- Re-exported from the Prelude and "Data.Ord"
keyOrder
) where
Expand All @@ -70,13 +70,16 @@ import qualified Data.Vector as V (toList)
data PState = PState { pstIndent :: Int
, pstLevel :: Int
, pstSort :: [(Text, Value)] -> [(Text, Value)]
, pstNullValues :: Bool -- ^ allow keys with null values in the output
}

data Config = Config
{ confIndent :: Int
-- ^ Indentation spaces per level of nesting
, confCompare :: Text -> Text -> Ordering
-- ^ Function used to sort keys in objects
, confNullValues :: Bool
-- ^ Set to 'False' to suppress object pairs with null values. Compare to <http://hackage.haskell.org/package/aeson/docs/Data-Aeson-TH.html#v:omitNothingFields 'omitNothingFields'>
}

-- |Sort keys by their order of appearance in the argument list.
Expand All @@ -90,11 +93,11 @@ keyOrder ks = comparing $ \k -> fromMaybe maxBound (elemIndex k ks)


-- |The default configuration: indent by four spaces per level of nesting, do
-- not sort objects by key.
-- not sort objects by key, and preserve keys with null values.
--
-- > defConfig = Config { confIndent = 4, confCompare = mempty }
-- > defConfig = Config { confIndent = 4, confCompare = mempty, confNullValues = True }
defConfig :: Config
defConfig = Config { confIndent = 4, confCompare = mempty }
defConfig = Config { confIndent = 4, confCompare = mempty, confNullValues = True }

-- |A drop-in replacement for aeson's 'Aeson.encode' function, producing
-- JSON-ByteStrings for human readers.
Expand All @@ -120,15 +123,19 @@ encodePrettyToTextBuilder = encodePrettyToTextBuilder' defConfig
encodePrettyToTextBuilder' :: ToJSON a => Config -> a -> Builder
encodePrettyToTextBuilder' Config{..} = fromValue st . toJSON
where
st = PState confIndent 0 condSort
st = PState confIndent 0 condSort confNullValues
condSort = sortBy (confCompare `on` fst)


fromValue :: PState -> Value -> Builder
fromValue st@PState{..} = go
where
go (Array v) = fromCompound st ("[","]") fromValue (V.toList v)
go (Object m) = fromCompound st ("{","}") fromPair (pstSort (H.toList m))
go (Object m) = fromCompound st ("{","}") fromPair (pstSort filtered_pairs)
where original_pairs = H.toList m
filtered_pairs = if pstNullValues
then original_pairs
else filter (\p -> (snd p) /= Null) original_pairs
go v = Aeson.encodeToTextBuilder v

fromCompound :: PState
Expand Down
21 changes: 21 additions & 0 deletions aeson-pretty.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,27 @@ executable aeson-pretty
ghc-options: -Wall
ghc-prof-options: -auto-all

test-suite aeson-pretty-tests
hs-source-dirs: test/src
main-is: RunTest.hs

type: exitcode-stdio-1.0
build-depends:
aeson >= 0.6,
aeson-pretty,
base == 4.*,
bytestring >= 0.9,
containers,
filepath,
HUnit,
MissingH,
test-framework,
test-framework-hunit,
utf8-string

ghc-options: -Wall
ghc-prof-options: -auto-all

source-repository head
type: git
location: http://github.com/informatikr/aeson-pretty
1 change: 1 addition & 0 deletions test/data/suppress-nulls/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"bar": null, "foo": "blah"}
4 changes: 4 additions & 0 deletions test/data/suppress-nulls/null-values-allowed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"bar": null,
"foo": "blah"
}
3 changes: 3 additions & 0 deletions test/data/suppress-nulls/null-values-suppressed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"foo": "blah"
}
48 changes: 48 additions & 0 deletions test/src/RunTest.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{-# LANGUAGE OverloadedStrings #-}

import Test.Framework
import Test.Framework.Providers.HUnit

import Data.Aeson
import Data.Aeson.Encode.Pretty
import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString.Lazy.UTF8 as U (toString)
import Data.Map (Map)
import Test.HUnit (assertEqual)
import System.FilePath.Posix
import Data.String.Utils (rstrip)

testDataDir :: FilePath
testDataDir = "test/data/suppress-nulls"

eitherDecodeMap :: IO (Map String (Maybe String))
eitherDecodeMap = do
d <- eitherDecode <$> B.readFile (testDataDir </> "input.json")
case d of
Left err -> error $ "ERROR: " ++ err
Right val -> return val


prettifyMap :: Bool -> Map String (Maybe String) -> String
prettifyMap s m = U.toString $ encodePretty' (Config 4 compare s) m


testEquality :: Bool -> FilePath -> IO ()
testEquality suppress data_filename = do
vals <- eitherDecodeMap
let pretty_output_computed = prettifyMap suppress vals
reference_output_file_content <- readFile $ testDataDir </> data_filename
let pretty_output_expected = rstrip $ reference_output_file_content

assertEqual "Checking equality..." pretty_output_expected pretty_output_computed


tests = [
testGroup "Null value suppression" [
testCase "nulls-allowed" $ testEquality True "null-values-allowed.json"
, testCase "nulls-suppressed" $ testEquality False "null-values-suppressed.json"
]
]


main = defaultMain tests