asyncapi / template-for-generator-templates

This is a GitHub repository template for generator templates to make it much easier to start writing your own generator template.
Apache License 2.0
19 stars 10 forks source link
get-global-docs-autoupdate get-global-node-release-workflows get-global-releaserc nodejs template

AsyncAPI logo

This repository is a template for generator templates to make it much easier to start writing your own generator template.

Now, wait a minute. First, what is the Generator? Second, what is the generator template? A generator is a tool that you can use to generate whatever you want to base on the AsyncAPI specification file as an input. Generator knows what to generate because you supplement it with a generator template. The template is a project where you provide all the files that must be generated using available generator features.

Minimum for your template

The most basic template must have the following:

What are all the other directories and files? This template provides an example that includes: all of the best practices; a demonstration of the generator features; and anything else needed to provide a production-ready template. In the next section, we will cover which features are required and which features are optional.

How to reuse this template

This repository is a GitHub template repository. Use it by just clicking the Use this template button visible on this repository's home page or check the official documentation.

There is nothing :sunglasses: here, there are no :fireworks: after creating a repository using this template. It is not going to scaffold a project for you with some custom values (like, for example, Yeoman). It just creates a boilerplate repository that includes all the features, which you then manually modify or remove. It is just simplifies starting a new project to use a template. Alternatively, if you are not using GitHub, or simply do not want to expose your Generator templates origin as a GitHub template, you can just clone this repository locally and then push to your Git hosting service.

Technical Requirements

The Generator is a Node.js application. Therefore, the template also depends on Node.js. The generator's technical requirements are:

Install both using official installer.

After that you can install the AsyncAPI Generator globally to use its CLI:

npm install -g @asyncapi/generator

Template development hints

The most straightforward command to use this template is:

ag https://raw.githubusercontent.com/asyncapi/generator/v1.0.1/test/docs/dummy.yml https://github.com/asyncapi/template-for-generator-templates -o output

For local development, you will need variations of this command.

There are three important CLI flags:

There are two ways you can work on template development:

Learning Resources

WHen developing a new template, please refer to the following documentation:

Also, remember that you can join us in Slack

How to use this template without modifications

To run templates, you need to install the Generator.

# This repo has a special AsyncAPI example with complex schema dependencies to get a more complex Mermaid diagram as a sample
ag https://raw.githubusercontent.com/asyncapi/generator/v1.0.1/test/docs/dummy.yml https://github.com/asyncapi/template-for-generator-templates -o output
open output/index.html

What you get with this template

Every resource in this repository is essential for the overall template setup. Nevertheless, not all the resources are necessary, and if you are not interested in everything, you can remove or modify certain parts. Every resource in this repo is part of the template, except for the .gitignore file that is just standard for Git repositories. Check the next sections to understand the meaning of a given resource and what you will lose if you remove it or do not configure it correctly.

Sample template that presents generator features and best practices in using them

The Generator depends on either the React (if you're using React) or Nunjucks (if you're using Nunjucks) templating engine. You can choose whichever one you prefer. Each rendering engine has a different way of working and a different set of features. Keep the template engine choice in mind when you are familiarizing yourself with the Generator functionality. This repository is focused on React renderer. If you want to use Nunjucks, check old nunjucks branch.

The list of resources that are relevant for this template:

NOTE: The reusable parts (components/helpers) can be located both in the template folder or in another named folder. The only exception is the hooks folder, it is reserved solely for the Generator.

Templates are highly configurable. This template also showcases most of the configuration options. Configurations are stored in the package.json file in the generator section.

This template contains an example implementation of all those features.

Template

Checkout the template directory to see how the different features of the generator are presented.

File component

For the generator to render a file with React certain conditions are required:

  1. The file should export a default function (example see the template/index.js file).
  2. That function should return a <File> component as root component which contains the necessary metadata for the Generator to render the file. Returning null, undefined or another negative value forces the Generator to not create the file. Metadata contains:
    • name describes the filename for which should be used when generating the file. If none is specified the filename for the template are used.
    • permissions describes the permissions the file should be created with. This is interpreted as an octal number such as 0o777.
/*
 * Each template to be rendered must have as a root component a File component,
 * otherwise it will be skipped.
 * 
 * If you don't want to render anything, you can return `null` or `undefined` and then Generator will skip the given template.
 */
export default function({ asyncapi, params }) {
  if (!asyncapi.hasComponents()) {
    return null;
  }

  // Notice that root component is the `File` component.
  return (
    <File name="index.html">
      <HTML>
        ...
      </HTML>
    </File>
  )
}

Template Context

The Generator passes to the render engine extra context, which you can access in templates:

AsyncAPI Document

Check out the template/index.js file to see an example of how you can access the contents of the AsyncAPI document:

/*
 * Notice also how to retrieve passed properties to custom component, by the destruction of the first argument.
 * Accessing document data is made easier thanks to the AsyncAPI JavaScript Parser - https://github.com/asyncapi/parser-js.
 */
function BodyContent({ asyncapi }) {
  const apiName = asyncapi.info().title();
  const channels = asyncapi.channels();

  // rest of implementation...
}

When accessing AsyncAPI document contents, use the Parser's API documentation.

Parameters Passed To Generator By The User

Check out the template/index.js file to see an example of how you can access custom parameters passed by the user:

export default function({ asyncapi, params }) {
  return (
    <File name="index.html">
      ...
      <Scripts params={params} />
      ...
    </File>
  );
}

/*
 * You can access "maxTextSide" parameter value without any conditions in case user didn't provide such a parameter. 
 * It is possible thanks to the functionality that makes it possible for template developer to specify default values for parameters.
 * Check out package.json file and look for `generator.parameters.maxTextSize` and its description and default value.
 */
function Scripts({ params }) {
  return `
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>
  mermaid.initialize({
    startOnLoad: true,
    maxTextSize: ${params.maxTextSize},
  });
</script>
`;
}

Custom (reusable) components

Check out the template/index.js file to see an example how you can create reusable components and use them inside template:

// Import custom components from file 
import { HTML, Head, Body } from "../components/common";

/* 
 * Below you can see how reusable chunks (components) could be called.
 * Just write a new component (or import it) and place it inside the File or another component.
 * 
 * Notice that you can pass parameters to components. In fact, underneath, each component is a pure Javascript function.
 */
export default function({ asyncapi, params }) {
  ...
  const cssLinks = [
    'https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css',
    'style.css',
  ];

  // Notice that root component is the `File` component.
  return (
    <File name="index.html">
      <HTML>
        <Head cssLinks={cssLinks} />
        <Body>
          <BodyContent asyncapi={asyncapi} />
          ...
        </Body>
      </HTML>
    </File>
  );
}

// Custom component inside template file
function BodyContent({ asyncapi }) {
  // implemntation...
}

Each custom component must returns as output pure string, another custom component, null or undefined. Nothing will be rendered for the last two.

The recommended place to create reusable chunks is the components folder, for helper functions it is the helpers folder. The reusable parts (components/helpers) can be located both in the template folder and in another named folder. The only exception is the hooks folder, it is reserved for the Generator.

Using JS in template

When you use React, you are actually using JS, so you can apply conditions to rendering, split functionality into separate/reusable functions, create a composition, extend/mix functions etc.

/* 
 * If asyncapi has `externalDocs` property then the Generator will return appropriate string,
 * otherwise it won't render anything.
 */
function ExternalDocs({ asyncapi }) {
  if (!asyncapi.hasExternalDocs()) return null;
  return `Don't forget to visit our website ${asyncapi.externalDocs().url()}.`
}

Retrieve rendered content from children

Each component has a childrenContent property. It is the processed children content of a component into a pure string. You can use it for compositions in your component.

function CustomComponent({ childrenContent }) {
  return `some text at the beginning: ${childrenContent}`
}

function RootComponent() {
  return (
    <CustomComponent>
      some text at the end.
    </CustomComponent>
  );
}

Then output from RootComponent will be some text at the beginning: some text at the end..

Render component to string

If you need to process the React component to string you should use render function from @asyncapi/generator-react-sdk package. This function transforms a given component (and its children) and returns the pure string representation. Look in template/index.js to see an example usage:

import { render } from "@asyncapi/generator-react-sdk";

function BodyContent({ asyncapi }) {
  ...
  return `
<div class="container mx-auto px-4">        
  <p> 
    <h1>${apiName}</h1>
    ${render(<ListChannels channels={channels} operationType='subscribe' />)}
    ${render(<ListChannels channels={channels} operationType='publish' />)}
    ${render(<DiagramContent asyncapi={asyncapi} />)}
    ${render(<Extension asyncapi={asyncapi} />)}
    ${render(<ExternalDocs asyncapi={asyncapi} />)}
  </p> 
</div>  
`;
}

Render multiple files

To render multiple files, it is enough to return an array of File components in the rendering component. Template file template/schemas/schema.js is an example of such a case:

export default function({ asyncapi }) {
  const schemas = asyncapi.allSchemas();
  // schemas is an instance of the Map
  return Array.from(schemas).map(([schemaName, schema]) => {
    const name = normalizeSchemaName(schemaName);
    return (
      <File name={`${name}.html`}>
        <SchemaFile schemaName={schemaName} schema={schema} />
      </File>
    );
  });
}

function SchemaFile({ schemaName, schema }) {
  // implementation...
}

This one template file results in multiple HTML files, one per schema.

Hooks

Hooks are functions called by the generator on a specific moment in the generation process. For more details, read about hooks.

Custom template hooks

Check out the hooks/generateExtraFormats.js file to see an example of a hook. The hook is invoked by the Generator once the template generation process completes. The Generator passes its context to hooks, which mean that you have access to data like targetDir (a path to the directory where the template is generated), or templateParams (information about custom parameters passed by the user). This example hook provides optional features to the template, like PNG/PDF/SVG generation, that the user decides on with custom parameters. That is not the only use case for a hook. There are many more use cases such as template cleanup operations after generation or modifications of the AsyncAPI document right before the generation.

Important things to notice:

Official AsyncAPI hooks

Hooks are reusable between templates. The AsyncAPI Initiative provides a library of hooks. You can also create a library for your own templates; you will need to add the library to dependencies in the package.json file and configure in generator.hooks section like this:

{
  ...
  "generator": {
    ...
    "hooks": [
      "@asyncapi/generator-hooks": "createAsyncapiFile"
    ]
  }
}

Notice that you can specify one or many hooks that you want to reuse from the library instead of all the hooks. In this template, we use createAsyncapiFile responsible for creating the asyncapi.yaml file in the directory where template files get generated. This hook also supports custom parameters that I can specify in my configuration:

"generator": {
  "parameters": {
    "asyncapiFileDir": {
      "description": "Custom location of the AsyncAPI file that you provided as an input into generation. By default, it is located in the root of the output directory."
    }
  }
}

Using, for example, the Generator CLI, you can, for example, pass -p asyncapiFileDir=nested/dir, and as a result, you will get asyncapi.yaml file in nested/dir directory.

Configuration

Put the configuration of the Generator in the package.json file in the generator section. This template covers most of the configuration options.

renderer

You can write template using tool which you prefer more. The template engine can be either react (recommended) or nunjucks (default). This can be controlled with the renderer property.

"generator": {
  "renderer": "react"
}

parameters

Templates can be customized using parameters. Parameters allow you to create more flexible templates. They can be required and also have default values that make their usage in template code less complicated. In this template, you have:

"generator": {
  "parameters": {
    "asyncapiFileDir": {
      "description": "Custom location of the AsyncAPI file that you provided as an input to generation. By default, it is located in the root of the output directory."
    },
    "pdf": {
      "description": "Set to `true` to get index.pdf generated next to your index.html",
      "default": false
    },
    "png": {
      "description": "Set to `true` to get index.png generated next to your index.html",
      "default": false
    },
    "svg": {
      "description": "Set to `true` to get index.svg generated next to your index.html",
      "default": false
    },
    "maxTextSize": {
      "description": "It is possible that in case of huge AsyncAPI document default mermaid recommended text size will not be enough. Then you need to make it larger explicitly",
      "default": 50000
    }
  }
}

nonRenderableFiles

This template has the binary and .css files that should not be rendered by the Generator to avoid generation errors.

"generator": {
  "nonRenderableFiles": [
    "style.css"
  ]
}

NOTE: All mentioned files in the nonRenderableFiles field are copied to the output folder.

generator

The generator property is used to specify the Generator's versions which your template is compatible with. The template depends on the Generator version. In case of new major releases of the Generator, this ensures that your template will not fail due to any breaking changes.

"generator": {
  "generator": ">=1.1.0 <2.0.0"
}

hooks

This template uses hooks from the official AsyncAPI Generator hooks library. For more details, read Official AsyncAPI hooks.

"generator": {
  "hooks": {
    "@asyncapi/generator-hooks": "createAsyncapiFile"
  }
}

Handling circular references

Schemas provided in the AsyncAPI document may contain circular references. This is not an error; circular references in the data model can happen. The Generator doesn't provide any features to handle circular references. Inside the template, the Generator gives you access to parsed AsyncAPI document with all the functions provided by AsyncAPI JavaScript Parser. In this way, indirectly, you get access to helpers for circular references. For more details see this paragraph.

This template demonstrates providing support for handling circular refs in objects and their properties.

In the generateMermaidDiagram function in helpers/mermaidDiagram.js note the usage of the circularProps() function, where we check if the property that you want to add to the diagram, introduces a circlular reference:

const circularProp = schema.circularProps() && schema.circularProps().includes(propName);
classContent += circularProp ? `${propName} [CIRCULAR] ${propValueMap.type()}\n` : `${propName} ${propValueMap.type()}\n`;

Later in the same file there is an example of how to recursive traversing in the case of a circular reference:

if (propertySchema.circularProps() && propertySchema.circularProps().includes(propName)) return;
recursiveSchema(propertySchema, callback, prop);

If you are at an early stage of template development and do not have time to handle circular references, you can still confirm whether a given AsyncAPI document contains circular references by calling asyncapi.hasCircular() and provide a helpful error message to the user. This is always better than the less informative RangeError: Maximum call stack size exceeded.

Documenting the template

The file is a sample readme provided for this template. Use it as a guideline for writing your own template readmes.

(Optional) Tests for each feature of the template

This is the list of resources that are relevant, and you can remove them if you do not want to use this feature:

If you remove them, you should also remove @asyncapi/parser, @asyncapi/generator, jest, @babel/preset-env and @babel/preset-react from devDependencies in the package.json file and test-related configuration from the jest and babel sections.

This template is tested using the Jest framework. There are tests for all integral template parts, filters, hooks, and the template generation result itself. Jest-related configuration from the package.json file are there only because the code coverage tool conflicts with the puppeeteer library. It is possible that you do not need it in your template.

These are the contents of the test directory:

This template generates static files, so there are no examples of integration tests that would, for example, start a generated application to test if it works with a real broker.

NOTE: The@babel/preset-env dependency is needed to transform your code to the appropriate version of NodeJS that you're using. The @babel/preset-react dependency is needed to transform JSX expressions into normal JS functions. The babel section in the package.json applies the BabelJS configuration to the jest.

(Optional) Release pipeline based on GitHub Actions and Conventional Commits specification

The release pipeline is based on GitHub Actions and Conventional Commits specification. In case you are using a different CI/CD solution or do not like to have commit messages prefixed with text like fix: or feat: then you should read the rest of this section carefully to understand what files should be removed.

If you want to keep this pipeline, make sure that you modify not only proper files but also put valid secrets in your repository settings:

This is the list of resources that are relevant for this part, and you can remove them if you do not want to use it:

(Optional) Quality assurance

This is the list of resources that are relevant for this part, and you can remove them if you do not want to use this feature:

You can remove both files in case you do not want to use ESLint. Keep in mind, though, that after removing them, you should also remove related packages from the package.json from the devDependencies section:

You should also remove the lint script from the scripts section of the package.json file.

What about the package.json and package-lock.json files

The package.json is the central part of the template. You cannot remove it. You can only modify it by following instructions from previous sections.

Whenever you make a change to package.json make sure you perform an update with npm install to synchronize with package-lock.json and validate if the file is not broken after your changes

Contributors

Thanks goes to these wonderful people (emoji key):


Lukasz Gornicki

💻 📖 🎨 🤔 🚧

Barbara Szwarc

👀

Maciej Urbańczyk

📖 💻 🤔

This project follows the all-contributors specification. Contributions of any kind welcome!