Typer des URL avec Haskell Servant
Voir aussi : video youtube - code source
Servant est une bibliothèque Haskell qui permet de définir des URL au niveau des types. On peut ainsi bénéficier des vérifications du compilateur mais également générer des clients et de la documentation.
Exemple 1
On veut implémenter une API simple, avec deux routes. La route log
retourne le logarithme d’un nombre donné (s’il existe) et la route add
ajoute deux nombres x
et y
optionnels (0 par défaut). Par exemple, l’URL log/1
retournera 0.0
et l’URL add?x=20&y=22
retournera 42
.
Typage d’URL
Servant permet de définir des routes par des types. Par exemple, la route log
peut être définie par le type LogRoute
suivant :
type LogRoute
= "log"
:> Capture "x" Double
:> Get '[JSON] (Maybe Double)
Ici, les deux premiers composants indiquent l’URL de base et le paramètre capturé. Le troisième composant indique la méthode HTTP (GET), le format de retourné (JSON) et le type des valeurs retournées (Maybe Double).
On définit la route add
de la même façon, mais pour des paramètres d’URL optionnels.
type AddRoute
= "add"
:> QueryParam' '[] "x" Double
:> QueryParam' '[] "y" Double
:> Get '[JSON] Double
Les deux types LogRoute
et AddRoute
peuvent maintenant être utilisés, par exemple pour implémenter un serveur qui y répond ou un client qui y accède.
Serveur
Pour implémenter le serveur, on écrit tout d’abord l’API auquel il répond. Cette API est également implémentée par un type, par exemple en combinant les deux routes définies précédemment.
type ServerApi
= LogRoute
:<|> AddRoute
On écrit ensuite une fonction qui va servir l’API, c’est-à-dire retourner les données correspondant aux différentes routes.
handleServerApi :: Server ServerApi
handleServerApi= handleLogRoute
:<|> handleAddRoute
Ici, on a découpé la gestion des deux routes en deux fonctions handleLogRoute
et handleAddRoute
:
handleLogRoute :: Double -> Handler (Maybe Double)
handleLogRoute x| x <= 0 = pure Nothing
| otherwise = pure $ Just $ log x
handleAddRoute :: Maybe Double -> Maybe Double -> Handler Double
=
handleAddRoute mx my let x = fromMaybe 0 mx
= fromMaybe 0 my
y in pure $ x+y
Enfin, il ne reste plus qu’à créer l’application serveur (en utilisant l’API et la fonction de gestion) et à lancer celle-ci dans la fonction principale :
serverApp :: Application
= serve (Proxy @ServerApi) handleServerApi
serverApp
main :: IO ()
= do
main let port = 3000
putStrLn $ "listening on port: " ++ show port ++ "..."
run port serverApp
On peut alors lancer le serveur et l’interroger via un client HTTP (navigateur web, curl…) :
$ curl "localhost:3000/log/1"
0.0
$ curl "localhost:3000/log/0"
null
$ curl "localhost:3000/add?x=22&y=20"
42.0
Client
À partir des types LogRoute
et AddRoute
, on peut également écrire un client. Par exemple, les deux fonctions suivantes interrogent directement les routes :
queryLog :: Double -> ClientM (Maybe Double)
= client (Proxy @LogRoute)
queryLog
queryAdd :: Maybe Double -> Maybe Double -> ClientM Double
= client (Proxy @AddRoute) queryAdd
On peut également écrire des requêtes plus complexes :
queryLogAdd :: ClientM Double
= do
queryLogAdd <- queryLog 0
mx Just 42) queryAdd mx (
Et enfin, exécuter ces requêtes dans un programme principal :
main :: IO ()
= do
main <- newManager defaultManagerSettings
myManager let myClient = mkClientEnv myManager (BaseUrl Http "localhost" 3000 "")
1) myClient >>= print
runClientM (queryLog Nothing (Just 42)) myClient >>= print
runClientM (queryAdd >>= print runClientM queryLogAdd myClient
Résultat :
$ runghc Client.hs
Right (Just 0.0)
Right 42.0
Right 42.0
Exemple 2
Dans ce second exemple, on veut implémenter un serveur qui fournit des informations sur des films.
Types personnalisés
Haskell permet de définir des types de données algébriques et de les convertir facilement au format JSON (voir le tuto 55). Par exemple, pour un type Film
avec titre, réalisateur et année de sortie :
data Film = Film
_title :: Text
{ _director :: Text
, _year :: Maybe Int
,deriving (Generic, Show)
}
instance ToJSON Film
On peut alors écrire des routes qui retournent des Film
au format JSON, par exemple d’un réalisateur donné :
type DirFilmsRoute = "dirFilms" :> Capture "director" Text :> Get '[JSON] [Film]
handleDirFilmsRoute :: Text -> Handler [Film]
= pure $ filter ((==dir) . _director) myFilms
handleDirFilmsRoute dir
myFilms :: [Film]
=
myFilms Film "First Blood" "Kotcheff" (Just 1982)
[ Film "Paths of Glory" "Kubrick" (Just 1957)
, Film "Full Metal Jacket" "Kubrick" Nothing
, ]
Résultat :
$ curl "localhost:3000/dirFilms/Kotcheff"
[{"_year":1982,"_title":"First Blood","_director":"Kotcheff"}]
Formats de reponse
Servant permet d’indiquer le format de réponse des routes. Par exemple, le type suivant indique que la route fournit les formats JSON et HTML.
type AllFilmsRoute = "allFilms" :> Get '[HTML,JSON] [Film]
Bien entendu, le type de retour doit être compatible avec les formats indiqués. Ici, le format JSON est géré car Film
instancie ToJSON
. Pour générer aussi une page HTML correspondant à la route, il faut également instancier ToHTML
:
instance ToHtml [Film] where
= toHtml
toHtmlRaw
= doctypehtml_ $ do
toHtml films $ do
body_ "films"
h2_ $ forM_ films $ \film -> li_ $ toHtml $ show film
ul_ "routes"
h2_ $ do
ul_ $ a_ [href_ (dirFilmsUrl "Kotcheff")] "dirFilmsUrl Kotcheff"
li_ $ a_ [href_ (dirFilmsUrl "Kubrick")] "dirFilmsUrl Kubrick" li_
On notera qu’on a ici intancié directement le type [Film]
, pour simplifier, mais qu’il serait certainement plus propre de définir explicitement un type pour cette route/rendu.
Enfin, dirFilmsUrl
est une fonction qui permet de générer une URL à partir du type de la route (ce qui est donc vérifié par le compilateur) :
dirFilmsUrl :: Text -> Text
= toUrlPiece $ safeLink (Proxy @ServerApi) (Proxy @DirFilmsRoute) dir dirFilmsUrl dir
Résultat :
Conclusion
Servant est une bibliothèque Haskell très populaire pour réaliser des API type-safe. Pour cela, elle permet d’écrire des routes sous forme de types, avec toutes les fonctionnalités classiques (méthodes HTTP, paramètres des URL, etc).
Une fois les routes typées, on peut écrire des clients qui y accèdent et des serveurs qui y répondent (avec différents formats comme JSON/HTML…). Et comme nos API sont définies au niveau des types, le compilateur les vérifie automatiquement, ce qui évite toute une classe d’erreurs.