-- |
-- Module:      Servant.OpenApi
-- License:     BSD3
-- Maintainer:  Nickolay Kudasov <nickolay@getshoptv.com>
-- Stability:   experimental
--
-- This module provides means to generate and manipulate
-- OpenApi specification for servant APIs.
--
-- OpenApi is a project used to describe and document RESTful APIs.
--
-- The OpenApi specification defines a set of files required to describe such an API.
-- These files can then be used by the OpenApi-UI project to display the API
-- and OpenApi-Codegen to generate clients in various languages.
-- Additional utilities can also take advantage of the resulting files, such as testing tools.
--
-- For more information see <http://swagger.io/ OpenApi documentation>.
module Servant.OpenApi (
  -- * How to use this library
  -- $howto

  -- ** Generate @'OpenApi'@
  -- $generate

  -- ** Annotate
  -- $annotate

  -- ** Test
  -- $test

  -- ** Serve
  -- $serve

  -- * @'HasOpenApi'@ class
  HasOpenApi(..),

  -- * Manipulation
  subOperations,

  -- * Testing
  validateEveryToJSON,
  validateEveryToJSONWithPatternChecker,
) where

import           Servant.OpenApi.Internal
import           Servant.OpenApi.Test
import           Servant.OpenApi.Internal.Orphans ()

-- $setup
-- >>> import Control.Applicative
-- >>> import Control.Lens
-- >>> import Data.Aeson
-- >>> import Data.OpenApi
-- >>> import Data.Typeable
-- >>> import GHC.Generics
-- >>> import Servant.API
-- >>> import Test.Hspec
-- >>> import Test.QuickCheck
-- >>> import qualified Data.ByteString.Lazy.Char8 as BSL8
-- >>> import Servant.OpenApi.Internal.Test
-- >>> :set -XDataKinds
-- >>> :set -XDeriveDataTypeable
-- >>> :set -XDeriveGeneric
-- >>> :set -XGeneralizedNewtypeDeriving
-- >>> :set -XOverloadedStrings
-- >>> :set -XTypeOperators
-- >>> data User = User { name :: String, age :: Int } deriving (Show, Generic, Typeable)
-- >>> newtype UserId = UserId Integer deriving (Show, Generic, Typeable, ToJSON)
-- >>> instance ToJSON User
-- >>> instance ToSchema User
-- >>> instance ToSchema UserId
-- >>> instance ToParamSchema UserId
-- >>> type GetUsers = Get '[JSON] [User]
-- >>> type GetUser  = Capture "user_id" UserId :> Get '[JSON] User
-- >>> type PostUser = ReqBody '[JSON] User :> Post '[JSON] UserId
-- >>> type UserAPI  = GetUsers :<|> GetUser :<|> PostUser

-- $howto
--
-- This section explains how to use this library to generate OpenApi specification,
-- modify it and run automatic tests for a servant API.
--
-- For the purposes of this section we will use this servant API:
--
-- >>> data User = User { name :: String, age :: Int } deriving (Show, Generic, Typeable)
-- >>> newtype UserId = UserId Integer deriving (Show, Generic, Typeable, ToJSON)
-- >>> instance ToJSON User
-- >>> instance ToSchema User
-- >>> instance ToSchema UserId
-- >>> instance ToParamSchema UserId
-- >>> type GetUsers = Get '[JSON] [User]
-- >>> type GetUser  = Capture "user_id" UserId :> Get '[JSON] User
-- >>> type PostUser = ReqBody '[JSON] User :> Post '[JSON] UserId
-- >>> type UserAPI  = GetUsers :<|> GetUser :<|> PostUser
--
-- Here we define a user API with three endpoints. @GetUsers@ endpoint returns a list of all users.
-- @GetUser@ returns a user given his\/her ID. @PostUser@ creates a new user and returns his\/her ID.

-- $generate
-- In order to generate @'OpenApi'@ specification for a servant API, just use @'toOpenApi'@:
--
-- >>> BSL8.putStrLn $ encodePretty $ toOpenApi (Proxy :: Proxy UserAPI)
-- {
--     "components": {
--         "schemas": {
--             "User": {
--                 "properties": {
--                     "age": {
--                         "maximum": 9223372036854775807,
--                         "minimum": -9223372036854775808,
--                         "type": "integer"
--                     },
--                     "name": {
--                         "type": "string"
--                     }
--                 },
--                 "required": [
--                     "name",
--                     "age"
--                 ],
--                 "type": "object"
--             },
--             "UserId": {
--                 "type": "integer"
--             }
--         }
--     },
--     "info": {
--         "title": "",
--         "version": ""
--     },
--     "openapi": "3.0.0",
--     "paths": {
--         "/": {
--             "get": {
--                 "responses": {
--                     "200": {
--                         "content": {
--                             "application/json;charset=utf-8": {
--                                 "schema": {
--                                     "items": {
--                                         "$ref": "#/components/schemas/User"
--                                     },
--                                     "type": "array"
--                                 }
--                             }
--                         },
--                         "description": ""
--                     }
--                 }
--             },
--             "post": {
--                 "requestBody": {
--                     "content": {
--                         "application/json;charset=utf-8": {
--                             "schema": {
--                                 "$ref": "#/components/schemas/User"
--                             }
--                         }
--                     }
--                 },
--                 "responses": {
--                     "200": {
--                         "content": {
--                             "application/json;charset=utf-8": {
--                                 "schema": {
--                                     "$ref": "#/components/schemas/UserId"
--                                 }
--                             }
--                         },
--                         "description": ""
--                     },
--                     "400": {
--                         "description": "Invalid `body`"
--                     }
--                 }
--             }
--         },
--         "/{user_id}": {
--             "get": {
--                 "parameters": [
--                     {
--                         "in": "path",
--                         "name": "user_id",
--                         "required": true,
--                         "schema": {
--                             "type": "integer"
--                         }
--                     }
--                 ],
--                 "responses": {
--                     "200": {
--                         "content": {
--                             "application/json;charset=utf-8": {
--                                 "schema": {
--                                     "$ref": "#/components/schemas/User"
--                                 }
--                             }
--                         },
--                         "description": ""
--                     },
--                     "404": {
--                         "description": "`user_id` not found"
--                     }
--                 }
--             }
--         }
--     }
-- }
--
-- By default @'toOpenApi'@ will generate specification for all API routes, parameters, headers, responses and data schemas.
--
-- For some parameters it will also add 400 and/or 404 responses with a description mentioning parameter name.
--
-- Data schemas come from @'ToParamSchema'@ and @'ToSchema'@ classes.

-- $annotate
-- While initially generated @'OpenApi'@ looks good, it lacks some information it can't get from a servant API.
--
-- We can add this information using field lenses from @"Data.OpenApi"@:
--
-- >>> :{
-- BSL8.putStrLn $ encodePretty $ toOpenApi (Proxy :: Proxy UserAPI)
--   & info.title        .~ "User API"
--   & info.version      .~ "1.0"
--   & info.description  ?~ "This is an API for the Users service"
--   & info.license      ?~ "MIT"
--   & servers           .~ ["https://example.com"]
-- :}
-- {
--     "components": {
--         "schemas": {
--             "User": {
--                 "properties": {
--                     "age": {
--                         "maximum": 9223372036854775807,
--                         "minimum": -9223372036854775808,
--                         "type": "integer"
--                     },
--                     "name": {
--                         "type": "string"
--                     }
--                 },
--                 "required": [
--                     "name",
--                     "age"
--                 ],
--                 "type": "object"
--             },
--             "UserId": {
--                 "type": "integer"
--             }
--         }
--     },
--     "info": {
--         "description": "This is an API for the Users service",
--         "license": {
--             "name": "MIT"
--         },
--         "title": "User API",
--         "version": "1.0"
--     },
--     "openapi": "3.0.0",
--     "paths": {
--         "/": {
--             "get": {
--                 "responses": {
--                     "200": {
--                         "content": {
--                             "application/json;charset=utf-8": {
--                                 "schema": {
--                                     "items": {
--                                         "$ref": "#/components/schemas/User"
--                                     },
--                                     "type": "array"
--                                 }
--                             }
--                         },
--                         "description": ""
--                     }
--                 }
--             },
--             "post": {
--                 "requestBody": {
--                     "content": {
--                         "application/json;charset=utf-8": {
--                             "schema": {
--                                 "$ref": "#/components/schemas/User"
--                             }
--                         }
--                     }
--                 },
--                 "responses": {
--                     "200": {
--                         "content": {
--                             "application/json;charset=utf-8": {
--                                 "schema": {
--                                     "$ref": "#/components/schemas/UserId"
--                                 }
--                             }
--                         },
--                         "description": ""
--                     },
--                     "400": {
--                         "description": "Invalid `body`"
--                     }
--                 }
--             }
--         },
--         "/{user_id}": {
--             "get": {
--                 "parameters": [
--                     {
--                         "in": "path",
--                         "name": "user_id",
--                         "required": true,
--                         "schema": {
--                             "type": "integer"
--                         }
--                     }
--                 ],
--                 "responses": {
--                     "200": {
--                         "content": {
--                             "application/json;charset=utf-8": {
--                                 "schema": {
--                                     "$ref": "#/components/schemas/User"
--                                 }
--                             }
--                         },
--                         "description": ""
--                     },
--                     "404": {
--                         "description": "`user_id` not found"
--                     }
--                 }
--             }
--         }
--     },
--     "servers": [
--         {
--             "url": "https://example.com"
--         }
--     ]
-- }
--
-- It is also useful to annotate or modify certain endpoints.
-- @'subOperations'@ provides a convenient way to zoom into a part of an API.
--
-- @'subOperations' sub api@ traverses all operations of the @api@ which are also present in @sub@.
-- Furthermore, @sub@ is required to be an exact sub API of @api. Otherwise it will not typecheck.
--
-- @"Data.OpenApi.Operation"@ provides some useful helpers that can be used with @'subOperations'@.
-- One example is applying tags to certain endpoints:
--
-- >>> let getOps  = subOperations (Proxy :: Proxy (GetUsers :<|> GetUser)) (Proxy :: Proxy UserAPI)
-- >>> let postOps = subOperations (Proxy :: Proxy PostUser) (Proxy :: Proxy UserAPI)
-- >>> :{
-- BSL8.putStrLn $ encodePretty $ toOpenApi (Proxy :: Proxy UserAPI)
--   & applyTagsFor getOps  ["get"  & description ?~ "GET operations"]
--   & applyTagsFor postOps ["post" & description ?~ "POST operations"]
-- :}
-- {
--     "components": {
--         "schemas": {
--             "User": {
--                 "properties": {
--                     "age": {
--                         "maximum": 9223372036854775807,
--                         "minimum": -9223372036854775808,
--                         "type": "integer"
--                     },
--                     "name": {
--                         "type": "string"
--                     }
--                 },
--                 "required": [
--                     "name",
--                     "age"
--                 ],
--                 "type": "object"
--             },
--             "UserId": {
--                 "type": "integer"
--             }
--         }
--     },
--     "info": {
--         "title": "",
--         "version": ""
--     },
--     "openapi": "3.0.0",
--     "paths": {
--         "/": {
--             "get": {
--                 "responses": {
--                     "200": {
--                         "content": {
--                             "application/json;charset=utf-8": {
--                                 "schema": {
--                                     "items": {
--                                         "$ref": "#/components/schemas/User"
--                                     },
--                                     "type": "array"
--                                 }
--                             }
--                         },
--                         "description": ""
--                     }
--                 },
--                 "tags": [
--                     "get"
--                 ]
--             },
--             "post": {
--                 "requestBody": {
--                     "content": {
--                         "application/json;charset=utf-8": {
--                             "schema": {
--                                 "$ref": "#/components/schemas/User"
--                             }
--                         }
--                     }
--                 },
--                 "responses": {
--                     "200": {
--                         "content": {
--                             "application/json;charset=utf-8": {
--                                 "schema": {
--                                     "$ref": "#/components/schemas/UserId"
--                                 }
--                             }
--                         },
--                         "description": ""
--                     },
--                     "400": {
--                         "description": "Invalid `body`"
--                     }
--                 },
--                 "tags": [
--                     "post"
--                 ]
--             }
--         },
--         "/{user_id}": {
--             "get": {
--                 "parameters": [
--                     {
--                         "in": "path",
--                         "name": "user_id",
--                         "required": true,
--                         "schema": {
--                             "type": "integer"
--                         }
--                     }
--                 ],
--                 "responses": {
--                     "200": {
--                         "content": {
--                             "application/json;charset=utf-8": {
--                                 "schema": {
--                                     "$ref": "#/components/schemas/User"
--                                 }
--                             }
--                         },
--                         "description": ""
--                     },
--                     "404": {
--                         "description": "`user_id` not found"
--                     }
--                 },
--                 "tags": [
--                     "get"
--                 ]
--             }
--         }
--     },
--     "tags": [
--         {
--             "description": "GET operations",
--             "name": "get"
--         },
--         {
--             "description": "POST operations",
--             "name": "post"
--         }
--     ]
-- }
--
-- This applies @\"get\"@ tag to the @GET@ endpoints and @\"post\"@ tag to the @POST@ endpoint of the User API.

-- $test
-- Automatic generation of data schemas uses @'ToSchema'@ instances for the types
-- used in a servant API. But to encode/decode actual data servant uses different classes.
-- For instance in @UserAPI@ @User@ is always encoded/decoded using @'ToJSON'@ and @'FromJSON'@ instances.
--
-- To be sure your Haskell server/client handles data properly you need to check
-- that @'ToJSON'@ instance always generates values that satisfy schema produced
-- by @'ToSchema'@ instance.
--
-- With @'validateEveryToJSON'@ it is possible to test all those instances automatically,
-- without having to write down every type:
--
-- >>> instance Arbitrary User where arbitrary = User <$> arbitrary <*> arbitrary
-- >>> instance Arbitrary UserId where arbitrary = UserId <$> arbitrary
-- >>> hspec $ validateEveryToJSON (Proxy :: Proxy UserAPI)
-- <BLANKLINE>
-- [User]
-- ...
-- User
-- ...
-- UserId
-- ...
-- Finished in ... seconds
-- 3 examples, 0 failures
--
-- Although servant is great, chances are that your API clients don't use Haskell.
-- In many cases @swagger.json@ serves as a specification, not a Haskell type.
--
-- In this cases it is a good idea to store generated and annotated @'OpenApi'@ in a @swagger.json@ file
-- under a version control system (such as Git, Subversion, Mercurial, etc.).
--
-- It is also recommended to version API based on changes to the @swagger.json@ rather than changes
-- to the Haskell API.
--
-- See <example/test/TodoSpec.hs TodoSpec.hs> for an example of a complete test suite for a swagger specification.

-- $serve
-- If you're implementing a server for an API, you might also want to serve its @'OpenApi'@ specification.
--
-- See <example/src/Todo.hs Todo.hs> for an example of a server.