diff --git a/openapi3-code-generator/src/OpenAPI/Generate/Model.hs b/openapi3-code-generator/src/OpenAPI/Generate/Model.hs index d73b323..5a1ad6e 100644 --- a/openapi3-code-generator/src/OpenAPI/Generate/Model.hs +++ b/openapi3-code-generator/src/OpenAPI/Generate/Model.hs @@ -419,6 +419,8 @@ defineOneOfSchema schemaName description allSchemas discriminator = do let paramName = mkName "val" body = case discriminator of + -- When discriminator is present, we'll use it to figure out + -- which constructor to parse Just disc -> do let fnArgName = mkName "obj" discriminatorPropertyName = mkName "propertyName" @@ -426,6 +428,9 @@ defineOneOfSchema schemaName description allSchemas discriminator = do schema <- allSchemas guard $ E.isLeft $ extractSchemaWithFixedValue FixedValueStrategyExclude schema pure schema + -- Map from refs in anyOf to schema name. We'll use this to + -- discover the actual relevant constructor names (and + -- their indexes which we may need if the setting is on). schemaLookupFromRef = Map.fromList $ do (n, schema) <- nonFixedSchemas case schema of @@ -434,6 +439,9 @@ defineOneOfSchema schemaName description allSchemas discriminator = do oneOfSchemaRefs = do (ref, (_, name')) <- Map.toList schemaLookupFromRef pure (name', ref) + -- Per the spec: if `mapping` exists we'll use the keys + -- specified there. If not, we'll use the schema component + -- names. propertyNamesWithReferences = maybe oneOfSchemaRefs Map.toList $ OAS.discriminatorObjectMapping disc let mkMatchedCase (propName, fullRef) = case Map.lookup fullRef schemaLookupFromRef of @@ -443,9 +451,12 @@ defineOneOfSchema schemaName description allSchemas discriminator = do parseConstructor constructorName = [|($(varE constructorName) <$> Aeson.parseJSON $(varE paramName))|] [match (litP $ stringL $ T.unpack propName) (normalB [|$(parseConstructor $ haskellifyConstructor $ schemaName <> haskellifyPartialConstructor caseName <> suffix)|]) []] matchedCases = propertyNamesWithReferences >>= mkMatchedCase + -- Fallback. If we don't parse the property, we'll fail. unmatchedCase = match (varP $ mkName "_unmatched") (normalB [|fail "No match for discriminator property"|]) [] propertyCases = matchedCases <> [unmatchedCase] getDiscProp = [|$(varE fnArgName) Aeson..:? $(stringE $ T.unpack $ OAS.discriminatorObjectPropertyName disc)|] + -- Annotate as `Text` otherwise we get ambiguity since we're + -- just matching on string literals. annotatedDiscriminatorPropertyName = [|$(varE discriminatorPropertyName) :: Text|] withObjectLamda = [| @@ -457,6 +468,8 @@ defineOneOfSchema schemaName description allSchemas discriminator = do $(caseE annotatedDiscriminatorPropertyName propertyCases) |] [|Aeson.withObject $(stringE $ T.unpack schemaName) $(lam1E (varP fnArgName) withObjectLamda) $(varE paramName)|] + -- When there's no discriminator, we'll just try to parse the + -- different variants, in the order they're defined. Nothing -> do constructorNames' <- sequence constructorNames let resultExpr = diff --git a/specifications/petstore-running-example.yaml b/specifications/petstore-running-example.yaml index df4caa7..be53112 100644 --- a/specifications/petstore-running-example.yaml +++ b/specifications/petstore-running-example.yaml @@ -610,6 +610,46 @@ paths: "$ref": "#/components/schemas/NullableAndOptionalTest" description: Nullable and optional values as input required: true + "/discriminator/fish/{fishType}": + get: + summary: Get a fish by discriminator type + operationId: getFishByType + parameters: + - name: fishType + in: path + required: true + description: The fish type (guppie, minnow, shark) + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Fish" + '400': + description: Invalid fish type + "/discriminator/lizard/{lizardType}": + get: + summary: Get a lizard by discriminator type + operationId: getLizardByType + parameters: + - name: lizardType + in: path + required: true + description: The lizard type (gecko, gilaMonster) + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Lizard" + '400': + description: Invalid lizard type /query/array/form: get: operationId: queryArrayForm @@ -836,6 +876,70 @@ components: type: number B: type: number + Fish: + type: object + oneOf: + - $ref: '#/components/schemas/Guppie' + - $ref: '#/components/schemas/Minnow' + - $ref: '#/components/schemas/Shark' + discriminator: + propertyName: fishType + mapping: + guppie: '#/components/schemas/Guppie' + minnow: '#/components/schemas/Minnow' + shark: '#/components/schemas/Shark' + Guppie: + type: object + properties: + color: + type: string + fishType: + type: string + required: + - fishType + Minnow: + type: object + properties: + color: + type: string + fishType: + type: string + required: + - fishType + Shark: + type: object + properties: + teethRemaining: + type: integer + fishType: + type: string + required: + - fishType + Lizard: + type: object + oneOf: + - $ref: '#/components/schemas/Gecko' + - $ref: '#/components/schemas/GilaMonster' + discriminator: + propertyName: discriminatorTag + Gecko: + type: object + properties: + hasTail: + type: boolean + discriminatorTag: + type: string + required: + - discriminatorTag + GilaMonster: + type: object + properties: + hasTail: + type: boolean + discriminatorTag: + type: string + required: + - discriminatorTag requestBodies: Pet: content: diff --git a/testing/level3/mock-server/src/Lib.hs b/testing/level3/mock-server/src/Lib.hs index b32d7ea..4a2c467 100644 --- a/testing/level3/mock-server/src/Lib.hs +++ b/testing/level3/mock-server/src/Lib.hs @@ -11,10 +11,9 @@ where import Control.Monad (when) import Data.Aeson -import qualified Data.Aeson.Key as Key -import qualified Data.Aeson.KeyMap as KeyMap import Data.Aeson.TH import Data.Char +import qualified Data.HashMap.Strict as KeyMap import Network.Wai import Network.Wai.Handler.Warp import Servant @@ -36,6 +35,8 @@ type API = :<|> "echo" :> Header "User-Agent" String :> Get '[JSON] String :<|> "echo" :> Capture "path" String :> Get '[JSON] String :<|> "nullable-optional" :> Capture "mode" String :> ReqBody '[JSON] Value :> Post '[JSON] Value + :<|> "discriminator" :> "fish" :> Capture "fishType" String :> Get '[JSON] Value + :<|> "discriminator" :> "lizard" :> Capture "lizardType" String :> Get '[JSON] Value startApp :: IO () startApp = run 8887 app @@ -47,7 +48,7 @@ api :: Proxy API api = Proxy server :: Server API -server = pure getInventory :<|> findByStatus :<|> addPet :<|> userAgentEcho :<|> pathEcho :<|> checkNullableAndOptional +server = pure getInventory :<|> findByStatus :<|> addPet :<|> userAgentEcho :<|> pathEcho :<|> checkNullableAndOptional :<|> getFish :<|> getLizard getInventory :: Value getInventory = object ["pet1" .= Number 23, "pet2" .= Number 2] @@ -77,110 +78,146 @@ pathEcho = pure checkNullableAndOptional :: String -> Value -> Handler Value checkNullableAndOptional "filled" (Object map) = do - when (KeyMap.lookup (Key.fromText "requiredNonNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "requiredNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "optionalNonNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "optionalNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedRequiredNonNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedRequiredNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedOptionalNonNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedOptionalNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("requiredNonNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("requiredNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("optionalNonNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("optionalNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("referencedRequiredNonNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("referencedRequiredNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("referencedOptionalNonNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("referencedOptionalNullable") map /= Just "x") $ throwError err400 pure $ Object $ KeyMap.fromList - [ (Key.fromText "requiredNonNullable", "x"), - (Key.fromText "requiredNullable", "x"), - (Key.fromText "optionalNonNullable", "x"), - (Key.fromText "optionalNullable", "x"), - (Key.fromText "referencedRequiredNonNullable", "x"), - (Key.fromText "referencedRequiredNullable", "x"), - (Key.fromText "referencedOptionalNonNullable", "x"), - (Key.fromText "referencedOptionalNullable", "x") + [ ("requiredNonNullable", "x"), + ("requiredNullable", "x"), + ("optionalNonNullable", "x"), + ("optionalNullable", "x"), + ("referencedRequiredNonNullable", "x"), + ("referencedRequiredNullable", "x"), + ("referencedOptionalNonNullable", "x"), + ("referencedOptionalNullable", "x") ] checkNullableAndOptional "emptyNull" (Object map) = do - when (KeyMap.lookup (Key.fromText "requiredNonNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "requiredNullable") map /= Just Null) $ throwError err400 - when (KeyMap.lookup (Key.fromText "optionalNonNullable") map /= Nothing) $ throwError err400 - when (KeyMap.lookup (Key.fromText "optionalNullable") map /= Just Null) $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedRequiredNonNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedRequiredNullable") map /= Just Null) $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedOptionalNonNullable") map /= Nothing) $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedOptionalNullable") map /= Just Null) $ throwError err400 + when (KeyMap.lookup ("requiredNonNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("requiredNullable") map /= Just Null) $ throwError err400 + when (KeyMap.lookup ("optionalNonNullable") map /= Nothing) $ throwError err400 + when (KeyMap.lookup ("optionalNullable") map /= Just Null) $ throwError err400 + when (KeyMap.lookup ("referencedRequiredNonNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("referencedRequiredNullable") map /= Just Null) $ throwError err400 + when (KeyMap.lookup ("referencedOptionalNonNullable") map /= Nothing) $ throwError err400 + when (KeyMap.lookup ("referencedOptionalNullable") map /= Just Null) $ throwError err400 pure $ Object $ KeyMap.fromList - [ (Key.fromText "requiredNonNullable", "x"), - (Key.fromText "requiredNullable", Null), - (Key.fromText "optionalNullable", Null), - (Key.fromText "referencedRequiredNonNullable", "x"), - (Key.fromText "referencedRequiredNullable", Null), - (Key.fromText "referencedOptionalNullable", Null) + [ ("requiredNonNullable", "x"), + ("requiredNullable", Null), + ("optionalNullable", Null), + ("referencedRequiredNonNullable", "x"), + ("referencedRequiredNullable", Null), + ("referencedOptionalNullable", Null) ] checkNullableAndOptional "emptyAbsent" (Object map) = do - when (KeyMap.lookup (Key.fromText "requiredNonNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "requiredNullable") map /= Just Null) $ throwError err400 - when (KeyMap.lookup (Key.fromText "optionalNonNullable") map /= Nothing) $ throwError err400 - when (KeyMap.lookup (Key.fromText "optionalNullable") map /= Nothing) $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedRequiredNonNullable") map /= Just "x") $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedRequiredNullable") map /= Just Null) $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedOptionalNonNullable") map /= Nothing) $ throwError err400 - when (KeyMap.lookup (Key.fromText "referencedOptionalNullable") map /= Nothing) $ throwError err400 + when (KeyMap.lookup ("requiredNonNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("requiredNullable") map /= Just Null) $ throwError err400 + when (KeyMap.lookup ("optionalNonNullable") map /= Nothing) $ throwError err400 + when (KeyMap.lookup ("optionalNullable") map /= Nothing) $ throwError err400 + when (KeyMap.lookup ("referencedRequiredNonNullable") map /= Just "x") $ throwError err400 + when (KeyMap.lookup ("referencedRequiredNullable") map /= Just Null) $ throwError err400 + when (KeyMap.lookup ("referencedOptionalNonNullable") map /= Nothing) $ throwError err400 + when (KeyMap.lookup ("referencedOptionalNullable") map /= Nothing) $ throwError err400 pure $ Object $ KeyMap.fromList - [ (Key.fromText "requiredNonNullable", "x"), - (Key.fromText "requiredNullable", Null), - (Key.fromText "referencedRequiredNonNullable", "x"), - (Key.fromText "referencedRequiredNullable", Null) + [ ("requiredNonNullable", "x"), + ("requiredNullable", Null), + ("referencedRequiredNonNullable", "x"), + ("referencedRequiredNullable", Null) ] checkNullableAndOptional "errorRequiredNonNullableWithNull" (Object map) = pure $ Object $ KeyMap.fromList - [ (Key.fromText "requiredNonNullable", Null), - (Key.fromText "requiredNullable", "x"), - (Key.fromText "optionalNonNullable", "x"), - (Key.fromText "optionalNullable", "x"), - (Key.fromText "referencedRequiredNonNullable", "x"), - (Key.fromText "referencedRequiredNullable", "x"), - (Key.fromText "referencedOptionalNonNullable", "x"), - (Key.fromText "referencedOptionalNullable", "x") + [ ("requiredNonNullable", Null), + ("requiredNullable", "x"), + ("optionalNonNullable", "x"), + ("optionalNullable", "x"), + ("referencedRequiredNonNullable", "x"), + ("referencedRequiredNullable", "x"), + ("referencedOptionalNonNullable", "x"), + ("referencedOptionalNullable", "x") ] checkNullableAndOptional "errorRequiredNonNullableWithAbsence" (Object map) = pure $ Object $ KeyMap.fromList - [ (Key.fromText "requiredNullable", "x"), - (Key.fromText "optionalNonNullable", "x"), - (Key.fromText "optionalNullable", "x"), - (Key.fromText "referencedRequiredNonNullable", "x"), - (Key.fromText "referencedRequiredNullable", "x"), - (Key.fromText "referencedOptionalNonNullable", "x"), - (Key.fromText "referencedOptionalNullable", "x") + [ ("requiredNullable", "x"), + ("optionalNonNullable", "x"), + ("optionalNullable", "x"), + ("referencedRequiredNonNullable", "x"), + ("referencedRequiredNullable", "x"), + ("referencedOptionalNonNullable", "x"), + ("referencedOptionalNullable", "x") ] checkNullableAndOptional "errorRequiredNullable" (Object map) = pure $ Object $ KeyMap.fromList - [ (Key.fromText "requiredNonNullable", "x"), - (Key.fromText "optionalNonNullable", "x"), - (Key.fromText "optionalNullable", "x"), - (Key.fromText "referencedRequiredNonNullable", "x"), - (Key.fromText "referencedRequiredNullable", "x"), - (Key.fromText "referencedOptionalNonNullable", "x"), - (Key.fromText "referencedOptionalNullable", "x") + [ ("requiredNonNullable", "x"), + ("optionalNonNullable", "x"), + ("optionalNullable", "x"), + ("referencedRequiredNonNullable", "x"), + ("referencedRequiredNullable", "x"), + ("referencedOptionalNonNullable", "x"), + ("referencedOptionalNullable", "x") ] checkNullableAndOptional "errorOptionalNonNullable" (Object map) = pure $ Object $ KeyMap.fromList - [ (Key.fromText "requiredNonNullable", "x"), - (Key.fromText "requiredNullable", "x"), - (Key.fromText "optionalNonNullable", Null), - (Key.fromText "optionalNullable", "x"), - (Key.fromText "referencedRequiredNonNullable", "x"), - (Key.fromText "referencedRequiredNullable", "x"), - (Key.fromText "referencedOptionalNonNullable", "x"), - (Key.fromText "referencedOptionalNullable", "x") + [ ("requiredNonNullable", "x"), + ("requiredNullable", "x"), + ("optionalNonNullable", Null), + ("optionalNullable", "x"), + ("referencedRequiredNonNullable", "x"), + ("referencedRequiredNullable", "x"), + ("referencedOptionalNonNullable", "x"), + ("referencedOptionalNullable", "x") ] checkNullableAndOptional _ _ = throwError err500 + +getFish :: String -> Handler Value +getFish "guppie" = + pure $ + object + [ "fishType" .= String "guppie", + "color" .= String "orange" + ] +getFish "minnow" = + pure $ + object + [ "fishType" .= String "minnow", + "color" .= String "silver" + ] +getFish "shark" = + pure $ + object + [ "fishType" .= String "shark", + "teethRemaining" .= Number 42 + ] +getFish _ = throwError err400 + +getLizard :: String -> Handler Value +getLizard "gecko" = + pure $ + object + [ "discriminatorTag" .= String "Gecko", + "hasTail" .= Bool True + ] +getLizard "gilaMonster" = + pure $ + object + [ "discriminatorTag" .= String "GilaMonster", + "hasTail" .= Bool False + ] +getLizard _ = throwError err400 diff --git a/testing/level3/petstore-running-example/src/Lib.hs b/testing/level3/petstore-running-example/src/Lib.hs index 23a4904..a3bc6e3 100644 --- a/testing/level3/petstore-running-example/src/Lib.hs +++ b/testing/level3/petstore-running-example/src/Lib.hs @@ -15,31 +15,39 @@ runAddPet = (mkPet []) { petId = Just 21, petName = Just "Harro", - petStatus = Just PetStatusEnumAvailable + petStatus = Just PetStatus'EnumAvailable } runGetInventory :: (MonadHTTP m) => m (Response GetInventoryResponse) -runGetInventory = getInventoryWithConfiguration defaultConfiguration +runGetInventory = runWithConfiguration defaultConfiguration getInventory runFindPetsByStatus :: (MonadHTTP m) => FindPetsByStatusParametersStatus -> m (Response FindPetsByStatusResponse) -runFindPetsByStatus status = findPetsByStatusWithConfiguration defaultConfiguration [status] +runFindPetsByStatus status = runWithConfiguration defaultConfiguration $ findPetsByStatus [status] runEchoUserAgent :: (MonadHTTP m) => m (Response EchoUserAgentResponse) runEchoUserAgent = - echoUserAgentWithConfiguration $ + runWithConfiguration defaultConfiguration { configApplicationName = "XYZ" } + echoUserAgent runEchoUserAgentWithoutUserAgent :: (MonadHTTP m) => m (Response EchoUserAgentResponse) runEchoUserAgentWithoutUserAgent = - echoUserAgentWithConfiguration $ + runWithConfiguration defaultConfiguration { configIncludeUserAgent = False } + echoUserAgent runEchoPath :: (MonadHTTP m) => EchoPathParametersPath -> m (Response EchoPathResponse) -runEchoPath = echoPathWithConfiguration defaultConfiguration +runEchoPath path = runWithConfiguration defaultConfiguration $ echoPath path runSendAndReceiveNullableAndOptional :: (MonadHTTP m) => Text -> NullableAndOptionalTest -> m (Response SendAndReceiveNullableAndOptionalResponse) -runSendAndReceiveNullableAndOptional = sendAndReceiveNullableAndOptionalWithConfiguration defaultConfiguration +runSendAndReceiveNullableAndOptional mode body = runWithConfiguration defaultConfiguration $ sendAndReceiveNullableAndOptional mode body + +runGetFishByType :: (MonadHTTP m) => Text -> m (Response GetFishByTypeResponse) +runGetFishByType fishType = runWithConfiguration defaultConfiguration $ getFishByType fishType + +runGetLizardByType :: (MonadHTTP m) => Text -> m (Response GetLizardByTypeResponse) +runGetLizardByType lizardType = runWithConfiguration defaultConfiguration $ getLizardByType lizardType diff --git a/testing/level3/petstore-running-example/test/Spec.hs b/testing/level3/petstore-running-example/test/Spec.hs index 96dde2b..b4f0e75 100644 --- a/testing/level3/petstore-running-example/test/Spec.hs +++ b/testing/level3/petstore-running-example/test/Spec.hs @@ -3,9 +3,8 @@ import Control.Exception import Data.Aeson hiding (Null) -import qualified Data.Aeson.Key as Key -import qualified Data.Aeson.KeyMap as KeyMap import Data.Either +import qualified Data.HashMap.Strict as HM import qualified Data.Text as T import Lib import Network.HTTP.Client @@ -22,9 +21,9 @@ main = response <- runGetInventory getResponseBody response `shouldBe` GetInventoryResponse200 - ( KeyMap.fromList - [ (Key.fromText "pet1", Number 23), - (Key.fromText "pet2", Number 2) + ( HM.fromList + [ ("pet1", Number 23), + ("pet2", Number 2) ] ) describe "runAddPet" $ @@ -41,7 +40,7 @@ main = [ (mkPet []) { petId = Just 23, petName = Just "Ted", - petStatus = Just PetStatusEnumPending + petStatus = Just PetStatus'EnumPending } ] describe "runFindPetsByStatus" $ @@ -204,3 +203,61 @@ main = getResponseBody response `shouldBe` SendAndReceiveNullableAndOptionalResponseError "Error in $.optionalNonNullable: parsing Text failed, expected String, but encountered Null" + + describe "Discriminator with mapping" $ do + describe "runGetFishByType" $ do + it "correctly parses guppie variant using discriminator" $ do + response <- runGetFishByType "guppie" + case getResponseBody response of + GetFishByTypeResponse200 (FishGuppie guppie) -> do + guppieFishType guppie `shouldBe` "guppie" + guppieColor guppie `shouldBe` Just "orange" + other -> expectationFailure $ "Expected Guppie variant, got: " ++ show other + + it "correctly parses minnow variant using discriminator" $ do + response <- runGetFishByType "minnow" + case getResponseBody response of + GetFishByTypeResponse200 (FishMinnow minnow) -> do + minnowFishType minnow `shouldBe` "minnow" + minnowColor minnow `shouldBe` Just "silver" + other -> expectationFailure $ "Expected Minnow variant, got: " ++ show other + + it "correctly parses shark variant using discriminator" $ do + response <- runGetFishByType "shark" + case getResponseBody response of + GetFishByTypeResponse200 (FishShark shark) -> do + sharkFishType shark `shouldBe` "shark" + sharkTeethRemaining shark `shouldBe` Just 42 + other -> expectationFailure $ "Expected Shark variant, got: " ++ show other + + it "returns error for invalid fish type" $ do + response <- runGetFishByType "invalidFish" + case getResponseBody response of + GetFishByTypeResponseError _ -> pure () + GetFishByTypeResponse400 -> pure () + other -> expectationFailure $ "Expected error response, got: " ++ show other + + describe "Discriminator without mapping" $ do + describe "runGetLizardByType" $ do + it "correctly parses gecko variant using discriminator (no mapping)" $ do + response <- runGetLizardByType "gecko" + case getResponseBody response of + GetLizardByTypeResponse200 (LizardGecko gecko) -> do + geckoDiscriminatorTag gecko `shouldBe` "Gecko" + geckoHasTail gecko `shouldBe` Just True + other -> expectationFailure $ "Expected Gecko variant, got: " ++ show other + + it "correctly parses gilaMonster variant using discriminator (no mapping)" $ do + response <- runGetLizardByType "gilaMonster" + case getResponseBody response of + GetLizardByTypeResponse200 (LizardGilaMonster gilaMonster) -> do + gilaMonsterDiscriminatorTag gilaMonster `shouldBe` "GilaMonster" + gilaMonsterHasTail gilaMonster `shouldBe` Just False + other -> expectationFailure $ "Expected GilaMonster variant, got: " ++ show other + + it "returns error for invalid lizard type" $ do + response <- runGetLizardByType "invalidLizard" + case getResponseBody response of + GetLizardByTypeResponseError _ -> pure () + GetLizardByTypeResponse400 -> pure () + other -> expectationFailure $ "Expected error response, got: " ++ show other