Introduction to Archetect

Archetect is a powerful content and code generation tool, with the ability to generate files, projects, or entire architectures.

Archetect is similar to Java's Maven Archetypes, JavaScript's yeoman, and Python's cookiecutter. These are all powerful content generators in their own right, each with their own strengths and weaknesses. Archetect endeavors to take the best aspects of each, and make something greater than the sum of their parts.

Below are some of the specific and key goals and motivations behind the making of Archetect.

Language Agnostic

Unlike some code generators, Archetect does not favor any target programming language. It has a simple but powerful yaml based configuration language, and uses a Jinja 2 style templating language. Archetect gives you all the tools you need to generate code in any language, without needing to resort to embedding Python or JavaScript into your templates.

Easy to Use

Generating a project from an archetype hosted in a remote git repo is as easy as:

archetect render git@github.com:archetect/archetype-rust-cli.git

No need to install templates locally. No need to guess parameters required to feed the template. No need to guess whether the template author is hoping you'll type in answers that are camelCased, PascalCased, or train-cased. It can be run interactively or headless, online or off.

Easy Authoring and Publishing

Archetect archetypes are nothing more than a directory with an archetype.yml file at the root. No special conventions are enforced, and you're free to organize your archetypes as simply or as sophisticated as you'd like. Making your fancy archetype available for the entire world to use is as simple as publishing it in a git repository.

Low Barrier to Entry

Archetect is a native binary written in the Rust programming language, and depends on nothing more than your local git binary and setup. Binaries are available for OSX, Linux, and Windows. No need to have the right version of Python and libraries installed. No need to install NodeJS/NPM. No need to install a JVM. It can run on your laptop, or just as easily within a CI/CD pipeline.

Archetect Features

Archetect was designed specifically with these goals in mind:

  • Language Agnostic
  • Easy to Author archetypes
  • Easy to Publish archetypes
  • Easy to Use archetypes
  • Usable within an Enterprise Environment

To meet these goals, Archetect has some core features unique amongst content generators.

Templating

Archetect leverages a Jinja2-like templating engine for generating content, with some unique extensions that make it easy to achieve common templating task without the need for custom scripting.

Smart Casing Functions

If an archetype author requests a consumer of their archetype to supply the name for a project, what happens if input is supplied that contains spaces in the name? How could the author safely use this input as a Java class name, as a field name, or a database column name?

Archetect's casing functions are capable of breaking inputs apart based on casing boundaries and reshape in various ways as needed throughout the archetype:

  • {{ 'Hello World' | snake_case }} => hello_world
  • {{ 'Hello World' | camel_case }} => helloWorld
  • {{ 'Hello World' | pascal_case }} => HelloWorld
  • {{ 'Hello World' | train_case }} => hello-world
  • {{ 'Hello World' | constant_case }} => HELLO_WORLD
  • {{ 'helloWorld' | title_case }} => Hello World
  • {{ 'HELLO_WORLD' | train_case }} => hello-world

Smart Pluralization

Most content generators that provide any sort of pluralization capability generally check to see if inputs end with an 's' or not, and then simply tack one on as necessary.

Archetect applies correct pluralizations based on the rules of, and exceptions to, the English Language.

As an example, if an archetype consumer inputs 'soliloquy' as the name of a new microservice, the archetype author can take this input and ensure that a generated REST API has a GET /soliloquies/ endpoint, with a Soliloquy object model.

  • {{ 'soliloquy' | pluralize }} => soliloquies
  • {{ 'calf' | pluralize }} => calves
  • {{ 'gas' | pluralize }} => gases
  • {{ 'tax' | pluralize }} => taxes
  • {{ 'wife' | pluralize }} => wives
  • {{ 'cactus' | pluralize }} => cacti

Configuration

In the simplest case, an archetype is nothing more than a directory with an archetype.yml file at the root. This file is a templating-aware DSL for describing how to acquire inputs, either interactively or through answer parameters or files, and how to render templated content based on those inputs.

Conditional Rendering

Archetect does not prescribe a specific directory structure for holding templated content. Instructions in the archetype.yml file are used to tell Archetect what to render. Multiple target template directories can be rendered, conditionally if desired, allowing for projects to be generated with optional features as determined based on the inputs acquired from the consumer of an archetype.

Composition

Archetypes can not only specify one or more directories to render, but also other archetypes, allowing for interesting compositions. As examples:

  • Having common archetypes containing content that should be shared across many other archetypes, while avoiding duplication and maintenance across all of them.
  • Having different archetypes for various remoting and database layers, and using conditional rendering to mix and match these technologies in different combinations. For instance, a composing archetype could ask "What kind of remoting layer would you like? [gRPC, OpenAPI, Thrift, Hessian]", and "What kind of database layer would you like? [JPA, DynamoDB, Cassandra]", and render each of the selected technologies together in a unified output project.

Catalogs

Rendering a one-off archetype from a git repository generally requires copying and pasting the location to the command line. If you are frequently generating projects while prototyping architectures, this can be a hassle. Archetect has a catalog system that allows for selecting archetypes interactively from a cli menu.

A catalog can be composed of archetypes, groups, or other catalogs including those hosted in remote git repositories. In a sense, this forms a kind of RESTful, distributed menu system.

As an example, a company could have a master catalog which Archetect is configured to use. Different language teams could each have their own catalogs they maintain containing archetypes for microservices, batch applications, messaging listeners, etc. Each of these catalogs could be referenced by the master catalog. When using Archetect, it will pull the latest version of catalogs as they are used, providing developers with the latest and greatest available archetypes from these various teams. Someone has developed a fancy high-performance microservice in the Rust programming language? Simply add it to a catalog and make it immediately available to the entire staff!

Getting Started

Getting started with Archetect is considerably easier than many content generators, but there are a few steps you'll want follow to ensure you're ready to go.

First, you'll need to check off a prerequisite or two first, which are likely already satisfied if you are a developer.

Naturally, you'll want to install the binary.

And last, you'll want to perform a post setup to ensure everything is working correctly.

Prerequisites

Git

Archetect is a single native binary designed for use on the command line. Technically, placing this on your shell's path is all you would need to get going. You could limit yourself to generating projects from archetypes authored or downloaded on your local machine. But that's no fun.

Archetect was designed to work with git, and you'll want to have git set up on your machine to unleash it's full power. Furthermore, to get the best experience, you'll also want to set up SSH password-less authentication on any git hosts you might expect to pull archetypes from.

Setting up git is beyond the scope of this book. There are many resources available online for your specific platform. GitHub's own documentation on the subject is a good place to start.

If you are able to execute the following command from your shell's command line in a directory you like storing coding projects, without being prompted for a password, you should be ready to go:

git clone git@github.com:archetect/archetect.git

A Note on Windows

Archetect works best with a Unix-like shell with Emoji support. Archetect works flawlessly with wt.exe.

In addition, Archetypes may contain long file names.

Windows, by default, notoriously has restrictions on path name lengths. Newer versions of Windows 10 and 11 now have support for Long Path Names, but it must be enabled in the Windows Registry (regedit.exe) by changing:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled

from 0 to 1, and rebooting.

You will also need to enable Long Path Names with git, as well:

git config --global core.longpaths true

Note that running the Windows Installer for Archetect will make these changes for you.

Installation

Homebrew

On MacOS (arm64) and on Linux (x64), Archetect can be installed with Homebrew:

brew tap archetect/archetect
brew install archetect

Windows Installer

There is a Windows Installer on Archetect's Releases GitHub Page (archetect-vX.X.X-windows_x64-installer.exe ) that:

  • Enables Windows Long Path Names
  • Enables Git Long Path Names
  • Installs Archetect
  • Add Archetect to the PATH

Pre-compiled binaries

Pre-compiled binaries are available on Archetect's Releases GitHub Page for 64 bit flavors of OSX, Linux, and Windows.

Installation is as simple as downloading the zip file that matches your platform, extracting the contents, and placing the archetect or archetect.exe binary within a location of your choosing. Ideally, that location would be on your path.

Note: On MacOS, unsigned cli binaries like Archetect are not allowed to run by default. To allow Archetect to run on MacOS, you'll need to browse to the binary with Finder, right or Control-click on it, and click "Open". Alternatively, you can remove the com.apple.quarantine flag using xattr:

xattr -d com.apple.quarantine <path to archetect>

For Rust Developers

If you're an existing or aspiring Rust developer with a recent version of the Rust toolchain installed, you can install Archetect from source:

cd ~/projects/rust
git clone git@github.com:archetect/archetect.git
cd archetect
cargo install --path ./archetect-bin --force 

Verifying Install

Be sure to verify your installation with Post Setup.

Post Setup

Once Archetect is installed, you can perform a couple of steps to ensure it is working correct.

First, check that you have a verion of a least 2.0.0:

archetect --version

Next, perform a check to ensure basic requirements are met for your Operating System:

archetect check

Finally, if you run archetect with no arguments, you should be presented with a default catalog menu. Browse the catalog and generate your first project. If everything is set up correctly, you should have a project generated without being prompted for a Git password.

Concepts

Archetypes

An archetype in Archetect is a self-contained directory containing everything needed to

When rendering an archetype, Archetect

Guide

This guide starts with the basics, and then presents new capabilities and features as we progress through each section. The completed work of each section is captured as branches in the archetect-tutorial Git repo, so you can progress through this guide and the tutorial examples at the same time.

If you'd like to use the companion tutorial examples, first switch into a directory where you keep projects, and then clone the tutorial's Git repo:

    git clone https://github.com/archetect/archetect-tutorial.git

You can visit each section by using git to list them out and then check out the corresponding branch that interests you.

    git branch
    git checkout 01_basics

Basics

Getting started by introducing a few basics.

Structuring an Archetype

Beging laying out the structure of our archetype.

Some Basics

If you are following along with the archetect-tutorial, you can switch to this section's examples from within the directory where it was check out:

    git checkout 01_basics

Let's begin making our first archetype, step by step, building up concepts as we go.

Creating our Script

Archetect gets it's instructions from a configuration file containing a simple, domain-specific scripting language.

Create a directory to start your archetype in. Then, create a YAML file at the root of this directory. For now, let's call it script.yml:

---
script:

  - info: "Hello, World!"

Within this directory, we can 'render' this script as follows:

archetect render script.yml

Prompting for Input

Printing "Hello, World!" is fine and dandy, but not particularly interesting. Modify our script to look as follows:

---
script:

  - set:
      name:
        prompt: "What is your name?"

  - info: "Hello, {{ name }}!"

If we have Archetect render the script as we did above, you will now be prompted to enter a name before displaying our custom greeting to the screen.

Using Functions

With our initial script, the name variable implies a proper noun. But currently, nothing stops someone using the script from entering in any of the following:

  • jane
  • JANE
  • jane doe
  • janeDoe
  • jane-doe
  • JANE_DOE
  • JANE DOE

Archetect makes it easy for archetype authors to accept inputs any way a user might enter them, providing all the tools necessary to reshape the inputs as needed based on the context where the inputs will be used.

Let's update our script so that our input is piped through a function, title_case, which will reshape the input to ensure our welcome message is formatted correctly. Use Archetect to render the script multiple times using the example inputs listed above. Also, try entering in an empty string to see what happens.

---
script:

  - set:
      name:
        prompt: "What is your name?"
  
  - info: "Hello, {{ name | title_case }}!"

Output

In addition to the rendering capabilities we'll begin exploring in Structuring an Archetype, Archetect provides multiple ways for outputting to both STDERR and STDOUT. Generally prompts, such as asking for input with the set action, and informational messages using the display, trace, debug, info, warn, and error actions print to STDERR. The print action sends output to STDOUT. This provides you tools to design an interactive CLI experience, yet allow select output to be piped to a file. Let's start trying these out:

---
script:

  - set:
    name:
    prompt: "What is your name?"
  
  - display: "Hello, {{ name | title_case }}!"
  
  - display: |
  
    Let's generate a simple Java class...
  
    We'll begin by prompting for the name of our class, and then create variables in different casings as needed
    in our template.
  
  - set:
    class:
    prompt: "What is the name of your class?"
  
    ClassName:
    value: "{{ class | pascal_case }}"
  
    className:
    value: "{{ class | camel_case }}"
  
  - info: "Our PascalCased class name is '{{ ClassName }}'"
  - debug: "Our camelCased field name is '{{ className }}'"

Render the script using Archetect as we've done before, trying various inputs to see how they behave:

archetect render script.yml

Notice that the line corresponding to the debug action did not show up. We need to increase the verbosity of output to see it by using the -v, or --verbose option. If we were to have a trace, we would need to pass an additional -v, as well.

archetect render script.yml -v
archetect render script.yml -vv

Putting It Together

The rendering capabilities we'll cover in the next chapter are Archetect's bread and butter, but we can use the tools provided so far to allow us to interactively generate basic code that we can pipe anywhere. We could use this to generate simple bits of JSON, YAML, SQL, etc.

---
script:

  - set:
    name:
    prompt: "What is your name?"
  
  - display: "Hello, {{ name | title_case }}!"
  
  - display: |
  
    Let's generate a simple Java class...
  
    We'll begin by prompting for the name of our class, and then create variables in different casings as needed
    in our template.
  
  - set:
    class:
    prompt: "What is the name of your class?"
  
    ClassName:
    value: "{{ class | pascal_case }}"
  
    className:
    value: "{{ class | camel_case }}"
  
  - info: "Our PascalCased class name is '{{ ClassName }}'"
  - debug: "Our camelCased field name is '{{ className }}'"
  
  
  - print: |
    public class {{ ClassName }} {
    private static {{ ClassName }} {{ className }} = new {{ ClassName }}();
  
          private {{ ClassName }}(){}
  
          public static {{ ClassName }} getInstance(){
             return {{ className }};
          }
  
          public void printMessage() {
              System.out.println("This is a contrived example. You can try this at home... but don't try it at work!");
          }
  
          public static void main(String[] args) {
              {{ ClassName }}.getInstance().printMessage();
          }
    }

While actions like trace, debug, info, warn, error, and display all output to STDERR, the print command outputs to STDOUT.

Render the script to the screen, and then render it to a file. Use inputs like 'SingletonGreeter', 'singleton greeter', etc as your input, and notice that your Java code will be rendered correctly in most reasonable cases.

archetect render script.yml > SingletonGreeter.java
javac SingletonGreeter.java                                                               
java SingletonGreeter

Whoa! Now we're cookin'!

Take notice that for two of the variables defined with the set command, we explicitly set their values derived from the previously set variable instead of prompting for a value. These derived variables are in the same shape as the casing functions applied. This is a common convention used in Archetect archetypes: take an input, and then create multiple variables in the cases required throughout a complex template. This is much preferable over repeatedly using the same functions everywhere, and makes it much easier to author and read the template later! This practice becomes especially important as we start using variables in directory structures.

Structuring an Archetype

If you are following along with the archetect-tutorial, you can switch to this section's examples from within the directory where it was check out:

    git checkout 02_structure

Using inline templates within our script is not very practical. For one, we can only pipe out a single file. This might prove convenient simple bits of JSON, YAML, or SQL, but not for entire projects. In addition, for some programming languages, the file names often correlate to the name of structures within the files themself. Take Java for instance; we have to enter in the name of our class twice (first when prompted by Archetect, and second when specifying the file name to pipe to), making this error-prone to ensure these match.

We can do a lot better.

Archetect provides a render action that allows us to specify a directory of templates to read files from, or another archetype being composed by this archetype. Not only can we use variables within the templates, but we can also use them in the directory and file names themselves.

Refactoring

So far, we've been rendering the script.yml file. However, Archetypes are usually contained within a directory or Git repository, and we can't always specify the name of a script to render.

Instead, we generally specify the path to a directory or the git URL. By convention, Archetect looks for either archetype.yml or archetype.yaml at the root of the directory. Let's fix this:

mv script.yml archetect.yml

Now, we will instead specify the directory to render instead of the YAML file:

archetect render ./

Now, let's create a directory to store our templates in, and move our Java template into a file within:

mkdir contents
touch "contents/{{ ClassName }}.java"

Copy and paste the Java class from our initial script into the new file, and format it appropriately:

public class {{ ClassName }} {
    private static {{ ClassName }} {{ className }} = new {{ ClassName }}();
    
    private {{ ClassName }}(){}
    
    public static {{ ClassName }} getInstance(){
        return {{ className }};
    }
    
    public void printMessage() {
        System.out.println("This is a contrived example. You can try this at home... but don't try it at work!");
    }
    
    public static void main(String[] args) {
        {{ ClassName }}.getInstance().printMessage();
    }
}

With these changes, we can reformat our archetype.yml file to make use of our new templates directory:

---
script:

  - set:
      class:
        prompt: "What is the name of your class?"

      ClassName:
        value: "{{ class | pascal_case }}"

      className:
        value: "{{ class | camel_case }}"

  - render:
      directory:
        source: contents

Our directory is now becoming an archetype, and something we'll eventually check into revision control. We don't want to pollute our archetype with generated files. Therefore, we should start rendering into a different directory. As an example, if we had ~/projects/ and~/archetypes/ directories, where the former is where we render our projects and the latter is where we author our archetypes, we would now render as follows:

cd ~/projects/
archetect render ~/archetypes/archetect-tutorial

We no longer need to pipe our output to a file, and whatever we supply as our class name will now be reflected within both the file name and file contents using the same variable. Easy!

Concepts

Scripting Engine

Templating Engine

Templating Configuration

Some aspects of the templating engine can be configured through settings in an archetype's archetype.yaml manifest.

Undefined Behavior

Since: 2.0.0-ALPHA.17

Undefined Behavior can be tuned based on the needs of an archetype. This configuration determines what happens if a context value is "undefined" (null).

Archetect's templating engine allows for the following three options:

Strict (default)

The templating engine will throw an error if an undefined value is encountered. For most archetypes, this setting is suitable, and helps an archetype author identify problems with their archetype quicker.

For example, if a variable first_name has not been defined when rending Jinja content, such as:

Hello, {{ first_name }}!

Archetect will throw an error and abort any further execution of the archetype.

This is the default behavior if this setting is not explicitly set, but may be set in archetype.yaml:

---
templating:
  undefined_behavior: Strict

Lenient

The templating engine will silently insert an empty string if a value is undefined. Useful when generating from more complex context models where you want to conditionally render content based on whether a variable is defined or not.

For example, if first_name has been defined, but greeting is optional and possibly undefined:

{% if greeting %}{{ greeting }}{% else %}Hello{% endif %}, {{ first_name}}!

Archetype will allow the conditional check on greeting, and silently insert an empty string for first_name if it has not been defined.

To enable this behavior in archetype.yaml:

templating:
  undefined_behavior: Lenient

Chainable

The templating engine will silently insert an empty string, even if there are a chain of undefined values. Useful for complex, dynamic generation from models.

For example, if an object with properties is optionally expected in the context, Archetect will silently insert an empty string if either person or person.first_name is undefined:

{{ person.first_name }}

To enable this behavior in archetype.yaml:

templating:
  undefined_behavior: Chainable

Configuration

Credits

These are some of the key libraries that are core to Archetect's functionality':

  • Tera: templating engine
  • Heck: case conversion
  • Infector: pluralization/sinularization