Skip to content

Authoring Generators

Pascal Honegger edited this page Jun 15, 2023 · 16 revisions

Overview

This page describes the process required to create a custom generator. The required steps can be summarized to:

  1. Decide on plugin programming language (e.g. Kotlin or Java)
  2. Decide on transport technology (e.g. official SAMT HTTP or a custom transport configuration)
  3. Create SAMT plugin with a new generator
  4. Release SAMT plugin

Prerequisites

All you need to get started with writing your own SAMT generator is a working Java or Kotlin development environment. SAMT is built and tested against Java 17, we recommend targeting a modern JVM version. This guide will use Kotlin for the example code, but the same principles apply to Java or any other JVM language as well. An example of a generator written in Java can be found at samtkit/example-java-plugin.

You will need to add the following dependencies to your project:

Maven
<dependencies>
  <dependency>
      <groupId>tools.samt</groupId>
      <artifactId>public-api</artifactId>
      <version>${samt.version}</version>
  </dependency>
  <dependency>
      <groupId>tools.samt</groupId>
      <artifactId>common</artifactId>
      <version>${samt.version}</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>tools.samt</groupId>
      <artifactId>compiler</artifactId>
      <version>${samt.version}</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>tools.samt</groupId>
      <artifactId>codegen</artifactId>
      <version>${samt.version}</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>tools.samt</groupId>
      <artifactId>samt-config</artifactId>
      <version>${samt.version}</version>
      <scope>test</scope>
  </dependency>
</dependencies>
Gradle (Kotlin)
dependencies {
  implementation("tools.samt:public-api:$samtVersion")
  testImplementation("tools.samt:common:$samtVersion")
  testImplementation("tools.samt:compiler:$samtVersion")
  testImplementation("tools.samt:codegen:$samtVersion")
  testImplementation("tools.samt:samt-config:$samtVersion")
}
Gradle (Groovy)
dependencies {
  implementation 'tools.samt:public-api:$samtVersion'
  testImplementation 'tools.samt:common:$samtVersion'
  testImplementation 'tools.samt:compiler:$samtVersion'
  testImplementation 'tools.samt:codegen:$samtVersion'
  testImplementation 'tools.samt:samt-config:$samtVersion'
}

Writing the Generator

New generators can be created by implementing the Generator interface and overriding the name field and generate method. For the purposes of this example, we will create a generator that creates a markdown file containing a list of all the declared enums in the SAMT project. This can be achieved by iterating over all the provided SAMT packages and accessing the list of declared enums. Once the output has been generated, it must be returned as a list of CodegenFile objects.

object EnumMarkdownGenerator : Generator {
    override val name: String = "enum-markdown-generator"

    override fun generate(params: GeneratorParams): List<CodegenFile> {
        // extract all declared enums
        val enums = params.packages.flatMap { it.enums }

        // create the markdown file
        val markdown = buildString {
            appendLine("# Enums")
            appendLine()
            // We simply generate a flat list of enums, completely ignoring their SAMT package
            // Alternatively, qualifiedName would include the package, but we ignore it here for simplicity
            enums.forEach { enum ->
                appendLine("## ${enum.name}")
                appendLine()
                enum.values.forEach { value ->
                    appendLine("- ${value.name}")
                }
                appendLine()
            }
        }

        // Construct generated file, this will be placed in the configured "out" directory
        val output = CodegenFile(
            name = "enums.md",
            content = markdown
        )
        return listOf(output)
    }
}

Running the generator

When developing a custom generator, you'll want to test it locally. This can be done by adding the required

You can however create a new project, include SAMT as a dependency and manually execute the generator that way. This approach, even though it works, is rather cumbersome and will be improved in the future.

fun main() {
    // Parse example samt.yaml configuration
    val (configuration ,_) = CliConfigParser.readConfig(Path("path/to/samt.yaml"), controller) ?: return

    // Read *.samt files from disk
    val sourceFiles = collectSamtFiles(configuration.source.toUri()).readSamtSource(controller)

    // Parse *.samt files
    val fileNodes = sourceFiles.map { source ->
        val context = controller.getOrCreateContext(source)
        val tokenStream = Lexer.scan(source.content.reader(), context)
        Parser.parse(source, tokenStream, context)
    }

    // Build semantic model
    val model = SemanticModel.build(fileNodes, controller)

    if (controller.hasErrors()) {
        // Nicely printing all errors isn't easy here, it's best to use a debugger to analyze the controller
        println("Has errors!")
        return
    }

    // Register your generators / transport parsers here
    Codegen.registerGenerator(MyGenerator)
    Codegen.registerTransportParser(MyTransportParser)

    for (generatorConfig in configuration.generators) {
        for (generatedFile in Codegen.generate(model, generatorConfig, controller)) {
            println(generatedFile.filepath)
            println("-".repeat(generatedFile.filepath.length))
            println(generatedFile.source)
        }
    }
}

Publishing Your Plugin

DISCLAIMER: As currently implemented, there is no way to load your own generator into the SAMT CLI. This feature hasn't been implemented yet, but will possible in the future.