Documenter mon API

Introduction

Quand on est un intermittent du développement, il y a quelque chose de super compliqué : se souvenir ce que l’on a fait la dernière fois et que chaque modification ne casse pas tout …

Pour le moment, je palliais aux soucis en :

  • Committant souvent : me permet de revenir voir ce que j’ai fait,
  • Mettant en place un maximum de tests unitaires : ce qui peut-être frustrant quand on prototype mais bon.

Il reste un dernier souci : euh … l’API elle attend quoi déjà ?

Bref … faut documenter. Mais bon tant qu’à faire j’aimerais bien ne pas y passer trop de temps car bon : c’est quand même du dev perso / jetable !

Swagger

Dans les différents projets sur lesquels j’interviens, la documentation des API Rest est réalisé via Swagger ou un équivalent (Nelmio en php/symfony).

Ici, je ne vais repartir des outils proposés comme l’éditeur, la génération etc car … le mal est fait : l’API existe …

Alors autant dire que des solutions, il en existe plusieurs … et pour certaines, il existe même différentes combinaisons. Donc, j’ai fait des choix …

Swagger-jsdoc / Swagger-ui-express

Présentation

  • swagger-jsdoc  : permet d’utiliser la syntaxe JSDoc pour générer la documentation au format swagger,
  • swagger-ui-express : permet d’ajouter une route qui va exposer la documentation sous la forme d’une interface Swagger-ui. Ici dédié à Express.

Mise en place

Je vous passe les npm install habituel pour la mise en place directe.

Il faut commencer par mettre un peu de doc sur le service. L’idée est bien d’utiliser la documentation faîte sur les méthodes des contrôleurs directement :

/**
     * @swagger
     * /api/zones:
     *    get:
     *      description: Return all the zone
     */
    public getAll = async (req: IAppRequest, res: Response) => {
       const appResponse: AppResponse<any> = await ZoneService.getAll();
       return res.status(appResponse.code).json(appResponse);
    } // getAll

Ensuite, j’ai crée un fichier ~/src/config.doc.ts dont le but est de centraliser la mise en place de la documentation. Le fichier contient la configuration de la génération de la documentation par swagger-jsoc :

    private static options = {
        swaggerDefinition: {
          info: {
            title: "The Beer Project - API",
            version: "1.0.0",
            description: "API Rest for the Beer Project",
          },
        },
        // Uniquement les fichiers controllers
        apis: ["src/controllers/*"],
      };

    private static specs = swaggerJSDoc(ConfigDoc.options);

Puis, une fonction qui enregistre la route au niveau de l’application :

/**
     * Ajoute la route de la documentation
     * @param app
     */
    public static addDocRoute(app: any) {
        app.use("/api/docs", swagger.serve, swagger.setup(ConfigDoc.specs));
    }

Cette fonction utilise swagger-ui-express pour servir la route (swagger.serve) et passe les éléments générés par swagger-jsdoc en paramètre.

Test

Un petit npm start puis direction http://localhost:3000/api/doc et :

C’est déjà pas mal. Test ? Là au début, j’ai été un peu déçu car je n’ai eu aucune réponse … Le souci ne venait pas du code en tant que tel mais de la documentation. Pour que l’interface, gère une réponse, il faut lui fournir l’info :

/**
     * @swagger
     * /api/zones:
     *    get:
     *      description: Return all the zone
     *      responses:
     *        200:
     *         description: OK
     *         schema:
     *           type: object
     */
    public getAll = async (req: IAppRequest, res: Response) => {
       const appResponse: AppResponse<any> = await ZoneService.getAll();
       return res.status(appResponse.code).json(appResponse);
    } // getAll

Et là :

Bilan

Cà marche mais il faut doubler l’information et tout documenter manuellement …

TSOA

Présentation

En naviguant à droite et à gauche, j’ai fini par tomber sur ce projet : TSOA. Le principe me plaît car il semble qu’il soit possible de faire d’une pierre deux coups :

  • Des décorateurs qui définissent l’action,
  • Les décorateurs servent également à la documentation.

Et j’aime bien ce genre de principe.

Mise en place

Alors la documentation bien que longue ne contient pas de [Get Started] donc j’ai un galéré à savoir comment mettre en place tout le système et je ne suis même pas sûr d’avoir tout bon …

Tout commence par un npm install tsoa puis il faut créer le fichier tsoa.json qui va permettre de définir deux choses :

  • L’emplacement des contrôleurs : routes,
  • L’emplacement de la documentation générée : swagger.
{
    "swagger": {
        "outputDirectory": "./src/swagger",
        "entryFile": "./src/app.ts"
    },
    "routes": {
        "entryFile": "./src/app.ts",
        "routesDir": "./src/routes"
    }
}

Le projet va aller lire les sources et pour cela, il lui faut un point d’entrée « entryFile » qui peut-être votre fichier contenant la description de votre application.

Une fois cela fait, il faut aller modifier le contrôleur pour ajouter des décorateurs. Dans mon cas, j’ai ajouté @Route et @Get :

/**
 * Controller pour zone controller
 */
@Route("/tsoa/v1/zones")
export class TSOAZoneController {

    @Get()
    public async getZones(): Promise<IZone[]> {
        const appResponse: AppResponse<any> = await ZoneService.getAll();
        return new Promise<IZone[]>((resolve, reject) => {
            if (appResponse.success) {
                resolve(appResponse.data);
            } else {
                reject(appResponse.message);
            }
        });
    }

}

Premier petit truc : pour éviter des erreurs, il faut ajouter une entrée dans le fichier tsconfig.json :

 "experimentalDecorators": true

Sinon, les décorateurs ne passent pas.

Comme pour les autres contrôleurs, il faut l’ajouter dans le fichier d’entrée (app.ts dans mon cas):

// Oui le nom est pour test :o) 
import { TSOAZoneController } from "./controllers/api-tsoa.zone.controller";

Maintenant, il faut lancer une commande qui va générer un fichier contenant toutes les routes. Et oui, rien de magique ! C’est du code généré !

$> tsoa routes
$> tsoa swagger

La première va générer un fichier routes.ts dans le répertoire pointé par routeDir et le deuxième va générer un fichier swagger.json dans le répertoire paramétré plus haut.

Premier souci

Alors, un premier souci lors du lancement de la commande. Désolé, je n’ai pas la capture mais en gros: il n’arrivait à déterminer les types de mon objet. En fait, dans mon cas, ma classe hérité de Mongoose.Document et çà : il n’aime pas. Il faut des objets tout simple. J’ai fait les modifications et hop çà marche !

Plus d’informations ici.

Nouvelle génération

J’ai bien mon fichier de route et mon fichier swagger.json.

Pour le fichier de route, il faut l’enregistrer dans mon fichier app.ts (normal). Dans l’ordre :

  • Il faut importer le fichier de route généré juste avant : import { RegisterRoutes } from "./routes/routes";
  • Via l’export RegisterRoutes, on le laisse enregistrer ses routes : RegisterRoutes(this.app as express.Express);

La documentation indique qu’il est intéressant d’utiliser le package methodOverride pour ajouter des verbes supplémentaires. Enfin …. elle le dit pas mais c’est dans les exemples de code.

Test

On lance et miracle : ça marche.

Documentation

Pour la documentation, il y a encore un peu de travail mais pas tant que cela par rapport à ce que l’on a fait avant. En fait, il faut modifier la commande qui récupère la documentation pour qu’elle aille directement lire le fichier swagger.json généré :

/**
     * Ajoute la route de la documentation
     * @param app
     */
    public static addDocRoute(app: any) {
        // Version JSODC
        app.use("/api/jsDocs", swagger.serve, swagger.setup(ConfigDoc.specs));
        // Version TSOA
        app.use("/swagger.json", express.static(__dirname + "/swagger/swagger.json"));
        app.use("/api/tsoa", swagger.serve, swagger.setup(swaggerJSON));
    }

Et bim, ça marche. On profite même de la récupération des documentations réalisées directement sur les entités :

Générer à la volée

Alors par contre souci : il faut que cela rentre dans mon build de dev car sinon, je serais jamais à jour. Pour cela, deux nouvelles entrées dans le package json :

"watch-tsoa": "onchange 'src/**/*tsoa*.ts' -- npm run tsoa",
"tsoa": "tsoa routes && tsoa swagger"

La première appelle la deuxième si quelques choses dans mes contrôleurs et l’autre appelle les commandes TSOA.

Bilan

C’est pas tout mal … J’aime bien le principe qu’une information serve deux fois …

Mais il reste pas mal de choses à valider :

  • Gestion des POST, PUT & Co …
  • Et surtout la sécurité !!!

Bilan de la journée

Clairement la première solution est pratique pour un projet dans lequel tout est déjà en place. On vient vraiment documenter. Donc je vais la privilégier dans ce cas.

Par contre, j’ai une nouvelle API à faire (enfin … disons que j’ai envie) et je vais essayer de pousser TSOA.

Mais ça … c’est pour un autre demain.

Tu m’entends quand je te parle ? (partie 2)

Suite

Dans la continuité de l’article précédent : ici. L’idée est d’essayer de faire une application mobile qui intègre les possibilités d’écoute d’un utilisateur.

Point de départ

Afin de partir d’un existant, je suis partie d’une application réalisée suite au tuto suivant : https://devdactic.com/ionic-realtime-socket-io/. D’ailleurs, pour info, le tuto est super intéressant et montre une fonctionnalité intéressante de socket.io. Une fois n’est pas coutume, tout fonctionne du premier coup !

En terme de reprise, l’idée est de reprendre l’application, la navigation etc… Par contre, je vais enlever toute la partie Socket … A ce demander pourquoi je l’ai repris en fait … Bref …

SpeechToText

Mise en place

Pour faire cela, j’ai repris le plugin vu lors de la première partie : ici et . L’installation est assez simple 🙂


$ ionic cordova plugin add cordova-plugin-speechrecognition
$ npm install --save @ionic-native/speech-recognition

Au niveau de l’utilisation, il faut suivre la documentation dans laquelle je trouve qu’il ne manque qu’une seule chose: l’enregistrement dans les modules. Mais bon c’est pas complique :

  • Ajout d’un import import { SpeechRecognition } from '@ionic-native/speech-recognition';
  • Ajout d’un provider : SpeechRecognition

Voici le code pour lancer l’écoute :

    listenUserMessage() {
        let options : SpeechRecognitionListeningOptions = {
            language: 'fr-FR'
            , matches: 1
            , prompt: ''
            , showPopup: false
            , showPartial: false
        }

        this.speechRecognition.startListening(options)
            .subscribe(
                (matches: Array) => {
                    this.zone.run(() => { 
                        this.sendMessage(this.nickname, matches[0]); 
                        this.answer(matches[0])
                    });
                },
                (onerror) => console.error('error:', onerror)
            )
    }

Quelques petits soucis 🙂

Normal ! Les voicis :

Matches

Une option doit permettre de limiter le nombre de retour : matches. Bon de mon côté, il s’en moque totalement … Pour des phrases, il m’en retourne 5 qui est la valeur par défaut … Bon, je prends le premier résultat.

Mise à jour de l’écran

A la réception des données, l’interface ne se mettait pas à jour. J’ai cru comprendre que c’était un souci d’évènement que je remonte mal. Pour résoudre le souci à court terme : this.zone.run()

TextToSpeech

La même chose mais avec un autre plugin !

Il faut utiliser : ici & qui s’installe comme le premier :


$ ionic cordova plugin add cordova-plugin-tts
$ npm install --save @ionic-native/text-to-speech

Code

Simple à faire peur :

 this.tts.speak({
                text: answer
                , locale: 'fr-FR'
                , rate: 0.85
            })

.

Un truc assez fort !

Si le texte contient « 15:34 », on entend 15 heures 34. Fort quand même …

Bilan

Je suis content, j’ai une application qui fonctionne 🙂 Elle écoute et réponds à quelques questions. Comme vu la dernière fois, ce n’est pas là que se passe les choses. il faudrait maintenant la pluguer vers quelque chose d’intelligent. Mais cela c’est autre demain !

Tu m’entends quand je te parle ?

En discutant avec un client, il m’a indiqué qu’il aimerait bien pouvoir piloter certaines applications par la parole … Tiens, effectivement, cela peut-être intéressant …

Web Speech API

Présentation

Il s’agit d’une API qui semble exister depuis plusieurs années (2014 ?) mais qui ne semblent ne pas être très encore supportée :

avec quelques compléments :

  • Chrome: ne supporte pas l’extension directe mais il faut passer par WebKit,
  • Firefox: il faut activer une option media.webspeech.recognition.enable.

Autant dire que c’est pas encore la reconnaissance totale (jeu de mot).

Trouver où le faire …

Alors pour tester c’est pas simple :). En fait, cela ne semble pas supporter dans Firefox même avec le flag passer à true (confirmé ici & ). J’ai essayé de faire fonctionner les exemples et cela n’a jamais fonctionné … Donc passage sous chrome enfin chromium chez moi.

Pour pas perdre trop de temps: il faut faire tourner sur un serveur local (Merci Docker !) sinon souci de sécurité. En cas de MEP (?!?!), ne pas oublier l’HTTPS car sinon cela ne marche pas !

Code

Le code de test est très simple et est une adaptation des articles de MDN:


// Init en 100% Chrome
var recognition = new webkitSpeechRecognition();
// Ne France Monsieur !
recognition.lang = 'fr-FR';
// On retourne tout !
recognition.interimResults = true; 
// Valeur par défaut
recognition.maxAlternatives = 1;


// Listeners
$('#btnStart').click(function() { recognition.start(); });
$('#btnStop').click(function() { recognition.stop(); });

recognition.onresult = function(event) {
    console.log(event);
}

Console

Regarder le contenu en console est intéressant car on voir l’avancée de le reconnaissance. La phrase prononcée est « bonjour messieurs ». Avec les différents résultats, le log contient les différentes reconnaissances :

Un test avec une phrase plus longue: ‘de bon matin, je parle à un micro qui ne me réponds pas’:

Comme il faut tout tester : un gros mot 🙂

T’arrêtes pas !

Par défaut, le système d’écoute s’arrête quand le système atteint une phrase « finale ». Pour le mettre en continue : recognition.continuous = true;. Je pense qu’un petit time out serait pas mal quand même :).

Mode connecté

Le message par MDN est assez clair :

Donc attention: vous êtes sur écoute !!!

Bilan

Sous Chrome cela fonctionne pas mal quand même. Il faut définir l’après et l’utilisation mais la reconnaissance de texte fonctionne quand même pas mal. Par contre, en regardant d’un peu plus près les dates des différents articles, on voit que le sujet ne semble pas avoir beaucoup bougé depuis les premiers travaux en 2014 …

Liens

Cordova – Plugin

Il existe un plugin pour intégrer le même mécanisme dans une application mobile : ici. Le documentation est assez clair et le plus compliqué a été de mettre à jour Cordova.

Pour info et d’après la documentation, le plugin utilise l’API de Google (ici) mais elle le fait au travers SpeechRecognizer. A noter que la documentation indique que l’écoute ne peut être continue.

Bilan

Bon ben c’est pas mal mais le vrai travail est plus dans l’interprétation mais c’est assez sympa. A la prochaine …