packages feed

aeson-schemas-1.3.0: test/Tests/Object/FromJSON.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}

module Tests.Object.FromJSON where

import Data.Aeson (FromJSON(..), Value)
import Data.Aeson.QQ (aesonQQ)
import Data.Aeson.Types (parseEither)
import Data.Proxy (Proxy)
import Test.Tasty
import Test.Tasty.QuickCheck

import Data.Aeson.Schema (Object)
import Tests.Object.FromJSON.TH
import TestUtils (parseProxy, testGolden)
import TestUtils.Arbitrary (ArbitraryObject(..), forAllArbitraryObjects)

test :: TestTree
test = testGroup "FromJSON instance" $
  map runTestCase testCases ++
  [ testProperty "QuickCheck arbitrary Schema" $
      $(forAllArbitraryObjects) $ \(ArbitraryObject proxy v _) ->
        case parseProxy proxy v of
          Right _ -> property ()
          Left e -> error $ "Could not parse: " ++ e
  ]

testCases :: [FromJSONTestCase]
testCases =
 [ CheckValid "Scalar valid"
      [schemaProxy| { foo: Text } |]
      $ \(s :: String) -> [aesonQQ| { "foo": #{s} } |]
  , CheckError "Scalar invalid" "fromjson_scalar_invalid.golden"
      [schemaProxy| { foo: Text } |]
      [aesonQQ| { "foo": 1 } |]

  , CheckValid "Maybe valid"
      [schemaProxy| { foo: Maybe Int } |]
      $ \(x :: Maybe Int) -> [aesonQQ| { "foo": #{x} } |]
  , CheckError "Maybe invalid" "fromjson_maybe_invalid.golden"
      [schemaProxy| { foo: Maybe Int } |]
      [aesonQQ| { "foo": true } |]

  , CheckValid "Try valid with valid parse"
      [schemaProxy| { foo: Try Bool } |]
      $ \(x :: Bool) -> [aesonQQ| { "foo": #{x} } |]
  , CheckValid "Try valid with invalid parse"
      [schemaProxy| { foo: Try Bool } |]
      $ \(s :: String) -> [aesonQQ| { "foo": #{s} } |]

  , CheckValid "List valid"
      [schemaProxy| { foo: List Double } |]
      $ \(xs :: [Double]) -> [aesonQQ| { "foo": #{xs} } |]
  , CheckError "List invalid" "fromjson_list_invalid.golden"
      [schemaProxy| { foo: List Double } |]
      [aesonQQ| { "foo": true } |]
  , CheckError "List invalid inner" "fromjson_list_inner_invalid.golden"
      [schemaProxy| { foo: List Double } |]
      [aesonQQ| { "foo": [true] } |]

  , CheckError "Object invalid" "fromjson_object_invalid.golden"
      [schemaProxy| { foo: Int } |]
      [aesonQQ| 1 |]
  , CheckError "Object invalid in later keys" "fromjson_object_later_keys_invalid.golden"
      [schemaProxy| { foo: Int, bar: Int } |]
      [aesonQQ| { "foo": 1, "bar": true } |]

  , CheckValid "Nested object valid"
      [schemaProxy| { foo: { bar: Int } } |]
      $ \(x :: Int) -> [aesonQQ| { "foo": { "bar": #{x} } } |]
  , CheckError "Nested object invalid" "fromjson_nested_invalid.golden"
      [schemaProxy| { foo: { bar: Int } } |]
      [aesonQQ| { "foo": true } |]
  , CheckError "Nested object invalid inner" "fromjson_nested_inner_invalid.golden"
      [schemaProxy| { foo: { bar: Int } } |]
      [aesonQQ| { "foo": { "bar": true } } |]

  , CheckValid "Union object valid"
      [schemaProxy| { foo: Int | Text } |]
      $ \(x :: Int) -> [aesonQQ| { "foo": #{x} } |]
  , CheckError "Union object invalid" "fromjson_union_invalid.golden"
      [schemaProxy| { foo: Int | Text } |]
      [aesonQQ| { "foo": true } |]

  , CheckValid "Phantom key valid object"
      [schemaProxy| { [foo]: { bar: Int } } |]
      $ \(x :: Int) -> [aesonQQ| { "bar": #{x} } |]
  , CheckValid "Phantom key valid non-object try"
      [schemaProxy| { [foo]: Try Bool } |]
      $ \(b :: Bool) -> [aesonQQ| { "bar": #{b} } |]
  , CheckError "Phantom key invalid" "fromjson_phantom_invalid.golden"
      [schemaProxy| { [foo]: { bar: Int } } |]
      [aesonQQ| 1 |]
  , CheckError "Phantom key missing inner" "fromjson_phantom_inner_missing.golden"
      [schemaProxy| { [foo]: { bar: Int } } |]
      [aesonQQ| { "foo": true } |]
  , CheckError "Phantom key invalid inner" "fromjson_phantom_inner_invalid.golden"
      [schemaProxy| { [foo]: { bar: Int } } |]
      [aesonQQ| { "bar": true } |]

  , CheckError "Decode failure messages are truncated" "fromjson_error_messages_truncate.golden"
      [schemaProxy| { foo: Int } |]
      [aesonQQ|
        {
          "foo": [
            { "bar": 1, "baz": "a" },
            { "bar": 2, "baz": "b" },
            { "bar": 3, "baz": "c" },
            { "bar": 4, "baz": "d" }
          ]
        }
      |]
  ]

{- Helpers -}

data FromJSONTestCase where
  CheckValid
    :: (Arbitrary a, Show a, FromJSON (Object schema))
    => TestName              -- ^ Name of test case
    -> Proxy (Object schema) -- ^ The schema to parse with
    -> (a -> Value)          -- ^ A function that builds a Value that should satisfy the schema
    -> FromJSONTestCase

  CheckError
    :: (FromJSON (Object schema), Show (Object schema))
    => TestName              -- ^ Name of test case
    -> String                -- ^ Name of golden file
    -> Proxy (Object schema) -- ^ The schema to parse with
    -> Value                 -- ^ The value that should fail parsing the given schema
    -> FromJSONTestCase

runTestCase :: FromJSONTestCase -> TestTree
runTestCase = \case
  CheckValid name schema valueGen ->
    testProperty name $ \a ->
      case parse schema (valueGen a) of
        Right _ -> ()
        Left e -> error $ "Unexpected failure: " ++ e

  CheckError name fp schema value ->
    testGolden name fp $
      case parse schema value of
        Right o -> error $ "Unexpectedly parsed: " ++ show o
        Left e -> e

parse :: FromJSON a => Proxy a -> Value -> Either String a
parse _ = parseEither parseJSON