Skip to content

Latest commit

 

History

History
374 lines (242 loc) · 13.3 KB

authoring_templates.md

File metadata and controls

374 lines (242 loc) · 13.3 KB

Authoring templates

Output of the code generator is driven by a set of templates that are stored in the tool package's internal templates directory and are installed with the tool.

Template authors write text files that are processed by the template engine. You don't have to write any extensions or plugins to use the template engine. The template language, which is an extension of the Jinja2 template language, is built into the tool and has a set of extensions specific to code generation that cover even fairly complicated scenarios.

Custom templates can be provided by the user by creating a directory that has the same structure as this templates directory and passing the path to the --templates command line argument. Any templates found in the user-provided directory will override the corresponding language and style of the built-in templates and you can add new languages and styles.

The generator distinguishes between templates for endpoints and message definition groups (those are the styles).

Directory structure

The content of the templates directory is dynamic and extensible.

The common structure looks like this:

templates
├── {language}
│   ├── _templateinfo.json
│   ├── _common
│   │   ├── amqp.jinja.include
│   │   ├── cloudevents.jinja.include
│   │   └── mqtt.jinja.include
│   ├── {style}[/subdir]
│   │   ├── _templateinfo.json
│   │   ├── {template}.jinja
│   │   └── ...
│   └── ...
└── ...

In a templates directory reside subdirectories for each of the supported output languages. Adding support for a new language is as simple as adding a new directory with a unique name. That name should correspond to the common file name suffix for the language, e.g. if you were adding support for C++ you would name the directory cpp.

The tool uses the name of the directory as the language identifier for the --language command line argument. The language identifier is also used to find the correct templates for automatic generation of schema serializer code as shown in the illustration above.

Under each language directory, there are subdirectories for each template set. Template sets are called "styles" to avoid conflations with other overloaded terms. The tool uses the name of the subdirectory as the style identifier for the --style command line argument.

The _common directory may include files for template macros that are shared across all styles. Other directories that start with an underscore are ignored and reserved for internal/future use.

The _common directory is optional. If you don't need to share macros across styles, you don't need to provide any templates in the _common directory.

The full set of templates for the C# language is here.

Template Info

For tools that wrap the code generator, like the VS Code extension, you can place a _templateinfo.json file into the language directory to provide a description for the language:

{
  "description": "C# / .NET 6.0"
}

Inside each style directory, you can place a _templateinfo.json file that provides a description for the style and a priority for sorting the list of styles in a tool like the VS Code extension by relative importance:

{
  "description": "Azure Functions HTTP trigger",
  "priority": 1
}

The default priority is 100, putting a style at/near the bottom of the list if you don't specify a priority.

Template styles

All code templates live grouped in style directories, each set typically reflecting a full code project, including project files and other assets.

Code generation approach

The general code generation philosophy is that the code generator should yield code and assets that can be compiled and packaged without any further changes and that the code contains extensibility hooks to integrate it into application projects.

For instance, the code for C# consumers that is generated by the embedded templates always includes a "dispatch interface" that can be implemented by the application to handle messages. The dispatch interface shape is identical across all transports and hosts but differs by message definition format. Shows here is the dispatch interface for C# consumers that use the CloudEvents format:

namespace Contoso.ERP.Consumer
{
    public interface IEventsDispatcher
    {
        Task OnReservationPlacedAsync(CloudEvent cloudEvent, OrderData data);
        Task OnPaymentsReceivedAsync(CloudEvent cloudEvent, PaymentData data);
        ...
    }
}

The implementation of the interface is then passed to the generated consumer class, for instance in the factory methods that create the consumer instance for a declared endpoint.

var consumer = new EventsEventConsumer(endpoint, eventsDispatcher);

In scenarios like Azure Functions, the dispatcher is added by dependency injection.

 private static void Main(string[] args)
{
    var host = new HostBuilder()
        .ConfigureServices(s =>
        {
            s.AddSingleton<IEventsDispatcher, MyEventsDispatcher>();
        })
        .Build();
    host.Run();
}

Code template files

All files you want to emit must be suffixed with .jinja and are run through the Jinja template engine. If you don't put any Jinja template syntax in your file, it will be copied verbatim to the output directory, with the ".jinja" suffix stripped.

If you prefix a file with an underscore, it will be processed after all regular files and also after all schema files have been generated. This is useful for generating files that must know about all the generated classes or files, like a project file. The underscore prefix is stripped from the output file name.

The filenames of the templates are used to determine the output file name. Unless you use expansion macros, the file name is used as-is. For example, if you have a template named README.md.jinja, the output file will be named README.md.

Expansion macros are used to generate multiple files from a single template or to rename the file based on a variable. The following macros are supported:

  • {projectname} - expands into the name provided with the --projectname argument.
  • {classname} - expands into the name of the generated class, which the tool infers from the schema document or the definition document. If the macro is used, the tool will modify the input document for the template such that it is scoped down to the definition or schema type that is supposed to be generated and call the template once for each type.
  • {classdir} - this macro works like the {classname} option but also creates a subdirectory reflecting the package name. This is specifically meant to help with Java conventions.

Template variables

The input document, which is either a CloudEvents Discovery document or a schema document, is passed to the template as a variable named root. The root variable's structure reflects the respective input document.

  • For code generators for message payload schemas, the root variable is the root of the CloudEvents Discovery document, corresponding to the CloudEvent Discovery schema type document. Underneath root are three collections:
    • messagegroups - a dictionary of message definition groups, keyed by the message group's ID.
    • schemagroups - a dictionary of schema definition groups, keyed by the message group's ID.
    • endpoints - a dictionary of endpoints, keyed by the endpoint's ID.

As discussed above, the {classdir} and {classfile} filename expansion macros will modify the input document given to the template and scope it to the current object that shall be emitted.

Otherwise, the template always gets the full input document.

If the --definitions argument points to a URL or file name that returns/contains a fragment of a CloudEvents discovery document, such as a single schemagroup or messagegroup, the generator will synthesize a full discovery document around the fragment and pass it to the template.

Filters

In addition to the many filters built into Jinja, the following extra filters are available for use in templates:

pascal

Converts a string (including those in camelCase and snake_case) to PascalCase

Example:

{{ "foo_bar" | pascal }} -> FooBar

camel

Converts a string (including those in snake_case and PascalCase) to camelCase

Example:

{{ "foo_bar" | camel }} -> fooBar

snake

Converts a string (including those in camelCase and PascalCase) to snake_case

Example:

{{ "fooBar" | snake }} -> foo_bar

pad(len)

Left-justifies a string with spaces to the specified length. This is useful for sorting version strings in a template.

Example:

{{ "1.0" | pad(5) }} -> "  1.0"
{{ "1.0" | pad(5) }} -> " 11.0"

strip_namespace

Strips the namespace/package portion off an expression

Example:

{{ "com.example.Foo" | strip_namespace }} -> Foo

strip_invalid_identifier_characters

Strips invalid characters from an identifier. This is useful for converting strings to identifiers in languages that have stricter rules for identifiers. All unsupported characters are replaced with an underscore.

Example:

{{ "foo-bar" | strip_invalid_identifier_characters }} -> foo_bar
{{ "@foobar" | strip_invalid_identifier_characters }} -> _foobar

namespace

Gets the namespace/package portion of an expression

Example:

{{ "com.example.Foo" | namespace }} -> com.example

concat_namespace

Concatenates the namespace/package portions of an expression

Example:

{{ "com.example.Foo" | concat_namespace }} -> comexampleFoo

If you want to pascal case the expression, use the pascal filter first, e.g.

{{ "com.example.Foo" | pascal | concat_namespace }} -> ComExampleFoo

toyaml

Formats the given object as YAML. This is useful for emitting parts of the input document, for instance JSON Schema elements, into YAML documents. tojson is already built into Jinja.

Example:

{{ root | toyaml }}

exists(prop, value)

Checks whether the given property exists anywhere in the given object scope with the value prefixed with the given string (case-insensitive)

Example:

{% if root | exists("format", "amqp") %}
    // do something
{% endif %}

regex_search(pattern)

Performs a regex search. Returns a list of matches.

Example:

{% if "foo" | regex_search("f") %}
    // do something
{% endif %}

regex_replace(pattern, replacement)

Does a regex-based replacement. Returns the result.

Example:

{{ "foo_bar" | regex_replace("[^A-Za-z_]", "-") }} -> "foo-bar"

Functions

You can use all expressions and functions defined by the Jinja2 standard library. In addition, the following functions are available:

pop(stack_name)

Pops a value from a named stack collected with push. This works across template files. If the stack is empty, an empty string is returned.

Example:

{% "foo" | push("name") %}
{% "bar" | push("name") %}
{% "baz" | push("name") %}
{{ pop("name") }} -> "baz"
{{ pop("name") }} -> "bar"
{{ pop("name") }} -> "foo"

stack(stack_name)

Gets the full contents of a named stack collected with push. This works across template files.

Example:

{% "foo" | push("name") %}
{% "bar" | push("name") %}
{% "baz" | push("name") %}
{% for x in stack("name") %}{{ x }} {% endif %} -> foo bar faz

get(prop_name)

Gets the value of a named property collected with save. This works across template files.

Example:

{% "foo" | save("name") %}
{{ get("name") }} -> "foo"

latest_dict_entry(dict)

Gets the "latest" entry from a dictionary, which is specifically designed to work for the versions property of a schema object. The latest entry is the one with the highest version number.

Example:

{%- set schemaObj = schema_object(root, event.schemaurl ) -%}
    {%- if schemaObj.format is defined -%}
       {%- set schemaVersion = latest_dict_entry(schemaObj.versions) %}

schema_object(root, schemaurl)

Gets an object resolving a given relative URL. This is useful for getting the schema object for a given event or command.

Example:

{%- set schemaObj = schema_object(root, event.schemaurl ) -%}
    {%- if schemaObj.format is defined -%}

Tags

You can use all tags defined by the Jinja2 standard library. In addition, the following tags are available:

{% exit %}

Exits the template without producing any output. This is useful for skipping the file if the input document doesn't contain the required information or if the template creates output files using pushfile and you don't want a file to be emitted for the template itself.

Example:

{% if root.type is not defined %}{% exit %}{% endif %}

{% uuid %}

Generates a UUID and emits it into the output. This is useful for generating unique identifiers for things like namespaces. (Consider this tag deprecated. It will turn into a function).

Example:

{% uuid %} -> 3e8c0b0a-1b5a-4b3f-8c1c-1b5a4b3f8c1c

{% time %}

Generates a timestamp and emits it into the output. (Consider this tag deprecated. It will turn into a function).

Example:

{% time %} -> 2020-01-01T00:00:00Z