packages feed

schemas-0.4.0.2: test/SchemasSpec.hs

{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ImpredicativeTypes  #-}
{-# LANGUAGE OverloadedLists     #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE PatternSynonyms     #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}
module SchemasSpec where

import           Control.Exception
import           Control.Lens (_Just, _Nothing, _Empty, _Cons)
import           Control.Monad (join)
import           Control.Monad.Trans.Except (Except, ExceptT(..))
import qualified Data.Aeson                 as A
import qualified Data.Coerce
import           Data.Either
import           Data.Foldable
import           Data.Functor.Identity
import           Data.Generics
import qualified Data.List.NonEmpty        as NE
import           Data.Maybe
import           Generators
import           Looper
import           Person
import           Person2
import           Person3
import           Person4
import           Schemas
import qualified Schemas.Attempt            as Attempt
import           Schemas.Internal           (liftAttempt)
import           Schemas.Untyped            (Validators)
import           System.Timeout
import           Test.Hspec
import           Test.Hspec.Runner          (configQuickCheckMaxSuccess, hspecWith, defaultConfig)
import           Test.QuickCheck            (arbitrary, sized, forAll, suchThat)
import           Text.Show.Functions        ()
import           Unions

main :: IO ()
main = hspecWith defaultConfig{configQuickCheckMaxSuccess = Just 10000} spec

listSchema :: HasSchema a => TypedSchema [a]
listSchema = named "list" $ union
  [ ("Nil", alt _Empty)
  , ( "Cons"
    , altWith
      (record $ (,) <$> field "head" fst <*> fieldWith listSchema "tail" snd)
      _Cons
    )
  ]

encodeSpec :: Spec
encodeSpec = do
    it "prims"  $ do
      let encoder = encode
      shouldNotDiverge $ evaluate encoder
      shouldNotDiverge $ evaluate $ encoder True
    it "unions" $ do
      let encoder = encode
      shouldNotDiverge $ evaluate encoder
      shouldNotDiverge $ evaluate $ encoder (Left ())
      shouldNotDiverge $ evaluate $ encoder (Right ())

    it "recursive schemas" $ do
      let encoder = encodeWith listSchema
      shouldNotDiverge $ evaluate encoder
      shouldNotDiverge $ evaluate $ encoder [()]

    it "is the inverse of decoding for canonical schemas" $
      forAll (canonical <$> arbitrary) $ \sc ->
        getSuccess (pure encode >>= decode . ($ sc)) == Just sc

canonical :: Schema -> Schema
canonical = everywhere (mkT simplify)
  where
  simplify (OneOf [x]) = x
  simplify other = other

encodeToSpec :: Spec
encodeToSpec = do
    it "is lazy" $ do
      evaluate (attemptSuccessOrError (encodeToWith (record $ Just <$> field "bottom" fromJust) (Record [makeField "bottom" prim True])) (Nothing :: Maybe Bool))
        `shouldThrow` \(_ :: SomeException) -> True
      let encoded =
            attemptSuccessOrError
              (encodeToWith (record $ Just <$> field "bottom" fromJust) (Record []))
              (Nothing :: Maybe Bool)
      encoded `shouldBe` A.Object []

    it "SomeNone Some" $ do
      let encoded = encodeWith schemaSomeNone (Some ())
      shouldNotDiverge $ evaluate encoded
      encoded `shouldBe` A.Object []

    it "NoneSome Some" $ do
      let encoded = encodeWith schemaNoneSome (Some ())
      shouldNotDiverge $ evaluate encoded
      encoded `shouldBe` A.Object []

    it "SomeNone None" $ do
      let encoded = encodeWith schemaSomeNone (None @(Either () ()))
      shouldNotDiverge $ evaluate encoded
      encoded `shouldBe` A.Object []

    it "NoneSome None" $ do
      let encoded = encodeWith schemaNoneSome (None @(Either () ()))
      shouldNotDiverge $ evaluate encoded
      encoded `shouldBe` A.Object []

    it "Three" $ do
      let encoded = encodeWith (schemaThree schemaNoneSome schema) (Three @(Some ()) @())
      shouldNotDiverge $ evaluate encoded
      encoded `shouldBe` A.Object []

    it "Three" $ do
      let encoded = encodeWith (schemaThree' schema schemaNoneSome) (Three @() @(Some ()))
      shouldNotDiverge $ evaluate encoded
      encoded `shouldBe` A.Object []

    describe "Either" $ do
      let -- A schema supporting both camelCase and lowercase either
          source :: TypedSchema (Either () ())
          source = schema

      it "lowerCase" $ do
        let target = Union [("right", schemaFor @()), ("left", schemaFor @())]
            encoder = encodeToWith source target
            Right f = encoder
        encoder `shouldSatisfy` isRight
        f (Right ()) `shouldBe` A.object [("right", A.Object [])]

      it "camelCase" $ do
        let target = Union [("Right", schemaFor @()), ("Left", schemaFor @())]
            encoder = encodeToWith source target
            Right f = encoder
        f (Right ()) `shouldBe` A.object [("Right", A.Object [])]

    describe "Either (nested)" $ do
      let -- A schema supporting both camelCase and lowercase either
          source :: TypedSchema (((), Either () ()))
          source = schema

          wrap sc = Record [("$1", Field (schemaFor @()) True), ("$2", Field sc True)]

          wrapVal v = A.object [("$1", A.Object []), ("$2", v)]

      it "lowerCase" $ do
        let target = wrap $ Union [("right", schemaFor @()), ("left", schemaFor @())]
            encoder = encodeToWith source target
            Right f = encoder
        encoder `shouldSatisfy` isRight
        f ((),Right ()) `shouldBe` wrapVal (A.object [("right", A.Object [])])

      it "camelCase" $ do
        let target = wrap $ Union [("Right", schemaFor @()), ("Left", schemaFor @())]
            encoder = encodeToWith source target
            Right f = encoder
        encoder `shouldSatisfy` isRight
        f ((),Right ()) `shouldBe` wrapVal (A.object [("Right", A.Object [])])

    describe "canEncode" $ do

      it "Unions of 1 constructor" $ do
        union [("Just", alt (_Just @()))] `shouldBeAbleToEncodeTo` [Union [("Just", Unit)]]


spec :: Spec
spec = do
  describe "encode" encodeSpec
  describe "encodeTo" encodeToSpec
  describe "extractSchema" $ do
    it "Named" $
      shouldNotDiverge $ evaluate $ extractSchema $ schema @Schema
    it "Unions" $
      extractSchema (union [("Just", alt (_Just @())), ("Nothing", alt _Nothing)])
        `shouldBe` [Union [("Nothing", Unit) ,("Just", Unit)]]

  describe "isSubtypeOf" $ do
    it "is reflexive (in absence of OneOf)" $ forAll (sized genSchema `suchThat` (not . hasOneOf)) $ \sc ->
      sc `shouldBeSubtypeOf` sc
    it "subtypes can add fields" $ do
      Record [makeField "a" prim True, makeField "def" prim True]
        `shouldBeSubtypeOf` Record [makeField "def" prim True]
      Record [makeField "a" prim False, makeField "def" prim True]
        `shouldBeSubtypeOf` Record [makeField "def" prim True]
    it "subtypes cannot turn a Required makeField into Optional" $ do
      Record [makeField "a" prim False]
        `shouldNotBeSubtypeOf` Record [makeField "a" prim True]
    it "subtypes can turn an Optional makeField into Required" $ do
      Record [makeField "a" prim True]
        `shouldBeSubtypeOf` Record [makeField "a" prim False]
    it "subtypes can relax the type of a field" $ do
      Record [makeField "a" prim True]
        `shouldBeSubtypeOf` Record [makeField "a" (Array prim) True]
    it "subtypes can relax the type of a constructor field" $ do
      Union [constructor' "a" prim]
        `shouldBeSubtypeOf` Union [constructor' "a" (Array prim)]
    it "subtypes cannot remove Required fields" $ do
      Record [makeField "def" prim True] `shouldNotBeSubtypeOf` Record
        [makeField "def" prim True, makeField "a" prim True]
    it "subtypes can remove Optional fields" $ do
      Record [makeField "def" prim True] `shouldBeSubtypeOf` Record
        [makeField "def" prim True, makeField "a" prim (False)]
    it "subtypes can remove enum choices" $ do
      Enum ["def"] `shouldBeSubtypeOf` Enum ["A", "def"]
    it "subtypes cannot add enum choices" $ do
      Enum ["A", "def"] `shouldNotBeSubtypeOf` Enum ["def"]
    it "subtypes can remove constructors" $ do
      Union [constructor' "B" Unit]
        `shouldBeSubtypeOf` Union [constructor' "A" Unit, constructor' "B" Unit]
    it "subtypes cannot add constructors" $ do
      Union [constructor' "A" prim, constructor' "B" Unit]
        `shouldNotBeSubtypeOf` Union [constructor' "A" (prim)]
    it "subtypes can drop an array" $ do
      prim `shouldBeSubtypeOf` Array prim
    it "subtypes cannot introduce an array" $ do
      Array prim `shouldNotBeSubtypeOf` prim

  describe "HasSchema" $ do
    it "Left is a constructor of Either" $ do
      shouldBeAbleToDecode @(Either () ()) [Union [constructor' "Left" Unit]]
      -- shouldBeAbleToEncode @(Either () ()) [Union [constructor' "Left" Unit]]
    it "left is a constructor of Either too" $ do
      shouldBeAbleToDecode @(Either () ()) [Union [constructor' "left" Unit]]
      -- shouldBeAbleToEncode @(Either () ()) [Union [constructor' "left" Unit]]

  describe "examples" $ do
    describe "Schema" $
      schemaSpec schema (schemaFor @Person2)
    let   person4_v0 = schemaFor @Person4
          person2_v0 = schemaFor @Person2
          person2_v2 = extractSchema (schema @Person2) NE.!! 2
          person3_v0 = schemaFor @Person3
          person4_vPerson3 = person3_v0
          encoder_p4v0 = encodeTo person4_v0
          encoder_p3_to_p4 = encodeTo person4_vPerson3
          encoder_p2v0 = encodeTo person2_v0
          encoder_p3v0 = encodeTo @Person3 person3_v0
          decoder_p2v0 = decodeFrom @Person4 person2_v0
          decoder_p2v2 = decodeFrom person2_v2

    describe "NoneSome Bool" $
      schemaSpec schemaNoneSome (None :: Some Bool)

    describe "SomeNone Bool" $
      schemaSpec schemaSomeNone (None :: Some Bool)

    describe "NoneSome (Either () ())" $
      schemaSpec schemaNoneSome (None :: Some (Either () ()))

    describe "SomeNone (Either () ())" $
      schemaSpec schemaSomeNone (None :: Some (Either () ()))

    describe "Three Bool Int" $
      schemaSpec (schemaThree  schema schema) (Three :: Three Bool Int)

    describe "Three Int Bool" $
      schemaSpec (schemaThree' schema schema) (Three :: Three Int Bool)

    describe "Person" $ do
      schemaSpec schema pepe

    describe "Person2" $ do
      schemaSpec schema pepe2
      it "Person2 < Person" $ do
         shouldBeAbleToEncode @Person2 (extractSchema @Person schema)
         -- shouldBeAbleToDecode @Person (extractSchema @Person2 schema)
      it "pepe2 `as` Person" $ do
        let encoder = encodeTo (schemaFor @Person)
            encoded = attemptSuccessOrError encoder pepe2
        encoder `shouldSatisfy` isRight
        decode (encoded) `shouldBe` Success pepe
      it "pepe `as` Person2" $ do
        let decoder = decodeFrom (schemaFor @Person)
        decoder `shouldSatisfy` isSuccess
        (pure encode >>= getSuccessOrError decoder . ($ pepe))
          `shouldBe` Success pepe2{Person2.education = [Person.studies pepe]}
      it "Person < Person2" $ do
        -- shouldBeAbleToEncode @Person  (extractSchema @Person2 schema)
        shouldBeAbleToDecode @Person2 (extractSchema @Person schema)

    describe "Person3" $ do
      -- disabled because encode diverges and does not support IterT yet
      -- schemaSpec schema pepe3
      it "can show the Person 3 (circular) schema" $
        shouldNotDiverge $ evaluate $ length $ show $ schemaFor @Person3
      it "can compute an encoder for Person3 (circular schema)" $
        shouldNotDiverge $ evaluate encoder_p3v0
      it "can encode a finite example" $ do

        shouldNotDiverge $ evaluate $ encode martin
        shouldNotDiverge $ evaluate $ attemptSuccessOrError encoder_p3v0 martin

    describe "Person4" $ do
      schemaSpec schema pepe4
      let encoded_pepe4 = attemptSuccessOrError encoder_p4v0 pepe4
          encoded_pepe3 = attemptSuccessOrError encoder_p3_to_p4 pepe3{Person3.spouse = Nothing}
          encoded_pepe2 = attemptSuccessOrError encoder_p2v0 pepe2
      it "can compute an encoder for Person4" $ do
        shouldNotDiverge $ evaluate encoder_p4v0
        encoder_p4v0 `shouldSatisfy` isRight
      it "can compute an encoder to Person3 in finite time" $ do
        shouldNotDiverge $ evaluate encoder_p3_to_p4
      it "can compute an encoder to Person2 in finite time" $ do
        shouldNotDiverge $ evaluate encoder_p2v0
      it "can encode a Person4" $ do
        shouldNotDiverge $ evaluate $ A.encode encoded_pepe4
      it "can encode a Person2 as Person4 in finite time" $ do
        shouldNotDiverge $ evaluate $ A.encode encoded_pepe2
      it "can decode a fully defined record with source schema" $ do
        let res = getSuccessOrError (decodeFrom person4_v0) encoded_pepe4
        shouldNotDiverge $ evaluate res
        res `shouldBe` Success pepe4
      it "can decode a fully defined record without source schema" $ do
        let res = decode encoded_pepe4
        shouldNotDiverge $ evaluate res
        res `shouldBe` Success pepe4
      it "cannot construct a Person2 v0 decoder" $
        decoder_p2v0 `shouldSatisfy` isFailure
      it "can construct a Person2 v1 decoder" $
        decoder_p2v2 `shouldSatisfy` isSuccess
      it "can decode a Person2 v1" $ do
        let res = getSuccessOrError decoder_p2v2 encoded_pepe2
            holds = res == Success pepe4
        shouldNotDiverge $ evaluate holds
        shouldNotDiverge $ evaluate $ length $ show res
        res `shouldBe` Success pepe4
    describe "Looper" $ do
      schemaSpec schema looper1

schemaSpec :: forall a. (Eq a, Show a) => TypedSchema a -> a -> Spec
schemaSpec sc ex = do
  let encoder = encodeToWith sc s
      decoder = decodeFromWith sc s
      s = NE.head $ extractSchema sc
      encodedExample = attemptSuccessOrError encoder ex
  it "Can extract untyped schema" $
    shouldNotDiverge $ evaluate s
  it "Can encode itself" $ do
    shouldNotDiverge $ evaluate encoder
    encoder `shouldSatisfy` isRight
  it "Can decode itself" $ do
    shouldNotDiverge $ evaluate decoder
    decoder `shouldSatisfy` isSuccess
  it "Does not diverge decoding bad input" $ do
     let d = join $ Attempt.attemptSuccess $ runResult 1000 $ decodeFromWith sc (NE.head $ extractSchema sc)
     shouldNotDiverge $ evaluate $ d
     shouldNotDiverge $ evaluate $ join $ join $ Attempt.attemptSuccess $ runResult 1000 $ traverse ($ A.String "Foo") d
  it "Roundtrips ex" $ do
    let res = getSuccessOrError decoder encodedExample
    shouldNotDiverge $ evaluate encodedExample
    shouldNotDiverge $ evaluate res
    runResult 1000 res `shouldBe` Right (Just ex)
  it "Roundtrips ex (2)" $ do
    let res = pure (encodeWith sc) >>= decodeWith sc . ($ ex)
    shouldNotDiverge $ evaluate res
    runResult 1000 res `shouldBe` Right (Just ex)

shouldBeSubtypeOf :: HasCallStack => Schema -> Schema -> Expectation
shouldBeSubtypeOf a b = case isSubtypeOf primValidators a b of
  Right _ -> pure ()
  _       -> expectationFailure $ show a <> " should be a subtype of " <> show b

shouldNotBeSubtypeOf :: HasCallStack => Schema -> Schema -> Expectation
shouldNotBeSubtypeOf a b = case isSubtypeOf primValidators a b of
  Right _  -> expectationFailure $ show a <> " should not be a subtype of " <> show b
  _ -> pure ()

shouldDiverge :: (HasCallStack, Show a) => IO a -> Expectation
shouldDiverge act = do
  res <- timeout 1000000 act
  case res of
    Just{} -> expectationFailure "Did not diverge"
    Nothing -> return ()

shouldNotDiverge :: (HasCallStack, Show a) => IO a -> Expectation
shouldNotDiverge act = do
  res <- timeout 1000000 act
  case res of
    Nothing -> error "Did not terminate after 1s"
    Just {} -> return ()

shouldBeAbleToEncode :: forall a . HasCallStack => (HasSchema a) => NE.NonEmpty Schema -> Expectation
shouldBeAbleToEncode = shouldBeAbleToEncodeTo (schema @a)

shouldBeAbleToEncodeTo :: forall a . HasCallStack => TypedSchema a -> NE.NonEmpty Schema -> Expectation
shouldBeAbleToEncodeTo tsc sc = asumEither (fmap (encodeToWith tsc) sc) `shouldSatisfy` isRight

shouldBeAbleToDecode :: forall a . HasCallStack => (HasSchema a) => NE.NonEmpty Schema -> Expectation
shouldBeAbleToDecode sc = asum (fmap (decodeFrom @a) sc) `shouldSatisfy` isSuccess

makeField :: a -> Schema -> Bool -> (a, Field)
makeField n t isReq = (n, Field t isReq)

constructor' :: a -> b -> (a, b)
constructor' n t = (n, t)

prim :: Schema
prim = Prim "A"

primValidators :: Validators
primValidators = validatorsFor @(Schema, Double, Int, Bool)

getSuccessOrError :: Result a -> a
getSuccessOrError =  either (error . show) (fromMaybe (error "too many delays")) . Attempt.runAttempt . runResult 1000

attemptSuccessOrError :: Show e => Either e a -> a
attemptSuccessOrError = either (error.show) id

pattern Success :: a -> Result a
pattern Success x <- (runResult 1000 -> Attempt.Success (Just x))
  where Success x = liftAttempt $ Attempt.Success x

getSuccess :: Result a -> Maybe a
getSuccess = join . Attempt.attemptSuccess . runResult 1000

isSuccess :: Result a -> Bool
isSuccess = isJust . getSuccess

isFailure :: Result a -> Bool
isFailure = not . isSuccess

-- | Parallel 'asum' for 'Either'
asumEither :: forall e a . (Monoid e) => NE.NonEmpty (Either e a) -> Either e a
asumEither = Data.Coerce.coerce asumExcept
  where
    asumExcept :: NE.NonEmpty (Except e a) -> Except e a
    asumExcept = asum