servant-validate (empty) → 0.1.0.0
raw patch · 8 files changed
+589/−0 lines, 8 filesdep +basedep +containersdep +hspecsetup-changed
Dependencies added: base, containers, hspec, servant, servant-validate, should-not-typecheck, text
Files
- CHANGELOG.md +10/−0
- LICENSE +30/−0
- README.md +28/−0
- Setup.hs +2/−0
- servant-validate.cabal +86/−0
- src/Servant/Validate.hs +196/−0
- src/Servant/Validate/Internal.hs +193/−0
- test/Spec.hs +44/−0
+ CHANGELOG.md view
@@ -0,0 +1,10 @@+Changelog+=========++Version 0.1.0.0+---------------++<https://github.com/mstksg/servant-validate/releases/tag/v0.1.0.0>++* Initial release.+
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2020++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Author name here nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,28 @@+# servant-validate++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.++```haskell+data DeadlySinEnum = Lust | Gluttony | Greed | Sloth | Wrath | Envy | Pride++type MathApi = "sin" :> ReqBody '[JSON] Double :> Post '[JSON] NoContent+type SatanApi = "sin" :> ReqBody '[JSON] DeadlySinEnum :> Post '[JSON] NoContent++type MyApi = MathApi :<|> SatanApi++myApi :: Proxy MyApi+myApi = Proxy++validMyApi :: ValidApiTree MyApi+validMyApi = validApiTree myApi+```++```haskell+warning: [-Wdeferred-type-errors]+ • Duplicate method in API at path /sin: "POST"+ • In the expression: validApiTree myApi+ In an equation for ‘validMyApi’: validMyApi = validApiTree myApi+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ servant-validate.cabal view
@@ -0,0 +1,86 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: 9d03cea5ce068721828151ec4a161c3038d17f7dabe935243dfa8403378463db++name: servant-validate+version: 0.1.0.0+synopsis: Chekc static properties of servant APIs+description: 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.+ .+ ```haskell+ data DeadlySinEnum = Lust | Gluttony | Greed | Sloth | Wrath | Envy | Pride+ .+ type MathApi = "sin" :> ReqBody '[JSON] Double :> Post '[JSON] NoContent+ type SatanApi = "sin" :> ReqBody '[JSON] DeadlySinEnum :> Post '[JSON] NoContent+ .+ type MyApi = MathApi :<|> SatanApi+ .+ myApi :: Proxy MyApi+ myApi = Proxy+ .+ validMyApi :: ValidApiTree MyApi+ validMyApi = validApiTree myApi+ ```+ .+ ```haskell+ warning: [-Wdeferred-type-errors]+ • Duplicate method in API at path /sin: "POST"+ • In the expression: validApiTree myApi+ In an equation for ‘validMyApi’: validMyApi = validApiTree myApi+ ```+category: Web+homepage: https://github.com/mstksg/servant-validate#readme+bug-reports: https://github.com/mstksg/servant-validate/issues+author: Justin Le+maintainer: justin@jle.im+copyright: (c) 2021 Justin Le+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/mstksg/servant-validate++library+ exposed-modules:+ Servant.Validate+ Servant.Validate.Internal+ other-modules:+ Paths_servant_validate+ hs-source-dirs:+ src+ build-depends:+ base >=4.7 && <5+ , containers+ , servant+ , text+ default-language: Haskell2010++test-suite servant-validate-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_servant_validate+ hs-source-dirs:+ test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >=4.7 && <5+ , containers+ , hspec+ , servant+ , servant-validate+ , should-not-typecheck+ , text+ default-language: Haskell2010
+ src/Servant/Validate.hs view
@@ -0,0 +1,196 @@+{-# 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))
+ src/Servant/Validate/Internal.hs view
@@ -0,0 +1,193 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE QuantifiedConstraints #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module : Servant.Validate.Internal+-- Copyright : (c) Justin Le 2021+-- License : BSD3+--+-- Maintainer : justin@jle.im+-- Stability : experimental+-- Portability : non-portable+--+-- Internal type-level tools.++module Servant.Validate.Internal (+ ApiTree(..)+ , Compare, Cases, MergeMethods, MergePaths, MergeTree+ , compSym, sMergeMethods, sMergePaths, sMergeTree+ , SApiTree(..) , toSApiTree+ , Prod(..), Tup(..), SSym(..)+ , toProd, reflectProd, toTup, reflectTup, toSSym, reflectSSym+ ) where++import Data.Kind+import Unsafe.Coerce+import Data.Proxy+import Data.Text (Text)+import GHC.TypeLits+import Type.Reflection+import qualified Data.Text as T++data ApiTree = Branch [Symbol] [(Symbol, ApiTree)]++type family Compare (a :: k) (b :: k) :: Ordering+type instance Compare (a :: Symbol) (b :: Symbol) = CmpSymbol a b++type family Cases (c :: Ordering) (lt :: k) (eq :: k) (gt :: k) where+ Cases 'LT lt eq gt = lt+ Cases 'EQ lt eq gt = eq+ Cases 'GT lt eq gt = gt++type family MergeMethods err (xs :: [k]) (ys :: [k]) :: [k] where+ MergeMethods err '[] '[] = '[]+ MergeMethods err '[] (y ': ys) = y ': ys+ MergeMethods err (x ': xs) '[] = x ': xs+ MergeMethods err (x ': xs) (y ': ys) = Cases+ (Compare x y)+ (x ': MergeMethods err xs (y ': ys))+ (TypeError (err ':<>: 'Text ": " ':<>: ShowType x))+ (y ': MergeMethods err (x ': xs) ys)++type family MergePaths (base :: [Symbol]) (xs :: [(Symbol, ApiTree)]) (ys :: [(Symbol, ApiTree)]) :: [(Symbol, ApiTree)] where+ MergePaths base '[] '[] = '[]+ MergePaths base '[] ( '(b, y) ': bys ) = '(b, y) ': bys+ MergePaths base ( '(a, x) ': axs ) '[] = '(a, x) ': axs+ MergePaths base ( '(a, x) ': axs ) ( '(b, y) ': bys ) = Cases+ (Compare a b)+ ( '(a, x) ': MergePaths base axs ( '(b, y) ': bys ) )+ ( '(a, MergeTree (a ': base) x y) ': MergePaths base axs bys )+ ( '(b, y) ': MergePaths base ( '(a, x) ': axs ) bys )++type family MergeTree (base :: [Symbol]) (a :: ApiTree) (b :: ApiTree) :: ApiTree where+ MergeTree base ('Branch mA pA) ('Branch mB pB) =+ 'Branch+ (MergeMethods+ ('Text "Duplicate method in API at path " ':<>: 'Text ("/" `AppendSymbol` ShowPath base))+ mA mB+ )+ (MergePaths base pA pB)++type family ShowPath (path :: [Symbol]) :: Symbol where+ ShowPath '[] = ""+ ShowPath '[x] = x+ ShowPath (x ': y ': xs) = x `AppendSymbol` "/" `AppendSymbol` ShowPath (y ': xs)++++++-- a bunch of stuff to avoid needing a singletons dep. this isn't needed+-- for any of the compile-stuff, it's just useful for reflection to+-- a value-level map+++data Prod :: (k -> Type) -> [k] -> Type where+ PNil :: Prod f '[]+ (:<) :: f a -> Prod f as -> Prod f (a ': as)+infixr 5 :<+deriving instance (forall a. Show (f a)) => Show (Prod f as)++toProd :: forall k (as :: [k]) f. (forall (a :: k). TypeRep a -> f a) -> TypeRep as -> Prod f as+toProd f = go+ where+ go :: forall (bs :: [k]). TypeRep bs -> Prod f bs+ go = \case+ Con _ -> unsafeCoerce PNil+ App (App _ x) xs -> unsafeCoerce $ f (unsafeCoerce x) :< go (unsafeCoerce xs)++reflectProd :: forall k (as :: [k]) f r. (forall (a :: k). f a -> r) -> Prod f as -> [r]+reflectProd f = go+ where+ go :: forall (bs :: [k]). Prod f bs -> [r]+ go = \case+ PNil -> []+ x :< xs -> f x : go xs++data Tup :: (a -> Type) -> (b -> Type) -> (a, b) -> Type where+ Tup :: f x -> g y -> Tup f g '(x, y)+deriving instance (forall a. Show (f a), forall a. Show (g a)) => Show (Tup f g xy)++toTup :: (forall x. TypeRep x -> f x) -> (forall x. TypeRep x -> g x) -> TypeRep xy -> Tup f g xy+toTup f g (App (App _ x) y) = unsafeCoerce $ Tup (f (unsafeCoerce x)) (g (unsafeCoerce y))++reflectTup :: forall j k (xy :: (j, k)) f g a b. (forall (x :: j). f x -> a) -> (forall (y :: k). g y -> b) -> Tup f g xy -> (a, b)+reflectTup f g (Tup x y) = (f x, g y)++data SSym :: Symbol -> Type where+ SSym :: KnownSymbol s => SSym s+deriving instance Show (SSym s)+toSSym :: TypeRep s -> SSym s+toSSym tr = case someSymbolVal (read (show tr) :: String) of+ SomeSymbol (Proxy :: Proxy b) -> unsafeCoerce (SSym :: SSym b)+reflectSSym :: forall s. SSym s -> Text+reflectSSym s@SSym = T.pack $ symbolVal s++data SApiTree :: ApiTree -> Type where+ SBranch :: Prod SSym ms -> Prod (Tup SSym SApiTree) ts -> SApiTree ('Branch ms ts)+deriving instance Show (SApiTree api)+toSApiTree :: TypeRep api -> SApiTree api+toSApiTree (App (App _ ms) ts) = unsafeCoerce $+ SBranch (toProd toSSym (unsafeCoerce ms))+ (toProd (toTup toSSym toSApiTree) (unsafeCoerce ts))++data SOrdering :: Ordering -> Type where+ SLT :: SOrdering 'LT+ SEQ :: SOrdering 'EQ+ SGT :: SOrdering 'GT++compSym+ :: forall a b. ()+ => SSym a+ -> SSym b+ -> SOrdering (CmpSymbol a b)+compSym a@SSym b@SSym = case compare (symbolVal a) (symbolVal b) of+ LT -> unsafeCoerce SLT+ EQ -> unsafeCoerce SEQ+ GT -> unsafeCoerce SGT++sMergeMethods+ :: forall err xs ys. ()+ => Prod SSym xs+ -> Prod SSym ys+ -> Prod SSym (MergeMethods err xs ys)+sMergeMethods = \case+ PNil -> \case+ PNil -> PNil+ y :< ys -> y :< ys+ x :< xs -> \case+ PNil -> x :< xs+ y :< ys -> case compSym x y of+ SLT -> x :< sMergeMethods @err xs (y :< ys)+ SEQ -> error "sMergeMethods: forbidden by type system"+ SGT -> y :< sMergeMethods @err (x :< xs) ys++sMergePaths+ :: forall base xs ys. ()+ => Prod (Tup SSym SApiTree) xs+ -> Prod (Tup SSym SApiTree) ys+ -> Prod (Tup SSym SApiTree) (MergePaths base xs ys)+sMergePaths = \case+ PNil -> \case+ PNil -> PNil+ Tup b y :< bys -> Tup b y :< bys+ Tup (a :: SSym a) x :< axs -> \case+ PNil -> Tup a x :< axs+ Tup b y :< bys -> case compSym a b of+ SLT -> Tup a x :< sMergePaths @base axs (Tup b y :< bys)+ SEQ -> Tup a (sMergeTree @(a ': base) x y) :< sMergePaths @base axs bys+ SGT -> Tup b y :< sMergePaths @base (Tup a x :< axs) bys++sMergeTree :: forall base a b. SApiTree a -> SApiTree b -> SApiTree (MergeTree base a b)+sMergeTree (SBranch mA pA) (SBranch mB pB) = SBranch undefined (sMergePaths @base pA pB)
+ test/Spec.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+{-# OPTIONS_GHC -fdefer-type-errors #-}++import Test.Hspec (hspec, describe, it)+import Test.ShouldNotTypecheck (shouldNotTypecheck)++import Data.Proxy+import Servant.API+import Servant.Validate++type TestApi = "hello" :> Get '[] ()+ :<|> "ok" :> "bye" :> Get '[] ()+ :<|> "ok" :> "what" :> Get '[] ()+ :<|> "ok" :> "bye" :> Post '[] ()+ :<|> "ok" :> "bye" :> Get '[] ()++testApi :: Proxy TestApi+testApi = Proxy++validTestApi :: ValidApiTree TestApi+validTestApi = validApiTree testApi++data DeadlySinEnum = Lust | Gluttony | Greed | Sloth | Wrath | Envy | Pride++type MathApi = "sin" :> ReqBody '[JSON] Double :> Post '[JSON] NoContent+type SatanApi = "sin" :> ReqBody '[JSON] DeadlySinEnum :> Post '[JSON] NoContent++type MyApi = MathApi :<|> SatanApi++myApi :: Proxy MyApi+myApi = Proxy++validMyApi :: ValidApiTree MyApi+validMyApi = validApiTree myApi++main :: IO ()+main = hspec $ do+ describe "Servant" $ do+ it "should not allow overlapping routes" $+ shouldNotTypecheck validMyApi+ it "should not allow overlapping routes (nested)" $+ shouldNotTypecheck validTestApi