packages feed

acolyte-openapi-0.1.0.0: src/Acolyte/OpenApi/V3.hs

-- | OpenAPI 3.1 spec rendering.
--
-- Generates an OpenAPI 3.1 JSON document from the shared internal
-- 'Operation' representation.
--
-- @
-- import Acolyte.OpenApi.V3 (generateOpenApi3)
--
-- spec = generateOpenApi3 @MyAPI "My API" "1.0.0"
-- @
module Acolyte.OpenApi.V3
  ( -- * OpenAPI 3.1 spec
    OpenApi3Spec (..)
  , generateOpenApi3
  ) where

import Data.Aeson (Value (..), object, (.=), ToJSON (..))
import qualified Data.Aeson.Key as Key
import Data.Kind (Type)
import Data.Text (Text)
import qualified Data.Text as T

import Acolyte.OpenApi.Spec
  ( Operation (..), Parameter (..), ApiToOperations (..)
  , opMethodLower
  )
import Acolyte.OpenApi.Schema (Schema (..), schemaToJson)


-- | An OpenAPI 3.1 spec.
data OpenApi3Spec = OpenApi3Spec
  { oa3Title   :: !Text
  , oa3Version :: !Text
  , oa3Ops     :: ![Operation]
  } deriving (Show)

instance ToJSON OpenApi3Spec where
  toJSON spec = object
    [ "openapi" .= ("3.1.0" :: Text)
    , "info" .= object
        [ "title"   .= oa3Title spec
        , "version" .= oa3Version spec
        ]
    , "paths" .= oa3Paths (oa3Ops spec)
    ]


-- | Group operations by path, OpenAPI 3.x format.
oa3Paths :: [Operation] -> Value
oa3Paths ops = object
  [ Key.fromText (opPath op) .= object
      [ Key.fromText (opMethodLower op) .= oa3Operation op ]
  | op <- ops
  ]


-- | Render a single operation in OpenAPI 3.x format.
--
-- OpenAPI 3.x differences from Swagger 2.0:
-- * requestBody is a separate field (not a body parameter)
-- * Response content uses media type keys
-- * Richer content negotiation support
oa3Operation :: Operation -> Value
oa3Operation op = object $ concat
  [ case opOperationId op of
      Nothing  -> []
      Just oid -> ["operationId" .= oid]
  , case opSummary op of
      Nothing -> []
      Just s  -> ["summary" .= s]
  , case opDescription op of
      Nothing -> []
      Just d  -> ["description" .= d]
  , [ "responses" .= object
        [ Key.fromText (T.pack (show (opStatusCode op))) .= responseObj ]
    ]
  , if null (opParameters op) then []
    else ["parameters" .= map oa3Param (opParameters op)]
  , case opRequestBody op of
      Nothing -> []
      Just schema ->
        [ "requestBody" .= object
            [ "required" .= True
            , "content" .= object
                [ "application/json" .= object
                    [ "schema" .= schemaToJson schema ]
                ]
            ]
        ]
  ]
  where
    responseObj = case opResponseSchema op of
      Nothing -> object ["description" .= ("Success" :: Text)]
      Just schema -> object
        [ "description" .= ("Success" :: Text)
        , "content" .= object
            [ "application/json" .= object
                [ "schema" .= schemaToJson schema ]
            ]
        ]


-- | Render a parameter in OpenAPI 3.x format.
oa3Param :: Parameter -> Value
oa3Param p = object
  [ "name"     .= paramName p
  , "in"       .= paramIn p
  , "required" .= paramRequired p
  , "schema"   .= schemaToJson (paramSchema p)
  ]


-- | Generate an OpenAPI 3.1 spec from an API type.
generateOpenApi3
  :: forall api. ApiToOperations api
  => Text -> Text -> OpenApi3Spec
generateOpenApi3 title version = OpenApi3Spec
  { oa3Title   = title
  , oa3Version = version
  , oa3Ops     = toOperations @api
  }