JGen Chenile code generator

Authoring blueprints

Extending JGen

Template engines, language packs and path tags: add a template language, support another target language, or add a new tag, without changing JGen. Blueprints declare what they need with requires.

2.5.1

JGen started as a generator for Chenile’s Java services, but its engine doesn’t care what it generates. Three extension points let you take it elsewhere without forking it:

Kind What it adds Built in
Template engine a template language, chosen by file extension mustache (.mustache); pebble (.peb) ships with JGen
Language pack validation rules and template helpers for a target language python, node
Path tag a rule for file and folder names filelist, placeholder, dot, conditional

Unlike hooks, which belong to one blueprint, extensions are installed once and every blueprint can use them. jgen --extensions lists what is installed.

Template engines

The render step picks an engine by the template’s extension and drops the extension:

src/__module__.py.peb        → rendered by pebble   → src/orders.py
pom.xml.mustache             → rendered by mustache → pom.xml
README.md                    → copied as it is

A blueprint can mix engines freely. Mustache stays the default for Java because it is logic-less. Pebble (Twig and Jinja syntax) suits languages where you want real conditionals, loops with filters, and whitespace control:

class {{ entity | pascalCase }}:
{% for field in fields %}    {{ field.name | snakeCase }}: {{ field.type }}
{% endfor %}

JGen’s Pebble is set up for code, not HTML: output isn’t escaped, a missing value renders as empty (as in Mustache), and every template helper is a filter. Templates are self-contained: include and extends aren’t supported.

Language packs

A pack teaches JGen about a target language. Today that means two things.

Validation rules. Blueprints name them in validation exactly like the built-in rules:

{ "name": "module",  "type": "STRING", "validation": "PYTHON_MODULE" },
{ "name": "package", "type": "STRING", "validation": "NPM_PACKAGE" }
Pack Rule Accepts
python PYTHON_MODULE order_service: lowercase letters, digits and _, not a keyword
  PYTHON_PACKAGE acme.orders: dot-separated module names
  PYPI_PROJECT acme-orders: a PyPI project name
node NPM_PACKAGE @acme/orders: lowercase, optionally scoped, at most 214 characters
  JS_IDENTIFIER orderService: an identifier that is not a reserved word

A rule that no installed pack defines is an error as soon as the field is read: when the blueprint loads, not when someone first uses it. JGen checks a blueprint’s requires first, so a blueprint that needs a missing pack is told which pack is missing, rather than that its rule is unknown.

Template helpers. A pack can add helpers next to the core ones. They reach every engine and script.

Template helpers

Helper purchase order becomes
capitalize / uncapitalize first letter changed only
camelCase purchaseOrder
pascalCase PurchaseOrder
snakeCase purchase_order
upperSnakeCase PURCHASE_ORDER
kebabCase purchase-order

Words are split at spaces, -, _ and case changes, so purchaseOrder, PurchaseOrder and purchase-order all give the same results, and HTTPServer becomes http_server.

Where How
Mustache {{#snakeCase}}{{entity}}{{/snakeCase}}
Pebble {{ entity | snakeCase }}
JavaScript hooks jgen.snakeCase(inputs.entity), or jgen.helper("anyHelper", text)

Path tags

Path tags are covered in the template reference. Each tag has an order, which sets when it runs relative to the others.

Writing an extension

An extension is an ordinary jar. Implement one of these interfaces from org.chenile.jgen:jgen-base (a provided dependency, the same version as the JGen it runs in):

public interface TemplateEngine {                      // org.chenile.jgen.ext
    String name();                                     // "freemarker"
    String extension();                                // ".ftl"
    void render(Reader template, String templateName, Map<String,Object> data, Writer out) throws Exception;
}

public interface LanguagePack {                        // org.chenile.jgen.ext
    String name();                                     // "terraform"
    default List<ValidationRule> rules() { ... }       // ValidationRule.of("TF_RESOURCE", "a resource name", "[a-z][a-z0-9_]*", null)
    default Map<String, Function<String,String>> helpers() { ... }
}

public interface PathTag { ... }                       // org.chenile.jgen.template.tags

List each class in the matching services file:

META-INF/services/org.chenile.jgen.ext.TemplateEngine
META-INF/services/org.chenile.jgen.ext.LanguagePack
META-INF/services/org.chenile.jgen.template.tags.PathTag

Then drop the jar into a blueprints folder: ~/.jgen/blueprints, the installation’s blueprints/, or a folder in JGEN_BLUEPRINTS. Add addDefaultImplementationEntries to the jar’s manifest so that blueprints can ask for a minimum version. The jgen-ext-pebble module in chenile-gen is a complete example.

Rules that keep extensions safe:

  • Unique names. Extension names are unique across all three kinds, so a built-in can’t be replaced.
  • Unique claims. No two engines may claim the same file extension.
  • No overriding built-ins. A pack can’t redefine a built-in rule such as JAVA_IDENTIFIER, or an existing helper.

requires: what a blueprint needs

A blueprint that depends on an extension says so in blueprint.json:

{
  "name": "fastapi-service",
  "templateFolder": "fastapi-template",
  "requires": ["pebble", "python", "terraform@1.2"],
  "inputFields": [ ... ]
}
  • Format. Each entry is a name, optionally with a minimum version (name@version, compared with the extension jar’s Implementation-Version). The kind:name form is accepted too.
  • Missing extensions. A blueprint whose requirements aren’t installed is refused when it loads, before its input fields are read. The message names what is missing and where to put the jar.
  • The generation record. The generation record lists the added (not built-in) extensions the project depended on, as kind:name@version:
    • every added path tag;
    • the added engines that rendered files;
    • the packs whose rules the blueprint uses;
    • whatever it requires.

    jgen -r refuses to regenerate a project when one of them is missing, rather than produce different output.