json-spec-openapi-0.1.0.0: src/Data/JsonSpec/OpenApi.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-|
This module provides tools for integrating the type-level JSON
'Specification' with the "openapi" package.
You can use 'toOpenApiSchema' as a low-level tool to transform @json-spec@
'Specification's into @openapi3@ 'Schema's directly, irrespective of any
particular business data type.
More likely you will want to use @-XDerivingVia@ along with 'EncodingSchema'
or 'DecodingSchema' to derive 'ToSchema' instances for your data types.
Example, given this data type:
> data User = User
> { name :: Text
> , lastLogin :: UTCTime
> }
> deriving ToSchema via (EncodingSchema User) -- <-- ToSchema instance defined here
> instance HasJsonEncodingSpec User where
> type EncodingSpec User =
> JsonObject
> '[ '("name", JsonString)
> , '("last-login", JsonDateTime)
> ]
> toJSONStructure user =
> (Field @"name" (name user),
> (Field @"last-login" (lastLogin user),
> ()))
Calling @'Data.Aeson.encode' ('Data.OpenApi3.toSchema' ('Proxy' :: 'Proxy' User))@
will produce the following Schema:
> {
> "additionalProperties": false,
> "properties": {
> "last-login": {
> "format": "date-time",
> "type": "string"
> },
> "name": {
> "type": "string"
> }
> },
> "required": [
> "name",
> "last-login"
> ],
> "type": "object"
> }
If you needed more control over the content of the schema you might also
consider doing something like this, e.g. in the case where you would like to
allow additional properties:
> data User = User
> { name :: Text
> , lastLogin :: UTCTime
> }
> instance HasJsonEncodingSpec User where
> type EncodingSpec User =
> JsonObject
> '[ '("name", JsonString)
> , '("last-login", JsonDateTime)
> ]
> toJSONStructure user =
> (Field @"name" (name user),
> (Field @"last-login" (lastLogin user),
> ()))
> instance ToSchema User where
> declareNamedSchema _proxy =
> pure $
> NamedSchema
> Nothing
> (
> toOpenApiSchema (EncodingSpec User)
> & set
> additionalProperties
> (Just (AdditionalPropertiesAllowed True))
> )
-}
module Data.JsonSpec.OpenApi (
toOpenApiSchema,
EncodingSchema(..),
DecodingSchema(..),
) where
import Control.Lens (At(at), (&), over, set)
import Data.JsonSpec (HasJsonDecodingSpec(DecodingSpec),
HasJsonEncodingSpec(EncodingSpec), Specification(JsonArray, JsonBool,
JsonDateTime, JsonInt, JsonNum, JsonObject, JsonString))
import Data.OpenApi (AdditionalProperties(AdditionalPropertiesAllowed),
HasAdditionalProperties(additionalProperties),
HasFormat(format), HasItems(items), HasProperties(properties),
HasRequired(required), HasType(type_), NamedSchema(NamedSchema),
OpenApiItems(OpenApiItemsObject), OpenApiType(OpenApiArray,
OpenApiBoolean, OpenApiInteger, OpenApiNumber, OpenApiObject,
OpenApiString), Referenced(Inline), ToSchema(declareNamedSchema),
Schema)
import Data.String (IsString(fromString))
import Data.Text (Text)
import Data.Typeable (Proxy(Proxy), Typeable)
import GHC.TypeLits (KnownSymbol, symbolVal)
{-|
Convert a 'Specification' into an OpenAPI 'Schema'. The type class
'Internal' is an internal and opaque implementation detail and not
something you should have to worry about. It /should/ already have an
instance for every possible 'Specification' that can be constructed. If
it does not, then that is a bug. Please report it! :-)
-}
toOpenApiSchema
:: (Internal spec)
=> Proxy (spec :: Specification)
-> Schema
toOpenApiSchema = internal
class Internal (spec :: Specification) where
{-|
Given a `Specification`, produce an OpenApi schema equivalent to
the specification. Usually you will want to use this in conjunction with
`HasJsonEncodingSpec` or `HasJsonDecodingSpec`.
Example:
> data MyType = ...
> instance HasJsonEncodingSpec MyType where
> type EncodingSpec MyType = ...
>
> schema :: Schema
> schema = toOpenApiSchema (Proxy :: Proxy (EncodingSpec MyType))
-}
internal :: Proxy spec -> Schema
instance Internal 'JsonString where
internal _ =
mempty & set type_ (Just OpenApiString)
instance Internal 'JsonNum where
internal _ =
mempty & set type_ (Just OpenApiNumber)
instance Internal 'JsonInt where
internal _ =
mempty & set type_ (Just OpenApiInteger)
instance Internal ('JsonObject '[]) where
internal _ =
mempty
& set type_ (Just OpenApiObject)
& set additionalProperties (Just (AdditionalPropertiesAllowed False))
instance (KnownSymbol key, Internal spec, Internal ('JsonObject more)) => Internal ('JsonObject ( '(key, spec) : more )) where
internal _ =
let
propertyName :: Text
propertyName = sym @key
propertySchema :: Schema
propertySchema = internal (Proxy @spec)
in
internal (Proxy @('JsonObject more))
& over properties (set (at propertyName) (Just (Inline propertySchema)))
& over required (propertyName:)
instance (Internal spec) => Internal ('JsonArray spec) where
internal _ =
let
elementSchema :: Schema
elementSchema = internal (Proxy @spec)
in
mempty
& set type_ (Just OpenApiArray)
& set items (Just (OpenApiItemsObject (Inline elementSchema)))
instance Internal 'JsonBool where
internal _ =
mempty & set type_ (Just OpenApiBoolean)
instance Internal 'JsonDateTime where
internal _ =
mempty
& set type_ (Just OpenApiString)
& set format (Just "date-time")
{-|
Helper for defining `ToSchema` instances based on `HasJsonEncodingSpec`
using @deriving via@.
Example:
> data MyType = ...
> deriving ToSchema via (EncodingSchema MyType)
> instance HasJsonEncodingSchema MyType where
> ...
-}
newtype EncodingSchema a =
EncodingSchema {unEncodingSchema :: a}
instance (Typeable a, Internal (EncodingSpec a)) => ToSchema (EncodingSchema a) where
declareNamedSchema _ =
pure (NamedSchema Nothing (toOpenApiSchema (Proxy @(EncodingSpec a))))
{-|
Helper for defining `ToSchema` instances based on `HasJsonDecodingSpec`
using @deriving via@.
Example:
> data MyType = ...
> deriving ToSchema via (DecodingSchema MyType)
> instance HasJsonDecodingSchema MyType where
> ...
-}
newtype DecodingSchema a =
DecodingSchema {unDecodingSchema :: a}
instance (Typeable a, Internal (DecodingSpec a)) => ToSchema (DecodingSchema a) where
declareNamedSchema _ =
pure (NamedSchema Nothing (toOpenApiSchema (Proxy @(DecodingSpec a))))
{- | Shorthand for demoting type-level strings. -}
sym
:: forall a b.
( IsString b
, KnownSymbol a
)
=> b
sym = fromString $ symbolVal (Proxy @a)