Libellés

jeudi 21 juillet 2011

Ecrire un DSL en Groovy

Récemment j'ai écrit un DSL en Groovy pour WRO4J, et je vais vous expliquer comment y arriver soi même.

Voici à quoi ressemble au final le DSL :

groups {
g1 {
js "/static/app.js"
css "/static/app.css"
}
"an-other-group" {
js("classpath:com/application/static/app.js")
css(minimize: false, "http://www.site.com/static/app.css")
}
}

https://github.com/Filirom1/wro4j/blob/master/wro4j-extensions/src/test/resources/ro/isdc/wro/extensions/model/factory/Wro.groovy

Si l'on analyse un plus le DSL, on retrouve :

- une closure groups englobant l'ensemble du dsl
- deux closures g1 et an-other-group ayant des noms personnalisées
- deux methodes js et css, pouvant prendre 1 ou deux arguments : js(Map params = [:], String uri)


Maintenant pour construire ce DSL nous allons partir de la fin, les méthodes js et css.
Voici le code source, regarder la signature des méthodes :

class ResourceDelegate {
List resources = new ArrayList()

void css(Map params = [:], String name) {
def resource = Resource.create(name, ResourceType.CSS)
if (params.get("minimize") == false) resource.minimize = false
resources.add(resource)
}

void js(Map params = [:], String name) {
def resource = Resource.create(name, ResourceType.JS)
if (params.get("minimize") == false) resource.minimize = false
resources.add(resource)
}
}

Le premier paramètre params est facultatif. Nous pouvons donc utiliser ces méthodes comme ceci :

def resourceDelegate = new ResourceDelegate()

//avec params
resourceDelegate.js([minimize: true, key: value], "js/script.js")
resourceDelegate.js(minimize: true, key: value, "js/script.js")

//et sans params
resourceDelegate.js([:], "js/script.js")
resourceDelegate.js("js/script.js")
resourceDelegate.js "js/script.js"


Maintenant il nous faut intégrer ces méthodes dans une closure pour pouvoir écrire un DSL comme celui là :

myGroup{
js "script.js"
js(minimized: true, "script2.js")
}


Je défini ici une méthode myGroup prenant comme seul paramètre une closure et qui exécute cette closure.

class GroupDelegate {
def myGroup(Closure cl){
cl()
}
}

Je peux donc appeler cette méthode de différentes façons :

myGroup({
println "inside the closure"
})
myGroup {
println "inside the closure"
}

Je vais choisir la deuxième façon qui est plus joli pour un DSL.

Je modifie cette closure pour déléguer tout les appels se passant à l'interieur de cette closure vers une instance de ResourceDelegate (défini dans la première partie)

class GroupDelegate {
List groups = new ArrayList()

def myGroup(Closure cl){
cl.delegate = new ResourceDelegate()
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
groups.add(new Group(name: 'myGroup', resources: cl.resources))
}
}

Et maintenant je peux écrire :

@Test
public void testGroupDelegate() {
//setup:
def groupDelegate = new GroupDelegate()

//when:
groupDelegate.myGroup {
js("/js/script.js")
css("/css/style.css")
}

//then:
assert 1 == groupDelegate.groups.size()
assert "myGroup" == groupDelegate.groups[0].name
assert 2 == groupDelegate.groups[0].resources.size()
}


Notre DSL commence à prendre forme, mais nous ne pouvons créer qu'un seul groupe. L'idée maintenant serait de pouvoir écrire plusieurs groupes comme ci dessous :

groupDelegate.g1 {
js("/js/script.js")
css("/css/style.css")
}
groupDelegate.'second-group' {
js("/js/script.js")
css("/css/style.css")
}


Rien de plus simple en groovy, il suffit d'utiliser la méthode def methodMissing(String name, args).
La méthode methodMissing est appelée quand groovy ne trouve aucune méthode correspondant. La variable name nous donne le nom de la méthode appelée et args la liste des arguments passés à la méthode.

Voici cette fois la classe GroupDelegate utilisant methodMissing :

class GroupDelegate {
List groups = new ArrayList()

def methodMissing(String name, args) {
def cl = args[0]
cl.delegate = new ResourceDelegate()
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
groups.add(new Group(name: name, resources: cl.resources))
}
}


On approche de la fin, maintenant il nous faut intégrer les différents groupes à l'intérieur d'une closure comme ceci :

wroModelDelegate.groups {
g1 {
js("/js/script.js")
css("/css/style.css")
}
'second-group' {
js("/js/script2.js")
}
}

Comme nous avons déjà réalisé cette tâche précédemment, je vous donne directement la solution (il faut déléguer les appels se passant à l’intérieur de la closure vers une instance de GroupDelegate) :

class WroModelDelegate {
WroModel wroModel = new WroModel()

void groups(Closure cl) {
cl.delegate = new GroupDelegate()
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
wroModel = new WroModel(groups: (Collection) cl.getProperty("groups"))
}

}

Courage on approche de la fin. Maintenant on aimerait pouvoir écrire notre DSL dans un fichier dsl.groovy, parser ce fichier et récupérer la valeur.
Pour cela nous allons travailler avec la classe Script de groovy.

def dsl = """
groups {
g1 {
js("/js/script.js")
css("/css/style.css")
}
g2 {
js("/js/script2.js")
}
}
"""

Script dslScript = new GroovyShell().parse(dsl)
dslScript.metaClass.mixin(WroModelDelegate)
dslScript.run()

Ce script (dslScript) contient une liste d'instruction dont la première est d'appeler la methode groups(Closure). Sauf que cette méthode n'est pas connu naturellement dans dslScript.
C'est pour cela que nous faisons appel à dslScript.metaClass.mixin(WroModelDelegate).
Nous mixons le script avec WroModelDelegate, ce qui correspond à ajouter toute les méthodes et les attributs de WroModelDelegate dans le dslScript.

Maintenant il suffit d'executer le script et c'est gagné :D
dslScript.run()



L'ensemble du code source se trouve ici : https://github.com/Filirom1/wro4j/blob/master/wro4j-extensions/src/main/groovy/ro/isdc/wro/extensions/model/factory/GroovyWroModelParser.groovy

Et les tests là :
https://github.com/Filirom1/wro4j/blob/master/wro4j-extensions/src/test/groovy/ro/isdc/wro/extensions/model/factory/TestGroovyWroModelParser.groovy


Une version testable et modifiable en ligne est également disponible ici :
http://groovyconsole.appspot.com/script/516002

Amusez vous bien !!!

PS : je me suis fortement inspiré de http://groovy.dzone.com/news/groovy-dsl-scratch-2-hours

Aucun commentaire:

Enregistrer un commentaire