{-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PackageImports #-} {-# 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 "unordered-containers" 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