servant-validate-0.1.0.0: src/Servant/Validate.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
-- |
-- Module : Servant.Validate
-- Copyright : (c) Justin Le 2021
-- License : BSD3
--
-- Maintainer : justin@jle.im
-- Stability : experimental
-- Portability : non-portable
--
-- A package with "test suites" to help verify that your servant APIs are
-- valid at compile-time. Currently the only property tested is that there
-- are no duplicated paths. See README for more information on usage.
module Servant.Validate
( HasApiTree(..), MethodString(..)
, validApiTree, ValidApiTree
, reflectApiTree, reflectApiTree_
, SApiTree(..), reflectSApiTree
) where
import Data.Kind
import Data.Map (Map)
import Data.Proxy
import Data.Set (Set)
import Data.Text (Text)
import Data.Type.Bool
import Data.Type.Equality
import GHC.TypeLits
import Servant.API
import Servant.Validate.Internal
import Type.Reflection
import Unsafe.Coerce
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.Text as T
-- | Has a valid well-formed API Tree
class HasApiTree (api :: Type) where
type ToApiTree api :: ApiTree
-- | Useful runtime witness of the API tree; use to inspect it with
-- 'reflectApiTree'. This is not used in any part of the actual
-- validation; is just an extra treat.
sApiTree :: SApiTree (ToApiTree api)
instance (KnownSymbol path, HasApiTree api) => HasApiTree ((path :: Symbol) :> api) where
type ToApiTree (path :> api) = 'Branch '[] '[ '(path, ToApiTree api) ]
sApiTree = SBranch PNil (Tup SSym (sApiTree @api) :< PNil)
instance HasApiTree api => HasApiTree (Capture' mods sym a :> api) where
type ToApiTree (Capture' mods sym a :> api) =
'Branch '[] '[ '("<capture>", ToApiTree api) ]
sApiTree = SBranch PNil (Tup SSym (sApiTree @api) :< PNil)
instance HasApiTree api => HasApiTree (CaptureAll sym v :> api) where
type ToApiTree (CaptureAll sym v :> api) =
'Branch '[] '[ '("<capture>", ToApiTree api) ]
sApiTree = SBranch PNil (Tup SSym (sApiTree @api) :< PNil)
instance (HasApiTree a, HasApiTree b) => HasApiTree (a :<|> b) where
type ToApiTree (a :<|> b) = MergeTree '[] (ToApiTree a) (ToApiTree b)
sApiTree = sMergeTree @'[] (sApiTree @a) (sApiTree @b)
-- | A type-level version of 'Servant.API.Verbs.ReflectMethod'.
class MethodString k where
type ToMethodString (x :: k) :: Symbol
instance MethodString StdMethod where
type ToMethodString 'PATCH = "PATCH"
type ToMethodString 'OPTIONS = "OPTIONS"
type ToMethodString 'CONNECT = "CONNECT"
type ToMethodString 'TRACE = "TRACE"
type ToMethodString 'DELETE = "DELETE"
type ToMethodString 'PUT = "PUT"
type ToMethodString 'HEAD = "HEAD"
type ToMethodString 'POST = "POST"
type ToMethodString 'GET = "GET"
instance MethodString Symbol where
type ToMethodString (m :: Symbol) = m
instance (MethodString k, KnownSymbol (ToMethodString m)) => HasApiTree (Verb (m :: k) s t a) where
type ToApiTree (Verb m s t a) = 'Branch '[ToMethodString m] '[]
sApiTree = SBranch (SSym :< PNil) PNil
instance HasApiTree api => HasApiTree (Summary s :> api) where
type ToApiTree (Summary s :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (Description s :> api) where
type ToApiTree (Description s :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (QueryFlag s :> api) where
type ToApiTree (QueryFlag s :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (QueryParams s a :> api) where
type ToApiTree (QueryParams s a :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (QueryParam' mods sym a :> api) where
type ToApiTree (QueryParam' mods sym a :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (Header' mods sym a :> api) where
type ToApiTree (Header' mods sym a :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (HttpVersion :> api) where
type ToApiTree (HttpVersion :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (Vault :> api) where
type ToApiTree (Vault :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (BasicAuth realm a :> api) where
type ToApiTree (BasicAuth realm a :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (AuthProtect tag :> api) where
type ToApiTree (AuthProtect tag :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (IsSecure :> api) where
type ToApiTree (IsSecure :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (RemoteHost :> api) where
type ToApiTree (RemoteHost :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (ReqBody' mods ct a :> api) where
type ToApiTree (ReqBody' mods ct a :> api) = ToApiTree api
sApiTree = sApiTree @api
instance HasApiTree api => HasApiTree (StreamBody' mods framing ct a :> api) where
type ToApiTree (StreamBody' mods framing ct a :> api) = ToApiTree api
sApiTree = sApiTree @api
-- | To be used with 'validApiTree'.
type ValidApiTree api = TypeRep (ToApiTree api)
-- | The full validator. To use:
--
-- @
-- serverApi :: Proxy ServerApi
-- serverApi = Proxy
--
-- validServerApi :: ValidApiTree ServerApi
-- validServerApi = validApiTree serverApi
-- @
validApiTree :: forall api. (HasApiTree api, Typeable (ToApiTree api)) => Proxy api -> ValidApiTree api
validApiTree _ = typeRep
data ApiTreeMap = BranchesMap (Set Text) (Map Text ApiTreeMap)
deriving (Show, Eq, Ord)
-- | Version of 'reflectApiTree' taking an explicit 'TypeRep'.
reflectApiTree_ :: TypeRep (apiTree :: ApiTree) -> ApiTreeMap
reflectApiTree_ = reflectSApiTree . toSApiTree
-- | Useful utility to view the routing structure of a tree; similar to
-- 'Servant.Server.layout'.
reflectApiTree :: forall api. (HasApiTree api, Typeable (ToApiTree api)) => ApiTreeMap
reflectApiTree = reflectApiTree_ (typeRep @(ToApiTree api))
reflectSApiTree :: SApiTree api -> ApiTreeMap
reflectSApiTree (SBranch ms ts) = BranchesMap
(S.fromAscList (reflectProd reflectSSym ms))
(M.fromAscList (reflectProd (reflectTup reflectSSym reflectSApiTree) ts))