Haskell项目中Servant路由通用页面加载校验实现咨询
Great question! Repeating validation logic across handlers is definitely a pain, and Servant gives us a few clean ways to solve this. Let's walk through the most straightforward approaches tailored to your use case.
1. Higher-Order Function Wrapper (Simplest Approach)
The easiest way to add shared validation is to create a wrapper function that runs your check first, then executes the original handler if validation passes. This keeps your handler logic clean and avoids repetition.
Step 1: Define Your Validation Function
First, create a reusable check that takes a page identifier and either succeeds or throws an error (like 403 Forbidden):
import Servant import qualified Text.Blaze.Html as H -- Example validation: Check if the page is allowed to be loaded checkPageAccess :: String -> Handler () checkPageAccess pageName = do -- Replace this with your actual validation logic: -- e.g., check user permissions, feature flags, database state, etc. let isPageAllowed = pageName `elem` ["page1.html", "page2.html", "page3.html"] if not isPageAllowed then throwError err403 { errBody = "Access denied to page: " ++ pageName } else return ()
Step 2: Create the Wrapper
Write a higher-order function that wraps your handler with the validation:
-- Wrap a handler to run validation before executing it withPageValidation :: String -> Handler H.Html -> Handler H.Html withPageValidation page handler = do checkPageAccess page handler -- Only run the original handler if validation passes
Step 3: Update Your Server
Now you can apply this wrapper to each of your handlers without repeating the validation code:
type MyAPI = "base" :> "page1.html" :> Get '[HTML] H.Html :<|> "base" :> "page2.html" :> Get '[HTML] H.Html :<|> "base" :> "page3.html" :> Get '[HTML] H.Html serverAPI :: Server MyAPI serverAPI = withPageValidation "page1.html" pageH1 :<|> withPageValidation "page2.html" pageH2 :<|> withPageValidation "page3.html" pageH3
2. Custom API Combinator (Cleaner API Definition)
If you want to enforce validation directly in your API type (making it explicit which endpoints require checks), you can create a custom Servant combinator. This way, your server implementation stays identical to the original.
Step 1: Define the Combinator
Create a type that carries the page name as a type-level symbol:
import GHC.TypeLits (KnownSymbol, symbolVal) -- Custom combinator to mark endpoints that need page access validation data CheckPage (pageName :: Symbol)
Step 2: Implement Servant's Server Instance
Tell Servant how to route endpoints using this combinator by implementing HasServer:
instance (KnownSymbol pageName, HasServer api context) => HasServer (CheckPage pageName :> api) context where type ServerT (CheckPage pageName :> api) m = ServerT api m route _ ctx server = route (Proxy :: Proxy api) ctx $ do let pageName = symbolVal (Proxy :: Proxy pageName) server >>= \handler -> return $ \req -> do checkPageAccess pageName -- Run validation before handling the request handler req hoistServerWithContext _ ctx nt server = hoistServerWithContext (Proxy :: Proxy api) ctx nt server
Step 3: Update Your API Type
Add the CheckPage combinator to each endpoint that needs validation:
type MyAPI = "base" :> CheckPage "page1.html" :> "page1.html" :> Get '[HTML] H.Html :<|> "base" :> CheckPage "page2.html" :> "page2.html" :> Get '[HTML] H.Html :<|> "base" :> CheckPage "page3.html" :> "page3.html" :> Get '[HTML] H.Html
Step 4: Use Your Original Server
Now your server can use the original handlers directly—validation is handled automatically by the combinator:
serverAPI :: Server MyAPI serverAPI = pageH1 :<|> pageH2 :<|> pageH3
Which Approach Should You Choose?
- Use the higher-order function wrapper if you want minimal changes to your existing code and a quick solution.
- Use the custom combinator if you want to make validation requirements explicit in your API type (great for larger teams or self-documenting code).
内容的提问来源于stack exchange,提问作者QSpider

