CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
Cas d'étude : OpenAPI

L'objectif de ce cas d'étude est d'appliquer des techniques de MDE (métamodélisation, transformations de modèles) à la génération de code pour une application web.

Transparents de la présentation

Contexte

On souhaite écrire une application web qui utilise les services de Spotify. Sur Stackblitz, vous trouverez un environnement de développement dans lequel tourne l'application Blind-test. Vous pouvez cliquer sur SignIn, puis Sign in with GitHub. Une fois connecté, cliquez sur Fork pour dupliquer le repository et commencer vos modifications.

  1. Si vous souhaitez voir fonctionner l'application, créez un compte Spotify, likez quelques titres, et rendez-vous sur Spotify for developers. Cliquez sur Try it et récupérez un token dans l'onglet Network de la console de développement. Collez ce token dans Stackblitz dans App.tsx.

L'API de Spotify est décrite au format OpenAPI.

Script de génération des types

L'objectif est de générer des fichiers de code en typescript à partir d'un fichier de spécification OpenAPI.

La description du schema de l'API se trouve dans le fichier openapi.json ou openapi.yaml.

Un squelette du script est fourni dans le fichier scripts/generate-spotify-client.js. Pour exécuter le script, il suffit d'ouvrir un terminal et de lancer la commande npm run generate-spotify-client. Le script crée des fichiers dans le dossier src/lib/spotify/model. Le but est de remplir ces fichiers avec les types décrits dans la spécification.

Pour écrire dans la console (utile pour débuguer) : Boucle for : Boucle for sur un dictionnaire : Obtenir les clés d'un objet : Appliquer une fonction à chaque élément d'une liste : Joindre une liste de string en une seule string :

Exemple :

Instructions

Types simples

La fonction getGeneratedType (prédéfinie dans le squelette) prend en entrée un object schema conforme à la spécification openapi, et qui renvoit le type typescript associé, en fonction de la propriété type de l'objet passé en paramètre. Dans cette fonction, gérer tout d'abord les cas des types number, integer, string et boolean.

Par exemple, dans le fichier openapi.json, on trouve des schemas définis ainsi :

Après avoir exécuté votre première version du script, le fichier src/lib/spotify/model/Key.ts devrait contenir :

Types structurés

Des types plus complexes peuvent contenir plusieurs propriétés, ayant chacun un type différent. Dans la spécification OpenAPI, cela est indiqué par le champs type du schema, dont la valeur est "object", ainsi que par la présence d'un champ properties, dont la valeur est un tableau de schema.

Attention : certains objects n'ont pas de champs properties. Leur propriétés sont définies différemments, ces cas seront traités plus tard dans le sujet. Pour éviter les erreurs dans votre script, il faut donc mettre une condition pour vérifier que le champ properties est bien défini.

Par exemple, dans le fichier openapi.json, le schema AuthorObject est défini comme suit :

Écrire le code correspondant et vérifier le résultat dans le fichier AuthorObject.ts :

Champs requis

Dans la spécification OpenApi, on peut indiquer les champs qui doivent être obligatoirement remplis grâce à la propriété required du schema. Par exemple, le schema ImageObject est défini comme suit :

En typescript, un champ peut être marqué comme facultatif en accolant ? à sa définition (comme ceci: text?: string;). Modifier la génération des types structurés pour prendre en compte les champs requis et facultatifs.

Après une nouvelle exécution du script, le fichier src/lib/spotify/model/ImageObject.ts doit contenir

tandis que le fichier src/lib/spotify/model/AuthorObject.ts doit maintenant contenir

Import de types

Un objet peut avoir un champ dont le type est lui même défini dans un autre fichier.

Un schema ayant une proprété `$ref` indique que le champ a pour type un autre type défini dans le spécification. Par exemple, dans la définition du schema suivant, le champ key fait référence au type Key qui a déjà été généré. En typescript, cela nécessite d’importer le type depuis un autre fichier, comme ceci : import { Key } from "./Key";.

Après avoir exécuté le script, le contenu du fichier AudioFeaturesObject.ts devrait ressembler à :

  1. Pour écrire tous les imports nécessaires au début du fichier, il faut les stocker dans une variable dans laquelle on ajoute au fur et à mesure tous les types à importer. Il faut passer cette variable en argument de toutes les fonctions qui peuvent avoir besoin de la mettre à jour. Attention à ne pas importer plusieurs fois le même fichier si un type est utilisé par plusieurs champs !

Tableaux

Un champ peut être un tableau d'objets ayant tous le même type. En typescript, un tableau de string s’écrit string[]. Dans la spécification OpenAPI, un schema dont le type est array possède également un champ items indiquant le schema des éléments du tableau.

Par exemple :

Après exécution du script, le fichier AlbumBase.ts devrait contenir :

Union de types

Un type peut être une union de types. Par exemple, en typescript, un champ de type string | number peut être une chaîne de caractères ou un nombre. Dans la spécification OpenAPI, un schema peut avoir une propriété oneOf afin d’indiquer qu’il peut suivre indistinctement l’un des schema spécifiés. Par exemple, dans le type QueueObject, le champ currently_playing peut être de type TrackObject ou EpisodeObject :

Après avoir modifié le script, le fichier QueueObject.ts devrait contenir

Attention à l’interaction entre les types allOf et array : les types `(TrackObject | EpisodeObject)[]` et TrackObject | EpisodeObject[]` ne sont pas équivalents !

Intersection de types

En typescript, un type peut combiner toutes les propriétés de plusieurs autres types. Le type AudiobookBase & AlbumBase possède à la fois les propriétés de AudiobookBase et de AlbumBase. Dans la spécification OpenAPI, un tel type est indiqué par une propriété propriété allOf dans le schema. Par exemple, le type AudiobookObject possède toutes les propriétés du type AudiobookBase, ainsi qu'un champ supplémentaire chapter :

Le fichier Typescript correspondant devrait contenir :

Énumérations

Un champ de type string peut parfois spécifier un nombre fini de valeurs qui peuvent être prises par le champ. Celles-ci sont spécifiées dans la propriété enum du schema. En typescript, une enumération de string peut s’écrire simplement "value1" | "value2", ce qui signifie que le champ peut prendre l'une (et uniquement l'une) de ces valeurs.

Le type OpenApi ce-dessus doit correspondre au fichier Typescript suivant :

Transformation du modèle à l'aide d'une librairie

En réalité, de nombreux outils sont déjà disponibles en open source pour réaliser diverses transformations de modèle à partir d'un fichier de spécification OpenAPI, et générer du code pour différents frameworks de programmation web, par exemple Orval.

Pour faire fonctionner la librairie, on dispose d'un fichier de configuration orval.config.ts dans lequel on spécifie différents paramètres, comme le dossier cible, le type de client à générer, et le fichier d'input.

Dans une console de commande, lancer la commande npx orval et observer le code générer.

Par exemple, dans le fichier src/lib/spotify/api/tracks/tracks.ts, voir la fonction getUsersSavedTracksParams, son type de retour et ses arguments. Essayer d'utiliser cette fonction générée à la place de l'appel à fetchTracks dans App.tsx.

  1. Comme on peut le voir en observant la signature de la fonction, l'argument options (de type AxiosRequestConfig) est un objet qui possède un champ headers, qui contient les headers de la requête HTTP. Il faut passer un objet semblable à celui donné en arguments de fetchTracks pour envoyer le token.
  2. En pratique, on configurerait plutôt un système d'interception des requêtes pour paramétrer l'authentification plutôt que de spécifier le token à la main à chaque appel de fonction.