Mini projet avec Node : API Gitlab

Publier le 26 janvier 2021 18:45

Scène précédente

Mes premiers pas dans Node

Présentation du projet

Nettoie la scène jonchée de morceaux de tarte à la framboise.

Vous êtes encore là ? Vous voulez vraiment que je vous montre du code donc ? Bien dans ce cas, faisons cela.

J'ai eu le temps de réfléchir à un projet pendant que je mangeai vos tartes. Très bonne au passage. Ce projet sera certainement simplet de prime à bord, mais je compte en faire quelque chose de grandiose plus tard.

Ce projet est le suivant : requête d'API publique. Les API qui vont nous concerner pour le moment seront celle de Github et Gitlab. Nous allons récupérer des informations sur les utilisateurs, les transformer en un objet JavaScript de notre conception et afficher les résultats dans la console.

Voici ce que nous allons voir dans ce projet :

  • Les promises avec fetch : une API qui réalise une Promise sur une ressource donnée en paramètre.

  • Créer une classe en JavaScript (ES2015 et supérieur).

  • Tester son code avec le module assert de base et mocha.

  • Documenter son code avec jsdoc.

Nous ne verrons pas ces deux modules Node pour le moment, mais je tiens quand même à les lister pour leur utilité future :

  • EventEmmiter : permet de lancer des fonctions lorsqu'un événement est émis.

  • ChildProcess : permet de déléguer une partie du code dans un processus fils (principe de fork).

Aller, je n'en dis pas plus sur les deux suivants, mais si vous souhaitez mieux débugger votre application voici celles qui peuvent vous intéresser :

  • debugger : permet de mettre des points d'arrêt sur votre code pour l'inspecter ensuite.

  • v8 : c'est le moteur utilisé par Node et développé par Google. Il permet notamment de faire des tests de performance.

Bien, je vais arrêter la parlotte et on va enfin faire...

Création de la classe utilisateur

... DU CODE !

Oui c'est bon, pas la peine de crier. Juste avant, assurez-vous d'avoir l'architecture que j'ai présentée dans la scène précédente. J'estime maintenant que vous êtes dans votre dossier du projet.

Créons un fichier gitlab.js dans le dossier test/api/external/ et commençons par écrire nos tests.

D'abord, nous souhaitons avoir une classe qui représente notre utilisateur. Elle prendra le JSON retourné par l'API Gitlab lors de la construction de l'objet. Une méthode toJSON sera également disponible pour retourner une représentation de l'objet au format JSON.

const describe = require('mocha').describe
const it = require('mocha').it
const assert = require('assert')

describe('gitlab API', () => {
    describe('User class', () => {
        it('Should create a user with the right parameters', () => {
            let rightParameter = {
                'username': 'username',
                'web_url' : 'web_url'
            }
            let user = new User(rightParameter)
            assert.strictEqual(true, user instanceof User)

            rightParameter = {
                'username' : 'username',
                'web_url' : 'web_url',
                'injection' : 'injection'
            }
            user = new User(rightParameter)
            assert.strictEqual(true, user instanceof User)
        })
        it('Should throw an error when a wrong parameter is given', () => {
            let badParameter = {}
            try{
                const user = new User(badParameter) // Retourne un TypeError
                throw new Error(user, ' instanciate : should\'nt working')
            } catch(e){
                if (e.name === Error) {
                    throw new Error(e)
                }
                assert.strictEqual(true, e instanceof TypeError)
                assert.strictEqual('Sorry : User object was not created - invalid parameter', e.message)
            }

            badParameter = {
                'injection': 'injection'
            }
            try{
                const user = new User(badParameter) // Retourne un TypeError
                throw new Error(user, ' instanciate : should\'nt working')
            } catch(e){
                if (e.name === Error) {
                    throw new Error(e)
                }
                assert.strictEqual(true, e instanceof TypeError)
                assert.strictEqual('Sorry : User object was not created - invalid parameter', e.message)
            }
        })
        it('Should return a JSON object of the User', () => {
            const rightParameter = {
                'username': 'username',
                'web_url' : 'web_url'
            }
            assert.strictEqual(JSON.stringify(new User(rightParameter)), JSON.stringify(new User(rightParameter)))
            assert.strictEqual(JSON.stringify(rightParameter), JSON.stringify(new User(rightParameter)))
        })
        it('Should be different when creating two users with the same correct parameters.', () => {
            const rightParameter = {
                'username': 'username',
                'web_url': 'web_url'
            }
            const user1 = new User(rightParameter)
            const user2 = new User(rightParameter)
            assert.strictEqual(false, user1 === user2) // Deux références d'objets différents
        })
    })
})

Un petit coup de mocha avec la commande suivante :

./node_modules/.bin/mocha ./test/api/external/gitlab.js

ET PAF : alarmes critiques ! Bah oui, on n'a pas encore travaillé sur notre classe utilisateur, rien de surprenant !

Aller, on se retrousse les manches et on passe tout ça en vert avec le fichier src/api/external/gitlab.js.

/** @module api/external/Gitlab */


/** Class representing a Gitlab user */
class User {
    /**
    * username Field
    * @constant
    * @private
    */
    #username

    /**
    * web_url Field
    * @constant
    * @private
    */
    #web_url

    /**
    * Builds the Gitlab user
    * @param {json} json - JSON object containing Gitlab API information
    */
    constructor(json){
        const fieldsRequired = [
            'username',
            'web_url'
        ]
        for (let elt in fieldsRequired){
            if (!(fieldsRequired[elt] in json)) {
                throw new TypeError('Sorry : User object was not created - invalid parameter')
            }
        }
        this.#username = json.username
        this.#web_url = json.web_url
    }

    /**
    * Returns a representation of the object in JSON format
    * @return The object in JSON format
    */
    toJSON() {
        return {
            "username" : this.#username,
            "web_url" : this.#web_url
        }
    }
}

module.exports.User = User

Aller, je vous laisse rejouer les tests, corriger votre fichier test/api/external/gitlab.js et le rejouer de nouveau. Bon, je vous aide : on exporte la classe User... mais l'avez-vous importé dans votre test ?

À ce stade de l'aventure, on a déjà validé 3 points de notre objectif :

  • Rédiger une classe avec des champs privés et une méthode pour retourner l'objet au format JSON.

  • Ajouter de la documentation jsdoc associée à notre fichier source.

  • Tester notre code avec mocha et les asserts.

Quelques subtilités sont tout de même à noter sur mon code, surtout si vous débutez vraiment en JavaScript :

  • Je m'interdis d'utiliser les var, et les remplace constamment par des let ou des const. J'évite ainsi tout débordement de variables.

  • J'utilise des arrow-functions (aussi appelé lambda dans d'autres langages) dans mes tests. Regarder attentivement le deuxième paramètre de la fonction describe et it de mocha.

  • Je crée une classe avec un scope privée sur mes champs. Je veux avoir une cohérence dans ma classe et limiter le plus possible l'interaction extérieure.

Ouais OK, mais elle est où l'API ?

On y viendra, promis ! Maintenant qu'on a une classe utilisateur avec les informations qui nous intéresse, faisons un petit tour dans l'asynchrone !

Les promesses "Promise"

Aller prendre une boisson chaude ou fraiche avant de lire cette partie. Les notions de Promise ne sont pas forcément évidentes à comprendre pour un néophyte, mais je vais essayer de vous expliquer tout cela. Je vais également parler de Node et de son fonctionnement : accrochez-vous !

Première chose à savoir : JavaScript fonctionne avec une seule thread. Cette thread parcourt notre code et va effectuer les tâches à la suite des autres.

Les tâches sont plus ou moins couteuses en temps. Si je vous demande de lire "Un, deux, trois, trois petits chats" (aller hop c'est cadeau vous l'avez dans la tête), puis de me lire un livre complet de Balzac, je doute que vous mettiez le même temps. Ces tâches longues pour un ordinateur peuvent être :

  • Lire un fichier de plusieurs giga.

  • Attendre une réponse d'un serveur web.

  • Attendre une réponse d'un utilisateur.

Ces opérations peuvent bloquer notre programme si nous les exécutons à la suite des autres. Au secours, sauvez-nous !

Les promesses sont nos sauveuses !

Mais calmez-vous bon dieu ! et non ce n'est pas les promesses les sauveuses, mais Node.

En effet, Node est notre exécuteur de JavaScript, c'est lui qui va gérer la thread. Il vient avec une boucle événementielle qui est assez complexe et va nous permettre de réaliser du code non bloquant.

Et les Promises dans tout ça ?

Eh bien, les Promises sont des objets JavaScript qui retourne éventuellement une erreur ou une réponse suite à une opération asynchrone.

En d'autres termes, vous envoyez un objet Promise pour exécuter une tâche qui peut être bloquante (qui peut prendre du temps) puis vous décrivez les opérations à effectuer une fois la réponse ou l'erreur obtenue. On peut donc distinguer 3 états :

  • En attente (Pending) : le traitement de la tâche est toujours en cours.

  • Résolue (Resolved) : l'opération a réussi et nous retourne la réponse obtenue.

  • Rejeté (Rejected) : l'opération a echoué et nous retourne une erreur.

Pour notre projet, l'API Fetch nous permettra de réaliser des Promises sur les URLs des API. Voici un exemple d'utilisation (ça vous permettra d'y voir plus clair) :

const fetch = require('node-fetch')

const toJson = (data) => data.json() // arrow-function

fetch('https://api.github.com')	// #1
    .then(toJson)  // #2
    .then(console.log) // #3
    .catch(error => { // #4
        console.log('Error occured')
        console.log(error)
    })
console.log('Avant ou après ?') // #5

On installe le paquet node-fetch dans notre projet :

npm install --save node-fetch

Détaillons ce programme :

  • #1 (Pending) : on crée une Promise sur l'URL de l'API github qui nous rendra soit une réponse soit une erreur.

  • #2 (Resolved) : si #1 retourne une réponse, on parse la réponse en JSON.

  • #3 (Resolved) : si #2 ne retourne pas d'erreur (qui nous garantie que la réponse a une méthode json ?), on affiche le JSON dans la console.

  • #4 (Rejected) : si #1 ou #2 sortent en erreur, on affiche l'erreur dans la console.

  • #5 : une deuxième tâche qui se réalisera en premiere.

Si vous lancez ce programme, vous pouvez vous poser la question suivante : pourquoi le #5 s'affiche en premier et pas le #3 ou #4 ?

Sort un reveil : Tic... Tac...

Vous ne l'avez toujours pas ? Dans ce cas, je vous propose de lire cette documentation de nouveau.

Et oui, c'est une nouvelle fois grâce à Node que nous obtenons ce résultat. Dans l'event-loop (boucle événementielle), on réalise les étapes suivantes :

  • On affecte nos deux constantes.

  • On place le fetch #1 dans une queue.

  • On execute le console.log #5.

  • On regarde la queue et l'état de la promise renvoyer par fetch (Pending, Resolved, Rejected), puis on execute la suite de la promise.

Il est important à noter que le fait de parser notre réponse en JSON nous retourne aussi un objet Promise et nous permet de faire d'enchainer les then (aussi appelé promise chaining).

Je vous conseille de regarder via un debugger et de pratiquer d'autres exemples pour mieux comprendre le fonctionnement des promises et du code asynchrone.

Désormais que la notion de Promise a été expliqué, nous allons pouvoir nous occuper de notre API.

Fetch sur l'API Gitlab

Ici je vais me concentrer sur l'API de Gitlab, mais les codes seront assez semblables avec l'API de Github.

Notre objet API devra nous retourner un objet JSON ayant, au moins, les éléments username et web_url. Elle présentera une méthode getUser qui prendra en paramètre le nom de l'utilisateur Gitlab à rechercher. Si l'utilisateur n'existe pas dans la base de Gitlab, une erreur est indiquée avec un message dans le contenu JSON.

const { FetchError } = require('node-fetch')

describe('gitlab API File', () => {
    //...

    describe('API Object', function () {
        it('Should return JSON User API and check need fields', function (done) {
        const needFields = [
            'username',
            'web_url'
        ]
        API.getUser('Florent_pa')
            .then(jsonData => {
                for (const elt of needFields) {
                    assert.strictEqual(true, elt in jsonData[0])
                }
                done()
            })
            .catch(error => {
                const errorAllow = 'request to https://gitlab.com/api/v4/users?username=florent_pa failed, reason: getaddrinfo EAI_AGAIN gitlab.com'
                if (error.message === errorAllow) {
                    throw new FetchError('You are not connected')
                }
                done(error)
            })
            .catch(error => {
                done(error)
            })
        })
        it('Should catch error with invalid user', function (done) {
            const needFields = [
                'username',
                'web_url'
            ]
            API.getUser('')
                .then(jsonData => {
                    for (const elt of needFields) {
                        assert.strictEqual(false, elt in jsonData)
                    }
                    assert.strictEqual(true, 'message' in jsonData)
                    done()
                })
                .catch(error => {
                    const errorAllow = 'request to https://gitlab.com/api/v4/users?username= failed, reason: getaddrinfo EAI_AGAIN gitlab.com'
                    if (error.message === errorAllow) {
                        throw new FetchError('You are not connected')
                    }
                    done(error)
                })
                .catch(error => {
                    done(error)
                })
        })
    })
})

Maintenant, on crée notre objet API avec une méthode static getUser pour répondre à ces tests :

/** Class wrapping gitlab API. */
class API {
    /**
    * Get information about users of the Gitlab API
    * @param {string} username - The usernmae to fetch gitlab information
    * @return {json} - A json with gitlab User information
    * @static
    */
    static async getUser (username) {
        const response = await fetch('https://gitlab.com/api/v4/users?username=' + username)
        const data = await response.json()
        return data
    }
}

module.exports.API = API

Euh... Await ? Async ? Que se passe-t-il ?

Vous avez été attentif ! Le mot clé async nous indique que la fonction va nous exécuter du code asynchrone. Le mot clé await est valide uniquement dans une fonction précédée du mot clé async. Elle attend la résolution de la Promesse et retourne le résultat.

Désormais, tous les tests devraient être au vert (vérifier vos imports !) au même titre que nos objectifs pour ce projet ! Je vous laisse vous amusez à faire de même avec l'API Github, maintenant que vous avez un exemple sous la main.

Bilan

Pour cette première mise en bouche, nous avons pu voir comment mettre en place du code à travers une approche TDD.

Si vous souhaitez commencer votre projet en partant de ma base, je vous mets en lien le repository Gitlab avec un pipeline basique qui reprend le linter et les phases de tests. N'hésitez pas à le mettre en favoris pour éviter de le perdre.

Je vous remercie d'être passé et je vous souhaite un bon dev à tous !

Les rideaux se ferment.