swagger2-0.4: src/Data/Swagger/Schema/Internal.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Data.Swagger.Schema.Internal where
import Control.Lens
import Data.Aeson
import Data.Char
import Data.HashMap.Strict (HashMap)
import Data.HashSet (HashSet)
import Data.Int
import Data.IntSet (IntSet)
import Data.IntMap (IntMap)
import Data.Map (Map)
import Data.Monoid
import Data.Proxy
import Data.Scientific (Scientific)
import Data.Set (Set)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.Time
import Data.Word
import GHC.Generics
import Data.Swagger.Internal
import Data.Swagger.Lens
-- | A @'Schema'@ with an optional name.
-- This name can be used in references.
type NamedSchema = (Maybe String, Schema)
unnamed :: Schema -> NamedSchema
unnamed schema = (Nothing, schema)
named :: String -> Schema -> NamedSchema
named name schema = (Just name, schema)
-- | Convert a type into @'Schema'@.
--
-- An example type and instance:
--
-- @
-- {-\# LANGUAGE OverloadedStrings \#-} -- allows to write 'T.Text' literals
-- {-\# LANGUAGE OverloadedLists \#-} -- allows to write 'Map' as list
--
-- import Control.Lens
--
-- data Coord = Coord { x :: Double, y :: Double }
--
-- instance ToSchema Coord where
-- toNamedSchema = (Just \"Coord\", mempty
-- & schemaType .~ SchemaObject
-- & schemaProperties .~
-- [ ("x", toSchemaRef (Proxy :: Proxy Double))
-- , ("y", toSchemaRef (Proxy :: Proxy Double))
-- ]
-- & schemaRequired .~ [ "x", "y" ]
-- @
--
-- Instead of manually writing your @'ToSchema'@ instance you can
-- use a default generic implementation of @'toNamedSchema'@.
--
-- To do that, simply add @deriving 'Generic'@ clause to your datatype
-- and declare a @'ToSchema'@ instance for your datatype without
-- giving definition for @'toNamedSchema'@.
--
-- For instance, the previous example can be simplified into this:
--
-- @
-- {-\# LANGUAGE DeriveGeneric \#-}
--
-- import GHC.Generics (Generic)
--
-- data Coord = Coord { x :: Double, y :: Double } deriving Generic
--
-- instance ToSchema Coord
-- @
class ToSchema a where
-- | Convert a type into an optionally named schema.
toNamedSchema :: proxy a -> NamedSchema
default toNamedSchema :: (Generic a, GToSchema (Rep a)) => proxy a -> NamedSchema
toNamedSchema = genericToNamedSchema defaultSchemaOptions
-- | Get type's schema name according to its @'ToSchema'@ instance.
schemaName :: ToSchema a => proxy a -> Maybe String
schemaName = fst . toNamedSchema
-- | Convert a type into a schema.
toSchema :: ToSchema a => proxy a -> Schema
toSchema = snd . toNamedSchema
-- | Convert a type into a referenced schema if possible.
-- Only named schemas can be references, nameless schemas are inlined.
toSchemaRef :: ToSchema a => proxy a -> Referenced Schema
toSchemaRef proxy = case toNamedSchema proxy of
(Just name, _) -> Ref (Reference ("#/definitions/" <> T.pack name))
(_, schema) -> Inline schema
class GToSchema (f :: * -> *) where
gtoNamedSchema :: SchemaOptions -> proxy f -> Schema -> NamedSchema
gtoSchema :: GToSchema f => SchemaOptions -> proxy f -> Schema -> Schema
gtoSchema opts proxy = snd . gtoNamedSchema opts proxy
instance {-# OVERLAPPABLE #-} ToSchema a => ToSchema [a] where
toNamedSchema _ = unnamed $ mempty
& schemaType .~ SchemaArray
& schemaItems ?~ SchemaItemsObject (toSchemaRef (Proxy :: Proxy a))
instance {-# OVERLAPPING #-} ToSchema String where
toNamedSchema _ = unnamed $ mempty & schemaType .~ SchemaString
instance ToSchema Bool where
toNamedSchema _ = unnamed $ mempty & schemaType .~ SchemaBoolean
instance ToSchema Integer where
toNamedSchema _ = unnamed $ mempty & schemaType .~ SchemaInteger
instance ToSchema Int where toNamedSchema = unnamed . toSchemaBoundedIntegral
instance ToSchema Int8 where toNamedSchema = unnamed . toSchemaBoundedIntegral
instance ToSchema Int16 where toNamedSchema = unnamed . toSchemaBoundedIntegral
instance ToSchema Int32 where toNamedSchema = unnamed . toSchemaBoundedIntegral
instance ToSchema Int64 where toNamedSchema = unnamed . toSchemaBoundedIntegral
instance ToSchema Word where toNamedSchema = unnamed . toSchemaBoundedIntegral
instance ToSchema Word8 where toNamedSchema = unnamed . toSchemaBoundedIntegral
instance ToSchema Word16 where toNamedSchema = unnamed . toSchemaBoundedIntegral
instance ToSchema Word32 where toNamedSchema = unnamed . toSchemaBoundedIntegral
instance ToSchema Word64 where toNamedSchema = unnamed . toSchemaBoundedIntegral
instance ToSchema Char where
toNamedSchema _ = unnamed $ mempty
& schemaType .~ SchemaString
& schemaMaxLength ?~ 1
& schemaMinLength ?~ 1
instance ToSchema Scientific where
toNamedSchema _ = unnamed $ mempty & schemaType .~ SchemaNumber
instance ToSchema Double where
toNamedSchema _ = unnamed $ mempty & schemaType .~ SchemaNumber
instance ToSchema Float where
toNamedSchema _ = unnamed $ mempty & schemaType .~ SchemaNumber
instance ToSchema a => ToSchema (Maybe a) where
toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy a)
instance (ToSchema a, ToSchema b) => ToSchema (Either a b)
instance ToSchema ()
instance (ToSchema a, ToSchema b) => ToSchema (a, b)
instance (ToSchema a, ToSchema b, ToSchema c) => ToSchema (a, b, c)
instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d) => ToSchema (a, b, c, d)
instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e) => ToSchema (a, b, c, d, e)
instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e, ToSchema f) => ToSchema (a, b, c, d, e, f)
instance (ToSchema a, ToSchema b, ToSchema c, ToSchema d, ToSchema e, ToSchema f, ToSchema g) => ToSchema (a, b, c, d, e, f, g)
timeNamedSchema :: String -> String -> NamedSchema
timeNamedSchema name format = (Just name, mempty
& schemaType .~ SchemaString
& schemaFormat ?~ T.pack format
& schemaMinLength ?~ toInteger (length format))
-- |
-- >>> toSchema (Proxy :: Proxy Day) ^. schemaFormat
-- Just "yyyy-mm-dd"
instance ToSchema Day where
toNamedSchema _ = timeNamedSchema "Day" "yyyy-mm-dd"
-- |
-- >>> toSchema (Proxy :: Proxy LocalTime) ^. schemaFormat
-- Just "yyyy-mm-ddThh:MM:ss"
instance ToSchema LocalTime where
toNamedSchema _ = timeNamedSchema "LocalTime" "yyyy-mm-ddThh:MM:ss"
-- |
-- >>> toSchema (Proxy :: Proxy ZonedTime) ^. schemaFormat
-- Just "yyyy-mm-ddThh:MM:ss(Z|+hh:MM)"
instance ToSchema ZonedTime where
toNamedSchema _ = (Just "ZonedTime", mempty
& schemaType .~ SchemaString
& schemaFormat ?~ "yyyy-mm-ddThh:MM:ss(Z|+hh:MM)"
& schemaMinLength ?~ toInteger (length ("yyyy-mm-ddThh:MM:ssZ" :: String)))
instance ToSchema NominalDiffTime where
toNamedSchema _ = toNamedSchema (Proxy :: Proxy Integer)
-- |
-- >>> toSchema (Proxy :: Proxy UTCTime) ^. schemaFormat
-- Just "yyyy-mm-ddThh:MM:ssZ"
instance ToSchema UTCTime where
toNamedSchema _ = timeNamedSchema "UTCTime" "yyyy-mm-ddThh:MM:ssZ"
instance ToSchema T.Text where
toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy String)
instance ToSchema TL.Text where
toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy String)
instance ToSchema IntSet where toNamedSchema _ = toNamedSchema (Proxy :: Proxy (Set Int))
-- | NOTE: This schema does not account for the uniqueness of keys.
instance ToSchema a => ToSchema (IntMap a) where
toNamedSchema _ = toNamedSchema (Proxy :: Proxy [(Int, a)])
instance ToSchema a => ToSchema (Map String a) where
toNamedSchema _ = unnamed $ mempty
& schemaType .~ SchemaObject
& schemaAdditionalProperties ?~ toSchema (Proxy :: Proxy a)
instance ToSchema a => ToSchema (Map T.Text a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy (Map String a))
instance ToSchema a => ToSchema (Map TL.Text a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy (Map String a))
instance ToSchema a => ToSchema (HashMap String a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy (Map String a))
instance ToSchema a => ToSchema (HashMap T.Text a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy (Map String a))
instance ToSchema a => ToSchema (HashMap TL.Text a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy (Map String a))
instance ToSchema a => ToSchema (Set a) where
toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy [a])
& schemaUniqueItems ?~ True
instance ToSchema a => ToSchema (HashSet a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy (Set a))
instance ToSchema All where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy Bool)
instance ToSchema Any where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy Bool)
instance ToSchema a => ToSchema (Sum a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy a)
instance ToSchema a => ToSchema (Product a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy a)
instance ToSchema a => ToSchema (First a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy a)
instance ToSchema a => ToSchema (Last a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy a)
instance ToSchema a => ToSchema (Dual a) where toNamedSchema _ = unnamed $ toSchema (Proxy :: Proxy a)
-- | Options that specify how to encode your type to Swagger schema.
data SchemaOptions = SchemaOptions
{ -- | Function applied to field labels. Handy for removing common record prefixes for example.
fieldLabelModifier :: String -> String
-- | Function applied to constructor tags which could be handy for lower-casing them for example.
, constructorTagModifier :: String -> String
-- | Function applied to datatype name.
, datatypeNameModifier :: String -> String
-- | If @'True'@ the constructors of a datatype, with all nullary constructors,
-- will be encoded to a string enumeration schema with the constructor tags as possible values.
, allNullaryToStringTag :: Bool
-- | If @'True'@ direct subschemas will be referenced if possible (rather than inlined).
-- Note that this option does not influence nested schemas, e.g. for these types
--
-- @
-- data Object = Object String deriving Generic
-- instance ToSchema Object
--
-- newtype Objects = Objects [Object] deriving Generic
-- instance ToSchema Objects where
-- toNamedSchema = genericToNamedSchema defaultSchemaOptions
-- { useReferences = False }
-- @
--
-- Schema for @Objects@ __will not__ inline @Object@ schema because
-- it is nested in a @[]@ schema.
, useReferences :: Bool
-- | Hide the field name when a record constructor has only one field, like a newtype.
, unwrapUnaryRecords :: Bool
}
-- | Default encoding @'SchemaOptions'@.
--
-- @
-- 'SchemaOptions'
-- { 'fieldLabelModifier' = id
-- , 'constructorTagModifier' = id
-- , 'datatypeNameModifier' = id
-- , 'allNullaryToStringTag' = True
-- , 'useReferences' = True
-- , 'unwrapUnaryRecords' = False
-- }
-- @
defaultSchemaOptions :: SchemaOptions
defaultSchemaOptions = SchemaOptions
{ fieldLabelModifier = id
, constructorTagModifier = id
, datatypeNameModifier = id
, allNullaryToStringTag = True
, useReferences = True
, unwrapUnaryRecords = False
}
-- | Default schema for @'Bounded'@, @'Integral'@ types.
toSchemaBoundedIntegral :: forall a proxy. (Bounded a, Integral a) => proxy a -> Schema
toSchemaBoundedIntegral _ = mempty
& schemaType .~ SchemaInteger
& schemaMinimum ?~ fromInteger (toInteger (minBound :: a))
& schemaMaximum ?~ fromInteger (toInteger (maxBound :: a))
-- | Default generic named schema for @'Bounded'@, @'Integral'@ types.
genericToNamedSchemaBoundedIntegral :: forall a d f proxy.
( Bounded a, Integral a
, Generic a, Rep a ~ D1 d f, Datatype d)
=> SchemaOptions -> proxy a -> NamedSchema
genericToNamedSchemaBoundedIntegral opts proxy
= (gdatatypeSchemaName opts (Proxy :: Proxy d), toSchemaBoundedIntegral proxy)
-- | A configurable generic @'Schema'@ creator.
genericToSchema :: (Generic a, GToSchema (Rep a)) => SchemaOptions -> proxy a -> Schema
genericToSchema opts = snd . genericToNamedSchema opts
-- | A configurable generic @'NamedSchema'@ creator.
-- This function applied to @'defaultSchemaOptions'@
-- is used as the default for @'toNamedSchema'@
-- when the type is an instance of @'Generic'@.
genericToNamedSchema :: forall a proxy. (Generic a, GToSchema (Rep a)) => SchemaOptions -> proxy a -> NamedSchema
genericToNamedSchema opts _ = gtoNamedSchema opts (Proxy :: Proxy (Rep a)) mempty
gdatatypeSchemaName :: forall proxy d. Datatype d => SchemaOptions -> proxy d -> Maybe String
gdatatypeSchemaName opts _ = case name of
(c:_) | isAlpha c && isUpper c -> Just name
_ -> Nothing
where
name = datatypeNameModifier opts (datatypeName (Proxy3 :: Proxy3 d f a))
nullarySchema :: Schema
nullarySchema = mempty
& schemaType .~ SchemaArray
& schemaEnum ?~ [ toJSON () ]
instance GToSchema U1 where
gtoNamedSchema _ _ _ = unnamed nullarySchema
instance (GToSchema f, GToSchema g) => GToSchema (f :*: g) where
gtoNamedSchema opts _ = unnamed . gtoSchema opts (Proxy :: Proxy f) . gtoSchema opts (Proxy :: Proxy g)
instance (Datatype d, GToSchema f) => GToSchema (D1 d f) where
gtoNamedSchema opts _ s = (name, gtoSchema opts (Proxy :: Proxy f) s)
where
name = gdatatypeSchemaName opts (Proxy :: Proxy d)
instance {-# OVERLAPPABLE #-} GToSchema f => GToSchema (C1 c f) where
gtoNamedSchema opts _ = unnamed . gtoSchema opts (Proxy :: Proxy f)
-- | Single field constructor.
instance (Selector s, GToSchema f) => GToSchema (C1 c (S1 s f)) where
gtoNamedSchema opts _ s
| unwrapUnaryRecords opts = fieldSchema
| otherwise =
case schema ^. schemaItems of
Just (SchemaItemsArray [_]) -> fieldSchema
_ -> unnamed schema
where
schema = gtoSchema opts (Proxy :: Proxy (S1 s f)) s
fieldSchema = gtoNamedSchema opts (Proxy :: Proxy f) s
gtoSchemaRef :: GToSchema f => SchemaOptions -> proxy f -> Referenced Schema
gtoSchemaRef opts proxy = case gtoNamedSchema opts proxy mempty of
(Just name, _)
| useReferences opts -> Ref (Reference ("#/definitions/" <> T.pack name))
(_, schema) -> Inline schema
appendItem :: Referenced Schema -> Maybe SchemaItems -> Maybe SchemaItems
appendItem x Nothing = Just (SchemaItemsArray [x])
appendItem x (Just (SchemaItemsArray xs)) = Just (SchemaItemsArray (x:xs))
appendItem _ _ = error "GToSchema.appendItem: cannot append to SchemaItemsObject"
withFieldSchema :: forall proxy s f. (Selector s, GToSchema f) => SchemaOptions -> proxy s f -> Bool -> Schema -> Schema
withFieldSchema opts _ isRequiredField schema
| T.null fieldName = schema
& schemaType .~ SchemaArray
& schemaItems %~ appendItem fieldSchemaRef
| otherwise = schema
& schemaType .~ SchemaObject
& schemaProperties . at fieldName ?~ fieldSchemaRef
& if isRequiredField
then schemaRequired %~ (fieldName :)
else id
where
fieldName = T.pack (fieldLabelModifier opts (selName (Proxy3 :: Proxy3 s f p)))
fieldSchemaRef = gtoSchemaRef opts (Proxy :: Proxy f)
-- | Optional record fields.
instance {-# OVERLAPPING #-} (Selector s, ToSchema c) => GToSchema (S1 s (K1 i (Maybe c))) where
gtoNamedSchema opts _ = unnamed . withFieldSchema opts (Proxy2 :: Proxy2 s (K1 i (Maybe c))) False
-- | Record fields.
instance {-# OVERLAPPABLE #-} (Selector s, GToSchema f) => GToSchema (S1 s f) where
gtoNamedSchema opts _ = unnamed . withFieldSchema opts (Proxy2 :: Proxy2 s f) True
instance ToSchema c => GToSchema (K1 i c) where
gtoNamedSchema _ _ _ = toNamedSchema (Proxy :: Proxy c)
instance (GSumToSchema f, GSumToSchema g) => GToSchema (f :+: g) where
gtoNamedSchema opts _ s
| allNullaryToStringTag opts && allNullary = unnamed (toStringTag sumSchema)
| otherwise = unnamed sumSchema
where
(All allNullary, sumSchema) = gsumToSchema opts (Proxy :: Proxy (f :+: g)) s
toStringTag schema = mempty
& schemaType .~ SchemaString
& schemaEnum ?~ map toJSON (schema ^.. schemaProperties.ifolded.asIndex)
type AllNullary = All
class GSumToSchema f where
gsumToSchema :: SchemaOptions -> proxy f -> Schema -> (AllNullary, Schema)
instance (GSumToSchema f, GSumToSchema g) => GSumToSchema (f :+: g) where
gsumToSchema opts _ = gsumToSchema opts (Proxy :: Proxy f) `after` gsumToSchema opts (Proxy :: Proxy g)
where
(f `after` g) s = (a <> b, s'')
where
(a, s') = f s
(b, s'') = g s'
gsumConToSchema :: forall c f proxy. Constructor c =>
Bool -> Referenced Schema -> SchemaOptions -> proxy (C1 c f) -> Schema -> (AllNullary, Schema)
gsumConToSchema isNullary tagSchemaRef opts _ schema = (All isNullary, schema
& schemaType .~ SchemaObject
& schemaProperties . at tag ?~ tagSchemaRef
& schemaMaxProperties ?~ 1
& schemaMinProperties ?~ 1)
where
tag = T.pack (constructorTagModifier opts (conName (Proxy3 :: Proxy3 c f p)))
instance {-# OVERLAPPABLE #-} (Constructor c, GToSchema f) => GSumToSchema (C1 c f) where
gsumToSchema opts = gsumConToSchema False tagSchemaRef opts
where
tagSchemaRef = gtoSchemaRef opts (Proxy :: Proxy (C1 c f))
instance Constructor c => GSumToSchema (C1 c U1) where
gsumToSchema opts = gsumConToSchema True tagSchemaRef opts
where
tagSchemaRef = gtoSchemaRef opts (Proxy :: Proxy (C1 c U1))
instance (Constructor c, Selector s, GToSchema f) => GSumToSchema (C1 c (S1 s f)) where
gsumToSchema opts = gsumConToSchema False tagSchemaRef opts
where
tagSchemaRef = gtoSchemaRef opts (Proxy :: Proxy (C1 c (S1 s f)))
data Proxy2 a b = Proxy2
data Proxy3 a b c = Proxy3