packages feed

jordan-openapi-0.1.0.0: lib/Jordan/OpenAPI/Internal.hs

{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
module Jordan.OpenAPI.Internal
    where

import Control.Applicative (Alternative(..))
import Control.Monad (unless)
import qualified Data.Aeson.Types as Aeson
import Data.Functor (void)
import Data.Functor.Const
import Data.Functor.Contravariant
import Data.Functor.Contravariant.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 Jordan.FromJSON.Class
import Jordan.ToJSON.Class
import Optics.At.Core
import Optics.Operators
import Optics.Optic ((%))

newtype ConstDeclare env r a
  = ConstDeclare { runConstDeclare :: Declare env r }

instance Functor (ConstDeclare env r) where
  fmap _ (ConstDeclare d) = ConstDeclare d

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'

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

newtype ObjectSchema a
  = ObjectSchema
  { getObjectSchema
    :: Declare (Definitions Schema) Schema
  } deriving (Functor, Applicative, Contravariant, Divisible) via (ConstDeclare (Definitions Schema) Schema)

instance JSONObjectParser ObjectSchema where
  parseFieldWith t p = ObjectSchema $ do
    r <- getRefDef (getJSONSchema p)
    pure $ (#_schemaProperties % at t ?~ r) mempty
  parseField
    :: forall a. FromJSON a
    => T.Text
    -> ObjectSchema a
  parseField label = ObjectSchema $ do
    ref <- getJSONRef (Proxy :: Proxy a)
    pure $ (#_schemaProperties % at label ?~ ref) mempty

instance JSONObjectSerializer ObjectSchema where
  writeField f w = ObjectSchema $ do
    r <- getRefDef (getJSONSchema w)
    pure $ (#_schemaProperties % at f ?~ r) mempty

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

combineSchemas :: Referenced Schema -> Referenced Schema -> Schema
combineSchemas = curry $ \case
  (Inline a, Inline b) ->
    (#_schemaOneOf ?~ fromMaybe [Inline a] (_schemaOneOf a) <> fromMaybe [Inline b] (_schemaOneOf b)) mempty
  (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

-- | 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

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

instance JSONTupleSerializer TupleSchema where
  writeItem s = TupleSchema $ do
    r <- getRefDef $ getJSONSchema s
    pure [r]

instance JSONParser JSONSchema where
  parseObject n f = JSONSchema $ do
    d <- getObjectSchema f
    pure $ NamedSchema (Just n) d
  parseDictionary inner = JSONSchema $ do
    r <- getRefDef (getJSONSchema inner)
    pure $ unnamed $
      ( (#_schemaType ?~ OpenApiObject)
      . (#_schemaAdditionalProperties ?~ AdditionalPropertiesSchema r)
      ) mempty
  parseTuple parser = JSONSchema $ do
    items <- getTupleSchema parser
    pure $
      ( unnamed
      . (#_schemaType ?~ OpenApiArray)
      . (#_schemaItems ?~ OpenApiItemsArray items)
      ) mempty
  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
  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

instance JSONSerializer JSONSchema where
  serializeObject t f = JSONSchema $ do
    d <- getObjectSchema f
    pure $ NamedSchema (Just t) d
  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

-- | Get documentation for a type that implements FromJSON
getFromNamed :: forall a. (FromJSON a) => Proxy a -> Declare (Definitions Schema) NamedSchema
getFromNamed p = getJSONSchema (fromJSON :: JSONSchema a)

-- | Get documentation for a type that implements ToJSON
getToNamed :: forall a. (ToJSON a) => Proxy a -> Declare (Definitions Schema) NamedSchema
getToNamed p = getJSONSchema (toJSON :: JSONSchema a)