jordan-openapi-0.2.0.0: lib/Jordan/OpenAPI/Internal.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Internal module that provides JSON parser and serializers that convert to OpenAPI documentation.
--
-- Note that this module is full of internal-use-only functions and should probably n ever actually be imported.
module Jordan.OpenAPI.Internal where
import Control.Applicative (Alternative (..))
import Control.Monad (unless, when)
import qualified Data.Aeson.Types as Aeson
import Data.Function (on)
import Data.Functor (void)
import Data.Functor.Contravariant (Contravariant (contramap))
import Data.Functor.Contravariant.Divisible (Divisible (..))
import qualified Data.HashMap.Strict.InsOrd as InsOrd
import Data.Maybe (fromMaybe)
import Data.OpenApi.Declare
import Data.OpenApi.Internal
import Data.OpenApi.Internal.Schema (rename, unname, unnamed)
import Data.OpenApi.Optics
import Data.Proxy (Proxy (..))
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Debug.Trace (trace, traceM, traceShowId)
import GHC.Generics
import GHC.Stack (HasCallStack)
import Jordan.FromJSON.Class
import Jordan.ToJSON.Class
import Network.HTTP.Types.URI (urlEncode)
import Optics.At.Core
import Optics.Iso (non)
import Optics.Operators
import Optics.Optic ((%), (&))
-- | Declare with a phantom type parameter.
newtype ConstDeclare env r a = ConstDeclare {runConstDeclare :: Declare env r}
-- | Fmap ignores argument
instance Functor (ConstDeclare env r) where
fmap _ (ConstDeclare d) = ConstDeclare d
-- | Applicative combines declarations.
instance (Monoid r, Monoid env) => Applicative (ConstDeclare env r) where
pure _ = ConstDeclare $ pure mempty
(ConstDeclare f) <*> (ConstDeclare a) = ConstDeclare $ do
f' <- f
a' <- a
pure $ f' <> a'
-- | Contravariant ignores argument.
instance Contravariant (ConstDeclare env r) where
contramap _ (ConstDeclare d) = ConstDeclare d
instance (Monoid r, Monoid env) => Divisible (ConstDeclare env r) where
divide _ (ConstDeclare l) (ConstDeclare r) = ConstDeclare $ do
l' <- l
r' <- r
pure $ l' <> r'
conquer = ConstDeclare $ pure mempty
data PropertyDeclare = PropertyDeclare
{ requiredProperties :: [T.Text],
propertyTypes :: InsOrd.InsOrdHashMap T.Text (Referenced Schema)
}
deriving (Show, Generic)
instance Semigroup PropertyDeclare where
(PropertyDeclare r t) <> (PropertyDeclare r' t') =
PropertyDeclare
(r <> r')
(t <> t')
instance Monoid PropertyDeclare where
mempty = PropertyDeclare mempty mempty
addDescription ::
T.Text ->
Referenced Schema ->
Referenced Schema
addDescription text = \case
Ref ref ->
Inline $
#_schemaDescription ?~ text $
#_schemaOneOf ?~ [Ref ref] $
mempty
Inline sc -> Inline $ case sc ^. #_schemaDescription of
Nothing -> #_schemaDescription ?~ text $ sc
Just txt ->
#_schemaDescription ?~ text $
#_schemaOneOf ?~ [Inline sc] $
mempty
newtype ObjectSchema a = ObjectSchema
{ getObjectSchema ::
Declare (Definitions Schema) PropertyDeclare
}
deriving (Functor, Applicative, Contravariant, Divisible) via (ConstDeclare (Definitions Schema) PropertyDeclare)
instance JSONObjectParser ObjectSchema where
parseFieldWithDefault key = \prop _ -> ObjectSchema $ do
r <- getRefDef $ getJSONSchema prop
pure $ PropertyDeclare mempty $ InsOrd.singleton key r
parseDescribeFieldWithDefault key description = \prop _ -> ObjectSchema $ do
r <- getRefDef $ getJSONSchema prop
pure $ PropertyDeclare mempty $ InsOrd.singleton key (addDescription description r)
parseDescribeFieldWith key description = \prop -> ObjectSchema $ do
r <- getRefDef $ getJSONSchema prop
pure $ PropertyDeclare [key] $ InsOrd.singleton key (addDescription description r)
parseFieldWith t p = ObjectSchema $ do
r <- getRefDef (getJSONSchema p)
pure $ PropertyDeclare [t] $ InsOrd.singleton t r
instance JSONObjectSerializer ObjectSchema where
serializeFieldWith f w = ObjectSchema $ do
r <- getRefDef (getJSONSchema w)
pure $ PropertyDeclare [f] $ InsOrd.singleton f r
serializeJust f w = ObjectSchema $ do
r <- getRefDef (getJSONSchema w)
pure $ PropertyDeclare [] $ InsOrd.singleton f r
newtype TupleSchema a = TupleSchema
{ getTupleSchema ::
Declare (Definitions Schema) [Referenced Schema]
}
deriving (Functor, Applicative, Contravariant, Divisible) via (ConstDeclare (Definitions Schema) [Referenced Schema])
instance JSONTupleParser TupleSchema where
consumeItemWith p = TupleSchema $ do
r <- getRefDef $ getJSONSchema p
pure [r]
newtype JSONSchema a = JSONSchema
{ getJSONSchema :: Declare (Definitions Schema) NamedSchema
}
deriving (Functor, Contravariant) via (ConstDeclare (Definitions Schema) NamedSchema)
instance Semigroup (JSONSchema a) where
a <> b =
JSONSchema . fmap unnamed $
combineSchemas <$> getRefDef (getJSONSchema a) <*> getRefDef (getJSONSchema b)
instance Selectable JSONSchema where
giveUp = mempty
select _ (JSONSchema lhs) (JSONSchema rhs) = JSONSchema lhs <> JSONSchema rhs
-- | Empty instance: must be both a boolean and a text value, which is not possible (obviously!)
instance Monoid (JSONSchema a) where
mempty = JSONSchema $ do
t <- getJSONSchema parseText
b <- getJSONSchema parseBool
pure $ unnamed $ (#_schemaAllOf ?~ [Inline $ _namedSchemaSchema t, Inline $ _namedSchemaSchema b]) mempty
sameTypes :: Schema -> Schema -> Bool
sameTypes = (==) `on` (^. #_schemaType)
bothHaveEnum :: Schema -> Schema -> Bool
bothHaveEnum a b = enumValues a /= [] && enumValues b /= []
enumValues = (^. #_schemaEnum % non [])
combineInline :: Schema -> Schema -> Schema
combineInline a b
| sameTypes a b && bothHaveEnum a b = (#_schemaEnum ?~ (enumValues a <> enumValues b)) a
| otherwise =
(#_schemaOneOf ?~ fromMaybe [Inline a] (_schemaOneOf a) <> fromMaybe [Inline b] (_schemaOneOf b)) mempty
combineSchemas :: Referenced Schema -> Referenced Schema -> Schema
combineSchemas = curry $ \case
(Inline a, Inline b) -> combineInline a b
(Inline a, Ref b) ->
(#_schemaOneOf ?~ fromMaybe [Inline a] (_schemaOneOf a) <> [Ref b]) mempty
(Ref a, Inline b) ->
(#_schemaOneOf ?~ [Ref a] <> fromMaybe [Inline b] (_schemaOneOf b)) mempty
(Ref a, Ref b) -> (#_schemaOneOf ?~ [Ref a, Ref b]) mempty
getJSONRef ::
forall a.
FromJSON a =>
Proxy a ->
Declare (Definitions Schema) (Referenced Schema)
getJSONRef = getRefDef . getFromNamed
getRefDef ::
Declare (Definitions Schema) NamedSchema ->
Declare (Definitions Schema) (Referenced Schema)
getRefDef decl = case undeclare decl of
NamedSchema (Just name) schema -> do
known <- looks (InsOrd.member name)
unless known $ do
declare [(name, schema)]
void decl
return $ Ref (Reference name)
_ -> Inline . (^. #_namedSchemaSchema) <$> decl
onlyUnnamed :: Schema -> JSONSchema a
onlyUnnamed a = JSONSchema $ pure $ NamedSchema Nothing a
encodeRefName :: T.Text -> T.Text
encodeRefName = decodeUtf8 . urlEncode False . encodeUtf8
instance JSONTupleSerializer TupleSchema where
serializeItemWith s = TupleSchema $ do
r <- getRefDef $ getJSONSchema s
pure [r]
instance JSONParser JSONSchema where
parseObject f = JSONSchema $ do
d <- getObjectSchema f
pure $
NamedSchema Nothing $
mempty
& (#_schemaType ?~ OpenApiObject)
. (#_schemaProperties .~ (d ^. #propertyTypes))
. (#_schemaRequired .~ (d ^. #requiredProperties))
parseDictionary inner = JSONSchema $ do
r <- getRefDef (getJSONSchema inner)
pure $
unnamed $
( (#_schemaType ?~ OpenApiObject)
. (#_schemaAdditionalProperties ?~ AdditionalPropertiesSchema r)
)
mempty
parseTuple parser = JSONSchema $ do
items <- getTupleSchema parser
pure $
mempty
& ( unnamed
. (#_schemaType ?~ OpenApiArray)
. (#_schemaItems ?~ OpenApiItemsArray items)
)
parseArrayWith p = JSONSchema $ do
itemRef <- getRefDef (getJSONSchema p)
pure $
( unnamed
. (#_schemaType ?~ OpenApiArray)
. (#_schemaItems ?~ OpenApiItemsObject itemRef)
)
mempty
parseArray ::
forall a.
(FromJSON a) =>
JSONSchema [a]
parseArray = JSONSchema $ do
itemRef <- getRefDef $ getFromNamed (Proxy :: Proxy a)
pure $
( unnamed
. (#_schemaType ?~ OpenApiArray)
. (#_schemaItems ?~ OpenApiItemsObject itemRef)
)
mempty
parseNumber = onlyUnnamed $ (#_schemaType ?~ OpenApiNumber) mempty
parseInteger = onlyUnnamed $ (#_schemaType ?~ OpenApiInteger) mempty
parseTextConstant t =
void
. onlyUnnamed
. (#_schemaType ?~ OpenApiString)
. (#_schemaEnum ?~ [Aeson.String t])
$ mempty
parseNull = onlyUnnamed $ (#_schemaType ?~ OpenApiNull) mempty
parseText = onlyUnnamed $ (#_schemaType ?~ OpenApiString) mempty
parseBool = onlyUnnamed $ (#_schemaType ?~ OpenApiBoolean) mempty
validateJSON (JSONSchema d) = JSONSchema d
nameParser name = \schema -> JSONSchema $ do
(NamedSchema _ schema) <- getJSONSchema schema
pure $ NamedSchema (Just $ encodeRefName name) schema
addFormat format =
JSONSchema
. fmap
(#_namedSchemaSchema % #_schemaFormat ?~ format)
. getJSONSchema
instance JSONSerializer JSONSchema where
serializeObject f = JSONSchema $ do
d <- getObjectSchema f
pure $
NamedSchema Nothing $
mempty
& (#_schemaType ?~ OpenApiObject)
. (#_schemaProperties .~ (d ^. #propertyTypes))
. (#_schemaRequired .~ (d ^. #requiredProperties))
serializeTuple t = JSONSchema $ do
items <- getTupleSchema t
pure $
( unnamed
. (#_schemaType ?~ OpenApiArray)
. (#_schemaItems ?~ OpenApiItemsArray items)
)
mempty
serializeArray :: forall a. (ToJSON a) => JSONSchema [a]
serializeArray = JSONSchema $ do
itemRef <- getRefDef $ getToNamed (Proxy :: Proxy a)
pure $
( unnamed
. (#_schemaType ?~ OpenApiArray)
. (#_schemaItems ?~ OpenApiItemsObject itemRef)
)
mempty
serializeText = parseText
serializeBool = parseBool
serializeNumber = parseNumber
serializeDictionary ser = JSONSchema $ do
r <- getRefDef (getJSONSchema ser)
pure $
unnamed $
( (#_schemaType ?~ OpenApiObject)
. (#_schemaAdditionalProperties ?~ AdditionalPropertiesSchema r)
)
mempty
serializeNull =
case parseNull of
JSONSchema a -> JSONSchema a
serializeTextConstant t = let (JSONSchema a) = parseTextConstant t in JSONSchema a
nameSerializer t = \ser -> JSONSchema $ do
(NamedSchema _ s) <- getJSONSchema ser
pure $ NamedSchema (Just $ encodeRefName t) s
-- | Get documentation for a type that implements 'Jordan.FromJSON.Class.FromJSON', in the @ Declare @ environment.
--
-- This will be inline documention, IE, it will be named but not stored in the schema definitions.
getFromNamed :: forall a. (FromJSON a) => Proxy a -> Declare (Definitions Schema) NamedSchema
getFromNamed p = getJSONSchema (fromJSON :: JSONSchema a)
-- | Get documention for a type that implements 'Jordan.FromJSON.Class.FromJSON'.
--
-- This will store the type in the schemas key of the schema definitions, and give back a reference to it.
getFromRef :: forall a. (FromJSON a) => Proxy a -> Declare (Definitions Schema) (Referenced Schema)
getFromRef = getRefDef . getFromNamed
-- | Get documentation for a type that implements 'Jordan.ToJSON.Class.ToJSON'.
--
-- This will be inline documentation, IE, it will be named but not stored in the schema definitions.
getToNamed :: forall a. (ToJSON a) => Proxy a -> Declare (Definitions Schema) NamedSchema
getToNamed p = getJSONSchema (toJSON :: JSONSchema a)
-- | Get documentation for a type that implements 'Jordan.ToJSON.Class.ToJSON'.
--
-- This will store the type in the schemas key of the schemas definitions, then give back a reference to it.
getToRef :: forall a. (ToJSON a) => Proxy a -> Declare (Definitions Schema) (Referenced Schema)
getToRef = getRefDef . getToNamed