packages feed

openapi-hs-4.0.0: test/Data/OpenApi/Schema31Spec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

-- | Round-trip coverage for the OpenAPI 3.1 / JSON Schema 2020-12 additive fields
-- and the @$@-prefixed identification/reference keywords introduced in EP-4.
module Data.OpenApi.Schema31Spec where

import Data.Aeson (Value, decode, encode, object, toJSON, (.=))
import Data.Aeson.QQ.Simple (aesonQQ)
import Data.HashMap.Strict.InsOrd.Compat qualified as InsOrdHashMap
import Data.OpenApi
import Data.OpenApi.Internal
import Data.Text (Text)
import SpecCommon
import Test.Hspec

-- Small helpers to keep the cases terse.
strSchema :: Schema
strSchema = mempty {_schemaType = Just (OpenApiTypeSingle OpenApiString)}

numSchema :: Schema
numSchema = mempty {_schemaType = Just (OpenApiTypeSingle OpenApiNumber)}

spec :: Spec
spec = do
  describe "JSON Schema 2020-12 additive fields" $ do
    context "prefixItems with items:false" $
      mempty
        { _schemaPrefixItems = Just [Inline strSchema, Inline numSchema],
          _schemaItems = Just (OpenApiItemsBoolean False)
        }
        <=> [aesonQQ|
          { "prefixItems": [ {"type":"string"}, {"type":"number"} ]
          , "items": false }
        |]

    context "if/then conditional" $
      mempty
        { _schemaIf = Just (Inline (mempty {_schemaConst = Just "USA"})),
          _schemaThen = Just (Inline strSchema)
        }
        <=> [aesonQQ|
          { "if": {"const":"USA"}, "then": {"type":"string"} }
        |]

    context "const" $
      mempty {_schemaConst = Just (toJSON (42 :: Int))} <=> [aesonQQ| {"const": 42} |]

    context "contains with minContains" $
      mempty
        { _schemaContains = Just (Inline (mempty {_schemaType = Just (OpenApiTypeSingle OpenApiInteger)})),
          _schemaMinContains = Just 1
        }
        <=> [aesonQQ| {"contains": {"type":"integer"}, "minContains": 1} |]

    context "examples (3.1 array form)" $
      mempty {_schemaExamples = Just [toJSON (1 :: Int), toJSON ("x" :: Text)]}
        <=> [aesonQQ| {"examples": [1, "x"]} |]

  describe "$-prefixed identification/reference keywords" $ do
    context "$defs" $
      mempty
        { _schemaDefs = Just (InsOrdHashMap.fromList [("A", Inline strSchema)])
        }
        <=> [aesonQQ| {"$defs": {"A": {"type":"string"}}} |]

    it "$defs emits the literal \"$defs\" key" $ do
      let s = mempty {_schemaDefs = Just (InsOrdHashMap.fromList [("A", Inline strSchema)])}
      toJSON s `shouldBe` object ["$defs" .= object ["A" .= object ["type" .= ("string" :: Text)]]]

    it "$id + sibling $ref stays Inline and round-trips" $ do
      let s = mempty {_schemaId = Just "https://example/s", _schemaRef = Just "#/$defs/A"}
      toJSON s
        `shouldBe` object
          [ "$id" .= ("https://example/s" :: Text),
            "$ref" .= ("#/$defs/A" :: Text)
          ]
      (decode (encode s) :: Maybe Schema) `shouldBe` Just s
      -- As a sub-schema, a $ref that is not a component reference parses to Inline (not Ref):
      (decode (encode s) :: Maybe (Referenced Schema)) `shouldBe` Just (Inline s)

    it "a pure component $ref still parses to Ref" $ do
      (decode "{\"$ref\":\"#/components/schemas/Foo\"}" :: Maybe (Referenced Schema))
        `shouldBe` Just (Ref (Reference "Foo"))

  describe "boolean sub-schemas (read direction)" $ do
    it "decodes contains:true / unevaluatedItems:false to canonical inline schemas" $ do
      let expected =
            mempty
              { _schemaContains = Just (Inline mempty),
                _schemaUnevaluatedItems = Just (Inline (mempty {_schemaNot = Just (Inline mempty)}))
              }
      (decode "{\"contains\":true,\"unevaluatedItems\":false}" :: Maybe Schema)
        `shouldBe` Just expected