Ceci n'est qu'une ébauche de solution pour activité 01, pas une solution complète. L'idée est de vous orienter pour résoudre des problèmes connexes dans le futur, et de vous aider à aborder des situations semblables quand cela surviendra.
Puisqu'un modèle de services nous était confié, inspirons-nous en, et commençons par des tâches simples et mécaniques :
Ne reste qu'à se trouver un contrôleur existant à titre d'inspiration, puis on y va.
J'ai regardé un peu ce que faisaient les API existantes, et j'ai pris celle de contacts comme base, car cette API ressemble à ce que demande Activité 01 (en plus simple, évidemment).
Pour une simple requête GET de base (p. ex. : GET sur l'URI qu'est /api/bookmarks), de même que pour un GET par identifiant (p. ex. : GET sur /api/bookmarks/3 pour le signet dont le champ Id vaut 3), c'est presque déjà fait. On remplace le mot contact par le mot bookmark dans le code de BookmarksController et ça fonctionne tel quel.
Faut ajuster un peu plus le code pour les autres requêtes GET par contre, soit celles qui comportent des paramètres. Le temps est venu de se retrousser les manches...
Sans grandes surprises, faut lire un peu le code existant pour comprendre la mécanique qui y existe déjà; on ne peut pas vraiment avancer par chance (on peut, mais c'est pas une technique fiable), alors faut se faire un plan et prendre des notes.
Si on creuse un peu le code de router.js, qui relaie le code vers notre contrôleur, on constatera qu'il y a un couplage un peu malheureux entre la fonction de validation API_Endpoint_Ok et certaines extractions d'information de l'URI reçue, en particulier le paramètre Id s'il y en a un. C'est... embêtant, mais refactoriser à ce stade serait prématuré.
Si on examine API_Endpoint_Ok plus en détail, on constate une extraction de la queryString de la requête, mais dans un passage qui ne conserve pas les paramètres. On peut en profiter pour ajouter une variable (essentiellement globale, comme l'est id dans ce code...) nommée args qui conservera les paramètres de la requête.
Avant | Après |
---|---|
|
|
Pour en tenir compte lors de l'appel des services get du contrôleur, on peut ajuster l'appel à la méthode get du contrôleur pour lui relayer les paramètres :
Avant | Après |
---|---|
|
|
... et ajuster la méthode get pour qu'elle devienne d'arité 2 (à deux paramètres) dans le contrôleur pour en tenir compte :
Avant | Après |
---|---|
|
|
Nous voilà prêts à commencer à programmer, à partir de ce nouveau else qui sera notre point de départ.
Procédons à partir de maintenant par étapes :
Allons-y.
Nous voudrons donc :
Pour permettre à des clés et à des valeurs de contenir des symboles spéciaux (comme = ou & par exemple), nous accepterons des délimiteurs autour d'eux. Pour cet exemple, je me limiterai au caractère '\"' (en JavaScript : '"'), mais il serait plus idiomatique de supporter aussi '\'' (en JavaScript : "'"). Notez toutefois que le traitement qui suit est trop naif pour gérer correctement ces caractères spéciaux.
Mon type de paire sera :
class Pair {
constructor(key, value) {
this.key = key;
this.value = value;
}
}
Ma fonction pour traiter la queryString se nommera parseArgs :
function parseArgs(args) {
let res = [];
let debut = 0;
let fin = args.indexOf('&');
while (fin != -1 && debut != fin) {
res.push(parseArgument(args.substr(debut, fin)));
debut = fin + 1;
fin = args.indexOf('&', debut);
}
res.push(parseArgument(args.substr(debut, args.length)));
return res;
}
Vous aurez noté que, suivant le plan de match annoncé plus haut, parseArgs (pluriel) délègue le traitement de chaque paramètre à parseArgument (singulier), que voici :
function parseArgument(str) {
let pos = str.indexOf('=');
let key = cleanArgument(str.substr(0, pos));
let value = cleanArgument(str.substr(pos + 1, str.length));
return new Pair(key, value);
}
La fonction cleanArgument élague ce qui n'est pas pertinent à chacune des string traitées, et utilise lui-même deQuotify qui élimine les guillemets englobants s'il y en a :
function deQuotify(s) {
if(s == null || s.length <= 1) return s;
let first = s[0];
let last = s[s.length-1];
if((first == '"' || last == '"') && first != last)
throw "not well-formed quoted string";
return first == '"'? s.substr(1, s.length - 2) : s;;
}
function cleanArgument(s) {
return deQuotify(decodeURI(s).trim());
}
Une fois la chaîne décortiquée, nous voudrons générer une séquence de transformations tenant compte du fait qu'il peut y avoir plusieurs paramètres combinés dans le code client (p. ex. : name="..."&category="..."&sorted="name" par exemple). Nous appliquerons les transformations dans l'ordre où elles apparaissent, pour que le code client soit en contrôle de la qualité de sa propre expérience.
L'idée est de :
Pour simplifier le modèle, je lèverai une exception si un problème survient en cours de route
J'y irai de l'approche suivante :
Pour faciliter la compréhension, commençons par les opérations les plus concrètes, pour aller vers les plus abstraites.
Un filtre sera une fonction qui prend une liste de valeurs et ne conserve que certaines d'entre elles. Dans notre cas, on parle par exemple de conserver les instances de Bookmark ayant un nom spécifique ou appartenant à une catégorie choisie.
Une implémentation possible serait :
|
|
Vous remarquerez que le code est très semblable, ce qui change étant le membre d'un Bookmark auquel nous accédons en pratique. Si nous faisons abstraction de cet accès à un membre à l'aide d'une fonction, on peut nettement simplifer le tout :
function keepIf(val,member) {
return function(lst) {
let res = [];
for(let i = 0; i != lst.length; ++i) {
if(cleanArgument(member(lst[i])) === val) {
res.push(lst[i]);
}
}
return res;
}
}
function keepName(name) {
return keepIf(name, function(bm) { return bm.Name; });
}
function keepCategory(cat) {
return keepIf(cat, function(bm) { return bm.Category; });
}
Je n'ai pas implémenté les patterns comme abc* ou *def mais le modèle serait le même; il faudrait passer un prédicat en paramètre qui contrôlerait le test fait par keepIf, pour remplacer la comparaison brute avec === par quelque chose de plus fin au besoin.
Remarquez que la fonction keepIf retourne une fonction destinée à être appliquée plus tard. C'est une fabrique de fonctions.
Pour les besoins des tris, j'ai implémenté un banal tri à bulles (c'est un algorithme médiocre, mais l'idée est de garder l'exemple simple), tout en paramétrant le prédicat utilisé pour tester l'ordre selon lequel les objets sont placés. En gros :
function sort(lst,pred) {
for(let i = 0; i < lst.length - 1; ++i)
for(let j = i + 1; j < lst.length; ++j)
if(!pred(lst[i], lst[j])) {
let temp = lst[i];
lst[i] = lst[j];
lst[j] = temp;
}
return lst;
}
Dans le but de générer des fonctions de tri respectueuses des critères soumis par le code client, j'ai écrit une fabrique :
function sortBy(att) {
if (att === 'name') {
return function(lst) {
return sort(lst, function(a,b) {
return cleanArgument(a.Name) < cleanArgument(b.Name);
});
}
} else if (att === 'category') {
return function(lst) {
return sort(lst, function(a,b) {
return cleanArgument(a.Category) < cleanArgument(b.Category);
});
}
} else {
throw "bad sort criterion";
}
}
On aurait pu faire plus simple avec un dictionnaire dont les clés seraient les critères selon lesquels trier et les valeurs seraient des prédicats. Si ça vous amuse, l'écrire ainsi peut être instructif.
Remarquez que la fonction sortBy, comme la fonction keepIf, retourne une fonction destinée à être appliquée plus tard. C'est une fabrique de fonctions.
Nous avons donc deux fabriques de transformations, soit keepIf et sortBy plus haut. Reste à choisir quelle transformation générer pour chaque paramètre reçu dans une requête GET donnée. À cette fin, j'utiliserai makeTransform ci-dessous :
// créer le tableau de fonctions
function makeTransform(key, value) {
if(key === 'name') {
return keepName(value);
} else if (key === 'category') {
return keepCategory(value);
} else if (key === 'sort') {
return sortBy(value);
} else {
throw "invalid operation name";
}
}
Ne reste plus qu'à itérer à travers les paramètres reçus pour construire la séquence de transformations correspondante :
function buildProcess(pairs) {
let funcs = [];
for(let i = 0; i != pairs.length; ++i) {
funcs.push(makeTransform(pairs[i].key, pairs[i].value));
}
return funcs;
}
Et voilà, l'infrastructure est en place. Reste à générer la réponse à la requête reçue.
À titre de rappel, notre version modifiée (d'arité 2) de la méthode get du BookmarksController a présentement la forme suivante :
// ...
// GET: api/bookmarks
// GET: api/bookmarks/{id}
// GET: api/bookmarks/...args...
get(id,args){
if(args === undefined) {
if(!isNaN(id))
this.response.JSON(this.bookmarksRepository.get(id));
else
this.response.JSON(this.bookmarksRepository.getAll());
} else {
this.response.JSON(null); // en attendant
}
}
// ...
C'est la ligne qui génère une réponse null que nous allons remplacer par le fruit de nos efforts. Il nous faudra d'abord consommer les paramètres pour en faire une liste de paires :
let parsedArgs = parseArgs(args);
... puis générer des transformations à partir de cette liste :
let funcs = buildProcess(parsedArgs);
... puis prendre l'ensemble des données disponibles à titre de point de départ :
let src = this.bookmarksRepository.getAll();
Ne reste plus qu'à transformer src en lui appliquant successivement chacune des transformations dans le tableau funcs :
for(let i = 0; i != funcs.length; ++i) {
src = funcs[i](src);
}
À la fin, src contient ce que le client a demandé :
this.response.JSON(src);
Le code complet est donc :
// ...
// GET: api/bookmarks
// GET: api/bookmarks/{id}
// GET: api/bookmarks/...args...
get(id,args){
if(args === undefined) {
if(!isNaN(id))
this.response.JSON(this.bookmarksRepository.get(id));
else
this.response.JSON(this.bookmarksRepository.getAll());
} else {
let parsedArgs = parseArgs(args);
let funcs = buildProcess(parsedArgs);
let src = this.bookmarksRepository.getAll();
for(let i = 0; i != funcs.length; ++i) {
src = funcs[i](src);
}
this.response.JSON(src);
}
}
// ...
... et le tour est joué.
Nous avons quelques répétitives for avec indices dans nos exemples, or dans la plupart des cas nous souhaitons vraiment itérer sur les éléments, sans avoir réellement d'intérêt pour les indices. Les tableaux de JavaScript exposent un service forEach acceptant une λ et l'appliquant sur chacun des éléments parcourus.
Ceci permet d'aller plus à l'essentiel dans bien des cas. Comparez :
Avec boucle à indices | Avec forEach |
---|---|
|
|
|
|
|
|
On pourrait aller plus loin encore, mais ça donne une idée.
Une autre simplification facile à apporter et le remplacement des fonctions anonymes par des expressions λ. Quelques exemples :
Avec fonction anonyme | Avec expression λ |
---|---|
|
|
|
|
|
|
L'allègement est visible.