nodemda

2.2.0 • Public • Published

NodeMDA 2.0

*NOTE: This README covers the NodeMDA code generation engine and how to create new plugin templates.

For a free, open source, and completely integrated UML diagram creation tool with NodeMDA integrated, see the NodeUML VSCode Extension

Introduction

NodeMDA is a code generation engine written in NodeJS that accepts UML Class Diagrams as its input then processes them using Handlebars templates to create code in any language. This documentation focuses on using the NodeMDA Command Line Interface to generate code, as well as how to create new plugins.

Project goals

  1. Create an extensible code generator for any programming language or software stack

  2. Use a capable template engine for code templates

  3. Plugin system for code template selection

  4. Support combining multiple plugins that each handle a different area of concern to simplify building new software stacks. For example, one plugin may handle generating persistent entities, another may handle the remote protocol (REST, SOAP, JSON RPC, etc.), a third may handle UI (web based, mobile, etc.). A single software stack can be defined by combining these smaller parts, allowing for reusability and ease of development of new stacks. For example, you could write a plugin to handle a new UI technology and combine it with existing plugins to handle backend code.

  5. Simplified usage via Convention over Configuration: By eliminating as many configuration files as possible, development of plugins is greatly simplified

  6. Streamlined template creation: Javascript allows for existing objects to be extended at runtime by simply declaring additional functions and properties. NodeMDA supplies a mixin API for adding new properties to the meta model making writing templates faster.

Workflow

The general workflow for using NodeMDA is as follows:

  1. An object graph of your software classes, known as the model, is created using an external UML diagraming tool. These classes are marked with certain stereotypes to indicate to NodeMDA what the classes represent (see General Modeling Conventions below).

  2. The model is processed using NodeMDA to generate source code and other project artifacts.

  3. Where necessary, code stubs are filled in manually to supply the business logic or add other functionality.

  4. The resulting code is tested. If additions or changes to your modeled classes are required, the process returns to step #1 where the model is modified and the entire cycle is repeated. NodeMDA has a mechanism to ensure it does not overwrite certain files once you modify them.

Usage

Installation

In order to use NodeMDA, a "reader" must exist to translate your model into NodeMDA's Javascript based meta model. If one can not be found, you will need to write one. The current version of NodeMDA includes a reader for NodeUML.

There is also a reader for StarUML.

Command line


## The core NodeMDA system is best installed globally
npm install -g nodemda
npm install -g nodemda-nodeuml

## Install the plugins used to generate your desired software stack
npm install -g nodemda-stack-react-koa-mongo



## Create a new directory for your application

mkdir example-project

cd example-project


## Initialize a new project directory
nodemda init

## Here you would edit the UML file and create your software specifications

## Generate code with the code generator
nodemda gen

## After the initial generation, install the dependencies

npm install

## Follow the directions in the README of your plugins stack for details on how to test and run

General Modeling Conventions

Code generation by NodeMDA is currently based on six key UML artifacts:

  • Classes
  • Stereotypes
  • Associations
  • Actors
  • Dependencies
  • Tags

The general strategy for creating your model is:

  1. Create one or more Class diagrams using a compatible UML modeling tool

  2. Add one or more classes to your diagram(s) based on your design.

  3. Populate your classes with attributes and/or operations, making sure to specify a Datatype for your attributes and operation parameters.

  4. Add a "Stereotype" to each class that you want to be processed by the code generator. Which stereotype you specify will depend on the NodeMDA plugin you are using. Two of the most common are Entity and Service

  5. Depending on the plugin stack you are using, you may also need to:

    • Add Associations between classes
    • Add dependencies to Actors to specify security roles
    • Add special Tag values to all of the above to fine tune code generation.

The specific modeling conventions to use will be dictated by the plugins you will use to generate code. Most plugins use the NodeMDA Standards modeling convention. See that plugin's README for details.

Code Generation Process

Each time NodeMDA's gen function is run:

  1. One or more plugins are discovered, traversed, and combined to form the set of templates and support files to be used.

  2. A "reader" plugin reads the UML model file and creates the meta model object using NodeMDA supplied Javascript classes

  3. Handlebars helpers are loaded from the plugins/_helpers directories

  4. Handlebars partials are loaded from the plugins/_partials directories

  5. Template support scripts are loaded from the plugins/_mixin directories and initPlatform() is called on each (if defined)

  6. Each class in the model that is marked with a stereotype is processed by all of the "stereotype specific" Handlebars templates

  7. The method initProjectTemplates() is called on each template support scripts from the plugins/_mixin (if defined)

  8. All of the Handlebars templates that are "project specific" (e.g NOT "stereotype specific) are then processed

  9. Any file in the plugins/templates directory that is NOT a Handlebars script is assumed to be boilerplate and is copied verbatim to the output directory.

Plugins

A plugin in NodeMDA:

  1. is a set of templates and support files used to generate a specific part of an application. While a single plugin could generate an ENTIRE application, plugins are usually broken down into small subsystems, allowing them to be mixed and matched to generate code for a variety of software stacks.

  2. is expected to export a module definition object that defines what the plugin does, and what it needs to perform its task. Example:

    index.js

     module.exports = {
     	type: 'templates',
         name: 'js-rest-koa',
         desc: 'Generates REST interface to access persistent Javascript objects exposed with EntityDaoJS',
         version: 2.0,
         contributes: [ "REST", "Js-REST-Koa", "NodeJS-Backend" ],
         requires: [	"EntityDaoJS", "NodeMDA-Standards" ]
     };   
    
  3. can be one of three types:

    • templates
    • mixin
    • stack
  4. is an npm package that is named nodemda-<pluginName> where "pluginName" is the name property from its module definition object

  5. contributes code by supplying it in the following directories inside its package:

  +-index.js
  |
  +-package.js
  |
  +-plugins
     |
     +-_helpers
     |   |
     |   +-camelCase.js
     |   +-outputIfEqual.js
     |
     +-_mixin
     |   |
     |   +-TemplateMixins.js
     |   +-DatatypeMixins.js
     |
     +-_partials
     |   |
     |   +-outputClassDef.hbs
     |
     +-templates
         |
         +-src
         |   |
         |   +-dataModels
         |       |
         |       +-{_package_}
         |           |
         |           +-{Entity}.js.hbs
         |           +-{Entity}-schema.sql.hbs
         |
         +-package.json.hbs
         +-README.md.hbs

Module Definition Object

The properties within the module definition object exported by an individual plugin are:

  • type Can be one of three values:

    • templates The plugin supplies templates in addition to optional mixin, partials, or helpers
    • mixin The plugin is purely code based and lacks a templates directory
    • stack The plugin defines a software stack. It has no plugins directory. Instead, the dependencies found in its package.json file are the other plugins that make up the software stack
  • name The name of plugin WITHOUT the nodemda- prefix that makes up its package name

  • desc A description of what the plugin does

  • version The NodeMDA version it is compatible with. Should be 2.0

  • contributes An array of identifiers that specify what this plugin contributes to the software stack

  • requires An array of identifiers that specify what this plugin needs to be contributed by other plugins to function.

plugins Directory

The pluginsdirectory of each plugins package is composed of zero or more of the following sub-directories:

  • _helpers Contains zero or more .js files that each export a single Handlebars helper function. The name of the file is used as the name of the helper when it is auto-registered with Handlebars. There is no need to call Handlebars.registerHelper()

  • _mixin Contains zero or more .js files that exports a single "template support object". These usually utilize the NodeMDA.Meta.Model.mixin() function to quickly add addtional getters or functions to the meta model to ease in template generation.

  • _partials Contains zero or more .hbs files that each contain a single Handlebars partial template fragment. The name of the file is used as the name of the partial when it is auto-registered with Handlebars. There is no need to call Handlebars.registerPartial()

  • templates A list of zero or more files and/or subdirectories that make up the template files contributed by the plugin. If the file name ends in .hbs, it is considered a template file and is processed by NodeMDA and Handlebars. If the file does NOT end in .hbs, it is considered a boilerplate file and is copied verbatim to the output directory.

Template file names

All files located in the templates directory that end in .hbs will be processed by NodeMDA and Handlebars. How the template file is named determines how and when the template is processed:

  • If a file name contains a string surrounded by curly braces, then the string inside of the curly braces is assumed to be the name of a Stereotype. The template will be processed once for each class in the UML model that is marked with that Stereotype. The name of the class will replace the name of the stereotype in the file name

  • If the name does NOT contain the stereotype marker above, the template will be processed ONCE after all individual class specific template files are processed

In all cases, the resulting file name will have the .hbs extension removed, and it will be placed in a subdirectory that corresponds to the template's location in the templates directory.

NodeMDA supports the use of a dynamic subdirectory name that is "UML package specific". If the subdirectory name in the templates directory is {_package_}, then that sub-directory name will be replaced with the actual package name of the class from the UML model.

Example

Assume the plugin directory has the following layout:

  +-plugins
     |
     +-templates
         |
         +-src
         |   |
         |   +-dataModels
         |       |
         |       +-{_package_}
         |           |
         |           +-{Entity}.js.hbs
         |           +-{Entity}-schema.sql.hbs
         |
         +-package.json.hbs
         +-README.md.hbs

Also assume the UML model contains is a class named Person that has a stereotype of <<Entity>> and was defined inside of the package services/contacts

NodeMDA will generate the following four files:

  • outputDir/src/dataModel/services/contacts/Person.js
  • outputDir/src/dataModel/services/contacts/Person-schema.sql
  • outputDir/package.json
  • outputDir/README.md

Plural modifier

A stereotype specific template can use the _s suffix when specifying the stereotype to indicate you want the file to be named with the plural representation of that class. For example, if the above example used the file name {Entity_s}-schema.sql.hbs, then the resulting file would have been

  • outputDir/src/dataModel/services/contacts/People-schema.sql

Template Context

The Handlebars template engine works by passing it an input object known as the context. Before each template is processed, NodeMDA prepares this context object with this set of properties:

{
    class: new NodeMDA.Meta.Class(),
    output: "path/to/code/output/directory",
    model: new NodeMDA.Meta.Model() 
}

Template Context Properties

  • class: The NodeMDA.Meta.Class object for the current class being processed.

  • output: The root output directory where generated code should be placed

  • model: The entire NodeMDA meta model for the project as read by the reader. The most interesting properties are:

    • name: The name of the project/model,
    • datatypes: an array of the NodeMDA.Meta.Datatype objects that appear in the model
    • stereotypes: an array of the NodeMDA.Meta.Stereotypes objects that appear in the model
    • classes: an array of the NodeMDA.Meta.Class objects that appear in the model
    • actors: an array of the NodeMDA.Meta.Actor objects that appear in the model

Template file example:

{POJO}.js.hbs

{{#with class}}
var {{name}} = {
   {{#each attributes}}
      {{name}} : "",
   {{/each}}
};
{{/with}}

Above file would be applied to any class that has the POJO stereotype. For example, a class named <<POJO>> Person would result in this file:

Person.js

var Person = {
      firstName : "",
      lastName : "",
      birthDate : "",
};

Output Directive

By default, a template's output will be placed in output directory at the same path as it appears in the plugin relative to the templates directory (see Template File Names) The default output behavior is to overwrite the file if it already exists.

The default output behavior can be overridden on a template by template basis by specifying an output directive. This directive must be placed in the template in such a way that it will result in the first line of output starting with the ##output prefix. It does not have to be the actual first line of the template, but anything that precedes it must not result in any output.

An output directive has the following format:

##output <outputMode> [<fileName>]

where outputMode is one of the following:

  • overwrite The file is overwritten each time NodeMDA is run.

  • preserve NodeMDA will not overwrite a preexisting file with the same name (usually created by a previous run of NodeMDA).

  • preserve! Adding the exclamation point to the end of preserve ensures the file is preserved, even if the "force overwrite" option has been specified.

  • aggregate The template output for each class will be gathered and placed in a single file of the specified name.

  • ignore The template output is discarded and no output is saved. It is sometimes useful for a template to render this option conditionally if it determines the output is not needed.

  • property The template output will be saved as an array property in the Handlebars context. The fileName specifier in this instance is the property name to use in the context. Each class' output will be an element in an array of strings. This is a special output specifier that can be used to place all of the output inside another template (usually a template in the platform's root directory, which is only processed once per NodeMDA run after all individual classes have been processed).

On all output modes except property, specifying the file name as the second paramter is optional. The property output mode requires that a valid Javascript property name be specified as the second parameter.

Custom Partials

Template reuse is facilitated by Handlebars' partials system, which is Handlebars' equivalent of an "include." Template partials are automatically loaded by NodeMDA for all .hbs files that it finds in the plugins/_partials directory. NodeMDA has a convenient "auto-registration" system for partials. All .hbs flies in the _partials directory are automatically pre-compiled and registered with Handlebars using the name of the template file (minus the .hbs extension). There is no need to call Handlebars.registerPartial() in any of your code.

Example

Assume you had a need to generate a json schema in multiple templates in your plugin. Rather than repeat the template code in multiple places, complicating maintenance, you can define a partial and then reference that in your various other templates.

Contents of plugins/_partials/json-schema.hbs

{
    "title": "{{class._name}}",
    "type": "object",
    ...
}

Contents of plugins/templates/src/forms/{Entity}Editor.js.hbs files

let {{class.jsIdentifierName}}Schema = {{> json-schema}};

// Rest of your code here...

Custom Helper Tags

Handlebars allows for custom tag creation using a mechanism it calls Helpers. NodeMDA includes a facility to auto load and register Handlebars helpers by using a mechanism similar to the above _partials system. To register a helper, simply place a properly formatted .js file in the plugins/_helpers directory. This Javascript module should export a function that returns the value of the helper. The name of the registered helper tag will be the name of the file (minus the .js extension). There is no need to call Handlebars.registerHelper.

Example

Contents of plugins/_helpers/lowercaseFirst.js

export function(str) {
	if (str !== undefined && str !== null) {
        return str
           .replace(/^./, str => str.toLowerCase()); // Lower case the first letter        
    }	
    else {
    	return "";
    }
};

Usage

let {{lowercaseFirst class.name}}Editor = new {{class.name}}Editor();

Template Support Scripts

The template engine can be enhanced by use of "support" scripts located in the plugins/_mixins directory. These scripts can perform a variety of tasks, such as modifying or enhancing the template context, as well as using the NodeMDA.Model.mixin() function to decorate the meta data with additional code or data that can simplify the templates themselves.

A support script file should follow this structure:

"use strict";

var MySupport = {};

(function() {

    // All five methods specified here are OPTIONAL. A given support script may contain
    // all, none, or any combination of them.
    

   /**
    * Called just after the model has been read into the metaModel.
    * Used to check the metaModel and ensure all modeling conventions
    * that are required by this plugin have been met.
    * 
    * @param {NodeMDA.Meta.Model} metaModel 
    * @returns {boolean} TRUE if the model passes validation, FALSE otherwise
    */
   MySupport.validateModel = function (metaModel) {
       return true;
   };


  /**
   * Called before the specified stereotype is used for
   * the first time.
   * 
   * @param {HandlebarsContext} context
   * @param {NodeMDA.Meta.Stereotype} stereotype
   */
   MySupport.initStereotype = function(context, stereotype) {

   };

	
  /**
   * Called just before the specified class is processed for the first time.
   * 
   * @param {HandlebarsContext} context
   * @param {NodeMDA.Meta.Class} metaClass
   */
   MySupport.initClass = function(context, metaClass) {
		// 
   };


  /**
	 * Called once after all partials and helpers have been loaded
   * and just before any classes are processed.
   * 
   * @param {HandlebarsContext} context
   */
   MySupport.initPlatform = function(context) {
	    
   };
	
	
  /**
	 * Called AFTER all classes have been processed, BUT BEFORE
   * any non sterotype specific templates are processed.
   * 
   * @param {HandlebarsContext} context
   */
   MySupport.initProjectTemplates = function(context) {
   };
	
})();

module.exports = MySupport;

NodeMDA.Model.mixin()

NodeMDA provides a mixin() function to quickly add new properties and functions to the meta model. These are designed to simplify templates by moving complex logic into Javascript functions vs. placing conditionals or complex markup directly in the template file.

The mixin() function takes as a parameter the following mixin specification:

  • An object with a set of properties, each named with the prefix "on" followed by the name of the UML meta element you wish to modify.

  • Each "on" property is an array of zero or more mixin commands.

  • Each mixin command is an object with the properties get and/or func

    • get adds "getters" to each instance of the specified "on" element
    • func adds functions that can be invoked to each instnace of the specified "on" element
  • Each get or func is an array of named functions which are added to the element instances

  • Arrays of single elements above can be abbreviated by using a single object rather than an array of a single object.

Example

"use strict";

const  NodeMDA = require("nodemda");
const pluralize = require('pluralize');

var TemplateSupport = {};

(function() {


  TemplateSupport.initPlatform = function(context) {

    let model = context.model;

    model.mixin({

      onAttribute: {
        get: [
					 function pluralName() {
					  	return pluralize(this.name);
					  },
        ],
      },

      onClass: {
        func: [
			    	function setUIProperty(name, value) {
			    		if (!this.hasOwnProperty('_uiProp')) {
			    			this._uiProp = {};
			    		}

			    		this._uiProp[name] = value;
			    	},          
        ],
      },

    }); // end mixin

  };
  
})();

module.exports = TemplateSupport;

With the above, any of the code templates can use a simple output format like this:

{{attribute.pluralName}} = [];

Any other template support javascript code could do this:

class.setUIProperty('hidden', true);

NodeMDA Meta Model Reference

For details on meta model classes from the NodeMDA.Meta namespace, see the meta source code, or refer to this diagram:

UML for NodeMDA Meta Model


Do you find my work useful?

Buy Me A Coffee

Package Sidebar

Install

npm i nodemda

Weekly Downloads

5

Version

2.2.0

License

MIT

Unpacked Size

94.6 kB

Total Files

8

Last publish

Collaborators

  • joelkoz