autodocodec-swagger2-0.0.0.0: src/Autodocodec/Swagger/Schema.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Autodocodec.Swagger.Schema where
import Autodocodec
import Control.Monad
import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap
import Data.List
import Data.Proxy
import Data.Scientific
import Data.Swagger as Swagger
import Data.Swagger.Declare as Swagger
import Data.Text (Text)
-- | Use a type's 'codec' to implement 'declareNamedSchema'.
declareNamedSchemaViaCodec :: HasCodec value => Proxy value -> Declare (Definitions Schema) NamedSchema
declareNamedSchemaViaCodec proxy = declareNamedSchemaVia codec proxy
-- | Use a given 'codec' to implement 'declareNamedSchema'.
declareNamedSchemaVia :: JSONCodec value -> Proxy value -> Declare (Definitions Schema) NamedSchema
declareNamedSchemaVia c' Proxy = go c'
where
go :: ValueCodec input output -> Declare (Definitions Schema) NamedSchema
go = \case
NullCodec ->
pure $
NamedSchema Nothing $
mempty
{ _schemaParamSchema =
mempty
{ _paramSchemaType = Just SwaggerNull
}
}
BoolCodec mname -> NamedSchema mname <$> declareSchema (Proxy :: Proxy Bool)
StringCodec mname -> NamedSchema mname <$> declareSchema (Proxy :: Proxy Text)
NumberCodec mname mBounds -> do
s <- declareSchema (Proxy :: Proxy Scientific)
let addNumberBounds NumberBounds {..} s_ =
s_
{ _schemaParamSchema =
(_schemaParamSchema s_)
{ _paramSchemaMinimum = Just numberBoundsLower,
_paramSchemaMaximum = Just numberBoundsUpper
}
}
pure $ NamedSchema mname $ maybe id addNumberBounds mBounds s
HashMapCodec c -> do
itemsSchema <- go c
itemsSchemaRef <- declareSpecificNamedSchemaRef itemsSchema
pure $
NamedSchema Nothing $
mempty
{ _schemaAdditionalProperties = Just $ AdditionalPropertiesSchema $ _namedSchemaSchema <$> itemsSchemaRef,
_schemaParamSchema =
mempty
{ _paramSchemaType = Just SwaggerObject
}
}
MapCodec c -> do
itemsSchema <- go c
itemsSchemaRef <- declareSpecificNamedSchemaRef itemsSchema
pure $
NamedSchema Nothing $
mempty
{ _schemaAdditionalProperties = Just $ AdditionalPropertiesSchema $ _namedSchemaSchema <$> itemsSchemaRef,
_schemaParamSchema =
mempty
{ _paramSchemaType = Just SwaggerObject
}
}
ValueCodec ->
pure $
NamedSchema
Nothing
mempty
{ _schemaAdditionalProperties = Just $ AdditionalPropertiesAllowed True
}
ArrayOfCodec mname c -> do
itemsSchema <- go c
itemsSchemaRef <- declareSpecificNamedSchemaRef itemsSchema
pure $
NamedSchema mname $
mempty
{ _schemaParamSchema =
mempty
{ _paramSchemaType = Just SwaggerArray,
_paramSchemaItems = Just $ SwaggerItemsObject $ _namedSchemaSchema <$> itemsSchemaRef
}
}
ObjectOfCodec mname oc -> do
ss <- goObject oc
pure $ NamedSchema mname $ combineObjectSchemas ss
EqCodec val valCodec ->
pure $
NamedSchema Nothing $
mempty
{ _schemaParamSchema = mempty {_paramSchemaEnum = Just [toJSONVia valCodec val]}
}
BimapCodec _ _ c -> go c
EitherCodec c1 c2 -> do
ns1 <- go c1
let s1 = _namedSchemaSchema ns1
ns2 <- go c2
let s2 = _namedSchemaSchema ns2
pure $ NamedSchema Nothing $ combineSchemaOr s1 s2
CommentCodec t c -> do
NamedSchema mName s <- go c
pure $ NamedSchema mName $ addDoc t s
ReferenceCodec n c -> do
d <- look
case InsOrdHashMap.lookup n d of
Nothing -> do
-- Insert a dummy to prevent an infinite loop.
let dummy = mempty
let (d', ns) = runDeclare (go c) (InsOrdHashMap.insert n dummy d)
-- Override the dummy once we actually know what the result will be.
declare $ InsOrdHashMap.insert n (_namedSchemaSchema ns) d'
pure ns
Just s -> pure $ NamedSchema (Just n) s
goObject :: ObjectCodec input output -> Declare (Definitions Schema) [Schema]
goObject = \case
RequiredKeyCodec key vs mDoc -> do
ns <- go vs
ref <- declareSpecificNamedSchemaRef ns
pure
[ mempty
{ _schemaRequired = [key],
_schemaProperties = [(key, addMDoc mDoc . _namedSchemaSchema <$> ref)],
_schemaParamSchema = mempty {_paramSchemaType = Just SwaggerObject}
}
]
OptionalKeyCodec key vs mDoc -> do
ns <- go vs
ref <- declareSpecificNamedSchemaRef ns
pure
[ mempty
{ _schemaProperties = [(key, addMDoc mDoc . _namedSchemaSchema <$> ref)],
_schemaParamSchema = mempty {_paramSchemaType = Just SwaggerObject}
}
]
OptionalKeyWithDefaultCodec key vs defaultValue mDoc -> do
ns <- go vs
ref <- declareSpecificNamedSchemaRef ns
pure
[ mempty
{ _schemaProperties = [(key, addMDoc mDoc . _namedSchemaSchema <$> ref)],
_schemaParamSchema =
mempty
{ _paramSchemaDefault = Just $ toJSONVia vs defaultValue,
_paramSchemaType = Just SwaggerObject
}
}
]
OptionalKeyWithOmittedDefaultCodec key vs defaultValue mDoc -> goObject (OptionalKeyWithDefaultCodec key vs defaultValue mDoc)
PureCodec _ -> pure []
EitherCodec oc1 oc2 -> do
ss1 <- goObject oc1
ss2 <- goObject oc2
pure [combineSchemaOr (combineObjectSchemas ss1) (combineObjectSchemas ss2)]
ApCodec oc1 oc2 -> do
ss1 <- goObject oc1
ss2 <- goObject oc2
pure $ ss1 ++ ss2
BimapCodec _ _ oc -> goObject oc
addMDoc :: Maybe Text -> Schema -> Schema
addMDoc = maybe id addDoc
addDoc :: Text -> Schema -> Schema
addDoc doc s =
s
{ _schemaDescription = case _schemaDescription s of
Nothing -> Just doc
Just doc' -> Just $ doc <> "\n" <> doc'
}
combineObjectSchemas :: [Schema] -> Schema
combineObjectSchemas = mconcat
combineSchemaOr :: Schema -> Schema -> Schema
combineSchemaOr s1 s2 =
let ps1 = _schemaParamSchema s1
ps2 = _schemaParamSchema s2
-- Swagger 2 doesn't support sum types so we have to work around that here.
--
-- We support a few cases:
--
-- * Enum: If both of them have the enum field set
-- * Two obects: If both of them have the object type set.
--
-- If none of these cases match, we just take an overapproximation of
-- the schema: one which lets any value through.
overApproximation =
mempty
{ _schemaAdditionalProperties = Just $ AdditionalPropertiesAllowed True
}
in case (,) <$> _paramSchemaEnum ps1 <*> _paramSchemaEnum ps2 of
(Just (es1, es2)) ->
mempty
{ _schemaParamSchema =
mempty
{ _paramSchemaEnum = Just $ es1 ++ es2,
_paramSchemaType =
case (,) <$> _paramSchemaType ps1 <*> _paramSchemaType ps2 of
Just (t1, t2)
| t1 == t2 -> Just t1
| otherwise -> Nothing
Nothing -> Nothing
}
}
Nothing ->
case (,) <$> _paramSchemaType ps1 <*> _paramSchemaType ps2 of
Just (SwaggerObject, SwaggerObject) ->
mempty
{ _schemaRequired = _schemaRequired s1 `intersect` _schemaRequired s2,
_schemaProperties = InsOrdHashMap.union (_schemaProperties s1) (_schemaProperties s2),
_schemaParamSchema = mempty {_paramSchemaType = Just SwaggerObject}
}
Just (a, b)
| a == b -> mempty {_schemaParamSchema = mempty {_paramSchemaType = Just a}}
| otherwise -> overApproximation
_ -> overApproximation
declareSpecificNamedSchemaRef :: Swagger.NamedSchema -> Declare (Definitions Schema) (Referenced NamedSchema)
declareSpecificNamedSchemaRef namedSchema =
fmap (NamedSchema (_namedSchemaName namedSchema))
<$> declareSpecificSchemaRef (_namedSchemaName namedSchema) (_namedSchemaSchema namedSchema)
declareSpecificSchemaRef :: Maybe Text -> Swagger.Schema -> Declare (Definitions Schema) (Referenced Schema)
declareSpecificSchemaRef mName s =
case mName of
Nothing -> pure $ Inline s
Just n -> do
known <- looks (InsOrdHashMap.member n)
when (not known) $ declare $ InsOrdHashMap.singleton n s
pure $ Ref (Reference n)