Fashion is a command line utility for simple model transformation and source code generation, aimed at a target audience of developers.
It only takes a few lines of code and a template library to throw together an ad hoc code generator. Many languages are suitable for writing transforms and generators. Models need not be complicated, or in strict formats like UML or MOF, be graphical, or even be usable by non-developers.
The problem is managing a growing number ad hoc models, transformations and code generators - and their dependencies.
The three-legged stool of fashion:
- A model is just a structured input file.
- A transform is just a simple script.
- A generator just substitutes values from a model into a template.
Fashion introduces a little structure, convention and metadata to make transformations and generators more managable. Python was selected as the language for transformation due to its' popularity and that no compilation step is required. Rather than introduce a new language for transformation, just learn python.
A model in fashion contains structured data, but no structure is prescribed. Typically a model resembles a key:value map like a python dictionary, but many structures are possible. A fashion model is not limited to UML or MOF-like formats. Fashion models are intended to be simple for developers to create, edit, diff and version control; they aren't intended to be used by non-developers.
A transform:
- reads 0 or more input models,
- produces 0 or more output models, (transformation)
- generates 0 or more output files using 0 or more templates. (generation)
"0 or more" means there is no requirement for a transform to either read input produce output. "1 or more" is more typical.
A single transform could do any combination of both transformation and generation. It is up to you to keep models and transform scripts at a size you decide is managable.
A template is a Mako template, which could look very similar to a source code file written in some programming language, with placeholders and looping driectives (in Mako syntax).
Note that templates are easier to read and contain less logic if the model they use is simple, and designed specifically for that template. A design intent of fashion is to use a hierarchy of simple models from very abstract, through more specific, to the final model which is specific to one template.
Fashion is a straightforward python 3.5 application.
- install python 3.5+
Fashion is available from PyPi under the name fashionModel (fashion was already taken).
$ pip install fashionModel
I got caught by the local cache and couldn't get a newer version. The solution was:
$ pip install --upgrade --no-cache-dir fashionModel
Create a new directory for your target project.
$ mkdir greet
$ cd greet
Set up fashion in this directory.
$ fashion init
This creates the subdirectory fashion
and an empty fashion project.
A fashion model is just a structured data file, typically in yaml format. Other formats are possible (e.g. json, xml, csv, ini, etc.). Any format is possible - as long as the transforms which use the model can parse it.
Create a simple model of kind 'greeting' in yaml format.
$ fashion create-model hellos.yaml greeting
This creates the file fashion/model/hellos.yaml
.
Edit the new model file. Initially it looks like:
# default empty yaml file
Add a list of greetings similar to hello, so that hellos.yaml
looks like:
# List of words meaning hello
- Hello
- Hallo
- Bonjour
Transforms are just python modules which strictly follow certain conventions, such as having functions with specific, pre-determined names. Otherwise, transforms can contain any legal python code, and dependencies (assuming the dependencies are properly installed and located, documentation coming soon).
Now create a transform to change this model into an output source file.
$ fashion create-xform greetWorld
The empty transform should look like this:
'''
greetWorld xform.
'''
import logging
from fashion.xformUtil import readModels
from fashion.xformUtil import generate
def outputKinds():
'''Outputs generated by this xform'''
return [ 'fashion_gen' ]
def police():
'''Validate input combinations.'''
pass
def xform():
'''Generate many source files from 2 input models.'''
logging.debug("greetWorld xform running")
model = { }
generate(model, "myTemplate", "myTarget")
Edit the transform to look like this:
'''
greetWorld xform.
'''
import logging
from fashion.xformUtil import readModels
from fashion.xformUtil import generate
def outputKinds():
'''Output kinds generated by this xform'''
return [ 'fashion_gen' ]
def police(greeting):
'''Validate input combinations.'''
pass
# xform argument names must match input kinds exactly, but order not significant
def xform(greeting):
'''Generate source files input models.'''
logging.debug("greetWorld xform running")
# read all the greeting input models
# since each input model is a list, flatten=True
# changes list of lists [[],[], ...] into a flat list []
greetings = readModels(greeting, flatten=True)
# greetings should be ['Hello', 'Hallo', 'Bonjour']
# create the model handed to the template
model = { 'greetings': greetings }
# generate the file from the template and the model
generate(model, "greetWorld_template.c", "greetWorld.c")
Fashion automatically recognizes the named arguments of police()
and xform()
, so the variable names must match the model kinds.
Templates are implemented by Mako. For details of template syntax, refer to the Mako documentation.
Create the template file fashion/template/greetWorld_template.c
/* greetWorld.c */
#include <stdio.h>
main() {
% for greet in greetings:
printf("${greet} world!\n");
% endfor
}
Now run the fashion build
command, which executes all the transforms.
$ fashion build
This should produce the file greetWorld.c which looks like;
/* greetWorld.c */
#include <stdio.h>
main() {
printf("Hello world!\n");
printf("Hallo world!\n");
printf("Bonjour world!\n");
}
Next, let's try to reverse engineer an existing file into a transform and template using the fashion 'nab' command.
If you were introducing fashion into an existing project, you'd expect there would already be existing source files. We'll create a file, and pretend this file already existed before we did fashion init
.
Create HelloWorld.java
with the content below:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
Now make that into a generated file:
$ fashion nab HelloWorld.java
This creates a template fashion/template/HelloWorld.java
and a transform fashion/xform/HelloWorld.py
. The new template is identical to the generation target HelloWorld.java
, just waiting for template placeholders to be added. Now we can modify the transform and template to finish our generation work.
Modify the template to look like:
public class HelloWorld {
public static void main(String[] args) {
% for greet in greetings:
System.out.println("${greet}, world!");
% endfor
}
}
Modify the transform to look like:
'''
HelloWorld xform.
'''
import logging
from fashion.xformUtil import readModels
from fashion.xformUtil import generate
def outputKinds():
'''Output kinds generated by this xform'''
return [ 'fashion_gen' ]
def police(greeting):
'''Validate input combinations.'''
pass
def xform(greeting):
'''Generate source files input models.'''
logging.debug("HelloWorld xform running")
# read all the greeting input models
# since each input model is a list, flatten=True
# changes list of lists [[],[], ...] into a flat list []
greetings = readModels(greeting, flatten=True)
# greetings should be ['Hello', 'Hallo', 'Bonjour']
# create the model handed to the template
model = { 'greetings': greetings }
# generate the file from the template and the model
generate(model, "HelloWorld.java", "HelloWorld.java")
Test the new transform.
$ fashion build
The output file HelloWorld.java
should look like:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
System.out.println("Hallo, world!");
System.out.println("Bonjour, world!");
}
}
So now we have source files in two languages generated from a common model.
Fashion police help track down bad input. Write a police function to verify our greeting models:
def police(greeting):
'''Check there are greetings and none are zero-length'''
if len(greeting) == 0:
logging.warn("no greeting inputs")
return True
for f in greeting:
m = f.loadFile()
for word in m:
if not word or len(word) == 0:
logging.error("found a zero-length greeting word: {0}".format(f.filename))
return True
Now try introducing an error by adding an empty greeting item. Modify the model to look like this:
# List of words meaning hello
- Hello
- Hallo
- Bonjour
-
And then fashion build
to test the new police(greetings)
function. After you've tested it, fix the model by removing the empty item.
Fashion generates documentation to explain what happened during a build to aid in debugging build problems.
build.html
is written to the fashion
directory. This is produced by the built-in transform fashion_doc
. So far there is little output, but in time all the fashion internal data will be exposed as models so very rich documentation will become possible.
Fashion creates a directory for itself under your project directory. Fashion is design to work within your existing project directory (as long as you don't already have a subdirectory named 'fashion'). The transformation and generation assets and metadata are stored under this directory.
fashion/
+ bak/ # backup files (not yet implemented)
+ mirror/ # copies of generated files to detect external modification
+ model/ # model files go here
+ template/ # template files go here
+ tmplMods/ # mako compiles templates into python modules here
+ xform/ # xforms (python modules) go here
+ fashion.yaml # model for this fashion project
+ library.yaml # library model for this fashion project
+ fashion.db # sqlite3 database for fashions' file metadata
It is recommended to store your transformation and generation assets in your source code version control system, but not everything under fashion/
needs to be controlled.
Control these:
fashion/
+ model/ # model files go here
+ template/ # template files go here
+ xform/ # xforms (python modules) go here
+ fashion.yaml # model for this fashion project
+ library.yaml # library model for this fashion project
Don't control these:
fashion/
+ bak/ # backup files (not yet implemented)
+ mirror/ # copies of generated files to detect external modification
+ tmplMods/ # mako compiles templates into python modules here
+ fashion.db # sqlite3 database for fashions' file metadata
A sample fashion.yaml is shown below:
bakPath: /home/bdillman/greet/fashion/bak
fashionDbPath: /home/bdillman/greet/fashion/fashion.db
fashionPath: /home/bdillman/greet/fashion
libraries:
- /home/bdillman/greet/fashion/library.yaml
- /home/bdillman/myFashionHome/fashion/fashion/library.yaml
mirrorPath: /home/bdillman/greet/fashion/mirror
projectPath: /home/bdillman/greet
tmplMods: /home/bdillman/greet/fashion/tmplMods
These entries record where the local fashion project locates various paths.
The libraries:
item contains a list of library.yaml
files. The order is significant because the libraries are searched from first to last. Typically, the first library is the local library, where commands like create-model
will create new models. Also the home library is usually last, which contains the built-in templates and transforms.
Users can add library entries from other fashion projects, making it easy to share models, templates and transforms between multiple projects.
A sample library.yaml
is shown below.
- fileFormat: yaml
glob: ./model/
kind: fashion_unknown
recursive: false
role: 3
- fileFormat: python3
glob: ./xform/
kind: fashion_unknown
recursive: false
role: 4
- fileFormat: mako
glob: ./template/
kind: fashion_unknown
recursive: false
role: 2
- fileFormat: python3
glob: ./xform/**/*.py
kind: fashion_unknown
recursive: true
role: 4
- fileFormat: yaml
glob: /home/bdillman/greet/fashion/model/greetWorlds.yaml
kind: greetings
recursive: false
role: 3
This is a list of library entries. Each entry associates some metadata with a specified group of files.
glob:
is a unix glob filespec which identifies the file(s).role:
is an integer which identifies the nature of the file(s).- reserved
- template
- model
- transform
kind:
is the kind of a model file.fileFormat:
describes how to read the file (currently only applies to models)recursive:
if true means recurse into directories using the**
inglob
, if any.
Commands like create-model
manipulate the local library file to add or remove entries as needed. Other than that, it is normal that users can edit these library files directly.
The following python libraries are used:
- Mako templates (MIT license)
- ruamel (MIT license)
- peewee (MIT license)
Think of "fashion" as a verb, e.g. "I fashioned this code from this simple model."