Vert-X + Groovy: Faire un framework pour paresseux

Ce qui est intĂ©ressant avec Groovy, c’est la “meta programmation”, c’est Ă  dire sa capacitĂ© Ă  ajouter des comportements Ă  des classes existantes (mais ce n’est pas que ça). Aujourd’hui voyons donc comment “commencer” un framework pour les “faignasses”.

Tout d’abord je remercie https://twitter.com/glaforge qui m’a permis de faire les choses dans les rùgles de l’art.

Mon objectif est de simplifier le code Ă  Ă©crire, et dans un 1er temps je suis parti sur le concept d’ExpandoMetaClass http://www.groovy-lang.org/metaprogramming.html# metaprogramming_emc (J’ajoute des mĂ©thodes aux classes de Vert-X au run-time).

Mais ma problĂ©matique, Ă©tait de “charger/greffer” ses nouvelles mĂ©thodes de la façon la plus transparente possible. Avec le principe d’ExpandoMetaClass, il faut explicitement “exĂ©cuter” les “greffons” dans le code. Et si on souhaite modulariser ses extensions, il faudra crĂ©er une classe dans un package avec une mĂ©thode qui exĂ©cute les extensions et appeler explicitement cette mĂ©thode:

class Augmentations {

  static pimpMyClasses() {

    String.metaClass.yo { ->
      return "yo " + delegate
    }

  }
}

Puis dans mon code j’appelerais:

Augmentations.pimpMyClasses()

ça fonctionne, mais j’aurais bien aimĂ© que mes classes soient “augmentĂ©es” de maniĂšre transparentes. Et c’est lĂ  que Guillaume m’a mis sur la piste des Extension Modules http://www.groovy-lang.org/metaprogramming.html# _extension_modules. En gros, c’est le moyen de charger automatiquement vos extensions sans avoir Ă  le faire de maniĂšre explicite dans votre code.

Mise en oeuvre d’un module d’extension

Pour cela il vous faut danns votre projet un répertoire (et sous-répertoires) /resources/META-INF/services dans lequel vous aurez un fichier org.codehaus.groovy.runtime.ExtensionModule

my-app/
├── src/ 
|   └── main/   
|       ├── groovy/ 
|       |    └── Starter.groovy    
|       └── resources/ 
|           └── META-INF/ 
|                └── services/ 
                      └── org.codehaus.groovy.runtime.ExtensionModule              

Dans ce fichier nous allons dĂ©finir oĂč sont nos extensions:

moduleName=Some extensions for my framework
moduleVersion=1.0-wip
extensionClasses=my.extensions.WebExtensions

Ensuite créez une classe WebExtensions dans un package my.extensions et créons nos extensions.

Extensions de classes

1Ăšre extension: param

Lorsque je veux rĂ©cupĂ©rer le paramĂštre d’une requĂȘte de type GET avec Vert-x, je dois Ă©crire:

String name = context.request().getParam("name").toString()

Et que je crĂ©e ma classe d’extensions comme ceci:

package my.extensions

import io.vertx.groovy.ext.web.RoutingContext
import io.vertx.groovy.ext.web.handler.StaticHandler

class WebExtensions {

  static Object param(RoutingContext self, String paramName) {
    return self.request().getParam(paramName)
  }
}

Maintenant je pourrais récupérer mes paramÚtres comme cela:

String name = context.param("name").toString()

Je suis d’accord, je n’ai pas gagnĂ© grand chose, donc allons un peu plus loin.

Nouvelle extension: sendJson

Pour renvoyer du Json Ă  mon navigateur, avec vert-x je dois Ă©crire:

context
    .response()
    .putHeader("content-type", "application/json")
    .end(Json.encodePrettily([
      "message":"Hi!",
      "name": "Bob Morane"
    ]))

si j’ajoute cette mĂ©thode Ă  ma classe d’extensions:

//import io.vertx.core.json.Json

static void sendJson(RoutingContext self, content) {
  self.response()
    .putHeader("content-type", "application/json")
    .end(Json.encodePrettily(content))
}

Maintenant je pourrais Ă©crire:

context.sendJson([
    "message":"Hi!",
    "name": "Bob Morane"
])

Dans le mĂȘme esprit, je voudrais pouvoir rĂ©cupĂ©rer les donnĂ©es Json lors d’un POST.

Nouvelle extension: bodyAsJson

Normalement je dois Ă©crire ceci:

Json.decodeValue(context.getBodyAsString(), Object.class)

si j’ajoute cette mĂ©thode Ă  ma classe d’extensions:

static Object bodyAsJson(RoutingContext self, klass) {
  return Json.decodeValue(self.getBodyAsString(), klass)
}

Maintenant je pourrais Ă©crire:

def obj = context.bodyAsJson(Object.class)

Mais allons encore un peu plus loin

Nouvelles extensions: GET et POST

Actuellement je définis mes routes comme ceci:

router.get("/api/hi/:name").handler({ context ->
  // foo
})
router.post("/api/humans").handler({ context ->
  // foo
})

si j’ajoute ces mĂ©thodes Ă  ma classe d’extensions:

//import io.vertx.groovy.ext.web.Router

static void GET(Router self, String uri, handler) {
  self.get(uri).handler(handler)
}

static void POST(Router self, String uri, handler) {
  self.post(uri).handler(handler)
}

Maintenant je pourrais Ă©crire:

router.GET("/api/hi/:name", { context -> 
  // foo
}

router.POST("/api/humans", { context -> 
  // foo
}

Et enfin une derniĂšre pour la route: start

Pour dĂ©marrer mon serveur http et lui expliquer oĂč sont mes assets statiques, je fais ceci:

router.route("/*").handler(StaticHandler.create())
server.requestHandler(router.&accept).listen(8080)

si j’ajoute ceci à ma classe d’extensions:

/*
import io.vertx.groovy.core.http.HttpServer
import io.vertx.groovy.ext.web.handler.StaticHandler
*/

static void start(HttpServer self, Router router, Integer port, String staticPath) {
  println("HttpServer is listening on " + port)
  router.route(staticPath).handler(StaticHandler.create())
  self.requestHandler(router.&accept).listen(port)
}

Maintenant je démarre mon serveur comme cela:

server.start(router, 8080, "/*")

Donc 


Au final le code de notre projet devrait ressembler Ă  ceci

def server = vertx.createHttpServer()
def router = Router.router(vertx)

router.route().handler(BodyHandler.create())

router.GET("/api/hi/:name", { context ->
  context.sendJson([
      "message":"Hi!",
      "name": context.param("name").toString()
  ])
})

router.POST("/api/humans", { context ->
  def obj = context.bodyAsJson(Object.class)
  obj.id = new Random().nextInt(100)
  context.sendJson(obj)
})

router.POST("/api/2/humans", { context ->
  Human bob = context.bodyAsJson(Human.class)
  bob.id = new Random().nextInt(100)
  context.sendJson(bob)
})

server.start(router, 8080, "/*")

Du bon code de faignasse, facile Ă  lire et Ă©crire ;)

@+

blog comments powered by Disqus

Related posts