A data-driven code generator with templates and a (very) simple scripting language. Written in .NET Core 2.0 so it should run on Mac/Linux/Windows.
The script language (see below) uses XPATH 1.0 expressions for iteration, conditions and text subsitutions.
Use can use ucg for Model Oriented Programming, working with higher-level abstractions than general purpose languages. ucg will typically be used to generate one or more patterns from a source XML model.
ucg is insprired by iMatrix's GSL. I tried and failed to get GSL to work on Win x64, so I thought I could write something similar for .NET Core. ucg is pretty simple and comes in at less than 1000 LOC.
UCG uses two types of files to generate any code you can imagine:
- XML model to generate from.
- Script files, which can be either pure scripts or templates.
To generate pass the script name and model name to ucg, for example:
dotnet ucg --script my-script.ucg my-model.xml
The script is then interpreted and output is sent to StdOut which can be changed via the output keyword (see below).
Any additional arguments after the model file name are added as attributes to the root model element, for example:
dotnet ucg --script my-script.ucg my-model.xml --cs-namespace BusterWood.Samples
Models are XML files that you define. An example would be a a list of entities (tables) that you wish to generate code for:
<?xml version="1.0" encoding="utf-8" ?>
<entities cs-namespace="BusterWood.Ucg">
<entity name="User">
<field name="User Id" nulls="not null" type="int" pk="true"/>
<field name="Full Name" nulls="not null" type="string" db-type="VarChar" db-size="50"/>
<field name="Email" nulls="not null" type="string" db-type="VarChar" db-size="50"/>
</entity>
</entities>Models are freeform XML, there is no "special" tags, but we recommend using:
- attributes for properties with at most one value
- elements for lists of things
The following attributes are automatically added to the root element of the model:
model-paththe file name/path of the model file supplied toucgon the command linescript-paththe file name/path of the script file supplied toucgon the command linedatetime-utcthe date and time of thatucgwas run
When a script is in template mode then input scripts are written to the output. All expressions in the for $(...) expanded (see below) before the line is output.
Any line with a first character of . is interpreted as script language, for example:
.template on
using System;
namespace $(/@cs-namespace)
{
public class $(@name:p)
{
. foreach field
public $(@type) $(@name:p) { get; set; }
. endfor
}
}
Expressions like $(...) are recursively expanded in the script language and as well as in templates.
Expressions are evaluated on the current model element, which is initally the root element of the XML, but this is changed by foreach blocks (see below).
Expressions can be either:
- an XPATH expression on the current model, e.g.
$(@name)to get the value of the name attribute. - double quoted text, typically used with the
:b,:,and:~modifiers (see below), for example$(" AND":~)
The value returned by an expression can be used as-is, for modified via the following format specifications:
:uforUPPER CASE, e.g.$(@name:u):lforlower case, e.g.$(@name:l):tforTitle Case, e.g.$(@name:t):pforPascalCase, e.g.$(@name:p):cforcamelCase, e.g.$(@name:c):_forunderscore_separated, e.g.$(@name:_):bfor(surround with brackets)empty string when empty, otherwise add brackets round the text:,for,prefixed with commaempty string when empty, otherwise the value with a comma added at the beginning:~means don't output the value for the last item in aforeachorforfilesloop.
Multiple modifiers can be specified and are applied in order, for example $(@db-size:b,) adds brackets then prefixes with a comma.
Expressions can contain ?? which is interpreted as the left hand side, if that has value, otherwise the right hand side. For example:
new SqlMetaData("$(@name:u_)", SqlDbType.$(@db-type??@type)$(@db-size:b,))$(",":~)
Comment lines start with //, for example:
// this is a comment
Turns template mode on or off, for example:
.template on
The output keyword changes the file that the script to writes to. The path name must be supplied inside double quotes and expressions can be included as part of the script file name/path. If the file exists then it is overwritten.
For example, the following sets the output file to be the value of the name attribute of the current model element:
output "$(@name).cs"
The include keyword runs another script file, passing the current model element to the script. The path name must be supplied inside double quotes and expressions can be included as part of the script file name/path.
For example, the following runs a script called cs-class.cs:
include "cs-class.cs"
A foreach block repeats the code inside the block for each element matching the XPATH expression.
Inside the block then the current node is changed to be the selected child node.
For example, the following code runs another script for each child element with the name entity:
foreach entity
include "insert-proc.ucg"
endfor
A forfiles block repeats the code inside the block for each file found. Each file found is added as a child element of the current model with the following attributes:
pathwhich is the full path to the filenamewhich is the file name without the file extensionextensionwhich is the file extensionfolderwhich is the name of the directory which contains the file
Inside the block then the current node has a file child element added.
For example, the following code runs each script found in the current directory with the extension .ucg:
forfiles "*.ucg"
include "$(file/@path)"
endfiles
A if block conditional runs the code in the block if the XPATH expression:
- evalutes to true
- evaluates to a non-empty string
- evalutes to a single element
When an else block is present then that code is run only when the the condition is false.
For example, the following code runs another script if the fk-rel attribute exists and is not empty
if string(@fk-rel)
include "select-fk.ucg"
endif
The load statement adds child elements to current model from an XML file where the elements matches an XPATH expression.
For example, the following loads entity elements where the name attribute has a value of order from the schema.xml file:
load "schema.xml" entity[name='order']
foreach entity
...
endfor
The inherit statement adds the attributes and child nodes (elements, text, etc) of another element specified via an XPATH expression only if they do not already exist in the model.
The inherit statement is used for de-normalizing model data, for example merging a domain type definitions with the field of an entity (table).
For example, the following inherits the type element with a typename attribute that matches the typename attribute of current model element:
foreach entity
foreach field
inherit //type[@typename='$(@typename)']
endfor
endfor
You can inherit just the attributes of the selected element by adding the attributes keyword before the xpath expression, for example:
inherit attributes //type[@typename='$(@typename)']
The merge statement adds and updates attributes and adds child nodes (elements, text, etc) of another element specified via an XPATH expression.
The merge statement is used for de-normalizing model data, for example merging a domain type definitions with the field of an entity (table).
For example, the following merges the type element with a typename attribute that matches the typename attribute of current model element:
foreach entity
foreach field
merge //type[@typename='$(@typename)']
endfor
endfor
You can merge just the attributes of the selected element by adding the attributes keyword before the xpath expression, for example:
merge attributes //type[@typename='$(@typename)']
The transform statement changes attributes of the current model element to child elements. Only attributes with names that matches an XPATH expression are transformed.
This statement is used to allow code to be generated from a list of attributes as XPATH does not allow iterating over attributes.
For example, given this model:
<entity name="Currency">
<field name="Currency Code" typename="short code" pk="true"/>
<field name="Currency Id" nulls="not null" type="short" db-type="SmallInt" uk="true"/>
<field name="Name" typename="name"/>
<static>
<data CurrencyCode="GBP" CurrencyName="Pound Stirling"/>
</static>
</entity>
When this script is run:
foreach static/data
transform ../../field/@name
endfor
Then the model is transformed into:
<entity name="Currency">
<field name="Currency Code" typename="short code" pk="true"/>
<field name="Name" typename="name"/>
<static>
<data>
<CurrencyCode name="Currency Code">GBP</CurrencyCode>
<Name name="Name">Pound Stirling</Name>
</data>
</static>
</entity>
NOTE that the transform is applied without spaces, e.g. the attribute CurrencyCode is found for the value of name="Currency Code" in the above example.
The writemodel keyword writes some elements of the model XML to the output.
- if no paramter is supplied then the current model element is written.
- if an xpath expression is suppplied then elements matching that expression are written to the output.
For example:
/*
WARNING: This file is generated for the following model:
.writemodel
*/
The echo keyword writes some text to StdErr. Any expression in the text are expanded before writing to StdErr.
For example:
.echo hello world!
XPATH 1.0 expressions are supported with the addition on the distinct-values() function.
The distinct-values() function works a bit differently in that it only compares the first attribute name and value and the text of the current element.
@namereturns the value of thenameattribute.text()returns the text of the current element.../@namereturns the value of thenameattribute of the parent element.state[@terminal]returns childstateelements that have aterminalattribute.state[not(@terminal)]returns childstateelements that do not have aterminalattribute.distinct-values(.//do)returns a set of decendentdoelements that have unique text and first attribute.