packages feed

aeson-schemas-1.3.2: test/Tests/SchemaQQ.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeApplications #-}

module Tests.SchemaQQ where

import qualified Data.Text as Text
import Test.Tasty
import Test.Tasty.HUnit
import Text.RawString.QQ (r)

import Tests.SchemaQQ.TH
import TestUtils (testParseError)

test :: TestTree
test = testGroup "`schema` quasiquoter"
  [ testValidSchemas
  , testInvalidSchemas
  , testKeys
  ]

testValidSchemas :: TestTree
testValidSchemas = testGroup "Valid schemas"
  [ testCase "Object with Bool field" $
      assertMatches
        [schemaRep| { a: Bool } |]
        [r| SchemaObject { "a": Bool } |]

  , testCase "Object with Int field" $
      assertMatches
        [schemaRep| { a: Int } |]
        [r| SchemaObject { "a": Int } |]

  , testCase "Object with Double field" $
      assertMatches
        [schemaRep| { foo123: Double } |]
        [r| SchemaObject { "foo123": Double } |]

  , testCase "Object with Text field" $
      assertMatches
        [schemaRep| { some_text: Text } |]
        [r| SchemaObject { "some_text": Text } |]

  , testCase "Object with a field with a custom type" $
      assertMatches
        [schemaRep| { status: Status } |]
        [r| SchemaObject { "status": Status } |]

  , testCase "Object with a field with a Maybe type" $
      assertMatches
        [schemaRep| { a: Maybe Bool } |]
        [r| SchemaObject { "a": Maybe Bool } |]

  , testCase "Object with a field with a Try type" $
      assertMatches
        [schemaRep| { a: Try Bool } |]
        [r| SchemaObject { "a": Try Bool } |]

  , testCase "Object with a nested object" $
      assertMatches
        [schemaRep| { a: { b: Int } } |]
        [r| SchemaObject { "a": { "b": Int } } |]

  , testCase "Object with a nullable nested object" $
      assertMatches
        [schemaRep| { a: Maybe { b: Int } } |]
        [r| SchemaObject { "a": Maybe { "b": Int } } |]

  , testCase "Object with a list of nested objects" $
      assertMatches
        [schemaRep| { a: List { b: Int } } |]
        [r| SchemaObject { "a": List { "b": Int } } |]

  , testCase "Object with an imported schema" $
      assertMatches
        [schemaRep| { user: #UserSchema } |]
        [r| SchemaObject { "user": { "name": Text } } |]

  , testCase "Object with a qualified imported schema" $
      assertMatches
        [schemaRep| { user: #(Tests.SchemaQQ.TH.UserSchema) } |]
        [r| SchemaObject { "user": { "name": Text } } |]

  , testCase "Object with an imported schema that uses a non-imported type" $
      assertMatches
        [schemaRep| { a: #SchemaWithHiddenImport } |]
        [r| SchemaObject { "a": { "a": CBool } } |]

  , testCase "Object with an imported schema that itself imports a schema" $
      assertMatches
        [schemaRep| { a: #WithUser } |]
        [r| SchemaObject { "a": { "user": { "name": Text } } } |]

  , testCase "Object with an extended schema" $
      assertMatches
        [schemaRep| { a: Int, #ExtraSchema } |]
        [r| SchemaObject { "a": Int, "extra": Text } |]

  , testCase "Object with a qualified extended schema" $
      assertMatches
        [schemaRep| { a: Int, #(Tests.SchemaQQ.TH.ExtraSchema) } |]
        [r| SchemaObject { "a": Int, "extra": Text } |]

  , testCase "Object with an extended schema that uses a non-imported type" $
      assertMatches
        [schemaRep| { #SchemaWithHiddenImport } |]
        [r| SchemaObject { "a": CBool } |]

  , testCase "Object with an extended schema with a shadowed key" $
      assertMatches
        [schemaRep| { extra: Bool, #ExtraSchema } |]
        [r| SchemaObject { "extra": Bool } |]

  , testCase "Object with a qualified extended schema with a shadowed key" $
      assertMatches
        [schemaRep| { extra: Bool, #(Tests.SchemaQQ.TH.ExtraSchema) } |]
        [r| SchemaObject { "extra": Bool } |]

  , testCase "Object with a union field" $
      assertMatches
        [schemaRep| { a: List Int | Text } |]
        [r| SchemaObject { "a": ( List Int | Text ) } |]

  , testCase "Object with a union field with a group" $
      assertMatches
        [schemaRep| { a: List (Int | Text) } |]
        [r| SchemaObject { "a": List ( Int | Text ) } |]

  , testCase "Object with a phantom key for an object" $
      assertMatches
        [schemaRep| { [a]: { b: Int } } |]
        [r| SchemaObject { [a]: { "b": Int } } |]

  , testCase "Object with a phantom key for a Maybe" $
      assertMatches
        [schemaRep| { [a]: Maybe { b: Int } } |]
        [r| SchemaObject { [a]: Maybe { "b": Int } } |]

  , testCase "Object with a phantom key for a Try" $
      assertMatches
        [schemaRep| { [a]: Try { b: Int } } |]
        [r| SchemaObject { [a]: Try { "b": Int } } |]

  , testCase "Object with a phantom key for a non-object Try" $
      assertMatches
        [schemaRep| { [a]: Try Bool } |]
        [r| SchemaObject { [a]: Try Bool } |]

  , testCase "Object with a phantom key for a union of valid schemas" $
      assertMatches
        [schemaRep| { [a]: { b: Int } | Int } |]
        [r| SchemaObject { [a]: ( { "b": Int } | Int ) } |]
  ]

testInvalidSchemas :: TestTree
testInvalidSchemas = testGroup "Invalid schemas"
  [ testCase "Object with a duplicate key" $
      [schemaErr| { a: Int, a: Bool } |] @?= "Key 'a' specified multiple times"

  , testCase "Object with a duplicate phantom key" $
      [schemaErr| { a: Int, [a]: { b: Bool } } |] @?= "Key 'a' specified multiple times"

  , testCase "Object with a duplicate key from extending" $
      [schemaErr| { #ExtraSchema, #ExtraSchema2 } |] @?= "Key 'extra' declared in multiple imported schemas"

  , testCase "Quasiquoter defining a non-object" $
      [schemaErr| List { a: Int } |] @?= "`schema` definition must be an object"

  , testCase "Object with a field with an unknown type" $
      [schemaErr| { a: HelloWorld } |] @?= "Unknown type: HelloWorld"

  , testCase "Object extending a non-schema" $
      [schemaErr| { #Int } |] @?= "'GHC.Types.Int' is not a Schema"

  , testCase "Object importing an unknown schema" $
      [schemaErr| { foo: #FooSchema } |] @?= "Unknown schema: FooSchema"

  , testCase "Object extending an unknown schema" $
      [schemaErr| { #FooSchema } |] @?= "Unknown schema: FooSchema"

  , testCase "Object with a phantom key for a scalar" $
      [schemaErr| { [a]: Int } |] @?= "Invalid schema for 'a': SchemaScalar Int"

  , testCase "Object with a phantom key for a list" $
      [schemaErr| { [a]: List Int } |] @?= "Invalid schema for 'a': SchemaList Int"

  , testCase "Object with a phantom key for a non-object Maybe" $
      [schemaErr| { [a]: Maybe Int } |] @?= "Invalid schema for 'a': SchemaMaybe Int"

  , testCase "Object with a phantom key for an invalid union" $
      [schemaErr| { [a]: Bool | Int } |] @?= "Invalid schema for 'a': SchemaUnion ( Bool | Int )"
  ]

testKeys :: TestTree
testKeys = testGroup "Keys in schemas"
  [ testCase "Quoted key same as plain key" $
      [schemaRep| { a: Int } |] @?= [schemaRep| { "a": Int } |]

  , testParseError "Key with invalid character" "schemaqq_key_with_invalid_character.golden"
      [schemaErr| { "a:b": Int } |]

  , testCase "Key with escaped invalid character" $
      assertMatches
        [schemaRep| { "a\:b": Int } |]
        [r| SchemaObject { "a:b": Int } |]

  , testParseError "Key with trailing escape" "schemaqq_key_with_trailing_escape.golden"
      [schemaErr| { "a\": Int } |]

  , testCase "Quoted key that starts with '//'" $
      assertMatches
        [schemaRep| { "//a": { b: Int } } |]
        [r| SchemaObject { "//a": { "b": Int } } |]

  , testCase "Phantom key that starts with '//'" $
      assertMatches
        [schemaRep| { [//a]: { b: Int } } |]
        [r| SchemaObject { [//a]: { "b": Int } } |]
  ]

{- Helpers -}

assertMatches :: String -> String -> Assertion
assertMatches a b = strip a @?= strip b
  where
    strip = Text.unpack . Text.strip . Text.pack