Tech'n bolts home about archive // tbd-in-practice ddd // (drafts)
{ version:1.3, what:["java", "scala", "nosql", "amqp", "programing", "functional", "[t|b|d]dd", ...] }

[T|B|D]DD par la pratique 7.2 - Code generation

Après avoir mis en place l'interpretation de notre DSL en un modèle manipulable en javascript. Nous allons nous atteler à exploiter et enrichir ce modèle pour générer le maximum de chose possible pour notre application. Le modèle généré ne sera certainement pas complet mais devrait permettre d'accélerer notre développement.

Il est évident que les méthodes fonctionnelles complexes ne pourront pas être gérées de cette manière, mais nous aurons tout de même une bonne partie du squelette qui sera généré. Par ailleurs notre approche restera très simple, et nous nous contenterons d'une approche de type one shot generation, c'est à dire que nous ne nous interesserons pas à la gestion de la personnalisation du code généré, ce sera à la charge du développeurs de faire ses modifications manuellement.

Avant de définir les modèles de génération, nous allons en profiter pour nous baser sur une librairie javascript facilitant la définition de classe. Après quelques recherches, j'opte pour Sslac.

Comme moteur de template, j'opte pour le moteur fournit (entre autre) par la librarie underscore qui permet de bénéficier de tout l'environement javascript dans le template. Ceci n'est pas conforme à la séparation stricte modèle-vue, comme prônée par StringTemplate, mais permet une plus grande souplesse dans la rédaction de nos templates.

npm install underscore

Nous utiliserons aussi la librairie underscore.string pour manipuler les chaînes de caractères (CamelCase, UnderscoreCase ...).

npm install underscore.string

La première version de notre générateur de code (et peut être la seule) sera très simple, il s'appuiera sur les transformations successives suivantes:

  • Lecture d'un flux décrivant notre domaine via notre DSL
  • Interpretation du flux via le parser définit dans la partie précédente de cet article
  • L'objet javascript ainsi généré est passé à travers la chaine de transformation fournie en paramètre
  • Certaine transformation applique un template underscore, ce qui générera le code correspondant, tandis que d'autre peuvent transformer un objet javascript en un autre, ou tout simplement l'enrichir.

Commençons par écrire notre machine à transformer:

lib/code-gen.js

 1 var parser = require("./parser").parser;
 2 // set parser's shared scope
 3 parser.yy = require("./models");
 4 
 5 var toString = function(input, indent) {
 6     return JSON.stringify(input, null, indent||"    ");
 7 };
 8 
 9 var noop_transformer = function() {
10     return { 
11         "transform" : function(model, output, chain) {},
12         "next"      : null
13     }; 
14 };
15 
16 var create_transformer = function(transformation, next) {
17     return {
18         "transform" : require('./transformation/'+transformation).transform,
19         "next"      : next
20     };
21 };
22 
23 var main = exports.main = function (args) {
24     var fs  = require("fs");
25     if (!args[1]) {
26         throw new Error("Usage: " + args[0] + " <cqrs-file> [<transformation>]*");
27     }
28     var input  = fs.readFileSync(args[1], 'utf8');
29     var parsed = parser.parse(input);
30 
31     if(args[2]) {
32         var _ = require('underscore');
33         var output = console.log;
34         var first  = _.foldr(args.slice(2), function(next, transformation) {
35             return create_transformer(transformation, next);
36         }, noop_transformer());
37 
38         first.transform(parsed, output, first.next);
39     }
40     else {
41         console.log(toString(parsed));        
42     }
43 };
44 
45 main(process.argv.slice(1));
  • Les lignes 1 à 8 devraient désormais être familières. On déclare et configure (ligne 3) notre parser. Et on se créé une méthode utilitaire pour convertir un objet Javascript en chaîne de caractère.
  • Lignes 9 à 14, nous definissons un maillon, de notre chaine de transformation, qui n'a aucun effet. Il pourra servir de transformation finale dans notre chaine: transform definit la transformation à appliquer et next fournit la transformation suivante de la chaine.
  • Lignes 16 à 21, nous permettons de créer un maillon à partir d'une transformation, et du maillon suivant à invoquer. Le nom de la transformation est utilisé pour charger le fichier correspondant, nous considererons que nos transformations se trouvent dans lib/transformation/.
  • Ligne 24 on déclare l'utilisateur du module fs qui nous permettra de lire le fichier écrit à l'aide de notre DSL (ligne 28).
  • Ligne 29, le contenu du fichier lu est transformé grâce à notre parser en objet javascript.
  • Si l'on a specifié au moins un fichier de transformation (ligne 31), la chaîne de transformation est construite en parcourant la liste depuis la fin et reduisant les entrées une à une en les chainant entre elles: lignes 34 à 36 (reduceRight aka foldRight, quelques explications en français ici: foldleft et foldright). Ligne 36, la réduction est initialisée avec la transformation sans effet. La chaine de transformation est démarée ligne 38.
  • Si aucune transformation n'est spécifiée, le contenu du modèle est affiché dans notre console (ligne 41).
  • Enfin la ligne 45 sera celle invoquée par la ligne de commande.

Crééons nos premiers fichier de données de test:

data/sample00.cqrs

aggregateRoot Story {
    def change_complexity(complexity:Integer)
}

data/sample01.cqrs

aggregateRoot Story {
    def change_complexity(complexity:Integer)
}
aggregateRoot Project {
    def activate()
}

Ainsi que nos deux première transformations: la première (forEach) prend une liste de modèle et applique pour chaque élément la suite de la transformation, la seconde (dump) se contentera d'afficher le contenu du modèle fournit.

lib/transformation/forEach.js

var asArray = function(model) {
    if(typeof model === 'object' && model instanceof Array) {
        return model;
    }
    else {
        return [model];
    }
};

var transform = exports.transform = function(model, output, next) {
  if(next === null) {
    return;
  }

  asArray(model).forEach(function(item) {
    next.transform(item, output, next.next);
  });
};

lib/transformation/dump.js

var toString = function(input, indent) {
    return JSON.stringify(input, null, indent||"    ");
};

var transform = exports.transform = function(model, output, next) {
  output("Dump: " + toString(model));
  output("Remaining transformation(s): " + toString(next));
};

Invoquons alors notre superbe transformateur de modèle en enchainant nos transformations forEach et dump:

$ node lib/code-gen.js data/sample01.cqrs forEach dump
Dump: {
    "type": "aggregate_root",
    "name": "Story",
    "inherits": [],
    "features": [
        {
            "type": "def",
            "name": "change_complexity",
            "arguments": [
                {
                    "type": "argument",
                    "argument_name": "complexity",
                    "argument_type": "Integer"
                }
            ]
        }
    ]
}
Dump: {
    "type": "aggregate_root",
    "name": "Project",
    "inherits": [],
    "features": [
        {
            "type": "def",
            "name": "activate",
            "arguments": []
        }
    ]
}

On vérifie alors que notre transformation dump est appellée deux fois: pour chacun des modèles lus (le fichier de test sample01.cqrs contient en effet la déclaration de deux modèles) comme prévu par la transformation forEach.

Nous utiliserons le script suivant afin de simplifier le chargement et l'application de nos template:

lib/transformation/template.js

 1 var _  = require('underscore'),
 2     fs = require('fs');
 3 
 4 exports.apply_template = function(template_path, model) {
 5   /*
 6    * Allow underscore.string to be available through underscore
 7    * within template
 8    */
 9 
10   // Import Underscore.string to separate object, because
11   // there are conflict functions (include, reverse, contains)
12   _.str = require('underscore.string');
13   // Mix in non-conflict functions to Underscore namespace if you want
14   _.mixin(_.str.exports());
15 
16     var template = fs.readFileSync(template_path, 'utf8');
17     var compiled = _.template(template);
18     return compiled(model);
19 };

Les lignes 5 à 15 permettent d'intégrer underscore.string et de rendre ses fonctions accessibles depuis le template. Ensuite, le template est lu depuis un fichier (ligne 16) et est compilé en template underscore (ligne 17). Le modèle est passé en paramètre à notre template compilé (ligne 18): il sert alors de racine à toutes les fonctions et propriétés qui seront invoquées à travers le template. Par exemple:

1 var model = {
2   hello: function() {
3     return "Salut";
4   },
5   world: "le monde!"
6 };
7 
8 var compiled = _.template("<%=hello()%> <%=world%>");
9 console.log(compiled(model));

affichera: Salut le monde!

Notre premier template!

Notre premier template va consister à crééer les classes (en utilisant le module Sslac) correspondant aux identités de chaque entité.

lib/transformation/entity_id.template

1 /**
2  * <%=namespace%>.<%=id.type%>
3  */
4 Sslac.Class("<%=namespace%>.<%=id.type%>")
5      .Extends("nscrum.Id")
6      .Constructor(function (uuid) {
7         this.Parent(uuid);
8      })
9      ;
  • Ligne 4, nous déclarons une classe nommée à partir des propriéts namespace et id disponibles dans le contexte courant d'éxecution du script. La propriété type est alors lue depuis la propriété id.
  • Ligne 5, nous faisons étendre notre classe de la classe nscrum.Id que nous définierons plus tard, comme faisant partie de notre librairie.
  • Enfin, lignes 6 à 8, nous définissons le constructeur de notre classe prenant en paramètre un uuid qui se contentera d'appeller le constructeur correspondant sur la classe parente (ligne 7).

La transformation permettant d'appliquer ce template peux alors être définie par:

lib/transformation/entity_id.js

1 var template = require('./template');
2 
3 var transform = exports.transform = function(model, output, next) {
4   var template_path = __dirname + "/entity_id.template";
5   var generated = template.apply_template(template_path, model);
6   output(generated);
7 };

En écrivant cette transformation, je me rend compte que cela fait beaucoup de ligne qui seront dupliquée à chaque transformation impliquant un template. De plus si l'on regarde de plus près le template de génération des classes d'identifiant, on peux se rendre compte que certaines variables (comme namespace et id) ne sont pas disponibles dans le contexte du template (définit par model, ligne 7 de entity_id.template).

Nous allons en effet ajouter une transformation supplémentaire afin d'enrichir le modèle lu depuis notre DSL: de nouvelles informations précalculée viendront enrichir notre modèle afin de faciliter sa manipulation. Cet enrichissement permettra une écriture plus simple de nos template en limitant les manipulations à faire pour construire certaine valeur (comme le nom d'une variable ou d'un type qui sont dérivés du nom du modèle correspondant).

lib/transformation/enhance.js

 1 var _  = require("underscore"),
 2     _s = require("underscore.string"),
 3     t  = require("./template");
 4 
 5 var enhance = function(model) {
 6   if(model.type === "aggregate_root") {
 7     model.namespace = "nscrum";
 8     model.variable_name = _s.underscored(model.name);
 9     model.id = {
10       type: model.name + "Id",
11       variable_name: _s.underscored(model.name + "Id")
12     };
13   }
14 
15   t.declare_apply_template(model);
16 };

Cette transformation permet d'ajouter plusieurs propriétés sur chacune de nos entités:

  • un espace de nommage (ligne 7) nscrum (namespace ou package en java) est définit (nous enrichirons certainement notre DSL pour intégrer cette notion directement sur notre modèle plus tard)
  • une variable contenant une instance de notre entité sera nommée par défaut comme l'entité en UnderscoreCase (ligne 8), par exemple backlog_item pour le type BacklogItem.
  • la classe correspondant à l'identifiant d'une entité est par convention le nom de l'entité suffixé de Id (ligne 10) par exemple StoryId pour Story.
  • une variable désignant l'identifiant d'une entité sera, comme pour l'entité, son nom en UnderscoreCase (ligne 11) par exemple backlog_item_id pour BacklogItemId.
  • lignes 14, nous déclarons sur le modèle lui-même une méthode permettant de lui appliquer un template. Cette méthode est ajoutée dans le module template.js.

lib/transformation/template.js

...
exports.declare_apply_template = function(model) {
  model.apply_template = function(template_path) {
    var generated = apply_template(template_path, model);
    return generated;
  };
};

Le code correspondant au contrat de notre moteur de transformation s'écrit alors:

lib/transformation/enhance.js

 1 var asArray = function(model) {
 2     if(_.isArray(model)) {
 3         return model;
 4     }
 5     else {
 6         return [model];
 7     }
 8 };
 9 
10 var transform = exports.transform = function(models, output, next) {
11   asArray(models).forEach(function(model) {
12     enhance(model);
13   });
14   next.transform(models, output, next.next);
15 };

Modifions alors notre transformation entity_id.js en appellant directement la méthode apply_template que nous avons ajoutée sur le modèle.

1 var transform = exports.transform = function(model, output, next) {
2   var generated = model.apply_template(__dirname + "/entity_id.template");
3   output(generated);
4 };

Et maintenant, le résultat !?! Lançons notre chaine de transformation:

$ node lib/code-gen.js data/sample01.cqrs forEach enhance entity_id

/**
 * nscrum.StoryId
 */
Sslac.Class("nscrum.StoryId")
     .Extends("nscrum.Id")
     .Constructor(function (uuid) {
        this.Parent(uuid);
     })
     ;
/**
 * nscrum.ProjectId
 */
Sslac.Class("nscrum.ProjectId")
     .Extends("nscrum.Id")
     .Constructor(function (uuid) {
        this.Parent(uuid);
     })
     ;

Cool! nous avons réussit à générer nos deux classes d'identifiants.

Au gré des évènements

Interessons-nous désormais à la génération des évènements liés à chacune des méthodes de nos aggrégats. Il s'agit sans doute de l'un des templates les plus compliqués que nous aurons à écrire.

Commençons par quelques exemples de ce que nous voudrions à partir du modèle suivant:

 1 aggregateRoot Story extends HasComment {
 2   title:String
 3   description:String
 4 
 5   factory create(story_id:StoryId, 
 6                  story_title:String, story_description:String)
 7 
 8   def change_title(title:String)
 9   def activate()
10   def affect_to_project(project_id:ProjectId)
11 }
MéthodeEvènement
create
nscrum.StoryCreatedEvent(story_id, story_title, 
                         story_description) {
  this.Parent("nscrum.StoryCreatedEvent", story_id);
  this.data = {
    story_title:story_title,
    story_description:story_description
  };
}
change_title
nscrum.StoryTitleChangedEvent(story_id, title) {
  this.Parent("nscrum.StoryTitleChangedEvent", story_id);
  this.data = {
    title:title
  };
}
activate
nscrum.StoryActivatedEvent(story_id) {
  this.Parent("nscrum.StoryActivatedEvent");
  this.data = {};
}
affect_to_project
nscrum.StoryAffectedToProjectEvent(story_id, project_id) {
  this.Parent("nscrum.StoryAffectedToEvent", story_id);
  this.data = {
    project_id:project_id
  };
}

“Un nom doit-il toujours signifier quelque chose ?” — Lewis Carroll

Procédons de manière empirique:

Prenons comme convention que le premier mot de la méthode est le verbe indiquant l'intention de celle-ci. Nous commençons donc par isoler le verbe du reste du nom de la méthode: change complexity. Puis, nous concatenons le nom de l'entité, la partie restante du nom de la méthode, le verbe mis au participe passé et enfin Event et tout ça en CamelCase: Story + Complexity + Changed + Event.

Gérons le cas où le verbe est suivit de to ou de from en le laissant à sa position initiale mais au passé: Story + Affected + ToProject + Event.

Commençons par définir quelques utilitaires afin de générer le nom de l'évènement.

lib/transformation/enhance.js

 1 ...
 2 var generate_event_name = 
 3     exports.generate_event_name = function(feature_name, model_name) {
 4     var parts = _s.words(feature_name.toLowerCase(), /[ \\-_]+/g);
 5     var verb = parts[0];
 6     verb = past_tense(verb);
 7 
 8     var remaining_index = 1;
 9     var in_order_mode = (parts.length>1) &&
10                         (parts[1]==="to" || parts[1]==="from");
11     var remaining = parts.slice(remaining_index).join("-");
12     var event_name;
13     if(in_order_mode) {
14         event_name = model_name + "-" + verb + "-" + remaining + "-Event";
15     } else {
16         event_name = model_name + "-" + remaining + "-" + verb + "-Event";
17     }
18     return _s.camelize(_s.dasherize(event_name));
19 };
20 
21 var past_tense = exports.past_tense = function(verb) {
22   var lowered = verb.toLowerCase();
23     if(_s.endsWith(lowered, "y")) {
24         return verb.substring(0,verb.length-1) + "ied";
25     }
26     else if(_s.endsWith(lowered, "e")) {
27         return verb + "d";
28     }
29     else if(_s.endsWith(lowered, "get")) {
30         return verb.substring(0,verb.length-3) + "got";
31     }
32     else if(_s.endsWith(lowered, "do")) {
33         return verb + "ne";
34     }
35     else {
36         return verb + "ed";
37     }
38 };

Les tests minimaux correspondant:

var enhance = require('../../lib/transformation/enhance');

exports["past_tense works for simple verb"] = function(test) {
  test.strictEqual(enhance.past_tense("Modify"), "Modified");
  test.strictEqual(enhance.past_tense("Change"), "Changed");
  test.strictEqual(enhance.past_tense("Alter"),  "Altered");
  test.strictEqual(enhance.past_tense("Do"),     "Done");
  test.strictEqual(enhance.past_tense("get"),    "got");
  test.done();
};

var generate_event_name_verifier = function(test, model_name) {
  return function(feature_name, event_name) {
    var generated = enhance.generate_event_name(feature_name, model_name);
    test.strictEqual(generated, event_name);
  };
};

exports["generate_event_name: basic case"] = function(test) {
  var verify = generate_event_name_verifier(test, "Story");
  verify("change_complexity", "StoryComplexityChangedEvent");
  test.done();
}

exports["generate_event_name: verb only"] = function(test) {
  var verify = generate_event_name_verifier(test, "Story");
  verify("activate", "StoryActivatedEvent");
  verify("publish",  "StoryPublishedEvent");
  verify("rename",   "StoryRenamedEvent");
  test.done();
}

exports["generate_event_name: 'to' and 'from' case"] = function(test) {
  var verify = generate_event_name_verifier(test, "Story");
  verify("assign_to_project", "StoryAssignedToProjectEvent");
  verify("load_from_history", "StoryLoadedFromHistoryEvent");
  test.done();
}

Lançons les tests contenu dans le dossier test et le sous-dossier test/transformation:

$ node_modules/.bin/nodeunit test test/transformation

...
enhance-test
✔ past_tense works for simple verb
✔ generate_event_name: basic case
✔ generate_event_name: verb only
✔ generate_event_name: 'to' and 'from' case
...

“Il est l'or, mon seignor, l'or de se réveiller...” — La Folie des grandeurs

Enrichissons notre modèle en ajoutant la description de l'évènement correspondant à chaque méthode.

Notre transformation d'enrichissement est modifiée pour permettre d'enrichir tous nos types de modèle:

  • les aggregate_root
  • les “features” def, factory et field.

Le code précédent est déplacé dans la fonction enhance_aggregate_root, nous obtenons:

lib/transformation/enhance.js

...
var enhance = exports.enhance = function(model, enhanced_parent) {
  if(model.type === "aggregate_root") {
    enhance_aggregate_root(model);
  }
  else if(model.type === "def") {
    enhance_def(model, enhanced_parent);  
  }
  else if(model.type === "factory") {
    enhance_factory(model, enhanced_parent);
  }
  else if(model.type === "field") {
    enhance_field(model, enhanced_parent);
  }

  t.declare_apply_template(model);
};

var enhance_aggregate_root = function(model) {
  model.enhanced  = true; // flag it
  model.namespace = "nscrum";
  model.id = {
    type: model.name + "Id",
    variable_name: _s.underscored(model.name + "Id")
  };
  model.variable_name = _s.underscored(model.name);

  enhance_array(model.features).forEach(function(feature) {
    enhance(feature, model);
  });
};

Définissions alors nos méthodes d'enrichissement pour chaque type de “feature”.

 1 var enhance_def = function(model, enhanced_parent) {
 2   enhance_with_event(model, enhanced_parent);
 3 };
 4 
 5 var enhance_factory = function(model, enhanced_parent) {
 6   enhance_with_event(model, enhanced_parent);
 7 };
 8 
 9 var enhance_field = function(model, enhanced_parent) {
10 };

A chaque “features” de type def et factory correspond un type d'évènement, nous definissons aussi la méthode enhance_with_event:

 1 /**
 2  * Enhance a feature with its corresponding event counterpart.
 3  */
 4 var enhance_with_event = function(feature, enhanced_parent) {
 5   if(_.isUndefined(feature.arguments)) {
 6     throw new Error("No argument on feature <" + feature.name + ">");
 7   }
 8   var event_name = generate_event_name(feature.name, enhanced_parent.name);
 9   var event_arguments = generate_event_arguments(feature, enhanced_parent);
10   feature.event = {
11     namespace : enhanced_parent.namespace,
12     name      : event_name,
13     arguments : enhance_array(event_arguments),
14     argument_names : event_arguments.map(function(arg) {return arg.name;})
15   };
16   t.declare_apply_template(feature.event);
17 };

Chaque évènement est constitué d'un nom et d'un ensemble de paramètres. Le nom c'est désormais chose faite, interessons-nous aux paramètres. Nous prendrons comme convention que le premier argument correspond à l'identifiant de l'entité. Ce paramètre est déjà présent pour les méthodes de type factory en revanche il est nécessaire de le rajouter pour les méthodes de type def s'appliquant directement à une entité.

lib/transformation/enhance.js

 1 var generate_event_arguments = exports.generate_event_arguments = function(feature, enhanced_parent) {
 2   if(enhanced_parent.enhanced === false) {
 3     throw new Error("Provided parent is not enhanced");
 4   }
 5 
 6   // define a factory function for event argument
 7   var event_arg_factory = function(name,type) {
 8     return { name : name, 
 9            type : type,
10            is_identifier : (_s.endsWith(name, "_id") || _s.endsWith(type, "Id"))
11     };  
12   };
13 
14   var id_variable = enhanced_parent.id.variable_name;
15   var id_variable_used = false;
16   var event_arguments = feature.arguments.map(function(argument) {
17     // be sure variable is in UnderscoreCase
18     var var_name = _s.underscored(argument.argument_name);
19     if(var_name === model_id_present) {
20       id_variable_used = true;
21     }
22     return event_arg_factory(
23         var_name,
24         argument.argument_type);
25     }
26   );
27 
28   var must_insert_id = true;
29   if(event_arguments.length>0) {
30     var first = event_arguments[0];
31     // insert id at first position if not already there
32     if(first.name === id_variable) {
33       must_insert_id = false;
34     }
35     else if(id_variable_used) { // sanity check
36       throw new Error("Convention not satisfied: model_id is not on first position, " +
37                         "you should probably rename the argument or set it first");
38     }
39   }
40 
41   if(must_insert_id) {
42     event_arguments.unshift(
43       event_arg_factory(enhanced_parent.id.variable_name, enhanced_parent.id.type));
44   }
45   return event_arguments;
46 };
47 ...

Rajoutons quelques indicateurs sur les tableaux afin de faciliter utilisation dans les templates. Il est ainsi possible de savoir si un élément est le premier ou le dernier du tableau. Ceci est notament utile pour savoir s'il faut positionner un séparateur comme , ou ; par exemple.

lib/transformation/enhance.js

 1 /**
 2  * Utility to mark first and last value in an array
 3  */
 4 var enhance_array = exports.enhance_array = function(array) {
 5   if(array.length>0) {
 6         var first_key = "_first";
 7         var last_key  = "_last";
 8         array.forEach(function(item) {
 9             item[first_key] = false;
10             item[last_key]  = false;
11         });
12         array[0][first_key] = true;
13         array[array.length-1][last_key] = true;
14     }
15     return array;
16 };

Le template de transformation d'un évèment peux alors s'écrire:

lib/transformation/event.template

 1 Sslac.Class("<%=namespace%>.<%=name%>")
 2      .Extends("nscrum.Event")
 3      .Constructor(function (<%=arguments[0].name%>/*owner aggregate*/, data) {
 4         <% if(arguments.length>1) {
 5         %> if(typeof data === "undefined") {
 6              throw nscrum.MissingEventData();
 7          } else {
 8             <% var iter = arguments.slice(1);
 9                var padding = _.max(iter, function(arg) { 
10                                 return arg.name.length; 
11                               }).name.length;
12                var has_id  = _.some(iter, function(arg) { 
13                                 return arg.is_identifier;
14                               });
15                if(has_id) { 
16             %> /* uuids are unwrapped for serialization*/
17             <% }
18             %> this.Parent("<%=namespace%>.<%=name%>", 
19                           <%=arguments[0].name%>, { // event data
20             <% iter.forEach(function (argument) { 
21             %>              <%=_.rpad(argument.name, padding, " ")%> :<% 
22                         if(argument.is_identifier) { 
23                     %> data.<%=argument.name%>.uuid() <% 
24                         } else { 
25                     %> data.<%=argument.name%><% 
26                         } %><% if(!argument._last) { %>,<% } %>
27             <% }); 
28             %> });
29          }<% 
30           } else { 
31         %> this.Parent("<%=namespace%>.<%=name%>", 
32                        <%=arguments[0].name%>);<% } %>
33      })<%
34       arguments.slice(1).forEach(function (argument) { %>
35      .Method("<%=argument.name%>", function () {
36         <% if(argument.is_identifier) { 
37         %> /*rewrap uuid*/
38          var uuid = this.data.<%=argument.name%>;
39          return new <%=namespace%>.<%=argument.type%>(uuid); <% 
40         }  else { 
41         %> return this.data.<%=argument.name%>; <% } %>
42      })<% }); %>
43      .Static("validate_event_data", function(data) {
44        <% if(arguments.length>1) {
45             var iter = arguments.slice(1);
46             iter.forEach(function (argument) { 
47               %>  if(typeof data.<%=argument.name%> === "undefined") {
48              throw nscrum.MissingEventProperty("<%=argument.name%>");
49          }
50        <%  });
51     } %>
52      });
  • lignes 3 à 33: le constructeur. Il est invoqué avec deux paramètes: l'identifiant de l'aggrégat et les données propres à l'évènement. Les paramètres de l'évènement sont regroupés dans un unique objet, nous avons ainsi un mécanisme similaire aux paramètres nommés. Les paramètres sont alors recopiés dans un nouvel objet. Un traitement spécial est fait pour les identifiants où l'on ne conserve que l'uuid afin de simplifier la serialization de nos évènements.
  • lignes 33 à 42: on génère pour chaque arguments le getter correspondant
  • lignes 43 à 51: on génère une méthode statique permettant de verifier qu'un objet dispose bien de toutes les propriétés d'un évènements. Cette méthode pourra par exemple être utilisée pour valider les données fournies au constructeur.

Comme notre template s'applique sur les évènements directement, la transformation associée est légèrement différente des précédentes et itère sur chaque évènements d'une entité. Elle peux s'écrire:

1 var transform = exports.transform = function(model, output, next) {
2   model.features.forEach(function(feature) {
3     var generated = feature.event.apply_template(__dirname + "/event.template");
4     output(generated);    
5   });
6 };

Voyons le résultat à partir d'un nouveau fichier de test data/sample02.cqrs:

aggregateRoot Story {
  factory create(story_id:StoryId, title:String)
  def activate()
    def change_complexity(on_behalf_of:UserId, complexity:Integer)
    def change_status(new_status:Status)
    def assign_to_project(project_id:ProjectId)
}

Attention, prêt? partez!

$ node lib/code-gen.js data/sample02.cqrs forEach enhance event

Sslac.Class("nscrum.StoryCreatedEvent")
     .Extends("nscrum.Event")
     .Constructor(function (story_id/*owner aggregate*/, data) {
         if(typeof data === "undefined") {
             throw nscrum.MissingEventData();
         } else {
             this.Parent("nscrum.StoryCreatedEvent", 
                          story_id, { // event data
                          title : data.title
             });
         }
     })
     .Method("title", function () {
         return this.data.title; 
     })
     .Static("validate_event_data", function(data) {
         if(typeof data.title === "undefined") {
             throw nscrum.MissingEventProperty("title");
         }
       
     });
Sslac.Class("nscrum.StoryActivatedEvent")
     .Extends("nscrum.Event")
     .Constructor(function (story_id/*owner aggregate*/, data) {
         this.Parent("nscrum.StoryActivatedEvent", 
                       story_id);
     })
     .Static("validate_event_data", function(data) {
       
     });
Sslac.Class("nscrum.StoryComplexityChangedEvent")
     .Extends("nscrum.Event")
     .Constructor(function (story_id/*owner aggregate*/, data) {
         if(typeof data === "undefined") {
             throw nscrum.MissingEventData();
         } else {
             /* uuids are unwrapped for serialization*/
             this.Parent("nscrum.StoryComplexityChangedEvent", 
                          story_id, { // event data
                          on_behalf_of : data.on_behalf_of.uuid() ,
                          complexity   : data.complexity
             });
         }
     })
     .Method("on_behalf_of", function () {
         /*rewrap uuid*/
         var uuid = this.data.on_behalf_of;
         return new nscrum.UserId(uuid); 
     })
     .Method("complexity", function () {
         return this.data.complexity; 
     })
     .Static("validate_event_data", function(data) {
         if(typeof data.on_behalf_of === "undefined") {
             throw nscrum.MissingEventProperty("on_behalf_of");
         }
         if(typeof data.complexity === "undefined") {
             throw nscrum.MissingEventProperty("complexity");
         }
       
     });
Sslac.Class("nscrum.StoryStatusChangedEvent")
     .Extends("nscrum.Event")
     .Constructor(function (story_id/*owner aggregate*/, data) {
         if(typeof data === "undefined") {
             throw nscrum.MissingEventData();
         } else {
             this.Parent("nscrum.StoryStatusChangedEvent", 
                          story_id, { // event data
                          new_status : data.new_status
             });
         }
     })
     .Method("new_status", function () {
         return this.data.new_status; 
     })
     .Static("validate_event_data", function(data) {
         if(typeof data.new_status === "undefined") {
             throw nscrum.MissingEventProperty("new_status");
         }
       
     });
Sslac.Class("nscrum.StoryAssignedToProjectEvent")
     .Extends("nscrum.Event")
     .Constructor(function (story_id/*owner aggregate*/, data) {
         if(typeof data === "undefined") {
             throw nscrum.MissingEventData();
         } else {
             /* uuids are unwrapped for serialization*/
             this.Parent("nscrum.StoryAssignedToProjectEvent", 
                          story_id, { // event data
                          project_id : data.project_id.uuid() 
             });
         }
     })
     .Method("project_id", function () {
         /*rewrap uuid*/
         var uuid = this.data.project_id;
         return new nscrum.ProjectId(uuid); 
     })
     .Static("validate_event_data", function(data) {
         if(typeof data.project_id === "undefined") {
             throw nscrum.MissingEventProperty("project_id");
         }
       
     });
blog comments powered by Disqus
Fork me on GitHub