packages feed

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 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