packages feed

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

-- | Swagger / OpenAPI 2.0 spec rendering.
--
-- Generates a Swagger 2.0 JSON document from the same internal
-- 'Operation' representation that OpenAPI 3.x uses. The type-level
-- API walking is shared; only the JSON format differs.
--
-- @
-- import Acolyte.OpenApi.V2 (generateSwagger)
--
-- spec = generateSwagger @MyAPI "My API" "1.0.0" "localhost" "/api"
-- @
module Acolyte.OpenApi.V2
  ( -- * Swagger 2.0 spec
    SwaggerSpec (..)
  , generateSwagger
  ) 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)


-- | A Swagger 2.0 spec.
data SwaggerSpec = SwaggerSpec
  { swaggerTitle    :: !Text
  , swaggerVersion  :: !Text
  , swaggerHost     :: !Text
  , swaggerBasePath :: !Text
  , swaggerOps      :: ![Operation]
  } deriving (Show)

instance ToJSON SwaggerSpec where
  toJSON spec = object
    [ "swagger" .= ("2.0" :: Text)
    , "info" .= object
        [ "title"   .= swaggerTitle spec
        , "version" .= swaggerVersion spec
        ]
    , "host"     .= swaggerHost spec
    , "basePath" .= swaggerBasePath spec
    , "schemes"  .= (["https", "http"] :: [Text])
    , "consumes" .= (["application/json"] :: [Text])
    , "produces" .= (["application/json"] :: [Text])
    , "paths"    .= swaggerPaths (swaggerOps spec)
    ]


-- | Group operations by path, Swagger 2.0 format.
swaggerPaths :: [Operation] -> Value
swaggerPaths ops = object
  [ Key.fromText (opPath op) .= object
      [ Key.fromText (opMethodLower op) .= swaggerOperation op ]
  | op <- ops
  ]


-- | Render a single operation in Swagger 2.0 format.
--
-- Swagger 2.0 differences from OpenAPI 3.x:
-- * Parameters include body params (not a separate requestBody)
-- * Responses use a simpler schema format
-- * No content negotiation per-operation
swaggerOperation :: Operation -> Value
swaggerOperation 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 ]
    ]
  , let allParams = map swaggerParam (opParameters op)
          ++ bodyParam (opRequestBody op)
    in if null allParams then []
       else ["parameters" .= allParams]
  ]
  where
    responseObj = case opResponseSchema op of
      Nothing -> object ["description" .= ("Success" :: Text)]
      Just schema -> object
        [ "description" .= ("Success" :: Text)
        , "schema" .= schemaToJson schema
        ]

    bodyParam Nothing = []
    bodyParam (Just schema) =
      [ object
          [ "in"       .= ("body" :: Text)
          , "name"     .= ("body" :: Text)
          , "required" .= True
          , "schema"   .= schemaToJson schema
          ]
      ]


-- | Render a path/query parameter in Swagger 2.0 format.
swaggerParam :: Parameter -> Value
swaggerParam p = object
  [ "name"     .= paramName p
  , "in"       .= paramIn p
  , "required" .= paramRequired p
  , "type"     .= schemaType (paramSchema p)
  ]


-- | Generate a Swagger 2.0 spec from an API type.
--
-- @
-- spec = generateSwagger @MyAPI "My API" "1.0.0" "localhost:3000" "/api"
-- @
generateSwagger
  :: forall api. ApiToOperations api
  => Text    -- ^ Title
  -> Text    -- ^ Version
  -> Text    -- ^ Host (e.g., "localhost:3000")
  -> Text    -- ^ Base path (e.g., "/api" or "/")
  -> SwaggerSpec
generateSwagger title version host basePath = SwaggerSpec
  { swaggerTitle    = title
  , swaggerVersion  = version
  , swaggerHost     = host
  , swaggerBasePath = basePath
  , swaggerOps      = toOperations @api
  }