samedi 20 mai 2017

Créer un router avec la librairie shelf

Avec Dart et la librairie shelf il est très simple de créer un serveur HTTP pour servir des pages statiques ou créer une API REST.

Mais il n'est rien prévu d'origine pour avoir un router qui permet de faire les deux en même temps, voir même plusieurs API REST.

J'ai bien proposé de l'intégrer directement, mais cela n'a pas été accepté pour des raisons que je comprend bien. Je vais donc partager ici ma solution.

Créer un fichier nommé shelf_router.dart et copier/coller :
import 'package:shelf/shelf.dart';
import 'dart:core';

/// Creates a Shelf [Handler] that allow to register some Shelf [Handler] like
/// [createStaticHandler] or [createRpcHandler] on same listener.
/// Just give map [routing] to link [key] that represent path to [value] that
/// contain handler.
/// The path [key] is only static path.
Handler createSubHandler(Map routing) {
  return (Request request) {
    // Selected path
    String selectedPath = '';

    routing.forEach((key, value) {
      String currentPath = '/' + request.url.path;

      if (currentPath.startsWith(key)
        && currentPath.length > selectedPath.length) {
        selectedPath = key;
      }
    });

    Handler h = routing[selectedPath];

    if (h == null) {
      return new Response.notFound('Not Found');
    } else {
      Uri requestUri = request.requestedUri;

      Uri newRequestedUri = new Uri(
        scheme: requestUri.scheme,
        userInfo: requestUri.userInfo,
        // Getter return empty string if host is null
        host: requestUri.host.length == 0 ? null : requestUri.host,
        port: requestUri.port,
        path: requestUri.path.substring(selectedPath.length - 1),
        query: requestUri.query,
        queryParameters: requestUri.queryParameters,
        fragment: requestUri.fragment);

      Uri url = request.url;

      Uri newUrl = new Uri(
        scheme: url.scheme,
        userInfo: url.userInfo,
        // Getter return empty string if host is null
        host: url.host.length == 0 ? null : url.host,
        port: url.port,
        path: url.path.substring(selectedPath.length - 1),
        query: url.query,
        queryParameters: url.queryParameters,
        fragment: url.fragment);

      Request newRequest = new Request(request.method, newRequestedUri,
        protocolVersion: request.protocolVersion,
        headers: request.headers,
        handlerPath: request.handlerPath,
        url: newUrl,
        body: request.readAsString().asStream(),
        encoding: request.encoding,
        context: request.context,
        onHijack: request.hijack);

      return h(newRequest);
    }
  };
}

Ensuite pour l'utiliser, rien de plus simple :
// Create a Shelf handler for your RPC API.
var handlerApi = shelf_rpc.createRpcHandler(apiServer);
// Create a Shelf handler for static file
var handlerStatic = shelf_static.createStaticHandler('example/files', defaultDocument: 'index.html');

var handler = shelf_router.createSubHandler({
  '/': handlerStatic,
  '/api/': handlerApi,
  '/api/toto/': otherHandler
});

Il vous suffit de créer une map qui associe le path et le handler.
Le système vérifie le path le plus long pour que comme dans l'exemple, il n'y ait pas collision même si le début du path est identique.

A noter que le router ne supporte pas les variables dans les path. C'est logique car ici, ça n'aurait pas de sens.

jeudi 16 février 2017

Créer des afficheurs ou des filtres dans les tempaltes d'Aurelia JS

Aurelia est un framework web comme l'est Angular JS.
Il est un peu moins connu, mais il est pourtant très agréable à utiliser.

Venant du monde d'Angular 1, j'ai été quelque peu perturbé par la gestion des filtres sur les données (comme les tableaux), à tort, car le fonctionnement est logique et unifié, comme chaque partie du framework Aurelia.

Aurelia utilise le même principe qu'Angular, dans le HTML il faut utiliser le pipe "|".
Là où Aurelia se distingue c'est par sa logique. Que ce soit un filtre ou un convertisseur, c'est la même chose.

En effet, convertir l'affichage d'un nombre (par exemple une monnaie) s'écrit simplement ${maVar | convertisseur}.

Créer un afficheur de prix en euro


Pour faire un afficheur en euro, rien de plus simple, créez une classe quelque part dans vos sources :
/**
 * Converti le prix en centime, en euro.
 */
export class PriceValueConverter {
  toView (price) {
    return (price / 100) + ' €'
  }
}

Dans votre template incluez le convertisseur et utilisez le :
<template>
  <require from="../../../converters/price/price"></require>
  ...
  <span>${o.prix | price}</span>
  ...
</template>

Vous pouvez aussi inclure votre convertisseur de façon globale :
export function configure (aurelia) {
  aurelia.use
    .basicConfiguration()
    .developmentLogging()
    .globalResources(
      './price-value-converter'
    )

  aurelia.start().then(() => aurelia.setRoot())
}

Notez que le nom du convertisseur est conventionné, c'est en minuscule la partie avant ValueConverter.

Ce n'est pas plus compliqué que ça.

Créer un filtre de tableau 1ère solution


Créer un filtre de tableau est identique.

Créons le filtre suivant :
export class FilterValueConverter {
  toView (array) {
    return array.filter(item =>
      filterFunc('recherche', item)
    )
  }
  
  filterFunc (searchExpression, value) {
    const itemValue = value.name

    return (!searchExpression || !itemValue)
      ? false
      : itemValue.toUpperCase().indexOf(searchExpression.toUpperCase()) !== -1
  }
}

Dans le template :
<template>
  ...
  <div repeat.for="s of array | filter" />
  ...
</template>

On filtre donc un tableau qui contient des structures de données avec un champ name.
L'inconvénient c'est qu'on ne peut pas préciser le terme à rechercher via un input type text.

Créer un filtre de tableau 2ème solution


Modifions le code comme ceci :
export class FilterValueConverter {
  toView (array, searchTerm) {
    return array.filter(item =>
      filterFunc(searchTerm, item)
    )
  }
  
  filterFunc (searchExpression, value) {
    const itemValue = value.name

    return (!searchExpression || !itemValue)
      ? false
      : itemValue.toUpperCase().indexOf(searchExpression.toUpperCase()) !== -1
  }
}

Côté template on spécifie la valeur via un input text :
<template>
  ...
  <input type="text" value.bind="searchTerm" />
  ...
  <div repeat.for="s of array | filter:searchTerm" />
  ...
</template>

On passe donc le texte à rechercher via un input html.
C'est mieux mais ça impose un ValueConverter par type de recherche.

Créer un filtre de tableau 3ème solution


D'une recherche à l'autre, ce qui change finalement c'est la partie filterFunc.
Nous créons donc un FilterValueConverter générique :
export class FilterValueConverter {
  toView (array, searchTerm, filterFunc) {
    return array.filter(item =>
      searchTerm && searchTerm.length > 0
        ? filterFunc(searchTerm, item)
        : true
    )
  }
}

Côté template, on ajout la fonction a appeler :
<template>
  ...
  <input type="text" value.bind="searchTerm" />
  ...
  <div repeat.for="s of array | filter:searchTerm:myFilter" />
  ...
</template>

Dans le controlleur du template, il faut ajouter la fonction de filtrage :
export class MyController {
  ...
    myFilter (searchExpression, value) {
    const itemValue = value.name

    return (!searchExpression || !itemValue)
      ? false
      : itemValue.toUpperCase().indexOf(searchExpression.toUpperCase()) !== -1
  }
  ...
}

Et voilà, le tour est joué.